MCP Auth is here
Drop-in OAuth for your MCP Servers
Learn more

What is PKCE? A Developer’s Guide to Secure OAuth Flows

Srinivas Karre
Founding Engineer

OAuth 2.0 is the industry-standard protocol for authorization, enabling apps to access APIs without exposing user credentials. But while the spec is flexible, its original design had a key assumption: the client application can securely store secrets. This assumption falls apart for mobile apps, single-page applications (SPAs), and other public clients where everything, including source code and request data, is exposed.

To fix this, the OAuth community introduced PKCE (Proof Key for Code Exchange), a mechanism that lets public clients use the Authorization Code flow securely, without needing a client secret. PKCE is not an extension anymore; it’s mandatory for public clients and a core part of OAuth 2.1.

This guide breaks down why PKCE exists, how it works, and why it’s a must-have in modern auth flows, especially for apps that run entirely in browsers or on user-controlled devices.

Interactions between Client app, authorization server

Why does PKCE even exist?

PKCE wasn’t created to add friction; it was introduced to solve a real, critical security problem: securing the Authorization Code flow for clients that can’t hold secrets. With OAuth 2.1, PKCE is no longer optional. It’s now mandatory for all public clients. This shift reflects its importance in closing a critical security gap, and it emphasizes the need for developers to treat PKCE not as a bonus, but as a baseline.

OAuth 2.0 was originally designed around confidential clients, think server-side apps, that run in controlled environments. These clients are issued a client_secret during registration and use it to authenticate themselves when exchanging authorization codes for access tokens. This secret was the backbone of client verification.

But public clients like:

  • Mobile apps (e.g., Android or iOS apps)
  • SPAs running in a user’s browser
  • CLI tools or desktop apps

These have no safe place to store a client secret. Their environments are fully exposed to the user (and any attacker on the device or network). If a malicious party gets access to the authorization code, they can redeem it without needing anything else.

PKCE, a secure, secret-less mechanism that protects public clients from authorization code interception attacks, without depending on the traditional client secret model.

Vulnerabilities in traditional authorization code flow

Risks in public client contexts: Mobile apps and SPAs

In traditional OAuth 2.0, the authorization code is passed back to the client via a browser redirect. In a confidential client, this is safe because the client needs to prove its identity (with a secret) to redeem the code. But in public clients, there’s no proof step.

So if an attacker:

  • Is running a malicious app on the same device
  • Has access to browser session storage or intercepts the redirect URI
  • Or is listening on an open Wi-Fi network

They can grab that authorization code and use it to get tokens on behalf of the user.

Code interception attacks in browser-based apps

In SPAs, this threat is amplified. The browser:

  • Executes JavaScript from various origins
  • May have third-party extensions or scripts with elevated access
  • Is vulnerable to open redirect misconfigurations or iframe hijacking

Without PKCE, the flow is:

  1. App sends the user to the authorization server
  2. User authenticates and grants access
  3. Authorization server redirects back with an authorization code
  4. App exchanges the code for tokens

If the code is intercepted at Step 3, it’s game over. The attacker can skip the rest and impersonate the app.

Replay attacks

Even if an attacker captures an authorization code, they can’t reuse it if the server expects a specific verifier. Without PKCE, some authorization servers didn’t enforce single-use codes or lacked strict expiry rules, making it easier to replay a valid authorization code within a short time window.

PKCE prevents this by making every authorization code bound to a unique verifier and ensuring it can only be exchanged once. Replaying the same code with a different verifier will fail.

Why storing client secrets is impossible for public clients

Let’s be blunt, if your app runs in an environment where users (or attackers) can inspect your code, you don’t have a secret. You might hardcode a client_secret, obfuscate it, or store it in local storage, but that’s security theater.

Here’s why client secrets are fundamentally insecure in public client environments like mobile apps, SPAs, and desktop tools:

The compiled app can be reverse-engineered

In native apps, secrets are often baked into the code. But with tools like apktool, Frida, or IDA Pro, extracting hard coded values becomes trivial. Even "encrypted" secrets can be decrypted if the decryption key is also in the app bundle.

The browser exposes everything

In SPAs, there’s simply no hiding anything. The JavaScript is loaded directly into the browser. Any user/attacker can open DevTools and see network requests, local storage, and source maps. If a client_secret is used here, it's basically public knowledge.

Users can modify runtime behavior

Modern tools allow live patching and runtime hooking, meaning even if the secret isn’t immediately visible, attackers can change the app’s behavior to log or leak it. Mobile debugging tools or browser extensions can intercept requests and expose any headers or tokens.

Even secure storage isn’t enough

You might think storing secrets in places like iOS’s Keychain or Android’s Keystore helps. It’s better than nothing, but not bulletproof. On rooted or jailbroken devices, even secure storage becomes accessible. Plus, malicious apps can often escalate privileges or piggyback on misconfigured entitlements.

How does PKCE work?

PKCE (Proof Key for Code Exchange) enhances the traditional Authorization Code flow by adding a dynamic, one-time-use secret. Originally introduced as an extension in OAuth 2.0, it became a default security mechanism in OAuth 2.1. Unlike client secrets used by confidential clients, PKCE works for public clients that cannot keep credentials secure.

In OAuth 2.0, PKCE was optional and primarily targeted at mobile use cases. But as browser-based and CLI apps became more common, OAuth 2.1 moved to make PKCE the default for all authorization code flows, eliminating the need for client secrets in public client contexts.

Code verifier and code challenge

What is the code verifier?
The code verifier is a high-entropy, cryptographically random string created by the client at the beginning of the OAuth flow. It serves as a one-time-use secret that ties the initial authorization request to the token exchange.

Transforming it into the code challenge (S256 method)
To protect the code verifier from being exposed during transmission, it is hashed using the SHA-256 algorithm to produce the code challenge. This challenge is then base64url-encoded and sent in the initial authorization request.

Why S256 is preferred over plain:
The plain method sends the verifier as-is in the authorization request. If an attacker is able to intercept the request or read the URL, they gain access to the full verifier and can impersonate the client. S256 improves security by sending only the hashed version (the challenge), so even if it’s intercepted, the actual verifier remains safe. It also ensures the code challenge cannot be reversed back into the original verifier.

Feature
The plain method
S256 (Recommended)
Transformation
None (sends the code verifier as-is)
SHA-256 hash of the code verifier
Security
Weak (code verifier exposed in transmission)
Strong (verifier is hashed, not exposed)
Protection against interception
Minimal
Strong defense against code leakage
Support in authorization servers
Optional
Widely supported and default in OAuth 2.1
Ease of implementation
Simple, but insecure
Slightly more complex, but secure
OAuth 2.1 recommendation
Not recommended unless explicitly required
Mandatory for public clients

How both are used across the OAuth transaction

  • During the initial request, the client sends the code challenge and method (S256) to the authorization server.
  • When redeeming the authorization code for tokens, the client must send the original code verifier.
  • The server verifies that the code verifier, when hashed using the declared method, matches the original challenge. If it doesn’t, the request is rejected.

End-to-end PKCE flow

End to end PKCE flow with client app, authorization server, resource server, tokens, and endpoints

Here’s how PKCE fits into the full OAuth Authorization Code flow.

Step-by-step walkthrough:

1. Generate code verifier and challenge

The client generates a random string (the code verifier), hashes it using SHA-256, and encodes it to form the code challenge.

2. Initiate auth request with code challenge

The client redirects the user to the authorization endpoint with these parameters:

  • code_challenge: the hashed string
  • code_challenge_method: S256
  • client_id, redirect_uri, response_type, scope, etc.

3. Receive auth code

After the user grants consent, the authorization server redirects the browser to the client’s redirect URI with an authorization code.

4. Redeem token using code verifier

The client sends a POST request to the token endpoint with:

  • code: the authorization code
  • code_verifier: the original random string
  • grant_type, client_id, redirect_uri, etc.

The server recomputes the hash and checks it against the original challenge. If they match, it issues the token.

Example implementation

Minimal client setup with curl or Node.js

To implement PKCE, you need to generate a code verifier and its corresponding code challenge. Here’s how you can do it in Node.js:
Node.js version >= 16.0.0 is required to use base64url encoding.

const crypto = require('crypto'); const codeVerifier = crypto.randomBytes(64).toString('base64url'); const codeChallenge = crypto .createHash('sha256') .update(codeVerifier) .digest('base64url'); console.log({ codeVerifier, codeChallenge });

After generating these, start the OAuth authorization request by including the code challenge and method

https://auth.example.com/oauth/authorize? response_type=code& client_id=your-client-id& redirect_uri=http://localhost/callback& code_challenge=CODE_CHALLENGE& code_challenge_method=S256& scope=openid profile email

Once the user approves, the server redirects back with an authorization code. Exchange this code for an access token by sending the original code verifier in the token request:

curl -X POST https://auth.example.com/oauth/token \ -d grant_type=authorization_code \ -d client_id=your-client-id \ -d code=RECEIVED_AUTH_CODE \ -d redirect_uri=http://localhost/callback \ -d code_verifier=ORIGINAL_CODE_VERIFIER

This proves to the server you possess the original verifier, enabling a secure token exchange without needing a client secret.

Authorization server perspective

On the server side, the authorization server plays a crucial role in verifying the integrity of the PKCE flow. It performs several key validation steps during the token exchange to ensure the request wasn't tampered with:

1. Storing the code challenge and method

When the client initiates the authorization request, it sends a code_challenge along with a code_challenge_method (typically S256).

The server must store both values temporarily because they are required to validate the token request that comes later. Without them, the server would have no way to confirm whether the incoming code_verifier matches the original challenge.

2. Validating during token exchange
When the client attempts to exchange the authorization code for an access token, it includes the code_verifier.
The server then:

  • Recalculates the code_challenge from the provided code_verifier using the stored code_challenge_method
  • Compares this recalculated value with the originally stored code_challenge

3. Decision

  • If the values match, the server issues the access token.
  • If they don’t match, the server rejects the request — this helps prevent attacks where an attacker tries to intercept the code and reuse it from a different client.
const expectedChallenge = base64url(crypto.createHash('sha256').update(codeVerifier).digest()); if (expectedChallenge !== storedChallenge) { throw new Error('PKCE code_verifier mismatch'); }

During token exchange, server logs might show:

[PKCE] Received method: S256 [PKCE] code_verifier: nJ1kY... [PKCE] Recomputed challenge: ZxQW3... [PKCE] Stored challenge: ZxQW3... [PKCE] Match: true

Security advantages

Protection against authorization code interception

The most immediate benefit of PKCE is its defense against authorization code interception. In traditional OAuth 2.0 flows without PKCE, a malicious actor monitoring network traffic, say on an open Wi-Fi, could intercept the authorization code during the redirect phase and attempt to redeem it to obtain tokens.

PKCE prevents this by requiring the attacker to also know the original code_verifier, which was never transmitted over the wire. Without it, the stolen code is useless.

But PKCE doesn’t stop at interception, it also helps prevent abuse from stolen tokens. Imagine a scenario where access tokens or authorization codes are cached or stored insecurely in an app. If someone manages to extract them (through memory inspection, rooted devices, or app decompilation), PKCE ensures they can’t be used elsewhere without the corresponding verifier. This significantly limits the blast radius of a compromised app session.

This means even if an attacker intercepts the authorization code, they cannot redeem it for tokens without the original code verifier, which never leaves the client application.

Here’s a simple example of generating a code verifier and challenge in Javascript:

function base64UrlEncode(str) { return btoa(String.fromCharCode.apply(null, new Uint8Array(str))) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); } async function generateCodeChallenge(verifier) { const encoder = new TextEncoder(); const data = encoder.encode(verifier); const digest = await crypto.subtle.digest('SHA-256', data); return base64UrlEncode(digest); } const codeVerifier = 'random-string-generated-securely'; const codeChallenge = await generateCodeChallenge(codeVerifier);

This approach mitigates multiple attack vectors such as:

  • Phishing attacks: Even if a malicious app tricks a user into initiating an OAuth flow, it cannot redeem the authorization code without the matching code verifier.
  • Authorization code injection: Attackers cannot inject unauthorized codes because the server validates the code verifier.
  • Replay attacks: Each code verifier is unique and tied to a single authorization code, preventing reuse.

Protection on mobile devices

Mobile environments introduce their own challenges:

  • Rooted or jailbroken devices expose normally protected storage.
  • Debugging tools and reverse engineering can extract secrets from memory.
  • Network monitoring on-device can intercept HTTP traffic if TLS is bypassed.

In such cases, storing a client secret is not secure, and traditional OAuth flows are easily compromised. PKCE acts as the first line of defense by keeping sensitive logic within the client, without relying on secrets that can be stolen. The challenge-verifier mechanism, combined with one-time-use authorization codes, ensures that even if a mobile app is compromised, the damage is limited.

Alignment with OAuth 2.1

OAuth 2.1 formalizes PKCE as a mandatory security measure rather than an optional extension. This reflects how critical PKCE has become for securing modern OAuth implementations, especially for public clients.

Previously, OAuth flows relied heavily on client secrets to authenticate clients during the token exchange. Confidential clients like backend servers can store these secrets securely, but public clients, mobile apps, SPAs, CLI tools cannot, because their code and storage can be inspected or extracted by users or attackers.

PKCE effectively replaces the need for client secrets in these public clients by using dynamically generated verifiers instead of static secrets. This improves security without complicating client development.

OAuth 2.1 requires all authorization code flows from public clients to use PKCE, enforcing better security standards. Providers that adopt OAuth 2.1 must implement server-side checks to ensure:

  • The code_challenge parameter is present in the authorization request.
  • The code_verifier is verified during the token exchange.
  • The code_challenge_method is supported and correctly validated (preferably S256).

This requirement also has compliance implications. OAuth providers that fail to enforce PKCE for public clients may expose themselves and their users to increased risk of token theft and unauthorized access, potentially violating data protection regulations or security best practices.

Example of OAuth 2.1 authorization request with PKCE

GET /authorize?response_type=code &client_id=public-client-id &redirect_uri=https://client.app/callback &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM &code_challenge_method=S256 &scope=openid profile &state=xyz123

Why code_challenge_method matters

The code_challenge_method tells the authorization server how the client generated the code_challenge from the original code_verifier. This is critical because:

  • The server must apply the same method (e.g., SHA-256) to the code_verifier during the token exchange to verify the code_challenge matches.
  • Without this parameter, the server can’t determine how to validate the challenge, and the flow breaks.
  • If not explicitly specified, many servers may fall back to the less secure plain method, which sends the original verifier as-is, defeating the purpose of PKCE.

In this request, the client sends the code_challenge derived from the code verifier using SHA-256 and Base64 URL encoding (S256 method).

Later, when exchanging the authorization code for tokens, the client sends:

POST /token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &code=auth_code_received &redirect_uri=https://client.app/callback &client_id=public-client-id &code_verifier=random-string-generated-securely

The server compares the hash of the provided code_verifier with the originally received code_challenge. If they match, the token is issued.

By enforcing this flow, OAuth 2.1 drastically reduces the attack surface for public clients and raises the bar for secure authorization flows across all applications.

PKCE in agentic systems

Why it matters for MCP clients and AI agents

The Model Context Protocol (MCP) is an open standard introduced by Anthropic to standardize how AI applications, particularly large language models (LLMs), connect with external data sources and tools. It serves as a universal interface, enabling AI models to access and interact with various systems seamlessly. MCP addresses the challenge of integrating AI models with multiple external systems by providing a standardized protocol, akin to a "USB-C" for AI applications. Find out more about MCP auth at Scalekit MCP auth.

In the context of MCP, AI agents, autonomous systems that perform tasks on behalf of users, require secure methods to authenticate and authorize their interactions with external resources. Traditional OAuth 2.0 flows, which rely on client secrets, are not suitable for these agentic systems due to their inability to securely store such secrets. This is where PKCE (Proof Key for Code Exchange) becomes crucial.

PKCE enhances OAuth 2.0 by introducing a code verifier and a code challenge, allowing public clients (like AI agents) to securely exchange authorization codes without the need for client secrets. This mechanism ensures that even if an authorization code is intercepted, it cannot be exchanged for tokens without the original code verifier, thereby protecting against interception attacks.

Implementing PKCE in MCP-based agentic systems ensures secure token exchanges across ephemeral or stateless contexts, which are common in AI agents. It provides a robust authentication mechanism that aligns with the dynamic nature of these systems, allowing them to securely access and interact with external data sources and tools.

Conclusion

PKCE is essential for securing OAuth flows used by public clients, including mobile apps, single-page applications, CLI tools, and especially agentic AI clients built on MCP. Using the S256 method for code challenge generation is strongly recommended due to its enhanced security compared to the plain method. While confidential clients with secure secret storage may choose whether to implement PKCE, adding it can still improve protection against attacks.

After reading this, it’s important to reevaluate your current authorization setups, particularly if you are developing or maintaining public clients or AI-driven systems. For those working with MCP or similar AI frameworks, focusing on robust, PKCE-based OAuth implementations will significantly improve security. Taking steps to implement or upgrade PKCE in your authentication flows is critical to safeguarding tokens and ensuring trusted communication between agents and services.

FAQs

Can I use PKCE with client secrets?

Yes. While PKCE was designed primarily for public clients without client secrets, confidential clients can also use PKCE as an added security layer. It provides protection against authorization code interception even when client secrets are in use.

When should I use PKCE?

You should always use PKCE with public clients such as mobile apps, SPAs, and CLI tools. It is also recommended in OAuth 2.1 as a standard security practice. For confidential clients, it’s optional but beneficial.

What is the difference between PKCE and JWT?

PKCE is a mechanism for securely exchanging authorization codes to obtain tokens. JWT (JSON Web Token) is a token format used to represent claims securely. They serve different purposes: PKCE protects the OAuth flow, while JWTs carry identity and authorization information.

What is the alternative to PKCE?

For confidential clients, client secrets are the traditional alternative to PKCE. However, for public clients that cannot securely store secrets, PKCE is the recommended and widely adopted approach. Other emerging methods include mutual TLS and token binding but are less commonly implemented.

Does PKCE add overhead or impact performance?

PKCE introduces minimal computational overhead due to generating and hashing the code verifier, which is negligible for most applications. The security benefits far outweigh any minor performance impact, making it a best practice for OAuth flows.

No items found.
On this page
Share this article
Ready to secure your APIs?

Acquire enterprise customers with zero upfront cost

Every feature unlocked. No hidden fees.
Start Free
$0
/ month
3 FREE SSO/SCIM connections
Built-in multi-tenancy and organizations
SAML, OIDC based SSO
SCIM provisioning for users, groups
Unlimited users
Unlimited social logins
API Authentication

What is PKCE? A Developer’s Guide to Secure OAuth Flows

Srinivas Karre

OAuth 2.0 is the industry-standard protocol for authorization, enabling apps to access APIs without exposing user credentials. But while the spec is flexible, its original design had a key assumption: the client application can securely store secrets. This assumption falls apart for mobile apps, single-page applications (SPAs), and other public clients where everything, including source code and request data, is exposed.

To fix this, the OAuth community introduced PKCE (Proof Key for Code Exchange), a mechanism that lets public clients use the Authorization Code flow securely, without needing a client secret. PKCE is not an extension anymore; it’s mandatory for public clients and a core part of OAuth 2.1.

This guide breaks down why PKCE exists, how it works, and why it’s a must-have in modern auth flows, especially for apps that run entirely in browsers or on user-controlled devices.

Interactions between Client app, authorization server

Why does PKCE even exist?

PKCE wasn’t created to add friction; it was introduced to solve a real, critical security problem: securing the Authorization Code flow for clients that can’t hold secrets. With OAuth 2.1, PKCE is no longer optional. It’s now mandatory for all public clients. This shift reflects its importance in closing a critical security gap, and it emphasizes the need for developers to treat PKCE not as a bonus, but as a baseline.

OAuth 2.0 was originally designed around confidential clients, think server-side apps, that run in controlled environments. These clients are issued a client_secret during registration and use it to authenticate themselves when exchanging authorization codes for access tokens. This secret was the backbone of client verification.

But public clients like:

  • Mobile apps (e.g., Android or iOS apps)
  • SPAs running in a user’s browser
  • CLI tools or desktop apps

These have no safe place to store a client secret. Their environments are fully exposed to the user (and any attacker on the device or network). If a malicious party gets access to the authorization code, they can redeem it without needing anything else.

PKCE, a secure, secret-less mechanism that protects public clients from authorization code interception attacks, without depending on the traditional client secret model.

Vulnerabilities in traditional authorization code flow

Risks in public client contexts: Mobile apps and SPAs

In traditional OAuth 2.0, the authorization code is passed back to the client via a browser redirect. In a confidential client, this is safe because the client needs to prove its identity (with a secret) to redeem the code. But in public clients, there’s no proof step.

So if an attacker:

  • Is running a malicious app on the same device
  • Has access to browser session storage or intercepts the redirect URI
  • Or is listening on an open Wi-Fi network

They can grab that authorization code and use it to get tokens on behalf of the user.

Code interception attacks in browser-based apps

In SPAs, this threat is amplified. The browser:

  • Executes JavaScript from various origins
  • May have third-party extensions or scripts with elevated access
  • Is vulnerable to open redirect misconfigurations or iframe hijacking

Without PKCE, the flow is:

  1. App sends the user to the authorization server
  2. User authenticates and grants access
  3. Authorization server redirects back with an authorization code
  4. App exchanges the code for tokens

If the code is intercepted at Step 3, it’s game over. The attacker can skip the rest and impersonate the app.

Replay attacks

Even if an attacker captures an authorization code, they can’t reuse it if the server expects a specific verifier. Without PKCE, some authorization servers didn’t enforce single-use codes or lacked strict expiry rules, making it easier to replay a valid authorization code within a short time window.

PKCE prevents this by making every authorization code bound to a unique verifier and ensuring it can only be exchanged once. Replaying the same code with a different verifier will fail.

Why storing client secrets is impossible for public clients

Let’s be blunt, if your app runs in an environment where users (or attackers) can inspect your code, you don’t have a secret. You might hardcode a client_secret, obfuscate it, or store it in local storage, but that’s security theater.

Here’s why client secrets are fundamentally insecure in public client environments like mobile apps, SPAs, and desktop tools:

The compiled app can be reverse-engineered

In native apps, secrets are often baked into the code. But with tools like apktool, Frida, or IDA Pro, extracting hard coded values becomes trivial. Even "encrypted" secrets can be decrypted if the decryption key is also in the app bundle.

The browser exposes everything

In SPAs, there’s simply no hiding anything. The JavaScript is loaded directly into the browser. Any user/attacker can open DevTools and see network requests, local storage, and source maps. If a client_secret is used here, it's basically public knowledge.

Users can modify runtime behavior

Modern tools allow live patching and runtime hooking, meaning even if the secret isn’t immediately visible, attackers can change the app’s behavior to log or leak it. Mobile debugging tools or browser extensions can intercept requests and expose any headers or tokens.

Even secure storage isn’t enough

You might think storing secrets in places like iOS’s Keychain or Android’s Keystore helps. It’s better than nothing, but not bulletproof. On rooted or jailbroken devices, even secure storage becomes accessible. Plus, malicious apps can often escalate privileges or piggyback on misconfigured entitlements.

How does PKCE work?

PKCE (Proof Key for Code Exchange) enhances the traditional Authorization Code flow by adding a dynamic, one-time-use secret. Originally introduced as an extension in OAuth 2.0, it became a default security mechanism in OAuth 2.1. Unlike client secrets used by confidential clients, PKCE works for public clients that cannot keep credentials secure.

In OAuth 2.0, PKCE was optional and primarily targeted at mobile use cases. But as browser-based and CLI apps became more common, OAuth 2.1 moved to make PKCE the default for all authorization code flows, eliminating the need for client secrets in public client contexts.

Code verifier and code challenge

What is the code verifier?
The code verifier is a high-entropy, cryptographically random string created by the client at the beginning of the OAuth flow. It serves as a one-time-use secret that ties the initial authorization request to the token exchange.

Transforming it into the code challenge (S256 method)
To protect the code verifier from being exposed during transmission, it is hashed using the SHA-256 algorithm to produce the code challenge. This challenge is then base64url-encoded and sent in the initial authorization request.

Why S256 is preferred over plain:
The plain method sends the verifier as-is in the authorization request. If an attacker is able to intercept the request or read the URL, they gain access to the full verifier and can impersonate the client. S256 improves security by sending only the hashed version (the challenge), so even if it’s intercepted, the actual verifier remains safe. It also ensures the code challenge cannot be reversed back into the original verifier.

Feature
The plain method
S256 (Recommended)
Transformation
None (sends the code verifier as-is)
SHA-256 hash of the code verifier
Security
Weak (code verifier exposed in transmission)
Strong (verifier is hashed, not exposed)
Protection against interception
Minimal
Strong defense against code leakage
Support in authorization servers
Optional
Widely supported and default in OAuth 2.1
Ease of implementation
Simple, but insecure
Slightly more complex, but secure
OAuth 2.1 recommendation
Not recommended unless explicitly required
Mandatory for public clients

How both are used across the OAuth transaction

  • During the initial request, the client sends the code challenge and method (S256) to the authorization server.
  • When redeeming the authorization code for tokens, the client must send the original code verifier.
  • The server verifies that the code verifier, when hashed using the declared method, matches the original challenge. If it doesn’t, the request is rejected.

End-to-end PKCE flow

End to end PKCE flow with client app, authorization server, resource server, tokens, and endpoints

Here’s how PKCE fits into the full OAuth Authorization Code flow.

Step-by-step walkthrough:

1. Generate code verifier and challenge

The client generates a random string (the code verifier), hashes it using SHA-256, and encodes it to form the code challenge.

2. Initiate auth request with code challenge

The client redirects the user to the authorization endpoint with these parameters:

  • code_challenge: the hashed string
  • code_challenge_method: S256
  • client_id, redirect_uri, response_type, scope, etc.

3. Receive auth code

After the user grants consent, the authorization server redirects the browser to the client’s redirect URI with an authorization code.

4. Redeem token using code verifier

The client sends a POST request to the token endpoint with:

  • code: the authorization code
  • code_verifier: the original random string
  • grant_type, client_id, redirect_uri, etc.

The server recomputes the hash and checks it against the original challenge. If they match, it issues the token.

Example implementation

Minimal client setup with curl or Node.js

To implement PKCE, you need to generate a code verifier and its corresponding code challenge. Here’s how you can do it in Node.js:
Node.js version >= 16.0.0 is required to use base64url encoding.

const crypto = require('crypto'); const codeVerifier = crypto.randomBytes(64).toString('base64url'); const codeChallenge = crypto .createHash('sha256') .update(codeVerifier) .digest('base64url'); console.log({ codeVerifier, codeChallenge });

After generating these, start the OAuth authorization request by including the code challenge and method

https://auth.example.com/oauth/authorize? response_type=code& client_id=your-client-id& redirect_uri=http://localhost/callback& code_challenge=CODE_CHALLENGE& code_challenge_method=S256& scope=openid profile email

Once the user approves, the server redirects back with an authorization code. Exchange this code for an access token by sending the original code verifier in the token request:

curl -X POST https://auth.example.com/oauth/token \ -d grant_type=authorization_code \ -d client_id=your-client-id \ -d code=RECEIVED_AUTH_CODE \ -d redirect_uri=http://localhost/callback \ -d code_verifier=ORIGINAL_CODE_VERIFIER

This proves to the server you possess the original verifier, enabling a secure token exchange without needing a client secret.

Authorization server perspective

On the server side, the authorization server plays a crucial role in verifying the integrity of the PKCE flow. It performs several key validation steps during the token exchange to ensure the request wasn't tampered with:

1. Storing the code challenge and method

When the client initiates the authorization request, it sends a code_challenge along with a code_challenge_method (typically S256).

The server must store both values temporarily because they are required to validate the token request that comes later. Without them, the server would have no way to confirm whether the incoming code_verifier matches the original challenge.

2. Validating during token exchange
When the client attempts to exchange the authorization code for an access token, it includes the code_verifier.
The server then:

  • Recalculates the code_challenge from the provided code_verifier using the stored code_challenge_method
  • Compares this recalculated value with the originally stored code_challenge

3. Decision

  • If the values match, the server issues the access token.
  • If they don’t match, the server rejects the request — this helps prevent attacks where an attacker tries to intercept the code and reuse it from a different client.
const expectedChallenge = base64url(crypto.createHash('sha256').update(codeVerifier).digest()); if (expectedChallenge !== storedChallenge) { throw new Error('PKCE code_verifier mismatch'); }

During token exchange, server logs might show:

[PKCE] Received method: S256 [PKCE] code_verifier: nJ1kY... [PKCE] Recomputed challenge: ZxQW3... [PKCE] Stored challenge: ZxQW3... [PKCE] Match: true

Security advantages

Protection against authorization code interception

The most immediate benefit of PKCE is its defense against authorization code interception. In traditional OAuth 2.0 flows without PKCE, a malicious actor monitoring network traffic, say on an open Wi-Fi, could intercept the authorization code during the redirect phase and attempt to redeem it to obtain tokens.

PKCE prevents this by requiring the attacker to also know the original code_verifier, which was never transmitted over the wire. Without it, the stolen code is useless.

But PKCE doesn’t stop at interception, it also helps prevent abuse from stolen tokens. Imagine a scenario where access tokens or authorization codes are cached or stored insecurely in an app. If someone manages to extract them (through memory inspection, rooted devices, or app decompilation), PKCE ensures they can’t be used elsewhere without the corresponding verifier. This significantly limits the blast radius of a compromised app session.

This means even if an attacker intercepts the authorization code, they cannot redeem it for tokens without the original code verifier, which never leaves the client application.

Here’s a simple example of generating a code verifier and challenge in Javascript:

function base64UrlEncode(str) { return btoa(String.fromCharCode.apply(null, new Uint8Array(str))) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); } async function generateCodeChallenge(verifier) { const encoder = new TextEncoder(); const data = encoder.encode(verifier); const digest = await crypto.subtle.digest('SHA-256', data); return base64UrlEncode(digest); } const codeVerifier = 'random-string-generated-securely'; const codeChallenge = await generateCodeChallenge(codeVerifier);

This approach mitigates multiple attack vectors such as:

  • Phishing attacks: Even if a malicious app tricks a user into initiating an OAuth flow, it cannot redeem the authorization code without the matching code verifier.
  • Authorization code injection: Attackers cannot inject unauthorized codes because the server validates the code verifier.
  • Replay attacks: Each code verifier is unique and tied to a single authorization code, preventing reuse.

Protection on mobile devices

Mobile environments introduce their own challenges:

  • Rooted or jailbroken devices expose normally protected storage.
  • Debugging tools and reverse engineering can extract secrets from memory.
  • Network monitoring on-device can intercept HTTP traffic if TLS is bypassed.

In such cases, storing a client secret is not secure, and traditional OAuth flows are easily compromised. PKCE acts as the first line of defense by keeping sensitive logic within the client, without relying on secrets that can be stolen. The challenge-verifier mechanism, combined with one-time-use authorization codes, ensures that even if a mobile app is compromised, the damage is limited.

Alignment with OAuth 2.1

OAuth 2.1 formalizes PKCE as a mandatory security measure rather than an optional extension. This reflects how critical PKCE has become for securing modern OAuth implementations, especially for public clients.

Previously, OAuth flows relied heavily on client secrets to authenticate clients during the token exchange. Confidential clients like backend servers can store these secrets securely, but public clients, mobile apps, SPAs, CLI tools cannot, because their code and storage can be inspected or extracted by users or attackers.

PKCE effectively replaces the need for client secrets in these public clients by using dynamically generated verifiers instead of static secrets. This improves security without complicating client development.

OAuth 2.1 requires all authorization code flows from public clients to use PKCE, enforcing better security standards. Providers that adopt OAuth 2.1 must implement server-side checks to ensure:

  • The code_challenge parameter is present in the authorization request.
  • The code_verifier is verified during the token exchange.
  • The code_challenge_method is supported and correctly validated (preferably S256).

This requirement also has compliance implications. OAuth providers that fail to enforce PKCE for public clients may expose themselves and their users to increased risk of token theft and unauthorized access, potentially violating data protection regulations or security best practices.

Example of OAuth 2.1 authorization request with PKCE

GET /authorize?response_type=code &client_id=public-client-id &redirect_uri=https://client.app/callback &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM &code_challenge_method=S256 &scope=openid profile &state=xyz123

Why code_challenge_method matters

The code_challenge_method tells the authorization server how the client generated the code_challenge from the original code_verifier. This is critical because:

  • The server must apply the same method (e.g., SHA-256) to the code_verifier during the token exchange to verify the code_challenge matches.
  • Without this parameter, the server can’t determine how to validate the challenge, and the flow breaks.
  • If not explicitly specified, many servers may fall back to the less secure plain method, which sends the original verifier as-is, defeating the purpose of PKCE.

In this request, the client sends the code_challenge derived from the code verifier using SHA-256 and Base64 URL encoding (S256 method).

Later, when exchanging the authorization code for tokens, the client sends:

POST /token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &code=auth_code_received &redirect_uri=https://client.app/callback &client_id=public-client-id &code_verifier=random-string-generated-securely

The server compares the hash of the provided code_verifier with the originally received code_challenge. If they match, the token is issued.

By enforcing this flow, OAuth 2.1 drastically reduces the attack surface for public clients and raises the bar for secure authorization flows across all applications.

PKCE in agentic systems

Why it matters for MCP clients and AI agents

The Model Context Protocol (MCP) is an open standard introduced by Anthropic to standardize how AI applications, particularly large language models (LLMs), connect with external data sources and tools. It serves as a universal interface, enabling AI models to access and interact with various systems seamlessly. MCP addresses the challenge of integrating AI models with multiple external systems by providing a standardized protocol, akin to a "USB-C" for AI applications. Find out more about MCP auth at Scalekit MCP auth.

In the context of MCP, AI agents, autonomous systems that perform tasks on behalf of users, require secure methods to authenticate and authorize their interactions with external resources. Traditional OAuth 2.0 flows, which rely on client secrets, are not suitable for these agentic systems due to their inability to securely store such secrets. This is where PKCE (Proof Key for Code Exchange) becomes crucial.

PKCE enhances OAuth 2.0 by introducing a code verifier and a code challenge, allowing public clients (like AI agents) to securely exchange authorization codes without the need for client secrets. This mechanism ensures that even if an authorization code is intercepted, it cannot be exchanged for tokens without the original code verifier, thereby protecting against interception attacks.

Implementing PKCE in MCP-based agentic systems ensures secure token exchanges across ephemeral or stateless contexts, which are common in AI agents. It provides a robust authentication mechanism that aligns with the dynamic nature of these systems, allowing them to securely access and interact with external data sources and tools.

Conclusion

PKCE is essential for securing OAuth flows used by public clients, including mobile apps, single-page applications, CLI tools, and especially agentic AI clients built on MCP. Using the S256 method for code challenge generation is strongly recommended due to its enhanced security compared to the plain method. While confidential clients with secure secret storage may choose whether to implement PKCE, adding it can still improve protection against attacks.

After reading this, it’s important to reevaluate your current authorization setups, particularly if you are developing or maintaining public clients or AI-driven systems. For those working with MCP or similar AI frameworks, focusing on robust, PKCE-based OAuth implementations will significantly improve security. Taking steps to implement or upgrade PKCE in your authentication flows is critical to safeguarding tokens and ensuring trusted communication between agents and services.

FAQs

Can I use PKCE with client secrets?

Yes. While PKCE was designed primarily for public clients without client secrets, confidential clients can also use PKCE as an added security layer. It provides protection against authorization code interception even when client secrets are in use.

When should I use PKCE?

You should always use PKCE with public clients such as mobile apps, SPAs, and CLI tools. It is also recommended in OAuth 2.1 as a standard security practice. For confidential clients, it’s optional but beneficial.

What is the difference between PKCE and JWT?

PKCE is a mechanism for securely exchanging authorization codes to obtain tokens. JWT (JSON Web Token) is a token format used to represent claims securely. They serve different purposes: PKCE protects the OAuth flow, while JWTs carry identity and authorization information.

What is the alternative to PKCE?

For confidential clients, client secrets are the traditional alternative to PKCE. However, for public clients that cannot securely store secrets, PKCE is the recommended and widely adopted approach. Other emerging methods include mutual TLS and token binding but are less commonly implemented.

Does PKCE add overhead or impact performance?

PKCE introduces minimal computational overhead due to generating and hashing the code verifier, which is negligible for most applications. The security benefits far outweigh any minor performance impact, making it a best practice for OAuth flows.

No items found.
Ship Enterprise Auth in days