OAuth 2.0 и JWT — доминирующие паттерны для защиты мобильных API, но их правильное тестирование требует специализированных знаний о жизненном цикле токенов и граничных случаях безопасности. По данным OWASP API Security Top 10 2023, нарушенная аутентификация и авторизация остаются главными уязвимостями в API, а недостатки реализации JWT напрямую ответственны за громкие утечки данных. По данным исследования Auth0 (теперь Okta), 63% мобильных приложений имеют как минимум одну уязвимость аутентификации в коде обработки токенов. Для QA-инженеров тестирование OAuth/JWT означает проверку полного жизненного цикла токенов.

TL;DR: Тестирование OAuth/JWT для мобильных приложений должно охватывать: полный поток authorization code (PKCE для мобильных), валидацию access token (подпись, срок действия, audience), ротацию refresh token, применение scope, отзыв токена и атаки безопасности (algorithm confusion, none algorithm).

OAuth 2.0 Потоки для Мобильных

Authorization Code Flow с PKCE

PKCE (Proof Key for Code Exchange) предотвращает атаки перехвата кода авторизации, делая его необходимым для мобильных приложений.

// Используя библиотеку AppAuth
class OAuthManager(private val context: Context) {
    fun startAuthFlow(activity: Activity) {
        val authRequest = AuthorizationRequest.Builder(
            serviceConfig,
            CLIENT_ID,
            ResponseTypeValues.CODE,
            Uri.parse("myapp://oauth-callback")
        )
        .setCodeVerifier(CodeVerifierUtil.generateRandomCodeVerifier())
        .setScope("openid profile email")
        .build()

        val authService = AuthorizationService(context)
        val intent = authService.getAuthorizationRequestIntent(authRequest)

        activity.startActivityForResult(intent, AUTH_REQUEST_CODE)
    }
}

Тестирование OAuth Потока

@Test
fun testAuthorizationCodeExchange() = runTest {
    val mockAuthServer = MockWebServer()

    mockAuthServer.enqueue(MockResponse()
        .setResponseCode(200)
        .setBody("""
            {
              "access_token": "mock_access_token",
              "refresh_token": "mock_refresh_token",
              "expires_in": 3600
            }
        """.trimIndent())
    )

    val result = oauthManager.exchangeAuthCode("mock_auth_code")

    assertNotNull(result.accessToken)
    assertEquals("Bearer", result.tokenType)
}

“OAuth and JWT testing is where you earn your security credibility as a QA engineer. Getting token validation right — especially edge cases like expired tokens, revoked tokens, and algorithm confusion attacks — is non-trivial and critically important.” — Yuri Kan, Senior QA Lead

Тестирование JWT Токенов

class JWTValidator {
    fun validate(token: String): Boolean {
        val parts = token.split(".")
        if (parts.size != 3) return false

        val payload = decodePayload(parts[1])

        if (payload.exp * 1000 < System.currentTimeMillis()) {
            throw TokenExpiredException()
        }

        return verifySignature(parts[0], parts[1], parts[2])
    }
}

@Test
fun testJWTValidation() {
    val validToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
    assertTrue(validator.validate(validToken))
}

Поток Обновления Токенов

class TokenManager(private val authService: AuthService) {
    suspend fun getValidAccessToken(): String {
        if (isTokenExpired()) {
            refreshAccessToken()
        }
        return accessToken ?: throw NoTokenException()
    }

    private suspend fun refreshAccessToken() {
        val response = authService.refreshToken(refreshToken!!)
        accessToken = response.accessToken
        expiresAt = System.currentTimeMillis() + (response.expiresIn * 1000)
    }
}

@Test
fun testAutomaticTokenRefresh() = runTest {
    tokenManager.setToken("expired_token", expiresAt = System.currentTimeMillis() - 1000)

    val validToken = tokenManager.getValidAccessToken()

    assertEquals("new_access_token", validToken)
}

Тестирование Биометрической Аутентификации

class BiometricAuthManager(private val context: FragmentActivity) {
    fun authenticate(onSuccess: () -> Unit, onFailure: (String) -> Unit) {
        val biometricPrompt = BiometricPrompt(
            context,
            executor,
            object : BiometricPrompt.AuthenticationCallback() {
                override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                    onSuccess()
                }

                override fun onAuthenticationFailed() {
                    onFailure("Аутентификация не удалась")
                }
            }
        )

        val promptInfo = BiometricPrompt.PromptInfo.Builder()
            .setTitle("Биометрический вход")
            .setSubtitle("Войдите используя биометрические данные")
            .setNegativeButtonText("Использовать пароль")
            .build()

        biometricPrompt.authenticate(promptInfo)
    }
}

Безопасное Хранение Токенов

Безопасное хранение токенов так же критично, как и сетевая безопасность. В то время как certificate pinning защищает данные в транзите, правильное хранение предотвращает несанкционированный доступ в покое.

class SecureTokenStorage(private val context: Context) {
    private val keyStore = KeyStore.getInstance("AndroidKeyStore")

    fun saveToken(token: String) {
        val cipher = cipher
        val encryptedData = cipher.doFinal(token.toByteArray())

        context.getSharedPreferences("auth", Context.MODE_PRIVATE)
            .edit()
            .putString("token", Base64.encodeToString(encryptedData, Base64.DEFAULT))
            .apply()
    }

    fun getToken(): String? {
        val prefs = context.getSharedPreferences("auth", Context.MODE_PRIVATE)
        val encryptedData = prefs.getString("token", null) ?: return null

        val cipher = Cipher.getInstance("AES/GCM/NoPadding")
        val decryptedData = cipher.doFinal(Base64.decode(encryptedData, Base64.DEFAULT))

        return String(decryptedData)
    }
}

Лучшие Практики

  1. Всегда используйте PKCE для мобильного OAuth - Предотвращает перехват кода
  2. Храните токены безопасно - Используйте Android Keystore / iOS Keychain
  3. Реализуйте автоматическое обновление - Бесшовный пользовательский опыт
  4. Тестируйте сценарии истечения - Обрабатывайте крайние случаи
  5. Используйте биометрическую аутентификацию - Улучшенная безопасность + UX

Для полного обзора современных практик мобильного тестирования, включая интеграцию OAuth тестирования, изучите наше руководство Мобильное Тестирование 2025. Также исследуйте Мастерство Тестирования API для комплексных стратегий тестирования API.

Заключение

OAuth 2.0 и JWT тестирование для мобильных требует:

  • Реализация и тестирование PKCE потока
  • Управление жизненным циклом токенов
  • Валидация безопасного хранения
  • Интеграция биометрической аутентификации
  • Обработка ошибок и крайних случаев

Правильное тестирование аутентификации обеспечивает безопасность при поддержании бесшовного пользовательского опыта через жизненные циклы приложения.

Смотрите также

Официальные ресурсы

FAQ

Что такое PKCE и почему он необходим для мобильного OAuth?

PKCE (Proof Key for Code Exchange) предотвращает атаки перехвата authorization code в мобильных приложениях, где client secrets не могут безопасно храниться. Вместо фиксированного client secret PKCE использует динамически генерируемый code_verifier и его SHA-256 хэш. Каждая мобильная OAuth-реализация должна использовать PKCE.

Как правильно тестировать валидацию JWT?

Тестируй: действительный токен, просроченный токен (401), недействительная подпись (401), неправильный алгоритм (атака algorithm confusion), токен с алгоритмом none (должен всегда отклоняться), неправильный audience claim и отсутствующие обязательные claims.

Как тестировать применение scope OAuth?

Создавай токены с разными подмножествами scope. Пробуй получить доступ к ресурсам, требующим scope, отсутствующих в токене. Проверяй: ресурсы возвращают 403 (не 401) для действительных токенов с недостаточным scope, и что эскалация scope правильно блокируется.

Как тестировать ротацию refresh token?

Проверяй: refresh token можно использовать один раз для получения новой пары access/refresh token. Старый refresh token немедленно аннулируется после использования. Тестируй обнаружение повторного использования refresh token и истечение срока действия долгоживущих refresh tokens.

See Also