PKCE for Native Mobile Applications
Implementing OAuth 2.0 Authorization Code + PKCE in iOS and Android native applications with custom URL schemes and universal links.
Detailed Explanation
PKCE for Mobile Apps
Mobile applications are public clients — they cannot securely store a client_secret because the app binary can be decompiled. PKCE is essential for securing the Authorization Code flow on iOS, Android, and other native platforms.
Mobile-Specific Considerations
Custom URL Schemes vs. Universal Links
Mobile apps register a redirect URI to receive the authorization callback:
| Method | Example | Security |
|---|---|---|
| Custom URL scheme | myapp://callback |
Low — any app can register the same scheme |
| Universal Links (iOS) | https://app.example.com/callback |
High — verified domain ownership |
| App Links (Android) | https://app.example.com/callback |
High — verified domain ownership |
Always prefer universal/app links over custom URL schemes. Custom schemes are vulnerable to interception by malicious apps that register the same scheme.
Flow Steps for Mobile
- Generate PKCE pair in the app before starting the flow
- Open system browser (ASWebAuthenticationSession on iOS, Custom Tabs on Android) — do not use embedded WebViews
- User authenticates in the browser
- Receive callback via universal link / custom URL scheme
- Exchange code + code_verifier for tokens
- Store tokens securely (Keychain on iOS, Keystore on Android)
iOS Example (Swift)
import CryptoKit
let verifier = Data(count: 32).map { _ in
UInt8.random(in: 0...255)
}
let codeVerifier = Data(verifier).base64URLEncodedString()
let challenge = SHA256.hash(data: Data(codeVerifier.utf8))
let codeChallenge = Data(challenge).base64URLEncodedString()
Android Example (Kotlin)
val verifier = ByteArray(32).also { SecureRandom().nextBytes(it) }
val codeVerifier = Base64.encodeToString(verifier, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
val digest = MessageDigest.getInstance("SHA-256").digest(codeVerifier.toByteArray())
val codeChallenge = Base64.encodeToString(digest, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
Security Best Practices
- Use the system browser, never an embedded WebView (which can intercept credentials)
- Prefer universal/app links over custom URL schemes
- Store tokens in the platform's secure storage (Keychain/Keystore)
- Implement certificate pinning for the token endpoint
- Use a new PKCE pair for every authorization request
Use Case
An iOS or Android app that authenticates users via an identity provider (e.g., Google Sign-In, Azure AD B2C). The app generates a PKCE pair, opens the system browser for login, receives the authorization code via a universal link, and exchanges it for tokens.