Authentication Patterns
Authentication is the cornerstone of mobile application security. Modern mobile apps require sophisticated authentication mechanisms that balance security, user experience, and platform capabilities. This section covers comprehensive authentication strategies from basic patterns to advanced enterprise-grade implementations.
Authentication Architecture Overview
Mobile authentication operates across multiple layers:
- User Authentication: Identity verification (something you know/are/have)
- Device Authentication: Device identity and integrity verification
- Application Authentication: App-to-service authentication
- Session Management: Secure session handling and lifecycle management
Core Authentication Patterns
OAuth 2.0 with PKCE
OAuth 2.0 with Proof Key for Code Exchange (PKCE) is the recommended pattern for mobile applications.
// Android OAuth 2.0 with PKCE implementation
class OAuth2Manager {
private val authService: AuthorizationService
private val authServiceConfig: AuthorizationServiceConfiguration
fun initiateAuthFlow(): Intent {
// Generate PKCE parameters
val codeVerifier = generateCodeVerifier()
val codeChallenge = generateCodeChallenge(codeVerifier)
val authRequest = AuthorizationRequest.Builder(
authServiceConfig,
CLIENT_ID,
ResponseTypeValues.CODE,
Uri.parse(REDIRECT_URI)
)
.setCodeVerifier(codeVerifier)
.setCodeChallenge(codeChallenge)
.setCodeChallengeMethod(CodeVerifierUtil.getCodeChallengeMethod())
.setScope("openid profile email")
.build()
return authService.getAuthorizationRequestIntent(authRequest)
}
suspend fun exchangeCodeForTokens(authCode: String, codeVerifier: String): TokenResponse {
val tokenRequest = TokenRequest.Builder(
authServiceConfig,
CLIENT_ID
)
.setAuthorizationCode(authCode)
.setRedirectUri(Uri.parse(REDIRECT_URI))
.setCodeVerifier(codeVerifier)
.build()
return withContext(Dispatchers.IO) {
authService.performTokenRequest(tokenRequest)
}
}
private fun generateCodeVerifier(): String {
return CodeVerifierUtil.generateRandomCodeVerifier()
}
private fun generateCodeChallenge(verifier: String): String {
return CodeVerifierUtil.deriveCodeChallengeS256(verifier)
}
}
// iOS OAuth 2.0 with PKCE implementation
import AuthenticationServices
class OAuth2Manager: NSObject, ASWebAuthenticationPresentationContextProviding {
private let authURL = URL(string: "https://auth.example.com/oauth/authorize")!
private let redirectScheme = "com.example.app"
func authenticateWithOAuth2() async throws -> AuthTokens {
let codeVerifier = generateCodeVerifier()
let codeChallenge = generateCodeChallenge(from: codeVerifier)
var components = URLComponents(url: authURL, resolvingAgainstBaseURL: false)!
components.queryItems = [
URLQueryItem(name: "client_id", value: "your_client_id"),
URLQueryItem(name: "response_type", value: "code"),
URLQueryItem(name: "redirect_uri", value: "com.example.app://oauth"),
URLQueryItem(name: "scope", value: "openid profile email"),
URLQueryItem(name: "code_challenge", value: codeChallenge),
URLQueryItem(name: "code_challenge_method", value: "S256")
]
let session = ASWebAuthenticationSession(
url: components.url!,
callbackURLScheme: redirectScheme
) { callbackURL, error in
// Handle callback
}
session.presentationContextProvider = self
session.prefersEphemeralWebBrowserSession = true
return try await withCheckedThrowingContinuation { continuation in
session.start()
}
}
private func generateCodeVerifier() -> String {
var buffer = [UInt8](repeating: 0, count: 32)
_ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer)
return Data(buffer).base64URLEncodedString()
}
private func generateCodeChallenge(from verifier: String) -> String {
let challenge = SHA256.hash(data: verifier.data(using: .utf8)!)
return Data(challenge).base64URLEncodedString()
}
}
JWT Token Management
JSON Web Tokens (JWT) provide stateless authentication with built-in expiration and claims.
// Android JWT implementation
class JWTManager {
private val jwtDecoder = JWT()
data class AuthTokens(
val accessToken: String,
val refreshToken: String,
val expiresAt: Long
)
fun parseJWT(token: String): DecodedJWT? {
return try {
jwtDecoder.decodeJwt(token)
} catch (e: JWTDecodeException) {
null
}
}
fun isTokenValid(token: String): Boolean {
val decoded = parseJWT(token) ?: return false
return decoded.expiresAt.time > System.currentTimeMillis()
}
suspend fun refreshTokenIfNeeded(tokens: AuthTokens): AuthTokens {
if (isTokenExpiringSoon(tokens.accessToken)) {
return refreshAccessToken(tokens.refreshToken)
}
return tokens
}
private fun isTokenExpiringSoon(token: String, bufferMinutes: Int = 5): Boolean {
val decoded = parseJWT(token) ?: return true
val bufferTime = bufferMinutes * 60 * 1000
return decoded.expiresAt.time - System.currentTimeMillis() < bufferTime
}
private suspend fun refreshAccessToken(refreshToken: String): AuthTokens {
return withContext(Dispatchers.IO) {
// Implement token refresh API call
authApiService.refreshToken(RefreshTokenRequest(refreshToken))
}
}
}
Multi-Factor Authentication (MFA)
// iOS MFA implementation
class MFAManager {
enum MFAMethod {
case sms(phoneNumber: String)
case email(emailAddress: String)
case authenticatorApp
case biometric
}
enum MFAError: Error {
case invalidCode
case expiredCode
case tooManyAttempts
case methodNotSupported
}
func initiateMFA(method: MFAMethod, userId: String) async throws -> String {
switch method {
case .sms(let phoneNumber):
return try await sendSMSCode(to: phoneNumber, userId: userId)
case .email(let email):
return try await sendEmailCode(to: email, userId: userId)
case .authenticatorApp:
return try await setupTOTP(userId: userId)
case .biometric:
return try await setupBiometric(userId: userId)
}
}
func verifyMFA(code: String, challengeId: String) async throws -> Bool {
let request = MFAVerificationRequest(
code: code,
challengeId: challengeId,
timestamp: Date().timeIntervalSince1970
)
let response = try await apiService.verifyMFA(request)
return response.isValid
}
private func sendSMSCode(to phoneNumber: String, userId: String) async throws -> String {
let request = SMSChallengeRequest(
phoneNumber: phoneNumber,
userId: userId,
method: "sms"
)
let response = try await apiService.initiateSMSChallenge(request)
return response.challengeId
}
}
Biometric Authentication
iOS Face ID / Touch ID
import LocalAuthentication
class BiometricAuthManager {
enum BiometricType {
case none
case touchID
case faceID
case opticID
}
enum BiometricError: Error {
case notAvailable
case notEnrolled
case lockout
case failed
case cancelled
case fallback
}
func getBiometricType() -> BiometricType {
let context = LAContext()
var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
return .none
}
switch context.biometryType {
case .touchID:
return .touchID
case .faceID:
return .faceID
case .opticID:
return .opticID
default:
return .none
}
}
func authenticateWithBiometrics(reason: String) async throws -> Bool {
let context = LAContext()
// Check if biometric authentication is available
var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
throw BiometricError.notAvailable
}
do {
let result = try await context.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: reason
)
return result
} catch {
throw mapLAError(error)
}
}
func authenticateWithBiometricsOrPasscode(reason: String) async throws -> Bool {
let context = LAContext()
do {
let result = try await context.evaluatePolicy(
.deviceOwnerAuthentication,
localizedReason: reason
)
return result
} catch {
throw mapLAError(error)
}
}
private func mapLAError(_ error: Error) -> BiometricError {
guard let laError = error as? LAError else {
return .failed
}
switch laError.code {
case .biometryNotAvailable:
return .notAvailable
case .biometryNotEnrolled:
return .notEnrolled
case .biometryLockout:
return .lockout
case .userCancel:
return .cancelled
case .userFallback:
return .fallback
default:
return .failed
}
}
}
Android Biometric Authentication
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
class BiometricAuthManager(private val fragmentActivity: FragmentActivity) {
enum class BiometricStatus {
SUCCESS,
HARDWARE_UNAVAILABLE,
FEATURE_UNAVAILABLE,
NONE_ENROLLED,
SECURITY_UPDATE_REQUIRED,
UNSUPPORTED,
STATUS_UNKNOWN
}
fun getBiometricStatus(): BiometricStatus {
val biometricManager = BiometricManager.from(fragmentActivity)
return when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
BiometricManager.BIOMETRIC_SUCCESS -> BiometricStatus.SUCCESS
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> BiometricStatus.HARDWARE_UNAVAILABLE
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> BiometricStatus.FEATURE_UNAVAILABLE
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> BiometricStatus.NONE_ENROLLED
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> BiometricStatus.SECURITY_UPDATE_REQUIRED
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> BiometricStatus.UNSUPPORTED
else -> BiometricStatus.STATUS_UNKNOWN
}
}
fun authenticateWithBiometrics(
title: String,
subtitle: String,
onSuccess: () -> Unit,
onError: (String) -> Unit,
onFailed: () -> Unit
) {
val executor = ContextCompat.getMainExecutor(fragmentActivity)
val biometricPrompt = BiometricPrompt(fragmentActivity, executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
onError(errString.toString())
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
onSuccess()
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
onFailed()
}
}
)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(subtitle)
.setNegativeButtonText("Cancel")
.build()
biometricPrompt.authenticate(promptInfo)
}
// Biometric authentication with cryptographic operations
fun authenticateWithCrypto(
cipher: Cipher,
title: String,
subtitle: String,
onSuccess: (BiometricPrompt.CryptoObject) -> Unit,
onError: (String) -> Unit
) {
val executor = ContextCompat.getMainExecutor(fragmentActivity)
val biometricPrompt = BiometricPrompt(fragmentActivity, executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
onError(errString.toString())
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
result.cryptoObject?.let { onSuccess(it) }
}
}
)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(subtitle)
.setNegativeButtonText("Cancel")
.build()
val cryptoObject = BiometricPrompt.CryptoObject(cipher)
biometricPrompt.authenticate(promptInfo, cryptoObject)
}
}
Session Management
Secure Session Implementation
class SessionManager {
private val secureStorage = SecureStorageManager()
private val sessionTimeout = 30 * 60 * 1000L // 30 minutes
data class Session(
val userId: String,
val accessToken: String,
val refreshToken: String,
val expiresAt: Long,
val lastActivity: Long
)
suspend fun createSession(authTokens: AuthTokens, userId: String): Session {
val session = Session(
userId = userId,
accessToken = authTokens.accessToken,
refreshToken = authTokens.refreshToken,
expiresAt = authTokens.expiresAt,
lastActivity = System.currentTimeMillis()
)
secureStorage.storeSession(session)
return session
}
suspend fun validateSession(): Session? {
val session = secureStorage.getSession() ?: return null
// Check if session is expired
if (System.currentTimeMillis() > session.expiresAt) {
invalidateSession()
return null
}
// Check for inactivity timeout
if (System.currentTimeMillis() - session.lastActivity > sessionTimeout) {
invalidateSession()
return null
}
// Update last activity
val updatedSession = session.copy(lastActivity = System.currentTimeMillis())
secureStorage.storeSession(updatedSession)
return updatedSession
}
suspend fun refreshSession(session: Session): Session? {
return try {
val newTokens = authApiService.refreshToken(session.refreshToken)
val newSession = session.copy(
accessToken = newTokens.accessToken,
expiresAt = newTokens.expiresAt,
lastActivity = System.currentTimeMillis()
)
secureStorage.storeSession(newSession)
newSession
} catch (e: Exception) {
invalidateSession()
null
}
}
suspend fun invalidateSession() {
secureStorage.clearSession()
}
}
Single Sign-On (SSO)
Enterprise SSO Integration
// iOS SSO with SAML/OpenID Connect
class SSOManager {
enum SSOProvider {
case okta
case azureAD
case pingIdentity
case custom(String)
}
func initiateSSO(provider: SSOProvider) async throws -> AuthResult {
let ssoConfig = getSSOConfiguration(for: provider)
let session = ASWebAuthenticationSession(
url: ssoConfig.authURL,
callbackURLScheme: ssoConfig.callbackScheme
) { callbackURL, error in
// Handle SSO callback
}
session.presentationContextProvider = self
session.prefersEphemeralWebBrowserSession = false // Allow SSO cookies
return try await withCheckedThrowingContinuation { continuation in
session.start()
}
}
private func getSSOConfiguration(for provider: SSOProvider) -> SSOConfiguration {
switch provider {
case .okta:
return SSOConfiguration(
authURL: URL(string: "https://company.okta.com/oauth2/v1/authorize")!,
callbackScheme: "com.company.app"
)
case .azureAD:
return SSOConfiguration(
authURL: URL(string: "https://login.microsoftonline.com/tenant/oauth2/v2.0/authorize")!,
callbackScheme: "com.company.app"
)
// Other providers...
default:
fatalError("Unsupported SSO provider")
}
}
}
Device Authentication
Device Fingerprinting
class DeviceAuthenticationManager {
fun generateDeviceFingerprint(): String {
val deviceInfo = collectDeviceInfo()
val fingerprint = hashDeviceInfo(deviceInfo)
return fingerprint
}
private fun collectDeviceInfo(): DeviceInfo {
return DeviceInfo(
androidId = Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID),
model = Build.MODEL,
manufacturer = Build.MANUFACTURER,
osVersion = Build.VERSION.RELEASE,
screenDensity = context.resources.displayMetrics.density.toString(),
timezone = TimeZone.getDefault().id,
language = Locale.getDefault().language
)
}
private fun hashDeviceInfo(deviceInfo: DeviceInfo): String {
val infoString = "${deviceInfo.androidId}-${deviceInfo.model}-${deviceInfo.manufacturer}-${deviceInfo.osVersion}"
return MessageDigest.getInstance("SHA-256")
.digest(infoString.toByteArray())
.joinToString("") { "%02x".format(it) }
}
fun verifyDeviceIntegrity(): Boolean {
// Implement device integrity checks
return checkRootStatus() && checkDebuggerAttachment() && checkEmulatorStatus()
}
private fun checkRootStatus(): Boolean {
// Check for root indicators
val rootIndicators = listOf(
"/system/app/Superuser.apk",
"/sbin/su",
"/system/bin/su",
"/system/xbin/su"
)
return rootIndicators.none { File(it).exists() }
}
}
Security Best Practices
Authentication Flow Security
- Secure Token Storage: Always use platform keychain/keystore
- Token Rotation: Implement automatic token refresh
- Biometric Fallback: Provide passcode fallback options
- Session Timeout: Implement inactivity timeouts
- Device Binding: Bind sessions to device fingerprints
Implementation Checklist
- [ ] OAuth 2.0 with PKCE implemented
- [ ] JWT token validation and refresh
- [ ] Biometric authentication with fallbacks
- [ ] Secure session management
- [ ] Device fingerprinting
- [ ] Multi-factor authentication support
- [ ] SSO integration for enterprise
- [ ] Proper error handling and user feedback
- [ ] Security logging and monitoring
- [ ] Compliance with platform guidelines
Authentication patterns form the foundation of mobile security. Proper implementation requires understanding platform capabilities, security best practices, and user experience considerations to create secure yet usable authentication flows.