Skip to main content

Understanding PKCE

If you already understand the OAuth 2.0 Authorization Code Flow, then PKCE can be viewed as an additional security layer that was introduced to solve a very specific problem. To appreciate why PKCE exists, we must first understand what OAuth looked like before PKCE was introduced and where the weakness existed.

Proof Key for Code Exchange

In a traditional Authorization Code Flow, a user attempts to log in to an application. Instead of sending the user's credentials directly to the application, the user is redirected to an Authorization Server such as Keycloak, Okta, or Auth0. After successful authentication, the Authorization Server sends an Authorization Code back to the application. The application then exchanges that Authorization Code for an Access Token.

A simplified version of the flow looks like this:

+---------+ +----------------+
| Client | ---> | Authorization |
| App | | Server |
+---------+ +----------------+
|
| Authorization Code
|
v
+----------------+
| Token Endpoint |
+----------------+

The Authorization Code acts like a temporary ticket. The client presents this ticket to the token endpoint and receives an Access Token in return.

At first glance, this seems secure because the Access Token itself is never exposed through the browser. Only the Authorization Code travels through the browser. Unfortunately, security researchers discovered a problem.

The Security Problem That Led to PKCE

Imagine that during the login process, an attacker somehow obtains the Authorization Code before the legitimate client can exchange it for an Access Token.

The situation would look something like this:

Client
|
|------ Authorization Code ------>
|
Attacker intercepts code

Suppose the intercepted code is:

authorization_code=ABC123

If the Authorization Server only verifies that the code is valid, then anyone possessing that code can attempt to exchange it for an Access Token.

POST /token

grant_type=authorization_code
code=ABC123

The Authorization Server sees a valid Authorization Code and may issue an Access Token. The problem is that the Authorization Server has no way to determine whether the request came from the legitimate client or from someone who simply stole the Authorization Code.

In other words, possession of the Authorization Code becomes equivalent to ownership of the login session. That is precisely the weakness PKCE was designed to eliminate.

The Core Idea Behind PKCE

The name PKCE stands for Proof Key for Code Exchange. The phrase sounds complicated, but the idea is surprisingly simple. Instead of treating the Authorization Code as the only proof of identity, the client generates an additional secret value before the login process even begins. Think of it as creating a secret password for this specific OAuth login attempt. This temporary password is called the code_verifier.

The important detail is that this secret never leaves the client during the authorization request. Only the client knows it.

Later, when exchanging the Authorization Code for an Access Token, the client must present both the Authorization Code and this secret value. If the secret matches what was originally registered, the Authorization Server issues tokens. Otherwise, the request is rejected.

Step 1: Creating the code_verifier

Before redirecting the user to the Authorization Server, the client generates a long random string.

For example:

code_verifier = A8hf7K9xY2mPq4Rt8Bw1Nf6DzX3JkLm

This value should be:

  • Random
  • Unpredictable
  • Cryptographically secure
  • Used only once

You can think of the code_verifier as a one-time password that belongs exclusively to this OAuth login attempt. The client stores this value locally because it will need it later.

Step 2: Creating the code_challenge

The Authorization Server should not receive the code_verifier directly during the first request because that would defeat the purpose of having a secret. Instead, the client transforms the code_verifier into another value called the code_challenge. Most modern implementations use the S256 method.

code_verifier

SHA256

Base64URL Encoding

code_challenge

Visually:

code_verifier

SHA256

8KJad8sX...

Base64URL

code_challenge

At this point:

Client Knows: code_verifier

Authorization Server Receives: code_challenge

Notice the clever design. The Authorization Server receives enough information to verify the client later, but it never receives the original secret.

Step 3: Sending the Authorization Request

The client now redirects the user to the Authorization Server. The request contains the code_challenge.

GET /authorize

response_type=code
client_id=my-client
redirect_uri=https://app.com/callback
code_challenge=xyz123
code_challenge_method=S256

The Authorization Server stores the challenge alongside the future Authorization Code. The user then authenticates normally using a username, password, MFA, biometric authentication, or whatever authentication method is configured.

Step 4: Receiving the Authorization Code

After successful authentication, the Authorization Server redirects the user back to the client application. The response contains the Authorization Code. Example:

code=ABC123

At this stage, nothing appears different from a normal Authorization Code Flow. The important difference is that the Authorization Server has secretly stored the code_challenge associated with this Authorization Code.

Step 5: Exchanging the Authorization Code

Now the client calls the token endpoint. Unlike a traditional Authorization Code Flow, the request contains the original code_verifier.

POST /token

grant_type=authorization_code
code=ABC123
code_verifier=A8hf7K9xY2mPq4Rt8Bw1Nf6DzX3JkLm

The Authorization Server now performs the same transformation that the client performed earlier. The resulting challenge is compared with the challenge stored during the authorization request.

Step 6: Verification

The Authorization Server now has two values:

  1. Stored value: code_challenge
  2. Calculated value: SHA256(code_verifier)

The server compares them. If they match access token is issued. If they do not match request is rejected. The Access Token is only issued when the client proves possession of the original code_verifier.

Why an Attacker Cannot Use a Stolen Authorization Code

This is where PKCE demonstrates its value. Suppose an attacker somehow steals Authorization Code = ABC123. The attacker attempts to exchange it.

POST /token
code=ABC123

The Authorization Server immediately asks an implicit question: Where is the code_verifier?. The attacker does not know it. Even worse for the attacker, the Authorization Server only stored code_challenge not code_verifier. Since SHA-256 is a one-way hash function the attacker cannot reverse the operation and recover the original secret. As a result, possession of the Authorization Code alone is no longer sufficient. The attacker must also possess the original code_verifier. That additional proof is exactly what PKCE introduces.

PKCE Versus Client Secret

A common misunderstanding among developers is believing that PKCE replaces a client secret. These are actually solving different problems.

A Client Secret identifies the application itself.

A backend application such as Spring Boot can safely store a client secret because the secret remains on the server. PKCE, on the other hand, protects the Authorization Code exchange.

Authorization Code
+
code_verifier

The purpose of PKCE is not to authenticate the application. Its purpose is to prove that the same client that started the OAuth flow is the client attempting to exchange the Authorization Code.

Why PKCE Is Essential for React Applications

Consider a React application running entirely inside a browser. Any client secret embedded in JavaScript can be viewed by users. Therefore Client Secret is not safe. PKCE solves this problem because no long-term secret needs to be stored. Instead:

Login Starts

Generate code_verifier

Use Once

Discard

The One Thing to Remember

If you forget everything else about PKCE, remember this mental model:

Without PKCE

Authorization Code

Access Token

With PKCE:

Authorization Code
+
code_verifier

Access Token

The Authorization Code becomes only one half of the proof. The second half is the code_verifier.

An attacker who steals only the Authorization Code possesses half of the puzzle and cannot obtain tokens. That simple idea is the entire reason PKCE exists, and it is why modern OAuth implementations strongly recommend or require it.