← Back to Blog

Authorization Code Flow with PKCE

oauthpkcesecurity

Authorization Code Flow with PKCE

April 15, 2026
4,123 views
5.0
Paal Gyula
Paal Gyula
gyula@pilab.hu

Deep dive into the OAuth 2.1 recommended flow with Proof Key for Code Exchange


Authorization Code Flow with PKCE

The Authorization Code Flow with PKCE (Proof Key for Code Exchange) is the recommended OAuth 2.1 flow for all client types, including public clients like single-page applications (SPAs) and native mobile apps.

What is PKCE?

PKCE (pronounced "pixy") was originally introduced in RFC 7636 as an extension to OAuth 2.0 to protect authorization code interception attacks. In OAuth 2.1, it becomes mandatory for public clients.

PKCE works by having the client generate a secret verifier at the start of the flow and proving possession of that verifier when exchanging the authorization code for tokens. This prevents attackers from using intercepted authorization codes.

How PKCE Works

The PKCE Flow

Step-by-Step Breakdown

Step 1: Generate Code Verifier and Challenge

The client generates two values:

  • code_verifier: A high-entropy cryptographic random string (43-128 characters)
  • code_challenge: A derived value from the code verifier
javascript
1// Generate code_verifier (43-128 characters from URL-safe alphabet)
2function generateCodeVerifier() {
3  const array = new Uint32Array(56);
4  crypto.getRandomValues(array);
5  return base64URLEncode(array);
6}
7
8// Generate code_challenge from verifier
9async function generateCodeChallenge(verifier) {
10  const encoder = new TextEncoder();
11  const data = encoder.encode(verifier);
12  const digest = await crypto.subtle.digest('SHA-256', data);
13  return base64URLEncode(new Uint8Array(digest));
14}
15
16// URL-safe Base64 encoding
17function base64URLEncode(buffer) {
18  return btoa(String.fromCharCode(...buffer))
19    .replace(/\+/g, '-')
20    .replace(/\//g, '_')
21    .replace(/=+$/, '');
22}

Step 2: Authorization Request

The client redirects the user to the authorization server with the code_challenge and code_challenge_method:

code
1https://auth.example.com/authorize?
2    response_type=code&
3    client_id=s6BhdRkqt3&
4    redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback&
5    scope=openid%20profile%20email&
6    state=af0ifjsldkj&
7    code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
8    code_challenge_method=S256

Key parameters:

  • response_type=code: Requests an authorization code
  • code_challenge: The derived challenge from the verifier
  • code_challenge_method=S256: Uses SHA-256 transformation (recommended)
  • state: CSRF protection token (must be validated on callback)

Step 3: User Authentication and Consent

The authorization server:

  1. Authenticates the user (if not already authenticated)
  2. Displays a consent screen showing what the application is requesting
  3. Records the user's consent decision

Step 4: Authorization Code Callback

After user consent, the authorization server redirects back to the client with:

code
1https://client.example.com/callback?
2    code=SplxlOBeZQQYbYS6WxSbIA&
3    state=af0ifjsldkj

The client MUST:

  1. Validate the state parameter matches the one sent in the authorization request
  2. Check for error parameters
  3. Extract the authorization code

Step 5: Token Exchange

The client exchanges the authorization code for tokens, including the code_verifier:

http
1POST /token HTTP/1.1
2Host: auth.example.com
3Content-Type: application/x-www-form-urlencoded
4Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
5
6grant_type=authorization_code&
7code=SplxlOBeZQQYbYS6WxSbIA&
8redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback&
9client_id=s6BhdRkqt3&
10code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

The authorization server:

  1. Validates the authorization code
  2. Verifies the code_verifier by transforming it and comparing to the stored code_challenge
  3. Validates the redirect URI matches the original request
  4. Issues tokens if all validations pass

Step 6: Token Response

json
1{
2  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
3  "token_type": "Bearer",
4  "expires_in": 3600,
5  "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
6  "scope": "openid profile email"
7}

Security Benefits of PKCE

Protection Against Authorization Code Interception

Without PKCE, if an attacker intercepts the authorization code (through malware, compromised redirect URIs, or network sniffing), they could exchange it for tokens. With PKCE:

Protection Against CSRF Attacks

The state parameter provides CSRF protection by ensuring the authorization response corresponds to a request initiated by the same client.

Protection Against Mixed-Up Clients

PKCE binds the authorization code to the specific client instance that initiated the flow, preventing code injection attacks between different clients.

Implementation Best Practices

Code Verifier Generation

javascript
1// RECOMMENDED: Use cryptographically secure random values
2function generateSecureCodeVerifier() {
3  const length = 128; // Maximum allowed length
4  const charset =
5    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
6  const array = new Uint32Array(length);
7  crypto.getRandomValues(array);
8  return Array.from(array, (num) => charset[num % charset.length]).join('');
9}

State Parameter Management

javascript
1// Generate and store state for CSRF protection
2function generateState() {
3  const state = crypto.randomUUID();
4  sessionStorage.setItem('oauth_state', state);
5  return state;
6}
7
8// Validate state on callback
9function validateState(receivedState) {
10  const storedState = sessionStorage.getItem('oauth_state');
11  sessionStorage.removeItem('oauth_state');
12  return storedState === receivedState;
13}

Token Storage

javascript
1// Store tokens securely
2async function storeTokens(tokenResponse) {
3  // Access token can be in memory for SPAs
4  sessionStorage.setItem('access_token', tokenResponse.access_token);
5
6  // Refresh token should be stored more securely if supported
7  if (tokenResponse.refresh_token) {
8    // Consider using httpOnly cookies for refresh tokens
9    // or secure storage mechanisms
10    localStorage.setItem('refresh_token', tokenResponse.refresh_token);
11  }
12
13  // Calculate expiration time
14  const expiresAt = Date.now() + tokenResponse.expires_in * 1000;
15  sessionStorage.setItem('token_expires_at', expiresAt.toString());
16}

Common Implementation Mistakes

1. Weak Code Verifier Generation

javascript
1// BAD: Using predictable values
2const codeVerifier = 'my-fixed-verifier'; // NEVER DO THIS
3
4// BAD: Using Math.random()
5const codeVerifier = Math.random().toString(36).substring(2); // NOT SECURE
6
7// GOOD: Using crypto.getRandomValues()
8const array = new Uint32Array(56);
9crypto.getRandomValues(array);
10const codeVerifier = base64URLEncode(array);

2. Not Validating State Parameter

javascript
1// BAD: Not checking state parameter
2function handleCallback(code) {
3  // Missing state validation - vulnerable to CSRF
4  exchangeCodeForTokens(code);
5}
6
7// GOOD: Always validate state
8function handleCallback(code, state) {
9  if (!validateState(state)) {
10    throw new Error('Invalid state parameter - possible CSRF attack');
11  }
12  exchangeCodeForTokens(code);
13}

3. Storing Tokens Insecurely

javascript
1// BAD: Storing tokens in URLs or logs
2const url = `https://api.example.com/data?token=${accessToken}`;
3
4// BAD: Storing in localStorage without consideration
5localStorage.setItem('access_token', token);
6
7// BETTER: Use httpOnly cookies for refresh tokens
8// Use memory storage for access tokens in SPAs
9let accessToken = null;
10function setToken(token) {
11  accessToken = token;
12}

Q&A: Authorization Code Flow with PKCE

Q: Why is PKCE mandatory in OAuth 2.1 but was optional in OAuth 2.0?

A: OAuth 2.0 was designed with the assumption that confidential clients could securely store client secrets. However, in practice, many "confidential" clients couldn't actually keep secrets secure (e.g., mobile apps can be reverse-engineered). PKCE provides equivalent security without requiring client secrets, making it suitable for all client types. OAuth 2.1 recognizes this reality and mandates PKCE for all public clients.

Q: Can I use PKCE with confidential clients?

A: Yes, absolutely. While PKCE is mandatory for public clients, confidential clients can also use PKCE in addition to client authentication. This provides defense-in-depth security. Many modern OAuth implementations recommend PKCE for all client types.

Q: What's the difference between S256 and plain code challenge methods?

A: The S256 method applies SHA-256 hashing to the code verifier before Base64URL encoding, while plain uses the verifier directly. OAuth 2.1 requires S256 because it provides better security - even if the code challenge is intercepted, the original verifier cannot be derived from it. The plain method is deprecated and should not be used.

Q: How long should the code verifier be?

A: The OAuth 2.1 specification requires code verifiers to be between 43 and 128 characters. Using the maximum length (128 characters) provides the highest entropy and is recommended for maximum security.

Q: What happens if the code verifier doesn't match during token exchange?

A: The authorization server MUST return an invalid_grant error and MUST NOT issue any tokens. This is the core security mechanism of PKCE - without the correct verifier, the intercepted authorization code is useless.

Q: Can PKCE prevent all types of OAuth attacks?

A: No, PKCE specifically addresses authorization code interception attacks. It does not protect against:

  • Phishing attacks
  • Token theft after issuance
  • Misconfigured redirect URIs
  • Insecure token storage
  • Server-side vulnerabilities

PKCE should be used as part of a comprehensive security strategy that includes proper redirect URI validation, secure token storage, appropriate token lifetimes, and regular security audits.

Q: How does PKCE work with OpenID Connect?

A: PKCE works seamlessly with OpenID Connect. When using the Authorization Code Flow with PKCE for OIDC, you simply add the code_challenge and code_challenge_method parameters to the authorization request and include the code_verifier in the token request. The ID token is returned along with the access token and refresh token.

Q: Should I implement PKCE myself or use a library?

A: Use a well-tested OAuth library whenever possible. Implementing OAuth flows from scratch is error-prone and can introduce security vulnerabilities. Popular libraries like openid-client (Node.js), AppAuth (Android/iOS), and MSAL (Microsoft) have robust PKCE implementations. Only implement PKCE manually if you have specific requirements that existing libraries cannot meet, and ensure thorough security review.

Follow us
All Rights Reserved
© 2011-2026
Progressive Innovation
LAB