Get a Pentest and security assessment of your IT network.

Cyber Security

SPA Authentication: Auth Code Flow with PKCE

TL;DR

This guide shows you how to securely authenticate Single Page Applications (SPAs) using the Authorization Code flow with Proof Key for Code Exchange (PKCE). This is the recommended method as it avoids storing credentials in the browser.

1. Understand the Flow

The Auth Code flow with PKCE involves these steps:

  1. Code Verifier Generation: The SPA generates a random string (the code verifier).
  2. Code Challenge Creation: The SPA creates a hash of the code verifier (the code challenge).
  3. Authorization Request: The SPA redirects the user to the authorization server with the code challenge.
  4. Authentication & Authorization: The user authenticates and authorizes the application on the authorization server.
  5. Redirection with Code: The authorization server redirects back to the SPA with an authorization code.
  6. Token Request: The SPA exchanges the authorization code for access tokens using the code verifier.
  7. Token Response: The authorization server validates the code verifier and returns access tokens (and optionally a refresh token).

2. Code Verifier & Challenge Generation

You’ll need a library to generate cryptographically secure random strings. Here’s an example using JavaScript:

function generateCodeVerifier(length = 128) {
  let result = '';
  const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
  const charactersLength = characters.length;
  for (let i = 0; i < length; i++) {
    result += characters.charAt(Math.floor(Math.random() * charactersLength));
  }
  return result;
}

function generateCodeChallenge(codeVerifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(codeVerifier);
  const hashBuffer = crypto.subtle.digest('SHA256', data);
  return btoa(String.fromCharCode(...new Uint8Array(hashBuffer)));
}

const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);

Store the codeVerifier securely in your SPA’s session storage (not local storage!). The codeChallenge is sent to the authorization server.

3. Constructing the Authorization Request

Build the URL that redirects the user to the authorization server. Include these parameters:

  • response_type: Set to code
  • client_id: Your application’s client ID.
  • redirect_uri: The URL the authorization server redirects back to after authentication.
  • scope: The permissions your application requests (e.g., openid profile email).
  • code_challenge: The generated code challenge.
  • code_challenge_method: Set to S256.

Example:

const authorizationEndpoint = 'https://your-auth-server/authorize';
const redirectUri = 'https://your-spa/callback';
const scope = 'openid profile email';

const authUrl = `${authorizationEndpoint}?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&code_challenge=${codeChallenge}&code_challenge_method=S256`;
window.location.href = authUrl;

4. Handling the Redirect (Callback)

After successful authentication, the authorization server redirects to your redirect_uri with an authorization code in the query parameters.

Extract the code from the URL.

5. Exchanging the Code for Tokens

Make a POST request to the token endpoint of your authorization server. Include these parameters:

  • grant_type: Set to authorization_code
  • code: The authorization code received in the redirect.
  • redirect_uri: Must match the one used in the authorization request.
  • client_id: Your application’s client ID.
  • code_verifier: The original codeVerifier generated in step 2.

Example (using fetch):

const tokenEndpoint = 'https://your-auth-server/token';

const requestBody = {
  grant_type: 'authorization_code',
  code: code,
  redirect_uri: redirectUri,
  client_id: clientId,
  code_verifier: codeVerifier
};

fetch(tokenEndpoint, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(requestBody)
})
.then(response => response.json())
.then(data => {
  // Handle the access tokens (and refresh token if provided)
  const accessToken = data.access_token;
  // Store the access token securely
});

6. Security Considerations

  • Never store credentials in local storage: Use session storage for the codeVerifier and avoid storing sensitive information long-term.
  • HTTPS only: Ensure your SPA and authorization server communicate over HTTPS.
  • Validate redirect URIs: Configure allowed redirect URIs on the authorization server to prevent open redirects.
  • Protect against Cross-Site Request Forgery (CSRF): Implement CSRF protection mechanisms.
Related posts
Cyber Security

Zip Codes & PII: Are They Personal Data?

Cyber Security

Zero-Day Vulnerabilities: User Defence Guide

Cyber Security

Zero Knowledge Voting with Trusted Server

Cyber Security

ZeroNet: 51% Attack Risks & Mitigation