Stackvora.tech

OAuth 2.1 + PKCE in plain English

OAuth 2.1, the latest iteration of the Open Authorization protocol, refines the existing OAuth 2.0 specification by removing ambiguous or less secure…

OAuth 2.1, the latest iteration of the Open Authorization protocol, refines the existing OAuth 2.0 specification by removing ambiguous or less secure grant types and tightening security requirements. A key enhancement for public clients, such as Single Page Applications (SPAs) and native mobile/desktop applications, is the mandatory adoption of Proof Key for Code Exchange (PKCE). PKCE effectively mitigates the authorization code interception attack, a significant vulnerability when a confidential client secret cannot be securely stored.

This article details the Authorization Code flow augmented with PKCE, illustrating its mechanics, implementation considerations, and why it's the recommended approach for public clients in OAuth 2.1 environments. We'll cover the roles of each component, the sequence of operations, and practical examples for integration.

Understanding OAuth 2.1 Roles and Terminology

Before diving into PKCE, let's briefly review the core roles within an OAuth 2.1 interaction:

  • Resource Owner: The entity (e.g., a user) who owns the protected resources and can grant access.
  • Client: The application (e.g., SPA, mobile app) requesting access to the resource owner's protected resources. Clients are categorized as:
    • Confidential Client: Capable of securely storing a client secret (e.g., a traditional web application running on a server).
    • Public Client: Incapable of securely storing a client secret (e.g., a browser-based SPA, a native mobile app). PKCE is mandatory for these clients.
  • Authorization Server: The server that authenticates the resource owner and issues access tokens to the client after obtaining authorization.
  • Resource Server: The server hosting the protected resources, capable of accepting and responding to protected resource requests using access tokens.

The Authorization Code Flow with PKCE

The Authorization Code flow is generally considered the most secure OAuth 2.x flow. PKCE (pronounced "pixie") enhances this security for public clients by introducing a dynamic secret for each authorization request, eliminating the need for a pre-registered client secret.

PKCE Mechanism: Code Verifier and Code Challenge

PKCE introduces two new parameters:

  1. code_verifier: A cryptographically random string generated by the client for each authorization request. It must be between 43 and 128 characters long, consisting of alphanumeric characters and the special characters -._~. It's kept secret by the client.
  2. code_challenge: A transformed version of the code_verifier. This challenge is sent to the Authorization Server in the initial authorization request. The transformation method, code_challenge_method, must also be specified.

The most common and recommended code_challenge_method is S256, which involves SHA256 hashing the code_verifier and then base64url-encoding the result. A less secure but still supported method is plain, which simply uses the code_verifier as the code_challenge (not recommended for production). OAuth 2.1 mandates S256 or a stronger method, deprecating plain.

Sequence of Operations

The Authorization Code flow with PKCE proceeds as follows:

  1. Client generates code_verifier: The public client generates a high-entropy code_verifier string.
  2. Client generates code_challenge: The client transforms the code_verifier into a code_challenge using the S256 method.
  3. Client redirects user to Authorization Server: The client initiates the authorization request by redirecting the user's browser to the Authorization Server's authorization endpoint. This request includes:
    • response_type=code
    • client_id (the public client's identifier)
    • redirect_uri (where the Authorization Server sends the authorization code)
    • scope (requested permissions)
    • state (CSRF protection, opaque value)
    • code_challenge (the transformed verifier)
    • code_challenge_method=S256
  4. User grants authorization: The Authorization Server authenticates the user, presents a consent screen (if needed), and upon user approval, records the code_challenge and code_challenge_method associated with this request.
  5. Authorization Server redirects user back to client: The Authorization Server redirects the user's browser back to the specified redirect_uri with an authorization code and the state parameter.
  6. Client exchanges authorization code for tokens: The client receives the authorization code. It then makes a direct back-channel POST request to the Authorization Server's token endpoint to exchange the code for an access_token (and optionally a refresh_token and id_token). This request includes:
    • grant_type=authorization_code
    • client_id
    • code (received from the previous step)
    • redirect_uri (must exactly match the one used in step 3)
    • code_verifier (the original, untransformed secret)
  7. Authorization Server verifies code_verifier: The Authorization Server computes the code_challenge from the received code_verifier using the stored code_challenge_method. If the computed code_challenge matches the one stored in step 4, the authorization is confirmed.
  8. Authorization Server issues tokens: If verification is successful, the Authorization Server issues the access_token (and others) to the client. If verification fails, the request is rejected.
  9. Client accesses resources: The client uses the access_token to make requests to the Resource Server.

Implementation Details and Code Examples

Generating Code Verifier and Challenge (JavaScript Example)

For a browser-based SPA, you'd typically use Web Crypto API for secure random string generation and hashing.


// Function to generate a random string for code_verifier
function generateRandomString(length) {
    const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
    let randomString = '';
    const randomBytes = new Uint8Array(length);
    window.crypto.getRandomValues(randomBytes);
    for (let i = 0; i < length; i++) {
        randomString += charset[randomBytes[i] % charset.length];
    }
    return randomString;
}

// Function to base64url-encode an ArrayBuffer
function base64urlencode(a) {
    let str = "";
    const bytes = new Uint8Array(a);
    const len = bytes.byteLength;
    for (let i = 0; i < len; i++) {
        str += String.fromCharCode(bytes[i]);
    }
    return btoa(str)
        .replace(/\+/g, "-")
        .replace(/\//g, "_")
        .replace(/=+$/, "");
}

// Main function to generate code_verifier and code_challenge
async function generatePkcePair() {
    const code_verifier = generateRandomString(96); // 43-128 chars
    
    // Hash the code_verifier using SHA256
    const encoder = new TextEncoder();
    const data = encoder.encode(code_verifier);
    const digest = await window.crypto.subtle.digest('SHA-256', data);
    
    // Base64url-encode the hash
    const code_challenge = base64urlencode(digest);

    return { code_verifier, code_challenge };
}

// Example usage:
// const { code_verifier, code_challenge } = await generatePkcePair();
// console.log("Code Verifier:", code_verifier);
// console.log("Code Challenge:", code_challenge);

The code_verifier must be securely stored by the client (e.g., in sessionStorage for SPAs, or securely persisted on native apps) until the token exchange step. It must be associated with the specific state parameter to prevent mix-up attacks.

Authorization Request Example

The redirect URL for the authorization endpoint:


GET https://authorization.server.com/oauth/authorize?
  response_type=code&
  client_id=your-spa-client-id&
  redirect_uri=https://your-spa.com/callback&
  scope=openid%20profile%20email&
  state=some_random_state_string&
  code_challenge=y_y788s_..._j92&
  code_challenge_method=S256
  • client_id: Example your-spa-client-id. This is typically a UUID or similar identifier assigned when you register your SPA with the Authorization Server.
  • redirect_uri: Must be pre-registered with the Authorization Server. For SPAs, this is usually an endpoint on your own domain.
  • scope: Standard scopes like openid, profile, email for OpenID Connect, or custom API scopes.
  • state: A value generated by the client to maintain state between the request and the callback, and to prevent CSRF attacks.

Token Exchange Request Example

After receiving the authorization code at the redirect_uri, the client makes a POST request to the token endpoint:


POST https://authorization.server.com/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
client_id=your-spa-client-id&
code=AQB_..._KLA&
redirect_uri=https://your-spa.com/callback&
code_verifier=some_long_random_string_that_was_kept_secret

The Authorization Server will then return a JSON response containing the tokens:


HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
  "access_token": "eyJhbGc...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "AbCdEf...",
  "id_token": "eyJhbGc..."
}

Why PKCE is Essential for Public Clients

The primary attack PKCE defends against is the authorization code interception attack. Without PKCE, if an attacker could intercept the authorization code (e.g., via a compromised browser extension, malicious application on a mobile device, or a misconfigured redirect_uri), they could exchange it for an access_token. For confidential clients, the attacker would still need the client secret. But for public clients, which inherently cannot have a secret, this would be trivial.

PKCE prevents this by requiring the original code_verifier during the token exchange. Even if an attacker intercepts the authorization code, they cannot exchange it for an access_token without also knowing the code_verifier, which was never sent over an insecure channel or directly exposed in the browser's address bar.

OAuth 2.1 Changes and PKCE

OAuth 2.1 formalizes and tightens several aspects of OAuth 2.0. Key changes relevant to PKCE and public clients include:

  • Mandatory PKCE for all public clients: Explicitly requires PKCE for authorization code flow with public clients, making it a first-class citizen of the specification rather than an extension.
  • Deprecation of Implicit Flow: The Implicit Flow (response_type=token) is removed due to its inherent vulnerabilities (e.g., token leakage via browser history, referrer headers). The Authorization Code + PKCE flow is the secure replacement for SPAs.
  • Reinforced Redirect URI validation: Strict comparison of redirect_uri is enforced.
  • Confidential client authentication: Client secrets for confidential clients are typically transmitted via HTTP Basic authentication or as a POST parameter. OAuth 2.1 still supports this for confidential clients.

Common Pitfalls and Troubleshooting

  • Incorrect redirect_uri: This is a common source of errors. The redirect_uri sent in the authorization request must precisely match one pre-registered with the Authorization Server, and also match the one sent in the token exchange request. Even a trailing slash can cause issues.
  • code_verifier not stored/retrieved correctly: If your SPA loses the code_verifier between the authorization request and the token exchange, the token exchange will fail. Ensure it's stored in a mechanism like sessionStorage and retrieved correctly using a key associated with the state parameter.
  • code_challenge_method mismatch: Ensure your client sends code_challenge_method=S256 and the Authorization Server supports it. Avoid plain method as it's less secure and deprecated in OAuth 2.1.
  • Expiration of authorization code: Authorization codes are short-lived, typically expiring within 1-10 minutes. If the token exchange doesn't happen quickly enough, the code will be invalid.
  • Time synchronization issues: While less common, significant clock drift between the client and Authorization Server could theoretically impact cryptographic operations, though PKCE is generally resilient to this as it's a direct comparison.
  • Using client secret for public clients: Public clients should never use a client secret. If your Authorization Server requires it for a public client, it's a misconfiguration or a non-compliant server. PKCE is designed to replace this.
  • Missing state parameter validation: Always validate the state parameter returned by the Authorization Server against the one you sent. This prevents CSRF attacks.

Back to the knowledge base · Ask the AI assistant