Diagram of OAuth 2.0 PKCE flow showing code verifier, code challenge, authorization request, and token exchange steps

A Developer's Guide to Authorization Code Flow with PKCE

PKCE protects OAuth 2.0 public clients — mobile apps and SPAs — from authorization code interception. Full flow walkthrough, cryptographic detail, and implementation guide for 2026.

Diagram of OAuth 2.0 PKCE flow showing code verifier, code challenge, authorization request, and token exchange steps

Why the OAuth 2.0 PKCE Flow Is Now a Security Baseline

The OAuth 2.0 PKCE flow is a security extension to the standard Authorization Code flow that protects against authorization code interception attacks — and as of the OAuth 2.1 draft specification, it is mandatory for all OAuth clients.

Quick answer: PKCE (Proof Key for Code Exchange, pronounced "pixie") works by binding an authorization request to its token exchange using a one-time cryptographic secret. Here's the core sequence:

  1. The client generates a random code verifier
  2. It hashes that verifier into a code challenge (using SHA-256)
  3. The code challenge is sent with the authorization request
  4. When exchanging the authorization code for a token, the original code verifier is sent
  5. The server verifies the two match — proving the same client initiated both steps

No match, no token. A stolen authorization code is useless without the original verifier.

PKCE was introduced in 2015 via RFC 7636 specifically to protect mobile apps, which can't securely store a client secret. But the threat it addresses — an attacker intercepting an authorization code before the legitimate app can use it — applies to every client type: mobile apps, single-page applications, desktop tools, and even traditional server-side apps.

The stakes are real. Russian threat actors have been observed exploiting OAuth authorization code workflows to hijack Microsoft 365 accounts, using social engineering to capture codes that remain valid for up to 60 days. PKCE alone wouldn't stop every variant of that attack, but it closes the specific gap where a stolen code can be replayed by any party that intercepts it.

This guide covers how PKCE works cryptographically, how to implement it correctly, where developers commonly get it wrong, and why both the OAuth 2.1 specification and the emerging Model Context Protocol for AI agents have made it a non-negotiable baseline.

The Security Gap in Standard Authorization Code Flow

Authorization code interception attack showing malicious app registration of custom URI scheme

To understand why the OAuth 2.0 PKCE flow is necessary, we must first look at the inherent vulnerability of the standard Authorization Code flow on public clients. A public client is any application that runs on a user's device or browser where the source code and assets are accessible to the public. This includes single-page applications (SPAs), mobile apps, and desktop software. Because these apps are distributed to end-users, they cannot securely store a client secret; any secret embedded in their binary or JavaScript bundle can easily be extracted by a motivated attacker.

In a standard Authorization Code flow without PKCE, a public client initiates authentication by redirecting the user to the authorization server. Once the user authenticates, the server redirects back to the client's registered redirect URI with an authorization code in the URL query string. The client is then supposed to take this code and exchange it at the token endpoint for an access token.

However, this architecture opens a major security vulnerability known as the authorization code interception attack.

Custom URI Schemes and the Interception Vector

On mobile and desktop operating systems, native apps often register custom URI schemes (e.g., myapp://oauth-callback) to handle redirect URIs. This is where the security gap widens into a chasm. Operating systems do not always enforce exclusive ownership of custom URI schemes. If a malicious application is installed on the same device, it can register the exact same custom URI scheme.

When the authorization server redirects the user's mobile browser back to myapp://oauth-callback?code=xyz123, the operating system may arbitrarily launch the malicious app instead of the legitimate one. The attacker now has the authorization code.

Because the client is a public client, it does not possess a client secret. Therefore, the authorization server does not require a client secret when exchanging the code at the /token endpoint. The malicious app can simply send the stolen authorization code directly to the token endpoint and receive valid access and refresh tokens.

Preconditions for a Successful Interception Attack

For an attacker to successfully execute this interception exploit, a few conditions must align:

  1. Public Client Architecture: The application cannot securely store a client secret to authenticate itself during the token exchange.
  2. Shared Redirect Handler: The operating system allows multiple applications to register or intercept the redirect URI (such as custom schemes or compromised system loopbacks).
  3. Lack of Cryptographic Binding: The authorization server has no way of verifying that the entity requesting the token is the exact same application instance that initiated the authorization request.

This is not a theoretical threat. Beyond custom URI schemes, attackers frequently target redirect URIs through cross-site scripting (XSS), browser history sniffing, and proxy manipulation. In the broader threat landscape, vulnerabilities in authorization protocols frequently lead to complete account takeovers. Understanding how to protect these channels is a core component of modern security, as detailed in Unlocked's Essential Guide To Auth Protocols Types And Security Best Practices.

What is the OAuth 2.0 PKCE flow?

The Proof Key for Code Exchange (PKCE) extension, defined in RFC 7636 - Proof Key for Code Exchange by OAuth Public Clients, was designed to eliminate authorization code interception attacks by introducing a dynamic, single-use secret generated on the fly for every single authorization request.

Instead of relying on a static client secret (which public clients cannot protect), PKCE forces the client to prove ownership of a dynamically generated cryptographic key before the authorization server will hand over any tokens. This proof-of-possession mechanism ensures that even if an attacker intercepts the authorization code, they cannot exchange it for tokens because they do not possess the dynamic secret that initiated the flow.

Public vs. Confidential Clients: Who Needs PKCE?

While PKCE was originally designed to protect public clients like mobile and browser-based applications, the security industry has shifted toward a universal standard. Today, in June 2026, PKCE is strongly recommended for all client types, including confidential clients (server-side web apps that can securely store client secrets).

For confidential clients, implementing PKCE provides defense-in-depth. It mitigates authorization code injection attacks and CSRF vulnerabilities, ensuring that even if an attacker manages to inject a stolen code into a user’s session, the server-side client will fail the token exchange because the verifier will not match.

Feature / Step Standard Authorization Code Flow OAuth 2.0 PKCE Flow
Primary Target Confidential clients (traditional web apps) Public clients (SPAs, mobile) & modern confidential clients
Client Authentication Requires a static client secret (or private key JWT) Uses a dynamic, per-request cryptographic key pair
Initial Request Parameters client_id, redirect_uri, response_type, scope Adds code_challenge and code_challenge_method
Token Exchange Parameters client_id, client_secret (if confidential), code Adds code_verifier
Interception Protection Vulnerable on public clients (no secret validation) Immune; intercepted codes cannot be exchanged without the verifier
Downgrade Attack Protection None built-in Prevented by enforcing S256 validation on the server

By implementing PKCE, developers move away from static secrets that can leak via source control, configuration errors, or decompilation, adopting a highly resilient, dynamic security posture. This transition mirrors the evolution of broader credential security, which has moved from basic cleartext transmission to robust, zero-knowledge verification frameworks. You can read more about this evolution in Unlocked's article on Understanding Password Authentication Protocols From Pap To Modern Security.

How the PKCE Flow Works Step-by-Step

Step-by-step cryptographic handshake of PKCE flow with code challenge and verifier

The elegance of the OAuth 2.0 PKCE flow lies in its simplicity. It requires no complex key management or PKI infrastructure on the client side. Instead, it relies on basic cryptographic hashing to establish a secure link between the initiation of the flow and the token exchange.

For IT security professionals managing enterprise identity systems, understanding this handshake is critical for verifying that your authorization servers and clients are interacting securely. A complete breakdown of these identity strategies can be found in Unlocked's Authentication Cheat Sheet Modern Security Strategies For It Pros.

Cryptographic Foundations: Code Verifier and Code Challenge

To understand the protocol flow, we must first look at its two core components:

  • code_verifier: This is a high-entropy, cryptographically random string generated locally by the client for every authorization request. According to RFC 7636, it must use only unreserved URL-safe characters ([A-Z], [a-z], [0-9], -, ., _, ~) and have a minimum length of 43 characters and a maximum length of 128 characters. This ensures it has at least 256 bits of entropy, making it practically impossible for an attacker to guess or brute-force.
  • code_challenge: This is the transformed version of the code verifier that is sent to the authorization server.
  • codechallengemethod: This parameter tells the server how the challenge was derived. The specification allows two methods:
    1. S256 (Recommended): The client hashes the code verifier using SHA-256 and then encodes the resulting digest using Base64URL encoding (without padding). $$\text{code_challenge} = \text{BASE64URL-ENCODE}(\text{SHA256}(\text{ASCII}(\text{code_verifier})))$$
    2. plain: The code challenge is simply equal to the code verifier. This method offers virtually no security benefit against eavesdroppers and should only be used in highly constrained environments where SHA-256 is computationally impossible.

For a deeper dive into the cryptographic parameters and specific code examples across different languages, you can consult PKCE (Proof Key for Code Exchange): A Practical Guide for Modern OAuth 2.0.

Step-by-Step Execution of the OAuth 2.0 PKCE flow

Once these cryptographic keys are ready, the execution follows a strict sequence:

Step 1: Generate the Keys

The client application generates a unique, cryptographically secure code_verifier and derives the code_challenge using the S256 method.

Step 2: Send the Authorization Request

The client redirects the user's browser to the authorization server's /authorize endpoint, appending the challenge parameters alongside standard OAuth parameters:

GET /authorize?
response_type=code
&client_id=your_client_id
&redirect_uri=https://app.example.com/callback
&scope=read
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWBuGJSstw-cM
&code_challenge_method=S256 HTTP/1.1
Host: auth.example.com

The authorization server validates the request, authenticates the user, and presents the consent screen.

Step 4: Server Stores the Challenge and Returns the Code

Crucially, the authorization server records the code_challenge and code_challenge_method and associates them directly with the generated authorization code. It then redirects the user back to the client's redirect URI with the short-lived authorization code:

HTTP/1.1 302 Found
Location: https://app.example.com/callback?code=splat987654321

Step 5: Client Initiates the Token Request

The client extracts the authorization code from the redirect URL. It then makes a secure POST request directly to the authorization server's /token endpoint, presenting the authorization code and the original, unhashed code_verifier:

POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&client_id=your_client_id
&code=splat987654321
&redirect_uri=https://app.example.com/callback
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

Step 6: Server Verifies and Issues Tokens

The authorization server retrieves the stored code_challenge associated with the presented authorization code. It then applies the recorded code_challenge_method (S256) to the incoming code_verifier provided by the client.

If the calculated hash matches the stored code_challenge, the server is guaranteed that the client requesting the token is the exact same client instance that initiated the request in Step 2. The server then issues the access and refresh tokens. If they do not match, the request is immediately rejected with an invalid_grant error.

For practical engineering patterns and an analysis of common implementation anti-patterns, developers should refer to OAuth2/OIDC and PKCE done right — Patterns & Anti‑Patterns — Practical Guide.

Implementing PKCE: Best Practices and Modern Standards

As of June 2026, the security landscape has moved firmly toward making PKCE the default standard for all architectures. This is driven by two major developments:

  1. OAuth 2.1: This consolidated specification deprecates insecure grant types (like the Implicit Grant) and elevates PKCE from an optional extension to a mandatory requirement for all clients utilizing the Authorization Code flow.
  2. Model Context Protocol (MCP): The emerging standard for AI agents and Large Language Models (LLMs) accessing third-party APIs has formally adopted OAuth 2.1, making PKCE mandatory to secure automated agent-to-service delegations.

For enterprise IT planners, aligning with these standards is a critical step in maintaining a robust security posture. Guidance on structuring these architectures can be found in Unlocked's guide on The Best Practices For Effective Application Authentication In 2026.

Securing the OAuth 2.0 PKCE flow in Production

Simply turning on PKCE is not enough. To ensure your implementation is secure against sophisticated attack vectors, developers must follow strict operational practices:

  • Enforce S256 Exclusively: Authorization servers must be configured to reject requests utilizing the plain code challenge method. Attackers can easily bypass plain if they can eavesdrop on the initial HTTP request.
  • Prevent Downgrade Attacks: Ensure your authorization server has settings like requireProofKey enabled. If an attacker intercepts a flow and attempts to request a token without a code_verifier (pretending to be a legacy client), the server must reject the exchange.
  • Keep State and Nonce Active: PKCE protects the authorization code, but it is not a replacement for the state parameter (which prevents CSRF) or the OpenID Connect nonce parameter (which prevents ID token replay attacks). Use all three in tandem.
  • Implement Refresh Token Rotation: Because SPAs and mobile apps run in untrusted environments, refresh tokens stored in browser memory or local storage are vulnerable. Implement Refresh Token Rotation, where the authorization server issues a new refresh token with every access token request, immediately invalidating the old one.

For developers working within the Java ecosystem, configuring these policies is straightforward when utilizing modern frameworks, as outlined in the official guide on How-to: Authenticate using a Single Page Application with PKCE :: Spring Authorization Server. Additionally, comparing these configurations against other modern access methodologies is explored in Unlocked's analysis of the Best Application Authentication Methods Of 2026 For Secure Access.

Developer Implementation and Tooling

Developers should rarely write cryptographic challenge generators from scratch in production. Standard, peer-reviewed SDKs and libraries should always be favored to prevent subtle implementation bugs (such as weak random number generators or incorrect Base64URL padding).

For example, in a modern JavaScript/TypeScript SPA, utilizing a standard OIDC client library handles the generation and secure storage of the code_verifier automatically in memory:

import { UserManager } from 'oidc-client-ts';

const userManager = new UserManager({
authority: "https://auth.example.com",
client_id: "spa-client-id",
redirect_uri: "https://app.example.com/callback",
response_type: "code",
scope: "openid profile read",
// The library automatically generates code_verifier and code_challenge with S256
usePkce: true
});

// Initiate login
userManager.signinRedirect();

On the backend, if you are manually verifying a PKCE exchange (for instance, when building a custom identity provider in Go), the cryptographic verification of the verifier against the challenge is highly precise:

package main

import (
"crypto/sha256"
"encoding/base64"
"fmt"
)

// VerifyPKCE validates the code_verifier against the code_challenge using S256
func VerifyPKCE(verifier, challenge string) bool {
// Compute SHA-256 hash of the verifier
hash := sha256.Sum256([]byte(verifier))

// Encode to Base64URL without padding
calculatedChallenge := base64.RawURLEncoding.EncodeToString(hash[:])

// Compare calculated challenge with the stored challenge
return calculatedChallenge == challenge
}

func main() {
verifier := "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
challenge := "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWBuGJSstw-cM"

if VerifyPKCE(verifier, challenge) {
fmt.Println("Verification successful: Tokens can be issued.")
} else {
fmt.Println("Verification failed: Reject exchange.")
}
}

Frequently Asked Questions about PKCE

Do I still need a client secret if I use PKCE?

If your application is a public client (SPA, mobile, desktop), you cannot use a client secret because you cannot secure it. PKCE completely replaces the need for a client secret in these environments.

However, if your application is a confidential client (running on a secure backend server), you should use both a client secret (or private key JWT) and PKCE. This provides defense-in-depth, protecting your server-side application from authorization code injection and session-fixation attacks.

Why is S256 preferred over the plain method?

The plain method simply sets the code_challenge equal to the code_verifier. If an attacker can inspect the initial authorization request (for instance, via a shared proxy or compromised local routing), they will see the cleartext verifier. They can then use that verifier to exchange any intercepted authorization code.

In contrast, the S256 method uses a one-way cryptographic hash (SHA-256). Even if an attacker intercepts the code_challenge from the initial request, they cannot reverse-engineer it to find the original code_verifier. Because the /token endpoint requires the original verifier, the attacker remains locked out.

Is PKCE mandatory in OAuth 2.1?

Yes. The draft OAuth 2.1 specification officially deprecates the Implicit Grant and makes the Authorization Code flow with PKCE mandatory for all clients. This change reflects a decade of security research and real-world breach data, establishing PKCE as the universal baseline for modern web and mobile applications.

Conclusion

The security of modern applications relies on eliminating static vectors of attack. By shifting from static client secrets to dynamic, per-request cryptographic proofs, the OAuth 2.0 PKCE flow eliminates the threat of authorization code interception, securing public and confidential clients alike.

As organizations navigate the complex landscape of identity management, compliance frameworks, and emerging protocols like OAuth 2.1, having a unified security strategy is critical. Platforms like Unlocked and enterprise access solutions like EveryKey help teams bridge the gap between technical protocol standards and real-world, zero-trust implementation. By adopting robust authentication standards, developers can build resilient applications designed to withstand the evolving threat landscape of 2026 and beyond.

For more deep dives into security architectures, browse Unlocked's comprehensive Essential Guide To Auth Protocols Types And Security Best Practices.

Share