PKCE Deep Dive
Deep dive into Proof Key for Code Exchange (PKCE). Why is this OAuth 2.0 extension so important to protect the authorization code grant flow?
The short version
PKCE - Proof Key for Code Exchange - is a cryptographic way to secure an authorization code grant flow by binding the authorization request to the access token request. It prevents replay of stolen authorization codes for both public and private OAuth clients.
If you have ever created an app that uses an external IDP with OpenID Connect, you probably have encountered PKCE. But, if you are like most developers, you probably treated it like a check box that needs to be ticked without giving it too much further thought. I can't claim I've been much better. In my job, PKCE is one of the OAuth extensions I've used and talked about on autopilot. Yet, I never quite bothered to get a deeper understanding of why it is used and where exactly. Of course, I knew it must be used for public clients. I could scribble a diagram on a whiteboard and set up an example flow in Postman. But what problem does PKCE really solve? This is what I'm aiming to find out in this article.
A misconception I come across about OAuth is that PKCE is a replacement for client secrets. Since Single Page Applications (SPA) load their whole code in the user's browser, it is quite easy to understand that they cannot safely keep a secret. Any secret that is stored in the SPA's OAuth client can easily be read out just by looking at the source. Similarly, a mobile app provides only a minimally bigger hurdle to decompile and find a secret that is shipped with the app. But PKCE is not client authentication; it does not replace client secrets. And as we will see, it is a good idea to use PKCE even if the app can keep a secret.
What is PKCE
Pronounced pixie, like the mythical fairy of Cornish origin. PKCE stands for Proof Key for Code Exchange. It is an OAuth 2.0 extension defined in RFC 7636 and aims to secure the authorization code grant flow. The authorization code grant flow is used by applications to get consent and authorization from a user without the need for the client to have any knowledge of the user's credentials. very often for authentication in an OpenID Connect (OIDC) flow. It can be used by both public and confidential clients. Public clients are those that cannot store a secret - like Single Page Applications (SPAs) and mobile applications - whereas confidential clients are those that can, like a server-side web application or desktop app.
Public clients are particularly vulnerable to authorization code interception attacks, and this is what PKCE is protecting against. Since the client is not authenticated, anyone can call the authorization server in its name. Additionally, there is no way for the authorization server to know if an authorization code it receives in an access token request is coming from the same client that originally requested it. PKCE provides a cryptographic binding of the authorization request to the access token request, thereby preventing authorization code attacks.
Authorization code injection attack
To understand PKCE, let's examine an authorization code attack on a Single Page Application (SPA) that doesn't use it.

- A legitimate SPA, that is vulnerable to cross site scripting (XSS) starts an authorization flow by calling the authorization server's authorization endpoint.
- This redirects the user to the authorization server. The user is prompted to login, and upon successful login, the authorization server redirects the user back to the SPA.
- So far, everything has gone according to plan. However, the attacker manages to intercept this request through a maliciously injected script, which sends the auth code to an attack server.
- The attacker can now exchange this authorization code at the authorization server to get an access token - and probably also an ID token - identifying the victim. With this access token, the attacker has impersonated the victim and can access the victim's information.
- When the legitimate client tries to exchange the authorization code for a token, the call fails. Most likely, the victim thinks this is a temporary error and tries again later.
Authorization code attack with confidential clients
With a confidential client on a backend web service, stealing the token is harder as there is no vulnerable app in the browser that gets hold of the authorization code. But there is still a potential for the code to be stolen, for example by a malicious browser extension that intercepts the redirect from the authorization server containing the authorization code and forwards the authorization code to an attacker controlled server.

With a confidential client, an attacker cannot simply exchange the stolen authorization code for an access token since this action requires client credentials. Instead, the attacker has to start its own authorization code flow with the legitimate client. The attacker intercepts the redirect from the authorization server containing the attacker's authorization code. This request passes through the attacker's browser and replaces the authorization code with the one stolen from the victim.
The client then exchanges the victim's code for an access token using its own credentials and issues a session for the attacker's browser. Again gives the attacker access to the victim's identity.
Ways for an attacker to steal an authorization code
An attacker has several possibilities for stealing an authorization code
- Manipulating the redirection URL. The attacker can try to get the victim to issue an authorization request using a redirect_uri that the attacker controls. If this succeeds, the authorization server will redirect the user with the authorization code to an attacker controlled server. To prevent this, an authorization server must strictly validate the redirect_uri parameter against the configured whitelist for a particular client. When configuring this whitelist, care has to be taken so that development URLs don't accidentally be whitelisted in production. I've seen such misconfigurations several times, with whitelisted URLs such as
http://localhost:8000/callback
for production OAuth Clients! - Registered URL schemes used as redirect URIs are a risk specific to mobile applications. These URLs are registered by an application, so instead of using the browser, the URL is opened in the registered app. An attacker can, under some circumstances, trick a victim into installing their own application that uses the same registered URL and intercept an authorization code in that way.
- Cross Scripting (XSS). If an attacker finds an XSS vulnerability in an SPA, the attacker can gain access to the authorization code after it is redirected from the Authorization Server. XSS is a general threat to web applications that also can lead to stolen access tokens or other attacks.
- Malicious Browser plugins can manipulate requests as they pass through a user's browser and, for example, steal an authorisation code as the user is redirected to the redirect URI with the authorization code.
How does PKCE stop authorization code injection attacks?
The principle of PKCE is to bind the authorization request to the access token request and thereby preventing an attacker from using a victim's authorization code.
Before starting the authorization flow, the client generates a secret key, called the code verifier. The code verifier is a string of between 43 and 128 random characters. The client stores this string in memory or in the session.
From this code verifier, it derives a code challenge with either of two methods
- PLAIN—The code challenge is the same as the code verifier. This method should be omitted and is only really good for testing.
- S256 - The code challenge is the SHA256 hash of the code verifier. This is the recommended method.

The client then sends the code_challenge
along with the challenge_method
, which indicates the method used to derive the code and the usual parameters, such as client_id
, return_uri
, etc., in the authorization request.
GET /authorize
?client_id=spa
&response_type=code
&state=xyz
&scope=openid
&redirect_uri=https://myapp.example.com/callback
&code_challenge=8IyrbTHBJiHWVrzUViJDTBy5RmIDg2C6ibyqGJj5Lx8
&code_challange_method=S256 HTTP/1.1
Host: authzserver.example.com
Authorization Request with PKCE Note the code_challenge
and code_challenge_method
parameters
The authorization server stores the code challenge and - after authenticating the user - an authorization code is returned to the user by redirecting to the redirect_uri.
HTTP/1.1 302 Found
Location: https://myapp.example.com/callback
?code=Ow-fck2QLyNUnaCc2fauU1PoMUg
&state=xyz
&client_id=spa
Response from Authorization Sever with Authorization Code in the code
parameter
The client then exchanges the authorization code for an access token, this time sending along the cleartext code verifier.
POST /access_token HTTP/1.1
Host: authzserver.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=xrc5s97vIvm-spdwVMJzRP0AbhI
&client_id=spa
&redirect_uri=https://myapp.example.com/callback
&code_verifier=M-aOEmoY19Rg1zCSTL9PwK4Mw8BJt16y0ir_uj_TKOFKEdV3zYJaZKrnOFSwIErHlRs
The client exchanges the authorization code for an access token. This time, the client sends the cleartext code_verifier
The authorization server validates that the access token request belongs to the authorization request, by comparing the SHA256 hash of the code verifier to the stored code challenge.
if SHA256(code_verifier) == code_challenge
Check authorization code and return access_token
else
return error
Pseudo Code for validating the code verifier
With that, PKCE has provided the cryptographic means to bind the authorization request to the access token request. The authorization server then completes the authorization code grant flow by validating the authorization code and returning the requested tokens.
If the code verifier or authorization code cannot be validated, the authorization server returns an error:
400 Bad Request
Content-Type: application/json;charset=UTF-8
{
"error_description": "The provided access grant is invalid, expired, or revoked.",
"error": "invalid_grant"
}
Error response in case the code challenge doesn't match the code verifier. This is the same message as if the authorization code is invalid, leaving an attacker without a hint of what went wrong.
Using PKCE effectively prevents code injection attacks, as the attacker doesn't know the code verifier. Even if an attacker intercepts the code challenge, it cannot derive the code verifier since SHA256 is a cryptographically secure one-way hash.
Protection for cross-site request forgery attacks
By binding the authorization and access token requests together, PKCE also prevents cross-site request forgery (CSRF) attacks. Traditional CSRF protection methods involve using a unique token tied to the user’s session. In the case of PKCE, the code verifier serves as a one-time secret that is bound to the original authorization request. Because this secret isn’t transmitted until the token exchange phase and isn’t accessible to a malicious third party, it prevents an attacker from hijacking the flow.
Even if an attacker tricks a user into initiating an authorization flow or intercepts the redirect containing the authorization code, they won’t possess the original code verifier. Without this verifier, they cannot complete the token exchange. This means that any request forged via CSRF that tries to reuse an intercepted authorization code will fail.
A client using PKCE can, therefore, omit specific CSRF parameters.
PKCE best practices
- Only use S256 challenge method. If the plain method is used, the attacker can simply steal the code challenge and use it as the code verifier. The plain method only exists for compatibility reasons.
- The server must ensure the challenge method parameter is set to an accepted value and enforce this method only. Normally, the authorisation server should only accept S256. This prevents a downgrade attack where the attacker changes the challenge method to PLAIN to be able to steal the code verifier.
- The security of the code verifier requires it to be created with enough entropy so the attacker cannot brute-force the code. To achieve this, the client has to use a secure random generator, such as JavaScript's Crypto.randomUUID method.
- The authorization server has to be configured to enforce the use of PKCE, not merely support it. For example, in PingAM‘s OAuth 2.0 provider configuration, set the
Code Verifier Parameter Required
parameter totrue
to require all clients to use PKCE or at least topublic
to require all public clients to use PKCE.
PKCE FAQ
What does PKCE stand for?
PKCE stands for Proof Key for Code Exchange
How is PKCE pronounced?
PKCE is pronounced pixie, like the fairy
When to use PKCE?
Any application using the authentication grant flow should use PKCE. Whether the client is public (cannot use a secret) or confidential (those that can safely store a secret), PKCE provides effective security against authorization code injection and mitigates against CSRF attacks.
If PKCE is initiated by the client, can't an attacker just omit it?
In a correctly configured authorization server, PKCE should be required at least for public clients so that an attacker cannot omit it. Additionally, once the client starts an authorization request, the authorization server will always request a code verifier to exchange the authorization code.
But isn't a confidential client safe?
While a confidential client makes it much harder to inject a stolen authorization code, there are still ways for an attacker to do so. In addition, the PKCE provides additional protection, such as from CSRF attacks. PKCE is simple to implement and prevents a serious threat. Use it to protect all your clients using authorization code flow, even confidential ones.