OAuth PKCE Authentication
Secure OAuth 2.0 authentication with PKCE for public clients and enhanced security
What is OAuth PKCE?
OAuth 2.0 with PKCE (Proof Key for Code Exchange) is an extension that makes the OAuth flow more secure for public clients like mobile apps, SPAs, and desktop applications. It prevents authorization code interception attacks by introducing a dynamically generated code verifier and challenge.
Mobile Apps
Secure authentication without client secrets
SPAs
Browser-based apps with enhanced security
Zero Trust
No client secrets stored in public code
PKCE Flow Overview
Generate Code Verifier & Challenge
Client creates a random code verifier and derives a challenge using SHA256
Authorization Request
Client redirects to authorization endpoint with code challenge
User Authorization
User authenticates and approves the authorization request
Token Exchange
Client exchanges authorization code + code verifier for tokens
// 1. Generate PKCE parameters
function generatePKCEParams() {
// Generate code verifier (43-128 characters)
const codeVerifier = generateRandomString(128);
// Generate code challenge using SHA256
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await crypto.subtle.digest('SHA-256', data);
// Base64url encode the challenge
const codeChallenge = base64urlEncode(digest);
// Store verifier for later use
sessionStorage.setItem('pkce_verifier', codeVerifier);
return {
codeVerifier,
codeChallenge,
challengeMethod: 'S256'
};
}
// 2. Build authorization URL
async function startOAuthFlow() {
const { codeChallenge, challengeMethod } = await generatePKCEParams();
const params = new URLSearchParams({
client_id: 'your-client-id',
redirect_uri: 'https://app.example.com/callback',
response_type: 'code',
scope: 'openid profile email api.read api.write',
state: generateRandomString(16), // CSRF protection
code_challenge: codeChallenge,
code_challenge_method: challengeMethod,
// Optional parameters
prompt: 'consent', // Force consent screen
max_age: 300, // Require recent authentication
ui_locales: 'en', // Preferred language
});
// Redirect to authorization endpoint
window.location.href = `https://api.parrotrouter.com/oauth/authorize?${params}`;
}
// 3. Handle callback and exchange code for tokens
async function handleCallback() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
// Verify state parameter (CSRF protection)
const savedState = sessionStorage.getItem('oauth_state');
if (state !== savedState) {
throw new Error('Invalid state parameter');
}
// Get stored code verifier
const codeVerifier = sessionStorage.getItem('pkce_verifier');
if (!codeVerifier) {
throw new Error('Missing PKCE code verifier');
}
// Exchange code for tokens
const response = await fetch('https://api.parrotrouter.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
grant_type: 'authorization_code',
client_id: 'your-client-id',
code: code,
redirect_uri: 'https://app.example.com/callback',
code_verifier: codeVerifier, // PKCE verification
}),
});
const tokens = await response.json();
// Clear PKCE data
sessionStorage.removeItem('pkce_verifier');
sessionStorage.removeItem('oauth_state');
// Store tokens securely
await storeTokensSecurely(tokens);
return tokens;
}
// Helper functions
function generateRandomString(length) {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
const values = crypto.getRandomValues(new Uint8Array(length));
return Array.from(values)
.map(x => charset[x % charset.length])
.join('');
}
function base64urlEncode(buffer) {
const base64 = btoa(String.fromCharCode(...new Uint8Array(buffer)));
return base64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
Mobile App Implementation
Implement PKCE in native mobile applications using platform-specific best practices:
iOS (Swift)
import AuthenticationServices
import CryptoKit
class OAuthManager {
private var currentAuthSession: ASWebAuthenticationSession?
private var codeVerifier: String?
func authenticate() {
// Generate PKCE parameters
let (verifier, challenge) = generatePKCEPair()
self.codeVerifier = verifier
// Build authorization URL
var components = URLComponents(string: "https://api.parrotrouter.com/oauth/authorize")!
components.queryItems = [
URLQueryItem(name: "client_id", value: "your-ios-client-id"),
URLQueryItem(name: "redirect_uri", value: "com.example.app://oauth/callback"),
URLQueryItem(name: "response_type", value: "code"),
URLQueryItem(name: "scope", value: "openid profile api.read"),
URLQueryItem(name: "code_challenge", value: challenge),
URLQueryItem(name: "code_challenge_method", value: "S256"),
URLQueryItem(name: "state", value: UUID().uuidString)
]
// Start authentication session
let session = ASWebAuthenticationSession(
url: components.url!,
callbackURLScheme: "com.example.app"
) { callbackURL, error in
guard error == nil, let callbackURL = callbackURL else {
print("Authentication error: \(error?.localizedDescription ?? "Unknown")")
return
}
self.handleCallback(url: callbackURL)
}
session.presentationContextProvider = self
session.prefersEphemeralWebBrowserSession = false // Allow SSO
session.start()
self.currentAuthSession = session
}
private func generatePKCEPair() -> (verifier: String, challenge: String) {
let verifier = generateRandomString(length: 128)
let data = verifier.data(using: .utf8)!
let hash = SHA256.hash(data: data)
let challenge = Data(hash)
.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
return (verifier, challenge)
}
private func handleCallback(url: URL) {
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
guard let code = components?.queryItems?.first(where: { $0.name == "code" })?.value else {
print("No authorization code found")
return
}
// Exchange code for tokens
exchangeCodeForTokens(code: code)
}
private func exchangeCodeForTokens(code: String) {
guard let verifier = self.codeVerifier else { return }
let tokenURL = URL(string: "https://api.parrotrouter.com/oauth/token")!
var request = URLRequest(url: tokenURL)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body = [
"grant_type": "authorization_code",
"client_id": "your-ios-client-id",
"code": code,
"redirect_uri": "com.example.app://oauth/callback",
"code_verifier": verifier
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data else { return }
if let tokens = try? JSONDecoder().decode(TokenResponse.self, from: data) {
// Store tokens in keychain
KeychainHelper.store(tokens: tokens)
DispatchQueue.main.async {
// Navigate to authenticated state
self.onAuthenticationSuccess()
}
}
}.resume()
}
}
Android (Kotlin)
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.Base64
class OAuthManager(private val activity: AppCompatActivity) {
private var codeVerifier: String? = null
companion object {
private const val REDIRECT_URI = "com.example.app://oauth/callback"
private const val AUTH_ENDPOINT = "https://api.parrotrouter.com/oauth/authorize"
private const val TOKEN_ENDPOINT = "https://api.parrotrouter.com/oauth/token"
}
fun startAuthentication() {
// Generate PKCE parameters
val (verifier, challenge) = generatePKCEPair()
codeVerifier = verifier
// Save state for CSRF protection
val state = generateRandomString(16)
saveState(state)
// Build authorization URL
val authUri = Uri.parse(AUTH_ENDPOINT).buildUpon()
.appendQueryParameter("client_id", "your-android-client-id")
.appendQueryParameter("redirect_uri", REDIRECT_URI)
.appendQueryParameter("response_type", "code")
.appendQueryParameter("scope", "openid profile api.read")
.appendQueryParameter("code_challenge", challenge)
.appendQueryParameter("code_challenge_method", "S256")
.appendQueryParameter("state", state)
.build()
// Launch Chrome Custom Tab
val customTabsIntent = CustomTabsIntent.Builder()
.setShowTitle(true)
.setToolbarColor(activity.getColor(R.color.primary))
.build()
customTabsIntent.launchUrl(activity, authUri)
}
fun handleAuthorizationResponse(uri: Uri) {
val code = uri.getQueryParameter("code")
val state = uri.getQueryParameter("state")
// Verify state
if (state != getSavedState()) {
throw SecurityException("Invalid state parameter")
}
code?.let { exchangeCodeForTokens(it) }
}
private fun generatePKCEPair(): Pair<String, String> {
// Generate code verifier
val verifier = generateRandomString(128)
// Generate code challenge
val bytes = verifier.toByteArray(Charsets.US_ASCII)
val digest = MessageDigest.getInstance("SHA-256").digest(bytes)
val challenge = Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(digest)
return Pair(verifier, challenge)
}
private fun exchangeCodeForTokens(code: String) {
val client = OkHttpClient()
val requestBody = FormBody.Builder()
.add("grant_type", "authorization_code")
.add("client_id", "your-android-client-id")
.add("code", code)
.add("redirect_uri", REDIRECT_URI)
.add("code_verifier", codeVerifier ?: "")
.build()
val request = Request.Builder()
.url(TOKEN_ENDPOINT)
.post(requestBody)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
if (response.isSuccessful) {
val tokens = parseTokenResponse(response.body?.string())
// Store tokens securely
TokenManager.storeTokens(tokens)
// Navigate to authenticated state
activity.runOnUiThread {
navigateToMainScreen()
}
}
}
override fun onFailure(call: Call, e: IOException) {
// Handle error
handleAuthError(e)
}
})
}
private fun generateRandomString(length: Int): String {
val charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
val random = SecureRandom()
return (1..length)
.map { charset[random.nextInt(charset.length)] }
.joinToString("")
}
}
SPA Implementation
Implement PKCE in single-page applications with proper security considerations:
import { useEffect, useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
// OAuth configuration
const OAUTH_CONFIG = {
clientId: 'your-spa-client-id',
authEndpoint: 'https://api.parrotrouter.com/oauth/authorize',
tokenEndpoint: 'https://api.parrotrouter.com/oauth/token',
redirectUri: window.location.origin + '/callback',
scopes: ['openid', 'profile', 'email', 'api.read', 'api.write'],
};
// PKCE OAuth Hook
export function useOAuth() {
const navigate = useNavigate();
const location = useLocation();
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [tokens, setTokens] = useState(null);
// Initialize authentication
const login = async () => {
// Generate PKCE parameters
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);
// Generate state for CSRF protection
const state = generateRandomString(16);
// Store in session storage
sessionStorage.setItem('pkce_code_verifier', codeVerifier);
sessionStorage.setItem('oauth_state', state);
// Build authorization URL
const params = new URLSearchParams({
client_id: OAUTH_CONFIG.clientId,
redirect_uri: OAUTH_CONFIG.redirectUri,
response_type: 'code',
scope: OAUTH_CONFIG.scopes.join(' '),
state: state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
// Additional security parameters
nonce: generateRandomString(16), // For ID token validation
response_mode: 'query', // or 'fragment' for implicit flow
// Optional UX parameters
prompt: 'select_account', // Force account selection
display: 'page', // Full page display
login_hint: 'user@example.com', // Pre-fill username
});
window.location.href = `${OAUTH_CONFIG.authEndpoint}?${params}`;
};
// Handle OAuth callback
const handleCallback = async () => {
const params = new URLSearchParams(location.search);
const code = params.get('code');
const state = params.get('state');
const error = params.get('error');
// Handle errors
if (error) {
console.error('OAuth error:', error, params.get('error_description'));
navigate('/login?error=' + error);
return;
}
// Verify state
const savedState = sessionStorage.getItem('oauth_state');
if (!state || state !== savedState) {
console.error('Invalid state parameter');
navigate('/login?error=invalid_state');
return;
}
// Get code verifier
const codeVerifier = sessionStorage.getItem('pkce_code_verifier');
if (!codeVerifier) {
console.error('Missing PKCE code verifier');
navigate('/login?error=missing_verifier');
return;
}
try {
// Exchange code for tokens
const response = await fetch(OAUTH_CONFIG.tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
grant_type: 'authorization_code',
client_id: OAUTH_CONFIG.clientId,
code: code,
redirect_uri: OAUTH_CONFIG.redirectUri,
code_verifier: codeVerifier,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error_description || 'Token exchange failed');
}
const tokens = await response.json();
// Validate ID token (if using OpenID Connect)
if (tokens.id_token) {
const isValid = await validateIdToken(tokens.id_token);
if (!isValid) {
throw new Error('Invalid ID token');
}
}
// Store tokens securely
await storeTokens(tokens);
// Clean up session storage
sessionStorage.removeItem('pkce_code_verifier');
sessionStorage.removeItem('oauth_state');
// Update state
setTokens(tokens);
setIsAuthenticated(true);
// Redirect to original destination or home
const returnTo = sessionStorage.getItem('auth_return_to') || '/';
sessionStorage.removeItem('auth_return_to');
navigate(returnTo);
} catch (error) {
console.error('Token exchange error:', error);
navigate('/login?error=token_exchange_failed');
}
};
// Token refresh
const refreshToken = async () => {
const storedTokens = getStoredTokens();
if (!storedTokens?.refresh_token) {
throw new Error('No refresh token available');
}
const response = await fetch(OAUTH_CONFIG.tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
grant_type: 'refresh_token',
client_id: OAUTH_CONFIG.clientId,
refresh_token: storedTokens.refresh_token,
}),
});
if (!response.ok) {
// Refresh failed, need to re-authenticate
logout();
throw new Error('Token refresh failed');
}
const newTokens = await response.json();
await storeTokens(newTokens);
setTokens(newTokens);
return newTokens;
};
// Logout
const logout = async () => {
// Revoke tokens if supported
const storedTokens = getStoredTokens();
if (storedTokens?.access_token) {
try {
await fetch('https://api.parrotrouter.com/oauth/revoke', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
token: storedTokens.access_token,
token_type_hint: 'access_token',
}),
});
} catch (error) {
console.error('Token revocation failed:', error);
}
}
// Clear stored tokens
clearStoredTokens();
setTokens(null);
setIsAuthenticated(false);
// Redirect to logout endpoint (optional)
const logoutUrl = new URL('https://api.parrotrouter.com/oauth/logout');
logoutUrl.searchParams.set('post_logout_redirect_uri', window.location.origin);
window.location.href = logoutUrl.toString();
};
// Auto-refresh tokens
useEffect(() => {
if (!tokens) return;
const expiresIn = tokens.expires_in * 1000; // Convert to milliseconds
const refreshBefore = 60000; // Refresh 1 minute before expiry
const timeout = setTimeout(() => {
refreshToken().catch(error => {
console.error('Auto-refresh failed:', error);
logout();
});
}, expiresIn - refreshBefore);
return () => clearTimeout(timeout);
}, [tokens]);
// Check authentication status on mount
useEffect(() => {
const storedTokens = getStoredTokens();
if (storedTokens) {
setTokens(storedTokens);
setIsAuthenticated(true);
}
}, []);
// Handle callback route
useEffect(() => {
if (location.pathname === '/callback') {
handleCallback();
}
}, [location]);
return {
isAuthenticated,
tokens,
login,
logout,
refreshToken,
};
}
// Helper functions
function generateCodeVerifier() {
const array = new Uint8Array(64);
crypto.getRandomValues(array);
return base64urlEncode(array);
}
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return base64urlEncode(new Uint8Array(digest));
}
function base64urlEncode(array) {
return btoa(String.fromCharCode.apply(null, array))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// Secure token storage using IndexedDB
async function storeTokens(tokens) {
// Encrypt tokens before storage (implementation depends on your crypto library)
const encryptedTokens = await encryptData(tokens);
// Store in IndexedDB (more secure than localStorage)
const db = await openTokenDB();
const tx = db.transaction(['tokens'], 'readwrite');
await tx.objectStore('tokens').put(encryptedTokens, 'current');
}
// Protected route component
export function ProtectedRoute({ children }) {
const { isAuthenticated } = useOAuth();
const location = useLocation();
useEffect(() => {
if (!isAuthenticated) {
// Save current location for redirect after auth
sessionStorage.setItem('auth_return_to', location.pathname);
}
}, [isAuthenticated, location]);
if (!isAuthenticated) {
return <Navigate to="/login" />;
}
return children;
}
Security Best Practices
PKCE Parameters
- Use cryptographically secure random generators for code verifier (minimum 43 characters)
- Always use SHA256 for code challenge generation (S256 method)
- Never reuse code verifiers across authentication attempts
Token Storage
// Secure token storage strategies
// 1. Memory-only storage (most secure, doesn't survive refresh)
class InMemoryTokenStore {
private tokens: TokenSet | null = null;
store(tokens: TokenSet) {
this.tokens = tokens;
}
get(): TokenSet | null {
return this.tokens;
}
clear() {
this.tokens = null;
}
}
// 2. Encrypted IndexedDB storage
class SecureTokenStore {
private dbName = 'oauth_tokens';
private encryptionKey: CryptoKey;
async initialize() {
// Generate or retrieve encryption key
this.encryptionKey = await this.getOrCreateEncryptionKey();
}
async store(tokens: TokenSet) {
const encrypted = await this.encrypt(tokens);
const db = await this.openDB();
const tx = db.transaction(['tokens'], 'readwrite');
await tx.objectStore('tokens').put({
id: 'current',
data: encrypted,
timestamp: Date.now()
});
}
async get(): Promise<TokenSet | null> {
const db = await this.openDB();
const tx = db.transaction(['tokens'], 'readonly');
const stored = await tx.objectStore('tokens').get('current');
if (!stored) return null;
// Check expiry
const age = Date.now() - stored.timestamp;
if (age > 3600000) { // 1 hour
await this.clear();
return null;
}
return await this.decrypt(stored.data);
}
private async encrypt(data: any): Promise<ArrayBuffer> {
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoded = new TextEncoder().encode(JSON.stringify(data));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
this.encryptionKey,
encoded
);
// Combine IV and encrypted data
const combined = new Uint8Array(iv.length + encrypted.byteLength);
combined.set(iv, 0);
combined.set(new Uint8Array(encrypted), iv.length);
return combined.buffer;
}
}
// 3. Secure cookie storage (server-side)
class CookieTokenStore {
store(tokens: TokenSet) {
// Send tokens to backend for secure cookie storage
fetch('/api/auth/store-tokens', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(tokens)
});
}
async get(): Promise<TokenSet | null> {
// Retrieve tokens from secure HTTP-only cookies
const response = await fetch('/api/auth/get-tokens', {
credentials: 'include'
});
if (!response.ok) return null;
return response.json();
}
}
Additional Security Measures
- 1.State Parameter
Always use a cryptographically random state parameter to prevent CSRF attacks
- 2.Nonce Validation
When using OpenID Connect, validate the nonce in ID tokens
- 3.Token Rotation
Use refresh token rotation for enhanced security
- 4.Redirect URI Validation
Use exact redirect URI matching, never use wildcards
Advanced Features
Silent Authentication
// Silent authentication for SSO
async function silentAuth() {
try {
// Create hidden iframe
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
// Generate PKCE parameters
const { codeVerifier, codeChallenge } = await generatePKCE();
// Build auth URL with prompt=none
const authUrl = new URL(OAUTH_CONFIG.authEndpoint);
authUrl.searchParams.set('client_id', OAUTH_CONFIG.clientId);
authUrl.searchParams.set('redirect_uri', OAUTH_CONFIG.redirectUri);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid profile');
authUrl.searchParams.set('prompt', 'none'); // Silent auth
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
// Load auth URL in iframe
iframe.src = authUrl.toString();
// Wait for response
const code = await waitForAuthCode(iframe, 10000); // 10s timeout
// Exchange code for tokens
const tokens = await exchangeCode(code, codeVerifier);
// Clean up
document.body.removeChild(iframe);
return { success: true, tokens };
} catch (error) {
// Silent auth failed, user needs to log in
return { success: false, error };
}
}
Device Authorization Flow
# Device authorization flow for CLI/TV apps
import requests
import time
from urllib.parse import urlencode
class DeviceAuthFlow:
def __init__(self, client_id):
self.client_id = client_id
self.device_endpoint = "https://api.parrotrouter.com/oauth/device"
self.token_endpoint = "https://api.parrotrouter.com/oauth/token"
def start_device_flow(self):
# Request device code
response = requests.post(self.device_endpoint, data={
'client_id': self.client_id,
'scope': 'openid profile api.read'
})
device_data = response.json()
# Display code to user
print(f"\n=== Device Authorization ===")
print(f"1. Visit: {device_data['verification_uri']}")
print(f"2. Enter code: {device_data['user_code']}")
print(f"\nOr visit: {device_data['verification_uri_complete']}")
print(f"\nWaiting for authorization...")
# Poll for token
return self.poll_for_token(
device_data['device_code'],
device_data['interval']
)
def poll_for_token(self, device_code, interval):
while True:
time.sleep(interval)
response = requests.post(self.token_endpoint, data={
'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
'device_code': device_code,
'client_id': self.client_id
})
if response.status_code == 200:
# Success!
tokens = response.json()
print("\nAuthorization successful!")
return tokens
error = response.json().get('error')
if error == 'authorization_pending':
# Still waiting for user
continue
elif error == 'slow_down':
# Increase polling interval
interval += 5
else:
# Error occurred
raise Exception(f"Device auth failed: {error}")
# Usage
device_auth = DeviceAuthFlow('your-cli-client-id')
tokens = device_auth.start_device_flow()
print(f"Access token: {tokens['access_token'][:20]}...")