Реалізація авторизації через 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 з 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 у базі. - Якщо сервер ведеметаж — access token теж інвалідується негайно.
Пункти 2 та 3 — серверна робота. Але мобільна сторона обов'язково повинна викликати logout endpoint, навіть якщо користувач в офлайні (ставимо у чергу через WorkManager/BackgroundTasks).
Терміни
JWT auth з нуля: зберігання, interceptor для attach/refresh, logout, unit-тесты — 4–7 робочих днів. Якщо потрібна інтеграція з існуючим бекендом та узгодження формату claims — плюс 2–3 дні на синхронізацію з backend-командою.







