Keycloak and Enterprise Identity
The PKCE Flow, One Step at a Time
Summary
PKCE is a mechanism that attaches to an authorization code the proof "this code belongs to a request I started". Even without a secret, intercepting the code becomes useless.
Why this was needed
In the days when mobile apps received callbacks through custom URL schemes, an attack in which a malicious app registered the same scheme and intercepted the authorization code was actually possible. This was because with the code alone you could obtain tokens (since a public client has no secret).
PKCE solves this problem. It makes it so that even if you intercept the code, you cannot obtain tokens.
How it works
Here is the flow in order.
First, the client creates a random string code_verifier. It is 43 to 128 characters and uses only URL-safe characters. Only the client knows this value.
Next, it computes code_challenge = BASE64URL(SHA256(code_verifier)). It sends code_challenge and code_challenge_method=S256 in the authorization request URL. The specification also has a plain method, but it does not hash, so it is meaningless.
When the user logs in at the authentication server, the authorization code comes back to the redirect_uri. At this time state also comes back, and you must always check that it is the same as the value the client originally sent. If it differs, it is not a request you started.
Finally, you send the original code_verifier together with the authorization code to the token endpoint. The server recomputes the hash and compares it with the stored code_challenge. If they match, it gives you the tokens.
Even if an attacker intercepted the code, they do not know the code_verifier, so the exchange fails.
What you meet in the field
Incidents often happen in redirect URI registration. If you register a broad wildcard such as http://localhost:* or https://example.com/*, tokens leak even if only one open redirector exists on that domain. The rule is to register the exact path.
Refresh token rotation is also a practical default. Each time you refresh, you give a new refresh token and invalidate the old one. If an old refresh token is used again, you suspect theft and cut off that entire session. The author's recommended combination is a short access token (5–15 minutes) and refresh token rotation — you get a practical invalidation effect without a blacklist.
And the BFF pattern is worth considering. The backend keeps the tokens without giving them to the browser at all and communicates only through a session cookie. The path by which tokens get stolen through XSS itself disappears.
What PKCE prevents
If you use the authorization code flow without PKCE, the code can be intercepted while it travels in the redirect URL. In mobile apps, it was especially dangerous because another app could intercept a custom scheme (myapp://).
1. 클라이언트가 무작위 verifier 를 만든다
verifier = base64url(random(32~96 bytes))
2. challenge = base64url(sha256(verifier)) ← 이것만 보낸다
3. 인가 요청에 challenge 를 실어 보낸다
4. 코드를 받아 토큰으로 바꿀 때 verifier 를 보낸다
5. 서버가 sha256(verifier) == challenge 인지 확인
An attacker who intercepted the code cannot exchange it for tokens, because they do not know the verifier. The one-way nature of the hash guarantees this.
For code_challenge_method, always use S256. plain sends the verifier as is, so it prevents nothing.
These days PKCE is recommended for web apps too. It used to be only for public clients (mobile and SPA), but OAuth 2.1 requires it for every authorization code flow.
state and nonce prevent different things
The three are easy to confuse, but each blocks a different attack.
| Value | What it prevents | Where it is checked |
|---|---|---|
state |
CSRF — attaching someone else's login to my session | When returning via the redirect |
nonce |
ID token replay attack | The claim in the ID token |
| PKCE | Authorization code interception | At token exchange |
You use all three. You store state in the session and compare it with the value that came back, and nonce is carried inside the ID token as is, so you check that.
The redirect URI must match exactly
This is the most common configuration incident. The URI registered at the authentication server and the URI in the request must be the same down to the last character.
등록: https://app.example.com/callback
요청: https://app.example.com/callback/ ← 슬래시 하나 차이로 거절
Do not use wildcards. If you allow https://app.example.com/*, the code can be sent to any path of that domain, so a single XSS leaks the code.
For local development, register even the port, as in http://localhost:3000/callback. If the port changes, you must register again, so it is convenient for the team to fix the development port.
What you will do in the next lab
You complete the entire PKCE flow with curl and scripts. You create a verifier and a challenge, send the authorization request, POST the login form to get the code, exchange it for tokens, and even confirm that it fails with a wrong verifier. Then in a separate lab you attach middleware that verifies that token to a FastAPI app.