Back to OAuth2 & OpenID Connect
Q3Question 3 of 5

PKCE (Proof Key for Code Exchange) for Mobile and SPA Clients

Key Concepts
  • PKCE secures the Authorization Code flow for public clients (mobile apps, SPAs) that cannot safely store a client secret.
  • The client generates a random code_verifier and derives a code_challenge by SHA-256 hashing it.
  • Only the code_challenge is sent in the authorization request; the code_verifier is kept secret by the client.
  • At token exchange, the client sends the code_verifier; the server hashes it and checks it matches the stored challenge.
  • A stolen authorization code is useless without the code_verifier that only the legitimate client holds.
  • Modern guidance recommends PKCE even for confidential clients as defense in depth; the Implicit flow is deprecated.

Definition

PKCE binds the code to a one-time secret only the real client knows, so a stolen code cannot be redeemed.
  • PKCE (Proof Key for Code Exchange) – an OAuth2 extension that secures the Authorization Code flow for public clients that cannot safely keep a client secret.
  • code_verifier – a high-entropy random string (43-128 characters) generated by the client and kept secret.
  • code_challenge – the SHA-256 hash of the code_verifier, Base64Url-encoded, sent in the authorization request.
  • Binding – at token exchange, the client sends the original code_verifier; the server re-hashes it and checks the match.
  • Protection – an intercepted authorization code is useless to an attacker who lacks the code_verifier.

A public app cannot keep a secret, so National Bank of India uses a one-time trick. When Rajesh starts logging in, his app invents a random secret phrase, keeps it hidden, and gives the bank only a sealed fingerprint of it (the hash). Later, to collect the access pass, the app must whisper the original phrase; the bank fingerprints the phrase on the spot and checks it matches the sealed fingerprint from the start. A thief who grabbed only the claim ticket cannot collect the pass because they never knew the secret phrase that produced the fingerprint.

Understanding the Concepts

What is PKCE and why does it exist?

PKCE (pronounced 'pixie') is an OAuth2 extension designed for public clients: mobile apps and single-page apps that cannot safely store a client secret. A mobile app can be decompiled, and a browser app's code is fully visible, so any secret embedded in them is not really a secret.

Without PKCE, an attacker who intercepts the authorization code (for example via a malicious app on the same custom URL scheme on a mobile device) could exchange it for a token. PKCE binds the code to a one-time verifier that only the legitimate client holds.

TEXT
The problem without PKCE:
  1. Rajesh's mobile app starts OAuth2 login
  2. Bank redirects back with ?code=AUTH_CODE
  3. Malicious app on the device intercepts the redirect
  4. Malicious app exchanges code for token --> account compromised!

With PKCE:
  3. Malicious app intercepts the code BUT
  4. Cannot present the code_verifier --> exchange rejected!

How does PKCE work step by step?

The client first creates a code_verifier: a random 43-to-128 character string, typically from 32 secure random bytes Base64Url-encoded. It then computes the code_challenge as BASE64URL(SHA256(code_verifier)) and sends only the challenge in the authorization request.

When the code comes back, the client exchanges it at the token endpoint and includes the original code_verifier. The server recomputes SHA256(code_verifier), Base64Url-encodes it, and compares it to the stored challenge. If they match, the token is issued.

JAVA
package com.bank.oauth2;

import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;

public class PkceGenerator {

    public static String generateCodeVerifier() {
        byte[] bytes = new byte[32];
        new SecureRandom().nextBytes(bytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
    }

    public static String generateCodeChallenge(String codeVerifier) throws Exception {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hash = digest.digest(codeVerifier.getBytes("US-ASCII"));
        return Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
    }

    public static void main(String[] args) throws Exception {
        String verifier = generateCodeVerifier();
        String challenge = generateCodeChallenge(verifier);

        System.out.println("code_verifier:  " + verifier);
        System.out.println("code_challenge: " + challenge);
        System.out.println("method:         S256");

        // Verify: server re-hashes verifier and checks match
        String recomputed = generateCodeChallenge(verifier);
        System.out.println("Match: " + challenge.equals(recomputed));
    }
}
TEXT
code_verifier:  dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
code_challenge: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
method:         S256
Match: true

How does the full PKCE flow look?

The PKCE flow is the standard Authorization Code flow with two extra parameters: code_challenge in the authorization request and code_verifier in the token exchange.

TEXT
Step 1: Generate verifier and challenge
  code_verifier  = random_43_to_128_chars
  code_challenge = BASE64URL(SHA256(code_verifier))

Step 2: Authorization request (front-channel)
  GET /authorize?response_type=code
      &client_id=mobile-app
      &redirect_uri=myapp://callback
      &scope=transactions:read
      &state=random_state
      &code_challenge=E9Melhoa2Ow...
      &code_challenge_method=S256

Step 3: User consents, server redirects with code
  302 myapp://callback?code=AUTH_CODE_789&state=random_state

Step 4: Token exchange (back-channel) with verifier
  POST /token
  grant_type=authorization_code
  &code=AUTH_CODE_789
  &redirect_uri=myapp://callback
  &client_id=mobile-app
  &code_verifier=dBjftJeZ4CVP...

Step 5: Server verifies
  SHA256(code_verifier) == stored code_challenge? --> Issue token

How does the server validate PKCE?

The Authorization Server stores the code_challenge and code_challenge_method when it issues the authorization code. When the client presents the code and the code_verifier, the server recomputes the challenge and compares.

JAVA
package com.bank.oauth2;

import java.security.MessageDigest;
import java.util.Base64;

public class PkceValidator {

    public boolean validate(String codeVerifier, String storedChallenge, String method) {
        try {
            if ("S256".equals(method)) {
                MessageDigest digest = MessageDigest.getInstance("SHA-256");
                byte[] hash = digest.digest(codeVerifier.getBytes("US-ASCII"));
                String computed = Base64.getUrlEncoder().withoutPadding()
                    .encodeToString(hash);
                boolean match = computed.equals(storedChallenge);
                System.out.println(match ? "PKCE valid: token issued."
                    : "PKCE invalid: verifier does not match challenge.");
                return match;
            } else if ("plain".equals(method)) {
                return codeVerifier.equals(storedChallenge);
            }
        } catch (Exception e) {
            System.out.println("Validation error: " + e.getMessage());
        }
        return false;
    }
}
TEXT
Legitimate client: code_verifier matches stored code_challenge
  --> PKCE valid: token issued.

Attacker with stolen code but wrong verifier:
  --> PKCE invalid: verifier does not match challenge.

What is the difference between S256 and plain methods?

The S256 method hashes the code_verifier with SHA-256 before comparing. The plain method sends the code_verifier as the code_challenge directly (no hashing).

The plain method offers no security: an attacker who intercepts the challenge in the authorization request has the verifier. Always use S256.

| Method | Security | How it works |
|---|---|---|
| S256 (recommended) | Strong | challenge = SHA256(verifier); verifier never exposed |
| plain (weak) | Weak | challenge = verifier; exposed in the authorize request |

How is PKCE different from client_secret?

| Aspect | Client Secret | PKCE |
|---|---|---|
| Who uses it | Confidential clients (server-side) | Public clients (mobile, SPA) |
| Longevity | Long-lived, shared secret | One-time, per-flow verifier |
| Storage | Server-side config or secrets manager | Generated fresh for each login |
| Can be extracted | Not from a server | Secret embedded in app can be extracted |
| Flow | Authorization Code | Authorization Code + PKCE |

JAVA
// Confidential client: exchanges code with client_secret
// POST /token ... &client_secret=SECRET_456

// Public client with PKCE: exchanges code with code_verifier
// POST /token ... &code_verifier=dBjftJeZ4CVP...

// Both prove the caller is the same entity that started the flow.

What are common mistakes?

The first mistake is using the plain method instead of S256. This defeats the purpose of PKCE because the challenge equals the verifier.

The second mistake is reusing the same code_verifier across multiple flows. The verifier must be fresh and random for every authorization request.

JAVA
// WRONG: using plain method
String challenge = codeVerifier; // challenge == verifier, no security!
// &code_challenge_method=plain

// CORRECT: using S256
String challenge = Base64.getUrlEncoder().withoutPadding()
    .encodeToString(MessageDigest.getInstance("SHA-256")
    .digest(codeVerifier.getBytes()));
// &code_challenge_method=S256

What should a senior engineer know?

Modern OAuth2 guidance (RFC 9126, OAuth 2.1 draft) recommends PKCE for all clients, including confidential ones, as defense in depth. PKCE complements, and does not replace, TLS, exact redirect_uri matching, and the state parameter for CSRF.

The Implicit flow is deprecated in favor of Authorization Code plus PKCE for SPAs, because the Implicit flow returned the token in the URL fragment where it was easy to leak.

Visual Overview

DIAGRAM
  PKCE Flow:
  ┌──────────────┐
  │ Mobile App   │  1. Generate:
  │              │     verifier = random_string
  │              │     challenge = SHA256(verifier)
  └──────┬───────┘
         │
         │  2. GET /authorize?code_challenge=SHA256_HASH
         ▼
  ┌──────────────┐
  │  Auth Server │  Stores challenge with issued code
  │              │
  │              │  3. Redirect: ?code=ABC
  └──────┬───────┘
         │
         ▼
  ┌──────────────┐
  │ Mobile App   │  4. POST /token code=ABC&code_verifier=ORIGINAL
  │              │
  └──────┬───────┘
         │
         ▼
  ┌──────────────┐
  │  Auth Server │  5. SHA256(verifier) == stored_challenge?
  │              │     Yes --> issue token
  │              │     No  --> reject (stolen code!)
  └──────────────┘
DIAGRAM
  Why PKCE Defeats Code Interception:

  Legitimate App:          Malicious App:
  ┌──────────┐             ┌──────────┐
  │ Has code │             │ Has code │  (intercepted)
  │ Has      │             │ No       │
  │ verifier │             │ verifier │
  └────┬─────┘             └────┬─────┘
       │                        │
       ▼                        ▼
  Exchange succeeds        Exchange FAILS
  (verifier matches)       (no matching verifier)

Key Takeaway

PKCE, Proof Key for Code Exchange, is an OAuth2 extension that secures the Authorization Code flow for public clients: mobile apps and single-page apps that cannot safely store a client secret. The client generates a random code_verifier, hashes it with SHA-256 to produce a code_challenge, and sends only the challenge in the authorization request. When it later exchanges the code for a token, it sends the original verifier, and the Authorization Server hashes it and checks it matches the stored challenge.

In the National Bank of India mobile app, the app is a public client: it can be decompiled, so it cannot hold a secret. Without PKCE, an authorization code intercepted on the device, for example by a malicious app that registered the same custom URL scheme, could be redeemed by the attacker. PKCE closes that hole by binding the code to a one-time secret that only the legitimate app holds.

The server re-hashes the verifier and confirms it matches the stored challenge before issuing the token. When an attacker presents a stolen code with the wrong verifier, the hash does not match and the server refuses, so the stolen code is useless.

The key points to remember are: use the S256 method rather than the weaker plain method, generate a fresh verifier for every flow, and note that modern guidance recommends PKCE even for confidential clients as defense in depth. The old Implicit flow is deprecated in favor of Authorization Code plus PKCE for SPAs. PKCE specifically defeats code interception; it complements TLS, exact redirect_uri matching, and the state parameter.

Remember one sentence: PKCE binds the authorization code to a one-time verifier that only the legitimate client knows, so a stolen code is worthless.

---

Share
Use ← → arrow keys to navigate