Реализация авторизации через JWT-токены в мобильном приложении
JWT — это формат токена, не протокол авторизации. Это важно понимать, потому что многие проекты строят собственный "JWT auth" без понимания того, что именно они защищают и от чего.
Типичная картина: бэкенд генерирует JWT, подписывает HS256 (HMAC-SHA256), мобильное приложение получает токен, кладёт в UserDefaults и отправляет в заголовке Authorization: Bearer. Работает. Но вопрос в том, насколько безопасно.
Проблема хранения и где она бьёт
UserDefaults (iOS) и SharedPreferences (Android) — это plaintext хранилища. На iOS без jailbreak добраться до них сложно, но iOS backup в iTunes/iCloud их включает. Пользователь бэкапит устройство на компьютер с macOS — ключи от бэкапа без пароля хранятся в открытом виде. Через idevicebackup2 и специализированные утилиты JWT можно извлечь за минуты.
Правило простое: JWT хранится только в Keychain (iOS) или Android Keystore / EncryptedSharedPreferences (Android). Никаких UserDefaults, AsyncStorage в React Native без дополнительного шифрования.
В React Native это особенно больно — AsyncStorage по умолчанию plaintext. Библиотека react-native-keychain решает проблему для iOS и Android, оборачивая платформенные хранилища.
Верификация на клиенте: что проверять и что нет
Мобильное приложение может декодировать JWT и читать claims — для отображения имени пользователя, проверки ролей в UI, определения времени истечения для проактивного refresh. Но верифицировать подпись на клиенте бессмысленно, если JWT подписан симметричным ключом (HS256) — клиент не должен знать этот ключ.
Что клиент должен проверять:
-
exp— не истёк ли токен (с небольшим запасом, ~30 секунд, для clock skew) -
iss— ожидаемый издатель -
aud— токен предназначен для нашего приложения
Если сервер использует RS256 или ES256 (асимметричный алгоритм) — клиент может верифицировать подпись через публичный ключ. Это имеет смысл для offline-сценариев, когда нет сети, но надо проверить токен локально.
Библиотеки: JWTDecode.swift (iOS), java-jwt от Auth0 или nimbus-jose-jwt (Android), jwt-decode (React Native).
Refresh и silent auth
Access token живёт 15–60 минут. Refresh token — дни/недели. Автоматическое обновление — обязанность HTTP-клиента, не бизнес-логики.
На iOS c URLSession: кастомный URLSessionTaskDelegate или middleware-паттерн. В Alamofire — RequestInterceptor с методами adapt и retry. На Android с Retrofit — Authenticator (вызывается при 401) или Interceptor (проверяет exp до запроса).
// Android Retrofit Authenticator
class TokenAuthenticator(private val tokenRepo: TokenRepository) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
if (response.code != 401) return null
val newToken = runBlocking { tokenRepo.refreshToken() } ?: return null
return response.request.newBuilder()
.header("Authorization", "Bearer $newToken")
.build()
}
}
Важна защита от параллельных refresh-запросов. Если пять запросов одновременно получили 401 — пять попыток refresh. Правильно: Mutex (Kotlin) / NSLock (Swift) или async let с actor (Swift Concurrency). Первый поток делает refresh, остальные ждут результата.
// iOS — actor для serialized refresh
actor TokenRefreshActor {
private var refreshTask: Task<String, Error>?
func refreshIfNeeded(using service: AuthService) async throws -> String {
if let task = refreshTask { return try await task.value }
let task = Task { try await service.refresh() }
refreshTask = task
defer { refreshTask = nil }
return try await task.value
}
}
Revocation и logout
JWT stateless по природе — нельзя "отозвать" токен без дополнительной инфраструктуры. Короткий exp + refresh token rotation — основная защита. При logout:
- Удаляем токены из Keychain/Keystore.
- Вызываем
/auth/logoutна сервере → сервер инвалидирует refresh token в базе. - Если сервер ведёт blocklist — access token тоже инвалидируется немедленно.
Пункт 2 и 3 — серверная работа. Но мобильная сторона обязана вызвать logout endpoint, даже если пользователь в оффлайне (ставим в очередь через WorkManager / BackgroundTasks).
Сроки
JWT auth с нуля: хранилище, interceptor для attach/refresh, logout, unit-тесты — 4–7 рабочих дней. Если нужна интеграция с существующим бэкендом и согласование формата claims — плюс 2–3 дня на синхронизацию с backend-командой.







