Authentication security
Authentication & Security Deep Dive¶
Overview¶
Authentication proves identity. Securely integrating auth into mobile is critical for production systems.
Core Concepts¶
JWT (JSON Web Tokens)¶
Structure: header.payload.signature
Contains:
- exp: expiration timestamp
- sub: subject (user ID)
- iat: issued at time
- custom claims
OAuth 2.0 for Mobile¶
Authorization code flow (for third-party login):
- App opens browser to provider
- User grants permission
- Provider sends authorization code
- App exchanges code for token
- App can now access resources
Secure Token Storage¶
Never hardcode or store in plain SharedPreferences:
// Use EncryptedSharedPreferences
val encryptedPrefs = EncryptedSharedPreferences.create(
context,
"secret",
MasterKey.Builder(context).build(),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
encryptedPrefs.edit().putString("auth_token", token).commit()
Internal Implementation¶
Token Refresh Flow¶
When token expires:
class TokenRefreshingInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (response.code == 401) {
synchronized(this) {
val newToken = refreshToken()
response.close()
return chain.proceed(addToken(chain.request(), newToken))
}
}
return response
}
}
Request/Response Flow¶
- User logs in with credentials
- Server validates, returns tokens
- Access token stored securely
- Refresh token stored separately (longer TTL)
- All requests include access token
- On 401, refresh flow triggered
Code Examples¶
JWT Token Structure¶
// Manually decode (never verify without server)
val parts = token.split(".")
val payload = String(Base64.getDecoder().decode(parts[1]))
// {"sub":"user123","exp":1640000000,"iat":1640000000}
OkHttp Auth Interceptor¶
class AuthInterceptor(tokenProvider: TokenProvider) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val token = tokenProvider.getToken()
val authenticatedRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
return chain.proceed(authenticatedRequest)
}
}
Common Interview Questions¶
Q: Should tokens be refreshed proactively? A: Yes, refresh shortly before expiration to avoid race conditions.
Q: Can JWT be revoked? A: Client can't know. Server maintains blacklist or uses short TTL.
Q: Is PKCE required for mobile? A: Yes for OAuth. Mobile can't keep secrets, so code challenge/verifier used.
Production Considerations¶
Token Lifetimes¶
- Access: 15-60 min (short, limited damage if leaked)
- Refresh: 7-30 days (enables long sessions)
- ID token (if OAuth): short like access
Multi-Account Support¶
Some apps support multiple accounts:
// Store tokens per user ID
val userTokens = mapOf(
"user1" -> token1,
"user2" -> token2
)
fun selectAccount(userId: String) {
currentToken = userTokens[userId]
}
Performance Insights¶
Cache Tokens in Memory¶
Avoid repeated disk access:
private var tokenCache: String? = null
private var cacheTime: Long = 0
fun getToken(): String {
if (System.currentTimeMillis() - cacheTime < 5000) {
return tokenCache!! // recent cache
}
tokenCache = loadFromDisk()
cacheTime = System.currentTimeMillis()
return tokenCache!!
}
Senior-Level Insights¶
Device Binding for Security¶
Link tokens to device to prevent theft:
val deviceId = Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ANDROID_ID
)
// Include in token or sign with device key
Biometric Integration¶
Protect token access with fingerprint/face:
BiometricPrompt(this, executor, callback).authenticate(promptInfo)
// Only on success do we unlock token