Start Learning
Javaneer
Back to stage
Module 12·Spring Authorization Server

Authorization Code & PKCE

The authorization-code flow step by step, why the implicit flow is dead, and how PKCE secures public clients like SPAs and mobile apps.

16 min readAdvanced
On this page

The authorization-code flow is the grant behind nearly every "log in" button you've ever clicked. It's designed so the client obtains a token without ever touching the user's password, using a two-step exchange: first a short-lived code, then a token. For public clients that can't keep a secret - SPAs, mobile apps - PKCE closes the one remaining hole. Trace the flow once and it stops being mysterious.

The flow, step by step

For a BookVault member logging into the SPA:

1. SPA redirects the browser to the auth server:
   GET /oauth2/authorize?response_type=code&client_id=bookvault-web
       &redirect_uri=https://app.bookvault.dev/callback&scope=openid catalog:read&state=xyz

2. Auth server authenticates the user (login form) and asks consent.

3. Auth server redirects back with a short-lived CODE:
   https://app.bookvault.dev/callback?code=ABC123&state=xyz

4. SPA exchanges the code for tokens (back-channel POST):
   POST /oauth2/token   grant_type=authorization_code&code=ABC123&redirect_uri=...

5. Auth server returns: access_token, id_token, (refresh_token).

The genius is the two steps. The code travels through the browser (the front channel, visible in URLs and history) but is useless on its own - short-lived, single-use, and bound to the client. The tokens are only ever returned in the direct /token response (the back channel), never exposed in a URL.

Always check 'state'

The state parameter you send in step 1 must come back unchanged in step 3. The client verifies it to prevent CSRF on the callback - an attacker can't forge a callback for a flow the user didn't start. Spring's OAuth2 client handles this for you, but if you implement a client by hand, never skip it.

Why the implicit flow died

The old implicit flow skipped the code and returned the access token directly in the redirect URL (step 3). That put a real token in browser history, server logs, and Referer headers - leaky and unrevocable. It existed because browsers once couldn't make cross-origin back-channel calls. CORS fixed that, so the implicit flow is now removed from the OAuth 2.1 guidance. Always use authorization code, even for SPAs.

PKCE: securing public clients

Authorization code was designed assuming the client has a secret to authenticate the token exchange in step 4. But a SPA's JavaScript or a mobile app's binary can't hide a secret - anyone can read it. So how does the auth server know the app redeeming the code is the same app that started the flow?

PKCE (Proof Key for Code Exchange, "pixie") solves it with a per-request secret:

1. Client generates a random `code_verifier`, and sends its SHA-256 hash
   as `code_challenge` on the /authorize request.
2. Auth server remembers the challenge alongside the issued code.
3. On the /token exchange, the client sends the original `code_verifier`.
4. Auth server hashes it and checks it matches the stored challenge.

A stolen code is now worthless without the matching code_verifier, which never left the client. PKCE started as a mobile fix and is now recommended for every client, including confidential ones. In Spring Authorization Server you require it per client with ClientSettings.builder().requireProofKey(true).

A coat check with a torn ticket

Imagine checking a coat. The implicit flow is the attendant handing you the coat itself at the door - if someone shoulder-surfs, they grab your coat. Authorization code is getting a numbered ticket at the door (the code) and redeeming it at the counter for the coat (the token) - a stolen ticket alone is suspicious and short-lived. PKCE is the attendant tearing the ticket in half: you keep the jagged other half (code_verifier) and must present it at the counter so the torn edges match. A thief who copies your ticket number still can't produce the matching torn edge, so the coat stays safe.

Why not just give the SPA a client secret?

A teammate suggests skipping PKCE and instead baking a client secret into the BookVault SPA's JavaScript to authenticate the token exchange. Explain why that doesn't work, and what PKCE provides that a hardcoded secret can't.

What problem does PKCE solve in the authorization-code flow?

Key takeaways

  • The authorization-code flow exchanges a short-lived, front-channel code for tokens on a direct back-channel call, so tokens never appear in URLs.
  • Always verify the state parameter round-trips unchanged to prevent CSRF on the callback.
  • The implicit flow returned tokens directly in the redirect URL and is now removed - use authorization code even for SPAs.
  • PKCE lets public clients (SPAs, mobile) that can't hold a secret prove they started the flow via a per-request code_verifier/code_challenge pair.
  • A stolen code is useless without the matching verifier; PKCE is now recommended for all clients - enable it with requireProofKey(true).
Was this lesson helpful?
Edit this page on GitHub