Skip to main content
โšก Calmops

OAuth for Mobile Apps: Implementation Guide for iOS and Android

Introduction

Implementing OAuth in mobile apps requires different considerations than web applications. Mobile apps face unique challenges including secure token storage, browser integration, and deep link handling. This guide covers best practices for implementing OAuth in iOS and Android applications.

Mobile OAuth Architecture

Token Flow for Mobile Apps

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                     Mobile OAuth Flow                             โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                  โ”‚
โ”‚  1. App initiates OAuth request                                  โ”‚
โ”‚     - Generate PKCE code_verifier                               โ”‚
โ”‚     - Generate code_challenge                                    โ”‚
โ”‚     - Store code_verifier securely                               โ”‚
โ”‚         โ”‚                                                        โ”‚
โ”‚         โ–ผ                                                        โ”‚
โ”‚  2. Open system browser (or custom tab)                          โ”‚
โ”‚     - Navigate to authorization URL                              โ”‚
โ”‚     - Pass client_id, redirect_uri, code_challenge              โ”‚
โ”‚         โ”‚                                                        โ”‚
โ”‚         โ–ผ                                                        โ”‚
โ”‚  3. User authenticates in browser                                โ”‚
โ”‚     - Provider shows login screen                                โ”‚
โ”‚     - User enters credentials                                    โ”‚
โ”‚     - User authorizes the app                                    โ”‚
โ”‚         โ”‚                                                        โ”‚
โ”‚         โ–ผ                                                        โ”‚
โ”‚  4. Browser redirects to app                                     โ”‚
โ”‚     - Uses custom URL scheme or universal link                  โ”‚
โ”‚     - App receives authorization code                            โ”‚
โ”‚         โ”‚                                                        โ”‚
โ”‚         โ–ผ                                                        โ”‚
โ”‚  5. App exchanges code for tokens                                โ”‚
โ”‚     - Send code + code_verifier to token endpoint               โ”‚
โ”‚     - Receive access_token, refresh_token                       โ”‚
โ”‚         โ”‚                                                        โ”‚
โ”‚         โ–ผ                                                        โ”‚
โ”‚  6. Store tokens securely                                         โ”‚
โ”‚     - iOS: Keychain                                             โ”‚
โ”‚     - Android: EncryptedSharedPreferences                       โ”‚
โ”‚                                                                  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

iOS Implementation with AppAuth

Setup

// Podfile
pod 'AppAuth', '~> 1.6'
pod 'auth0-oidc', '~> 2.0'  // Optional: Auth0 support

AppDelegate Configuration

import AppAuth

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    
    var authState: OIDAuthState?
    
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        
        // Restore previous auth state
        restoreAuthState()
        
        return true
    }
    
    // Handle OAuth callback via custom URL scheme
    func application(
        _ app: UIApplication,
        open url: URL,
        options: [UIApplication.OpenURLOptionsKey : Any] = [:]
    ) -> Bool {
        
        // Check if this is an OAuth callback
        if let authResponse = OIDAuthorizationResponse(
            fromURL: url,
            configuration: OAuthConfig.shared.authConfiguration
        ) {
            authState?.handleAuthorizationCode(
                authResponse.authorizationCode,
                state: authResponse.state
            ) { authState, error in
                if let error = error {
                    print("Auth error: \(error.localizedDescription)")
                    return
                }
                
                // Save auth state
                self.saveAuthState(authState!)
                NotificationCenter.default.post(
                    name: .oauthCallbackReceived,
                    object: nil
                )
            }
            
            return true
        }
        
        return false
    }
    
    // Store auth state
    private func saveAuthState(_ authState: OIDAuthState) {
        let data = try? NSKeyedArchiver.archivedData(
            withRootObject: authState,
            requiringSecureCoding: true
        )
        
        // Store in Keychain
        KeychainHelper.save(
            data: data!,
            service: "com.example.app.authstate"
        )
        
        self.authState = authState
    }
    
    private func restoreAuthState() {
        guard let data = KeychainHelper.load(
            service: "com.example.app.authstate"
        ) else { return }
        
        do {
            authState = try NSKeyedUnarchiver.unarchivedObject(
                ofClass: OIDAuthState.self,
                from: data
            )
        } catch {
            print("Failed to restore auth state: \(error)")
        }
    }
}

OAuth Configuration

// OAuthConfig.swift
import Foundation
import AppAuth

struct OAuthConfig {
    static let shared = OAuthConfig()
    
    // Google OAuth configuration
    let googleClientId = "YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com"
    let googleRedirectScheme = "com.example.app"
    let googleRedirectUri = "com.example.app:/oauth2callback"
    
    // GitHub OAuth configuration
    let githubClientId = "YOUR_GITHUB_CLIENT_ID"
    let githubRedirectScheme = "com.example.app"
    let githubRedirectUri = "com.example.app://oauth/callback"
    
    // Authorization endpoints
    let googleAuthUrl = "https://accounts.google.com/o/oauth2/v2/auth"
    let githubAuthUrl = "https://github.com/login/oauth/authorize"
    
    // Token endpoints
    let googleTokenUrl = "https://oauth2.googleapis.com/token"
    let githubTokenUrl = "https://github.com/login/oauth/access_token"
    
    // Scopes
    let googleScopes = ["openid", "email", "profile"]
    let githubScopes = ["read:user", "user:email"]
    
    lazy var googleConfiguration: OIDServiceConfiguration = {
        OIDServiceConfiguration(
            authorizationEndpoint: URL(string: googleAuthUrl)!,
            tokenEndpoint: URL(string: googleTokenUrl)!
        )
    }()
    
    lazy var githubConfiguration: OIDServiceConfiguration = {
        OIDServiceConfiguration(
            authorizationEndpoint: URL(string: githubAuthUrl)!,
            tokenEndpoint: URL(string: githubTokenUrl)!
        )
    }()
    
    lazy var googleAuthRequest: OIDAuthorizationRequest = {
        OIDAuthorizationRequest(
            configuration: googleConfiguration,
            clientId: googleClientId,
            clientSecret: nil,
            scope: googleScopes.joined(separator: " "),
            redirectUrl: URL(string: googleRedirectUri)!,
            responseType: OIDResponseTypeCode,
            additionalParameters: [
                "code_challenge_method": "S256"
            ]
        )
    }()
}

Keychain Helper

// KeychainHelper.swift
import Foundation
import Security

class KeychainHelper {
    
    static func save(data: Data, service: String, account: String = "default") -> Bool {
        // Delete existing item
        let deleteQuery: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: account
        ]
        SecItemDelete(deleteQuery as CFDictionary)
        
        // Add new item
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: account,
            kSecValueData as String: data,
            kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
        ]
        
        let status = SecItemAdd(query as CFDictionary, nil)
        return status == errSecSuccess
    }
    
    static func load(service: String, account: String = "default") -> Data? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: account,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        
        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        
        return result as? Data
    }
    
    static func delete(service: String, account: String = "default") -> Bool {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: account
        ]
        
        let status = SecItemDelete(query as CFDictionary)
        return status == errSecSuccess || status == errSecItemNotFound
    }
}

Login View Controller

// LoginViewController.swift
import UIKit
import AppAuth

class LoginViewController: UIViewController {
    
    private let authButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("Sign in with Google", for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
        button.backgroundColor = .white
        button.layer.cornerRadius = 8
        button.setTitleColor(.black, for: .normal)
        return button
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    private func setupUI() {
        view.backgroundColor = UIColor(red: 0.07, green: 0.13, blue: 0.26, alpha: 1.0)
        
        // Add subviews
        view.addSubview(authButton)
        
        // Layout
        authButton.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            authButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            authButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            authButton.widthAnchor.constraint(equalToConstant: 240),
            authButton.heightAnchor.constraint(equalToConstant: 50)
        ])
        
        // Add actions
        authButton.addTarget(self, action: #selector(signInWithGoogle), for: .touchUpInside)
    }
    
    @objc private func signInWithGoogle() {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            return
        }
        
        // Generate PKCE
        let codeVerifier = generateCodeVerifier()
        let codeChallenge = generateCodeChallenge(from: codeVerifier)
        
        // Store verifier for token exchange
        UserDefaults.standard.set(codeVerifier, forKey: "code_verifier")
        
        // Create authorization request
        var request = OIDAuthorizationRequest(
            configuration: OAuthConfig.shared.googleConfiguration,
            clientId: OAuthConfig.shared.googleClientId,
            clientSecret: nil,
            scope: OAuthConfig.shared.googleScopes.joined(separator: " "),
            redirectUrl: URL(string: OAuthConfig.shared.googleRedirectUri)!,
            responseType: OIDResponseTypeCode,
            additionalParameters: [
                "code_challenge": codeChallenge,
                "code_challenge_method": "S256"
            ]
        )
        
        // Create auth presentation context
        let presentingViewController = self
        let authViewController = OIDAuthorizationService.present(
            request,
            presenting: presentingViewController
        ) { authResponse, error in
            if let error = error {
                print("Authorization error: \(error.localizedDescription)")
                return
            }
            
            guard let authResponse = authResponse else { return }
            
            // Exchange code for tokens
            self.exchangeCodeForTokens(authResponse: authResponse)
        }
    }
    
    private func exchangeCodeForTokens(authResponse: OIDAuthorizationResponse) {
        let codeVerifier = UserDefaults.standard.string(forKey: "code_verifier")
        
        guard let codeVerifier = codeVerifier else {
            print("Code verifier not found")
            return
        }
        
        // Create token request
        let tokenRequest = authResponse.tokenExchangeRequest(
            withClientId: OAuthConfig.shared.googleClientId,
            codeVerifier: codeVerifier
        )
        
        // Perform token exchange
        OIDAuthorizationService.perform(tokenRequest!) { tokenResponse, error in
            if let error = error {
                print("Token exchange error: \(error.localizedDescription)")
                return
            }
            
            guard let tokenResponse = tokenResponse else { return }
            
            // Store tokens
            self.storeTokens(tokenResponse)
            
            // Navigate to main screen
            DispatchQueue.main.async {
                self.navigateToMain()
            }
        }
    }
    
    private func storeTokens(_ response: OIDTokenResponse) {
        // Access token
        if let accessToken = response.accessToken {
            KeychainHelper.save(
                data: Data(accessToken.utf8),
                service: "com.example.app",
                account: "access_token"
            )
        }
        
        // Refresh token
        if let refreshToken = response.refreshToken {
            KeychainHelper.save(
                data: Data(refreshToken.utf8),
                service: "com.example.app",
                account: "refresh_token"
            )
        }
        
        // Expiry
        if let expiry = response.accessTokenExpirationDate {
            UserDefaults.standard.set(expiry, forKey: "token_expiry")
        }
    }
    
    private func navigateToMain() {
        let mainVC = MainViewController()
        let navController = UINavigationController(rootViewController: mainVC)
        navController.modalPresentationStyle = .fullScreen
        
        if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
           let window = windowScene.windows.first {
            window.rootViewController = navController
            window.makeKeyAndVisible()
        }
    }
    
    // PKCE helpers
    private func generateCodeVerifier() -> String {
        var buffer = [UInt8](repeating: 0, count: 32)
        _ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer)
        return Data(buffer).base64EncodedString()
            .replacingOccurrences(of: "+", with: "-")
            .replacingOccurrences(of: "/", with: "_")
            .replacingOccurrences(of: "=", with: "")
    }
    
    private func generateCodeChallenge(from verifier: String) -> String {
        guard let data = verifier.data(using: .utf8) else { return "" }
        var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
        data.withUnsafeBytes {
            _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
        }
        return Data(hash).base64EncodedString()
            .replacingOccurrences(of: "+", with: "-")
            .replacingOccurrences(of: "/", with: "_")
            .replacingOccurrences(of: "=", with: "")
    }
}

Android Implementation

Dependencies

// build.gradle
dependencies {
    implementation 'net.openid:appauth:0.11.1'
    implementation 'androidx.browser:browser:1.7.0'
    implementation 'androidx.security:security-crypto:1.1.0-alpha06'
}

Android Manifest

<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.app">

    <!-- Custom URL scheme for OAuth callback -->
    <activity
        android:name=".MainActivity"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
        
        <!-- OAuth callback handling -->
        <intent-filter>
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
            <data android:scheme="com.example.app" />
        </intent-filter>
    </activity>
</manifest>

Token Management

// TokenManager.kt
package com.example.app

import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import net.openid.appauth.TokenRequest
import net.openid.appauth.TokenResponse

class TokenManager(context: Context) {
    
    private val masterKey = MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build()
    
    private val securePrefs: SharedPreferences = EncryptedSharedPreferences.create(
        context,
        "secure_prefs",
        masterKey,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
    )
    
    companion object {
        private const val KEY_ACCESS_TOKEN = "access_token"
        private const val KEY_REFRESH_TOKEN = "refresh_token"
        private const val KEY_EXPIRY = "token_expiry"
        private const val KEY_ID_TOKEN = "id_token"
    }
    
    fun saveTokens(response: TokenResponse) {
        response.accessToken?.let { token ->
            securePrefs.edit()
                .putString(KEY_ACCESS_TOKEN, token)
                .apply()
        }
        
        response.refreshToken?.let { token ->
            securePrefs.edit()
                .putString(KEY_REFRESH_TOKEN, token)
                .apply()
        }
        
        response.idToken?.let { token ->
            securePrefs.edit()
                .putString(KEY_ID_TOKEN, token)
                .apply()
        }
        
        response.accessTokenExpirationTime?.let { expiry ->
            securePrefs.edit()
                .putLong(KEY_EXPIRY, expiry)
                .apply()
        }
    }
    
    fun getAccessToken(): String? {
        return securePrefs.getString(KEY_ACCESS_TOKEN, null)
    }
    
    fun getRefreshToken(): String? {
        return securePrefs.getString(KEY_REFRESH_TOKEN, null)
    }
    
    fun isTokenValid(): Boolean {
        val expiry = securePrefs.getLong(KEY_EXPIRY, 0)
        return System.currentTimeMillis() < expiry
    }
    
    fun clearTokens() {
        securePrefs.edit().clear().apply()
    }
}

OAuth Activity

// OAuthActivity.kt
package com.example.app

import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import net.openid.appauth.*
import androidx.browser.customtabs.CustomTabsIntent

class OAuthActivity : AppCompatActivity() {
    
    private lateinit var authService: AuthorizationService
    private lateinit var tokenManager: TokenManager
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        authService = AuthorizationService(this)
        tokenManager = TokenManager(this)
        
        handleOAuthCallback(intent)
    }
    
    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        intent?.let { handleOAuthCallback(it) }
    }
    
    private fun handleOAuthCallback(intent: Intent) {
        val response = AuthorizationResponse.fromIntent(intent)
        val error = AuthorizationException.fromIntent(intent)
        
        when {
            response != null -> {
                exchangeCodeForTokens(response)
            }
            error != null -> {
                Log.e("OAuth", "Authorization failed: ${error.error}")
                finish()
            }
           private fun exchange }
    }
    
CodeForTokens(response: AuthorizationResponse) {
        val additionalParams = mutableMapOf<String, String>()
        
        // Add PKCE code verifier
        val codeVerifier = getSharedPreferences("oauth", MODE_PRIVATE)
            .getString("code_verifier", null)
        
        codeVerifier?.let {
            additionalParams["code_verifier"] = it
        }
        
        val tokenRequest = response.createTokenExchangeRequest(additionalParams)
        
        authService.performTokenRequest(tokenRequest) { response, error ->
            when {
                response != null -> {
                    tokenManager.saveTokens(response)
                    Log.d("OAuth", "Tokens saved successfully")
                    
                    // Navigate to main screen
                    startActivity(Intent(this, MainActivity::class.java))
                    finish()
                }
                error != null -> {
                    Log.e("OAuth", "Token exchange failed: ${error.errorDescription}")
                    finish()
                }
            }
        }
    }
    
    companion object {
        // Start OAuth flow
        fun startOAuth(context: android.content.Context) {
            val authService = AuthorizationService(context)
            
            // Generate PKCE
            val codeVerifier = generateCodeVerifier()
            val codeChallenge = generateCodeChallenge(codeVerifier)
            
            // Save verifier for callback
            context.getSharedPreferences("oauth", android.content.Context.MODE_PRIVATE)
                .edit()
                .putString("code_verifier", codeVerifier)
                .apply()
            
            // Build authorization request
            val request = AuthorizationRequest.Builder(
                AuthorizationServiceConfiguration(
                    Uri.parse("https://accounts.google.com/o/oauth2/v2/auth"),
                    Uri.parse("https://oauth2.googleapis.com/token")
                ),
                "YOUR_CLIENT_ID.apps.googleusercontent.com",
                ResponseTypeValues.CODE,
                Uri.parse("com.example.app:/oauth2callback")
            )
                .setScope("openid email profile")
                .setAdditionalParameters(
                    mapOf(
                        "code_challenge" to codeChallenge,
                        "code_challenge_method" to "S256"
                    )
                )
                .build()
            
            // Open custom tab
            val customTabsIntent = CustomTabsIntent.Builder()
                .setShowTitle(true)
                .build()
            
            authService.authorize(request) { intent, error ->
                if (intent != null) {
                    customTabsIntent.launchUrl(context, intent.toUri(Intent.URI_INTENT_SCHEME))
                } else {
                    Log.e("OAuth", "Failed to create authorization intent: ${error?.errorDescription}")
                }
            }
        }
        
        private fun generateCodeVerifier(): String {
            val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
            return (1..32)
                .map { chars.random() }
                .joinToString("")
        }
        
        private fun generateCodeChallenge(verifier: String): String {
            val bytes = verifier.toByteArray()
            val digest = java.security.MessageDigest.getInstance("SHA-256")
            val hash = digest.digest(bytes)
            return android.util.Base64.encodeToString(
                hash,
                android.util.Base64.NO_WRAP or android.util.Base64.URL_SAFE
            ).replace("=", "")
        }
    }
}

Token Refresh

// iOS Token Refresh
class TokenManager {
    // ... existing code ...
    
    func refreshAccessToken(completion: @escaping (Result<String, Error>) -> Void) {
        guard let refreshToken = getRefreshToken() else {
            completion(.failure(AuthError.noRefreshToken))
            return
        }
        
        // Create refresh request
        guard let config = authState?.authorizationServiceConfiguration else {
            completion(.failure(AuthError.noConfiguration))
            return
        }
        
        let request = OIDTokenRequest(
            configuration: config,
            grantType: OIDGrantType.refreshToken,
            authorizationCode: nil,
            clientId: OAuthConfig.shared.googleClientId,
            clientSecret: nil,
            redirectURL: nil,
            scopes: nil,
            refreshToken: refreshToken,
            codeVerifier: nil,
            additionalParameters: nil
        )
        
        authState?.performTokenRequest(request) { response, error in
            if let error = error {
                completion(.failure(error))
                return
            }
            
            guard let accessToken = response?.accessToken else {
                completion(.failure(AuthError.noAccessToken))
                return
            }
            
            // Update stored tokens
            self.saveTokens(response!)
            
            completion(.success(accessToken))
        }
    }
}

// Using token manager with URLSession
func authenticatedRequest(_ request: URLRequest) async throws -> Data {
    // Check if token is expired or close to expiring
    if !tokenManager.isTokenValid() {
        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
            tokenManager.refreshAccessToken { result in
                switch result {
                case .success:
                    continuation.resume()
                case .failure(let error):
                    continuation.resume(throwing: error)
                }
            }
        }
    }
    
    var authenticatedRequest = request
    authenticatedRequest.setValue(
        "Bearer \(tokenManager.getAccessToken())",
        forHTTPHeaderField: "Authorization"
    )
    
    return try await URLSession.shared.data(for: authenticatedRequest)
}
// Android Token Refresh
suspend fun refreshToken(): String? {
    val refreshToken = tokenManager.getRefreshToken() ?: return null
    
    return withContext(Dispatchers.IO) {
        val request = TokenRequest.Builder(
            AuthorizationServiceConfiguration(
                Uri.parse("https://oauth2.googleapis.com/token"),
                Uri.parse("https://oauth2.googleapis.com/token")
            ),
            "YOUR_CLIENT_ID.apps.googleusercontent.com"
        )
            .setGrantType(GrantTypeValues.REFRESH_TOKEN)
            .setRefreshToken(refreshToken)
            .build()
        
        try {
            val response = authService.performTokenRequest(request).await()
            tokenManager.saveTokens(response)
            response.accessToken
        } catch (e: Exception) {
            Log.e("Token", "Refresh failed: ${e.message}")
            tokenManager.clearTokens()
            null
        }
    }
}
// Associated Domains (Entitlements)
{
    "com.apple.developer.associated-domains": [
        "oauth:example.com",
        "applinks:example.com"
    ]
}

// Handle universal link in AppDelegate
func application(
    _ application: UIApplication,
    continue userActivity: NSUserActivity,
    restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
    
    if userActivity.activityType == NSUserActivityTypeBrowsingWeb,
       let url = userActivity.webPageURL {
        
        // Handle OAuth callback via universal link
        if url.path.contains("/oauth/callback") {
            // Process OAuth response
            handleOAuthCallback(url: url)
            return true
        }
    }
    
    return false
}
<!-- AndroidManifest.xml -->
<application>
    <activity android:name=".MainActivity">
        <intent-filter android:autoVerify="true">
            <action android:name="android.intent.action.VIEW" />
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
            <data
                android:scheme="https"
                android:host="example.com"
                android:pathPrefix="/oauth/callback" />
        </intent-filter>
    </activity>
</application>

Security Best Practices for Mobile

// iOS Security Checklist
mobile_oauth_security = [
    // Store tokens securely
    "Use Keychain for token storage",
    "Set appropriate access control (kSecAttrAccessibleWhenUnlockedThisDeviceOnly)",
    
    // PKCE is mandatory
    "Always use PKCE for authorization code exchange",
    "Generate cryptographically random code verifier",
    
    // Certificate pinning
    "Implement TLS certificate pinning for token endpoints",
    
    // Deep link security
    "Validate redirect URI matches exactly",
    "Avoid using data URLs for redirect URIs",
    
    // Biometric authentication
    "Consider adding biometric for sensitive operations",
    
    // Token handling
    "Use short-lived access tokens",
    "Implement proper token refresh logic",
    "Clear tokens on logout"
]
// Android Security Checklist
android_oauth_security = [
    // Encrypted storage
    "Use EncryptedSharedPreferences for tokens",
    "Use MasterKey with AES256-GCM",
    
    // PKCE
    "Always use PKCE",
    "Generate secure code verifier using SecureRandom",
    
    // ProGuard/R8
    "Obfuscate OAuth library code",
    "Keep AppAuth classes",
    
    // Custom tabs
    "Use CustomTabsIntent for OAuth flow",
    "Verify browser is not malicious",
    
    // App Links
    "Implement App Links for secure deep linking",
    "Verify links in Digital Asset Links"
]

Conclusion

Mobile OAuth implementation requires:

  1. Use AppAuth libraries - Battle-tested implementations for iOS/Android
  2. Implement PKCE - Mandatory for mobile OAuth flows
  3. Secure token storage - Keychain (iOS), EncryptedSharedPreferences (Android)
  4. Handle deep links - Custom URL schemes or universal/app links
  5. Token management - Automatic refresh, proper invalidation

Following these patterns ensures secure, production-ready OAuth integration in mobile applications.

Resources

Comments