Skip to content
Ashish.
All posts
Diagram illustrating the PKCE challenge and verifier mechanism in OAuth 2.0.
6 min readSecurityBackend Developers, Mobile Developers, Security EngineersFeatured#pkce#rfc 7636#oauth 2.0#security#authorization code flow#code challenge#code verifier#s256

RFC 7636: PKCE and the Authorization Code Interception Attack

RFC 7636 defines PKCE to prevent authorization code interception attacks in OAuth 2.0 flows for public clients.

By Ashish KumarPart 2 of OAuth 2.0 RFCs Every Engineer Should Read

OAuth 2.0 is the standard for authorization, but its original specification (RFC 6749) contained a critical flaw when applied to "public clients"—applications that cannot securely store a client secret. This includes Single Page Applications (SPAs) running in browsers and native mobile apps. The flaw allowed attackers to intercept the temporary authorization code and exchange it for a long-lived access token, effectively stealing the user's session. RFC 7636 introduced Proof Key for Code Exchange (PKCE) to close this gap.

Part 2 of the OAuth 2.0 RFCs Every Engineer Should Read series.

The Vulnerability: Interception of the Authorization Code

To understand PKCE, we must first understand the attack vector defined in RFC 6749 Section 4.1. In the standard OAuth 2.0 Authorization Code flow, the user authenticates with the provider (e.g., Google, GitHub) and is redirected back to the client app with a temporary authorization code. The client app then sends this code to the authorization server's token endpoint to receive an access token.

For "confidential clients" (server-side applications), this step is secured by requiring a client_secret. However, public clients cannot keep a secret. They expose their credentials in the client-side code or app bundle. Without a secret, the only thing protecting the authorization code is the secrecy of the code itself during the redirect.

If an attacker can intercept the authorization code—via a malicious app on the same device, a compromised network, or a referrer header leak—they can immediately call the token endpoint. Since the public client doesn't have a secret to prove its identity, the authorization server accepts the code exchange. The attacker now has a valid access token.

The Mechanism: Challenge and Verifier

PKCE solves this by introducing a cryptographic link between the initial authorization request and the final token request. It requires the client to generate two values: a code_verifier and a code_challenge.

  1. Code Verifier: As defined in RFC 7636 Section 4.1, the code_verifier is a high-entropy cryptographic random string. RFC 7636 recommends using 43 to 128 characters from the unreserved URL characters: A-Z, a-z, 0-9, -, ., _, ~.
  2. Code Challenge: As defined in RFC 7636 Section 4.2, the code_challenge is derived from the code_verifier. There are two methods:
    • plain: The code_challenge is identical to the code_verifier. This is simple but less secure if the channel is not encrypted (HTTPS).
    • S256: The code_challenge is the base64url encoding of the SHA256 hash of the code_verifier. This is the recommended method as it provides integrity even if the initial request is somehow observed.

The core mechanism is that the code_challenge is sent in the initial /authorize request. The authorization server stores this challenge associated with the pending authorization request. Later, when the client requests a token, it must provide the original code_verifier. The server then recomputes the challenge from the verifier and compares it to the stored value. If they match, the exchange is valid.

import hashlib
import base64
import secrets
 
def generate_pkce_params():
    # Generate a random code_verifier
    code_verifier = secrets.token_urlsafe(32) # 32 bytes = ~43 chars in base64url
    
    # Compute the S256 code_challenge
    sha256_digest = hashlib.sha256(code_verifier.encode('ascii')).digest()
    code_challenge = base64.urlsafe_b64encode(sha256_digest).decode('ascii').rstrip('=')
    
    return code_verifier, code_challenge

The Flow with PKCE

Let's trace the steps with named actors: Alice (user), MobileApp (public client), and AuthServer (OAuth provider).

  1. Preparation: MobileApp generates code_verifier and code_challenge (S256). It stores the code_verifier locally.
  2. Authorization Request: MobileApp redirects Alice to AuthServer with ?code_challenge=...&code_challenge_method=S256.
  3. User Consent: Alice authenticates and consents. AuthServer stores the code_challenge and generates an authorization code.
  4. Callback: AuthServer redirects Alice back to MobileApp with ?code=<authorization_code>.
  5. Token Request: MobileApp sends a POST to the token endpoint with:
    • grant_type=authorization_code
    • code=<authorization_code>
    • redirect_uri=...
    • code_verifier=<code_verifier>
  6. Validation: AuthServer looks up the pending authorization request, retrieves the stored code_challenge, and computes the SHA256 hash of the received code_verifier. If the hashes match, it issues the access token.

If an attacker intercepted the authorization code in step 4, they would not have the code_verifier. When they try to exchange the code in step 5, the computed challenge will not match the stored one, and the server rejects the request.

Why PKCE is Mandatory for Public Clients

Before RFC 7636, many developers avoided the Authorization Code flow for public clients, opting for the Implicit Flow (which returns tokens directly in the URL fragment). However, the Implicit Flow is now deprecated in OAuth 2.1 (currently an IETF draft) because it exposes tokens in URLs and browser history, creating other security risks.

PKCE allows public clients to use the more secure Authorization Code flow without needing a backend secret. It shifts the security model from "proving identity via a secret" to "proving possession of a transient cryptographic key."

RFC 7636 is not optional for modern public clients. Major providers like Google, Facebook, and Microsoft require PKCE for OAuth 2.0 flows involving public clients. Failure to implement PKCE results in authentication failures or, worse, a false sense of security.

Conclusion

PKCE is a simple yet effective addition to OAuth 2.0. By binding the authorization request to the token request via a challenge/verifier pair, it prevents authorization code interception attacks on public clients. For backend and mobile developers, implementing PKCE is a straightforward change: generate a verifier, send the challenge, and submit the verifier at the token endpoint. It is a critical best practice for securing modern web and mobile applications.

Related posts