Аутентификация критически важна для мобильных приложений. Тестирование OAuth 2.0 потоков и управления JWT токенами обеспечивает безопасный, бесшовный пользовательский опыт. Это руководство охватывает тестирование потоков аутентификации, жизненного цикла токенов, биометрической интеграции и лучших практик безопасности.

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

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)
}

Тестирование 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 потока
  • Управление жизненным циклом токенов
  • Валидация безопасного хранения
  • Интеграция биометрической аутентификации
  • Обработка ошибок и крайних случаев

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