Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
Sponsored Reserved space — layout preview until AdSense is connected

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

Sponsored

Sponsored Reserved space — layout preview until AdSense is connected

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 (###)

  1. Generate a high‑entropy code verifier (43‑128 characters).
  2. Derive the code challenge (SHA‑256 of the verifier, base64‑url encoded).
  3. Redirect the user to the authorization endpoint, including code_challenge and code_challenge_method=S256.
  4. User authenticates and the provider redirects back to the app with an authorization code.
  5. Exchange the code for tokens by POSTing to the token endpoint, including code_verifier.
  6. 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).

Code snippet
// 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_verifier only in memory (e.g., sessionStorage) until the token exchange finishes.
  • Always include a state parameter 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.

Code snippet
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 code on the callback, retrieves the verifier, and performs the token request.
  • Saves the access_token and optional refresh_token in 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 state parameter on every redirect to prevent CSRF.
  • Never expose the code_verifier to 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_token grant (if the provider supports it) to avoid frequent user re‑authentications.
  • Store tokens securely – for SPAs, keep them in memory or sessionStorage and avoid localStorage unless 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.

Sponsored

Sponsored Reserved space — layout preview until AdSense is connected

Sponsored

Sponsored Reserved space — layout preview until AdSense is connected

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.