OAuth 2.0 PKCE Flow: Secure Token Exchange for SPAs and Mobile Apps
This article explains the PKCE flow for OAuth 2.0 in single page apps and mobile apps, showing JavaScript and Python code to obtain secure tokens.
Focus: OAuth 2.0 PKCE flow
OAuth 2.0 in Single Page Apps and Mobile Apps – PKCE Flow with JavaScript and Python
OAuth 2.0 Basics
OAuth 2.0 is an authorization framework that lets third‑party apps obtain limited access to user resources without handling passwords. The core grant types include:
- Authorization Code – best for server‑side apps.
- Implicit – historically used by SPAs, but now discouraged because the access token is exposed in the browser.
- Resource Owner Password Credentials – not recommended for any client type.
- Client Credentials – for machine‑to‑machine communication.
For Single Page Apps (SPA) and mobile apps, the Authorization Code Grant with PKCE is the recommended approach. It combines the security of the authorization code flow with a proof‑that‑the‑client‑is‑the‑same‑one that initiated the request.
Why PKCE Is Needed for SPAs and Mobile
- No secret storage – SPAs and mobile apps cannot securely store a client secret.
- Public client – the client is visible to the user, making it easy for attackers to capture the authorization code.
- Authorization code interception – without PKCE, an attacker who steals the code can exchange it for tokens.
PKCE (Proof Key for Code Exchange) adds a code verifier (a random string) that the client sends twice: once to obtain the code challenge, and again when exchanging the code for tokens. The authorization server verifies that the challenge matches the verifier, ensuring the code originated from the same public client.
PKCE Flow Overview (###)
- Generate a high‑entropy code verifier (43‑128 characters).
- Derive the code challenge (SHA‑256 of the verifier, base64‑url encoded).
- Redirect the user to the authorization endpoint, including
code_challengeandcode_challenge_method=S256. - User authenticates and the provider redirects back to the app with an authorization code.
- Exchange the code for tokens by POSTing to the token endpoint, including
code_verifier. - Use the access token to call protected APIs; optionally refresh the token.
JavaScript Implementation (##)
Below is a minimal, framework‑agnostic example using the Authorization Code Flow with PKCE. It assumes you control the redirect URI (e.g., https://myapp.com/callback).
// 1. Generate a code verifier and challenge
function base64UrlEncode(arr) {
return btoa(String.fromCharCode(...arr))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
function generateCodeVerifier(length = 64) {
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
return crypto.subtle.digest('SHA-256', data).then(hash => base64UrlEncode(new Uint8Array(hash)));
}
// 2. Redirect user to the authorization server
async function startOAuthFlow() {
const verifier = generateCodeVerifier();
const challenge = await generateCodeChallenge(verifier);
// Store verifier temporarily, e.g., in sessionStorage
sessionStorage.setItem('pkce_verifier', verifier);
const params = new URLSearchParams({
response_type: 'code',
client_id: 'YOUR_CLIENT_ID',
redirect_uri: 'https://myapp.com/callback',
scope: 'openid profile email',
code_challenge: challenge,
code_challenge_method: 'S256',
state: 'random_state_string' // optional but recommended
});
window.location.href = `https://auth.example.com/authorize?${params}`;
}
// 3. Handle the redirect and exchange the code
async function handleCallback() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
const verifier = sessionStorage.getItem('pkce_verifier');
if (!code || !verifier) {
console.error('Missing code or PKCE verifier');
return;
}
// 4. POST to token endpoint
const tokenResponse = await fetch('https://auth.example.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: 'YOUR_CLIENT_ID',
redirect_uri: 'https://myapp.com/callback',
code,
code_verifier: verifier
})
});
const tokens = await tokenResponse.json();
console.log('Access token:', tokens.access_token);
console.log('Refresh token:', tokens.refresh_token);
}
// Start the flow on app load
if (window.location.pathname === '/callback') {
handleCallback();
} else {
document.getElementById('loginBtn').addEventListener('click', startOAuthFlow);
}Key points in the JavaScript code
- Use the Web Crypto API for cryptographic operations; it works in modern browsers without extra libraries.
- Store the
code_verifieronly in memory (e.g.,sessionStorage) until the token exchange finishes. - Always include a
stateparameter to mitigate CSRF attacks and verify it on return.
Python Backend Example (##)
The token exchange can be performed from a Python backend (e.g., Flask) if you need to hide the verifier from the client or want to refresh tokens server‑side.
from flask import Flask, request, redirect, session
import requests
import base64
import hashlib
import secrets
app = Flask(__name__)
app.secret_key = 'super_secret_key'
def code_challenge(verifier: str) -> str:
"""Create the PKCE code challenge (Base64URL‑encoded SHA‑256)."""
sha256 = hashlib.sha256(verifier.encode('utf-8')).digest()
return base64.urlsafe_b64encode(sha256).decode('utf-8').rstrip('=')
@app.route('/login')
def login():
# 1. Generate verifier and challenge
verifier = base64.urlsafe_b64encode(secrets.token_bytes(64)).decode('utf-8').rstrip('=')
session['pkce_verifier'] = verifier
challenge = code_challenge(verifier)
# 2. Redirect to the auth server
auth_url = (
f"https://auth.example.com/authorize?"
f"response_type=code&"
f"client_id=YOUR_CLIENT_ID&"
f"redirect_uri=https://myapp.com/callback&"
f"scope=openid%20profile%20email&"
f"code_challenge={challenge}&"
f"code_challenge_method=S256&"
f"state=random_state"
)
return redirect(auth_url)
@app.route('/callback')
def callback():
# 3. Retrieve the authorization code and verifier
code = request.args.get('code')
state = request.args.get('state')
verifier = session.pop('pkce_verifier', None)
if not code or not verifier:
return "Missing code or PKCE verifier", 400
# 4. Exchange code for tokens
token_resp = requests.post(
'https://auth.example.com/token',
data={
'grant_type': 'authorization_code',
'client_id': 'YOUR_CLIENT_ID',
'redirect_uri': 'https://myapp.com/callback',
'code': code,
'code_verifier': verifier,
},
headers={'Content-Type': 'application/x-www-form-urlencoded'}
)
tokens = token_resp.json()
session['access_token'] = tokens['access_token']
session['refresh_token'] = tokens.get('refresh_token')
return "Tokens received"
if __name__ == '__main__':
app.run(debug=True)What this Python snippet does
- Generates a code verifier and its challenge using the same algorithm as the JavaScript example.
- Stores the verifier in the Flask session (or another secure store) and sends the challenge to the auth server.
- Receives the
codeon the callback, retrieves the verifier, and performs the token request. - Saves the
access_tokenand optionalrefresh_tokenin the session for later API calls.
Best Practices Checklist (##)
- Use the
code_challenge_method=S256– it is mandatory for public clients and provides stronger security. - Generate a cryptographically random verifier (at least 43 characters). Do not reuse or truncate it.
- Validate the
stateparameter on every redirect to prevent CSRF. - Never expose the
code_verifierto the browser after the initial redirect; keep it server‑side or in short‑lived storage. - Prefer the Authorization Code flow over the Implicit flow; the latter leaks the token in the URL fragment.
- Implement token refresh using the
refresh_tokengrant (if the provider supports it) to avoid frequent user re‑authentications. - Store tokens securely – for SPAs, keep them in memory or
sessionStorageand avoidlocalStorageunless you encrypt them. - Always use HTTPS for all redirects and token exchanges.
- Validate the
id_token(if using OpenID Connect) to ensure the token is issued for your client and has not been tampered with. - Handle errors gracefully – network failures, invalid grants, or expired tokens should be caught and shown to the user without leaking internal details.
Summary (##)
OAuth 2.0 with PKCE lets SPAs and mobile apps securely obtain access tokens without a client secret. The flow adds a code verifier that proves the same public client initiated the request, protecting against authorization‑code interception. In JavaScript, use the Web Crypto API to create the verifier and challenge, then exchange the code on the redirect. In Python, a lightweight Flask app can perform the same exchange on the server side, keeping the verifier out of the browser. Follow best practices—use S256, generate strong random values, validate state, and store tokens safely—to build robust, production‑ready authentication experiences.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.