open atlas
↑ Back to track
Security SEC · 02 · 02

Authorization code flow with PKCE

How OAuth 2.1''''s mandatory PKCE prevents code interception, why exact-match redirect URIs block open-redirect exfiltration, and what state and nonce defend against.

SEC Middle ◷ 10 min
Level
FoundationsJuniorMiddleSenior

An attacker monitors a mobile app’s outgoing requests and captures the one-time authorization code in the redirect URL. Without PKCE, they exchange the code for tokens and own the account. PKCE is the lock that makes a stolen code worthless. In the next ten minutes you’ll see exactly how that lock works, what open-redirect and CSRF vectors look like without it, and which three parameters OAuth 2.1 makes non-negotiable.

How PKCE works — step by step

PKCE (Proof Key for Code Exchange, RFC 7636) adds a cryptographic challenge to the authorization code flow. OAuth 2.1 mandates it for every client, including confidential server-side apps.

Before the redirect:

  1. Client generates a code_verifier — a random 43–128 character URL-safe string (cryptographically random).
  2. Client computes code_challenge = base64url(SHA-256(code_verifier)).
  3. Client sends the code_challenge (but NOT the code_verifier) to the authorization server in the /authorize request.

After user consent: 4. Authorization server returns a one-time code in the redirect. It stores the code_challenge alongside the code.

Token exchange: 5. Client POSTs to /token with the code AND the original code_verifier. 6. Authorization server hashes the code_verifier, compares to the stored code_challenge. Match → tokens issued. No match → request rejected.

An attacker who intercepts the code in step 4 cannot redeem it — they need code_verifier, which was never sent over the wire.

Together, these six steps mean the authorization code becomes worthless without its matching secret. Without step 2 (the hash binding), any intercepted code could be freely exchanged for tokens — which is the exact attack PKCE was designed to close.

PKCE code binding
1Client generates code_verifier (random 43–128 chars, stays local)
2Client computes code_challenge = base64url(SHA-256(verifier))
3/authorize request carries code_challenge (hash only, verifier stays secret)
4Auth server stores challenge → issues code in redirect URL
5/token request carries code + code_verifier — auth server hashes, compares, mints tokens
Attacker gets the code in step 4 but not the verifier → /token call rejected.

The exact-match redirect URI rule

When you register a redirect URI with an identity provider, what stops an attacker from hijacking the code mid-flight by supplying their own destination? That’s exactly what open-redirect chaining exploits — and it’s why this rule exists.

OAuth 2.1 requires the redirect_uri in the token request to exactly match a pre-registered URI — no wildcards, no path prefixes, no partial matches.

Without this rule: an attacker can trigger a flow with redirect_uri=https://yourapp.com/comments?next=https://attacker.com. If your app has any open-redirect on /comments, the authorization code lands on the attacker’s server. The exact-match rule shuts this down: the URI must be character-for-character identical to a registered value.

For native apps where you cannot reliably register a URL (deep links, custom URI schemes), OAuth 2.1 + RFC 8252 require either http://127.0.0.1:RANDOM_PORT (loopback) or a private-use URI scheme registered with the OS.

State and nonce: two separate anti-attack tokens

State is OAuth’s anti-CSRF: the client generates a random value at /authorize time, the server echoes it back in the redirect, the client verifies it matches. Without state, an attacker can send a victim a crafted /authorize URL pointing to the attacker’s redirect — on click, the victim’s browser silently completes the flow under the attacker’s account.

Nonce is OIDC’s anti-replay: the client sends a random value in /authorize; the IdP embeds it inside the id_token. The client verifies the nonce in the token matches what it sent. Without nonce, an attacker who captures an id_token from another channel can replay it to a different session.

Both are random per-request. Both are validated server-side. Neither is optional in modern flows.

Quiz

Why does OAuth 2.1 mandate PKCE for ALL clients, including confidential server-side clients?

Quiz

A client validates the id_token signature and iss, but skips the nonce check. What attack is enabled?

Order the steps

Put the full authorization code + PKCE flow in order:

  1. 1 Client generates code_verifier and code_challenge (SHA-256 of verifier)
  2. 2 Client redirects browser to /authorize with code_challenge, state, nonce, client_id, redirect_uri
  3. 3 User authenticates at the authorization server and approves scopes
  4. 4 Authorization server redirects to redirect_uri with code and state
  5. 5 Client verifies state matches; POSTs to /token with code + code_verifier
  6. 6 Authorization server hashes verifier, compares to stored challenge, issues access + ID tokens
  7. 7 Client validates ID token (signature, iss, aud, exp, nonce) and reads user identity
Recall before you leave
  1. 01
    What does PKCE prevent, and how?
  2. 02
    Why does OAuth 2.1 require exact-match redirect URIs?
  3. 03
    What is the difference between the state parameter and the nonce parameter?
Recap

The authorization code flow with PKCE is the only grant type allowed for user-facing apps in OAuth 2.1. PKCE prevents code interception: the verifier never leaves the client, so a stolen code cannot be redeemed. Exact-match redirect URIs prevent open-redirect code exfiltration. The state and nonce parameters close two independent attack windows — CSRF on the flow and replay on the ID token. All three are mandatory in OAuth 2.1; skipping any one is a documented CVE vector. Now when you review an OAuth integration, check three things first: is PKCE enabled with S256, is the redirect URI registered exactly, and are state and nonce both validated on the callback?

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 5 done
Connected lessons
appears again in228

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

Apply this

Put this lesson to work on a real build.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.