1. Introduction
When dealing with modern authentication flows, especially in web and mobile applications, security becomes the top priority.
OAuth 2.0 introduced the Authorization Code Flow to enable secure access delegation — where users can grant access to applications without sharing their passwords.
However, traditional Authorization Code Flow had a weakness — it relied on the client secret, which cannot be safely stored in public clients such as:
- Single Page Applications (SPAs) written in JavaScript,
- Mobile applications (iOS/Android),
- Desktop apps.
To solve this, PKCE (Proof Key for Code Exchange) was introduced.
PKCE strengthens OAuth 2.0 by removing the need for a client secret while preventing authorization code interception attacks.
When JWT (JSON Web Token) is used as the token mechanism, PKCE ensures that only the legitimate client that started the authentication request can exchange the authorization code for a JWT access token.
2. Background: OAuth 2.0 Authorization Code Flow
Before diving into PKCE, let’s recall the traditional OAuth 2.0 Authorization Code Flow.
Basic Flow Steps
- User Authentication Request
The client redirects the user to the authorization server with parameters likeclient_id,redirect_uri,scope, andresponse_type=code. - Authorization Grant
The user logs in, and the authorization server returns an authorization code to the client (via redirect URI). - Token Exchange
The client then sends this code, along with its client secret, to the authorization server to get:- an Access Token (JWT), and
- optionally a Refresh Token.
Security Problem
In public clients (like React or mobile apps):
- The client secret cannot be securely stored.
- An attacker could intercept the authorization code and exchange it for tokens, pretending to be the real client.
This led to a significant vulnerability known as the authorization code interception attack.
3. Introduction to PKCE
PKCE (Proof Key for Code Exchange), pronounced as “pixy,” was designed by the IETF as an enhancement to OAuth 2.0 (RFC 7636).
Its goal is to mitigate code interception attacks by proving that the entity exchanging the authorization code for tokens is the same entity that initiated the request.
Key Idea
Instead of relying on a client secret, PKCE uses a temporary secret known as a code verifier.
- The client generates a code verifier (a random high-entropy string).
- It derives a code challenge (a hashed form of the verifier) and sends it in the authorization request.
- When exchanging the authorization code for a JWT token, the client must send the original code verifier.
- The server verifies the match between the verifier and the challenge before issuing the token.
This ensures that even if an attacker intercepts the authorization code, they cannot use it — because they don’t know the original code verifier.
4. How PKCE Works (Step-by-Step)
Let’s see how PKCE modifies the Authorization Code Flow.
Step 1: Client Creates a Code Verifier
The client first generates a cryptographically random string, for example:
code_verifier = "Yy8rP7PlYxU93s9L3xYtQzR4Z3jD9R4fH1pFf8jGkWz"
This verifier must:
- Contain 43–128 characters.
- Use only unreserved characters:
[A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~".
Step 2: Generate a Code Challenge
The client derives a code challenge from the verifier using a transformation method.
The most common method is SHA-256, as follows:
code_challenge = BASE64URL-ENCODE(SHA256(code_verifier))
Example:
code_challenge = "2F1e0MoehwRas9mPNu6_xuM4xZz3qY0Zz8WMEQh2iIY"
The method used is also sent to the authorization server (as code_challenge_method).
Step 3: Authorization Request
Now, the client sends an authorization request to the Authorization Server like:
GET /authorize? response_type=code& client_id=my-client& redirect_uri=https://myapp.com/callback& scope=openid profile email& code_challenge=2F1e0MoehwRas9mPNu6_xuM4xZz3qY0Zz8WMEQh2iIY& code_challenge_method=S256
The authorization server records the code challenge and the method along with the authorization code it will issue later.
Step 4: Authorization Code Issued
After the user logs in and grants permission, the authorization server redirects the user back to the client’s redirect_uri with the authorization code:
https://myapp.com/callback?code=abc123
Step 5: Token Request
Now, the client exchanges this authorization code for a JWT Access Token.
POST /token grant_type=authorization_code code=abc123 redirect_uri=https://myapp.com/callback code_verifier=Yy8rP7PlYxU93s9L3xYtQzR4Z3jD9R4fH1pFf8jGkWz
Step 6: Token Response (with JWT)
The authorization server validates that:
code_challenge == BASE64URL-ENCODE(SHA256(code_verifier))
If valid, it returns the tokens:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6...",
"token_type": "Bearer",
"expires_in": 3600,
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "def456"
}
The access_token and id_token are typically JWTs (JSON Web Tokens) containing encoded claims such as user ID, email, roles, and expiry information.
5. Relationship between PKCE and JWT
- PKCE ensures that the Authorization Code Flow is secure.
- JWT is the token format used to represent identity and access information once the token is issued.
In simpler terms:
- PKCE protects how you get the token.
- JWT defines what the token contains and how it’s validated.
When combined, they form a secure authentication and authorization flow ideal for:
- React / Angular / Vue SPAs,
- Native mobile apps (Android/iOS),
- Desktop Electron apps.
6. PKCE vs Client Secret
| Aspect | PKCE | Client Secret |
|---|---|---|
| Designed For | Public Clients (SPAs, mobile) | Confidential Clients (server-side apps) |
| Secret Storage | No permanent secret | Stored securely on the backend |
| Vulnerability | Protects against code interception | Can be stolen if stored insecurely |
| Exchange Proof | Uses temporary code_verifier | Uses client_secret |
Thus, PKCE replaces the need for a client secret in cases where it can’t be safely stored.
7. Example: PKCE Flow in a JWT-based React + Spring Boot Application
- React App starts the login flow by generating
code_verifierandcode_challenge. - It redirects the user to Keycloak or Auth0 (authorization server) with
code_challenge. - After successful login, Keycloak redirects back with
authorization_code. - The React app sends this code and
code_verifierto Spring Boot backend. - Spring Boot backend sends them to the token endpoint.
- The authorization server verifies the challenge and issues a JWT token.
- The Spring Boot backend validates and stores the JWT, forwarding it to the frontend for API use.
This ensures:
- The authorization code cannot be reused by any attacker.
- The JWT token represents verified identity and can be validated using public keys.
8. Benefits of PKCE
- Enhanced Security – Prevents authorization code interception attacks.
- No Client Secret Needed – Ideal for public apps.
- Works with Any OAuth 2.0 Provider – Including Google, Auth0, Okta, Keycloak.
- Standards Compliant – Defined by RFC 7636 and integrated into OAuth 2.1.
- Seamless with JWT – The final tokens can still be JWTs without affecting PKCE logic.
9. Summary
| Concept | Description |
|---|---|
| PKCE (Proof Key for Code Exchange) | Security enhancement to OAuth 2.0 Authorization Code Flow that replaces the need for client secret. |
| Code Verifier | Random string generated by the client. |
| Code Challenge | Hashed version of the verifier sent to the authorization server. |
| JWT (JSON Web Token) | Encoded token format representing user identity and claims. |
| Purpose | Prevent token issuance to attackers who intercept authorization codes. |
| Use Cases | Mobile apps, SPAs, desktop clients, and other public apps. |
