Аутентификация критически важна для мобильных приложений. Тестирование 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)
}
}
Лучшие Практики
- Всегда используйте PKCE для мобильного OAuth - Предотвращает перехват кода
- Храните токены безопасно - Используйте Android Keystore / iOS Keychain
- Реализуйте автоматическое обновление - Бесшовный пользовательский опыт
- Тестируйте сценарии истечения - Обрабатывайте крайние случаи
- Используйте биометрическую аутентификацию - Улучшенная безопасность + UX
Для полного обзора современных практик мобильного тестирования, включая интеграцию OAuth тестирования, изучите наше руководство Мобильное Тестирование 2025. Также исследуйте Мастерство Тестирования API для комплексных стратегий тестирования API.
Заключение
OAuth 2.0 и JWT тестирование для мобильных требует:
- Реализация и тестирование PKCE потока
- Управление жизненным циклом токенов
- Валидация безопасного хранения
- Интеграция биометрической аутентификации
- Обработка ошибок и крайних случаев
Правильное тестирование аутентификации обеспечивает безопасность при поддержании бесшовного пользовательского опыта через жизненные циклы приложения.