OAuth2 in Depth: PKCE, Scopes and Refresh Tokens
You'll learn to
- -Explain what PKCE protects against and how the verifier and challenge work
- -Design scopes and a consent screen that grant the least privilege needed
- -Rotate refresh tokens and detect a stolen one through reuse
- -Validate redirect URIs exactly and use the state parameter
The authorization code flow from the Developer Platforms chapter is the right shape, but a few details decide whether it is actually safe. Mobile apps and single-page apps cannot keep a client secret, an authorization code can be stolen in transit, and a long-lived refresh token is a juicy target. This chapter covers the parts an authorization server has to get right.
An Analogy: A Claim Ticket With a Secret Word
You leave your car with a valet and get a claim ticket. If anyone who finds the ticket can collect the car, a stolen ticket is a stolen car. Now suppose you also write a secret word on the stub the valet keeps, and you must say that word to collect. A thief with only the ticket gets nowhere. PKCE is that secret word for the authorization code.
The Problem PKCE Solves
In the code flow, the authorization server redirects the browser back to the app with a one-time code in the URL. On a phone, another installed app can register the same redirect address and read that URL. A public client has no client secret to prove that it is the app that asked for the code, so the thief could swap the code for a token. PKCE (Proof Key for Code Exchange, RFC 7636) closes this gap, and the OAuth 2.0 security best current practice (RFC 9700) asks for it with every kind of client.
How PKCE Works
- -The app creates a random secret, the code_verifier, and keeps it in memory.
- -It sends the code_challenge, the base64url-encoded SHA-256 hash of the verifier, with the authorize request.
- -The server stores the challenge next to the code it issues.
- -When the app exchanges the code, it sends the original verifier. The server hashes it and compares it with the stored challenge.
Here are the four hops in order. Each picture shows one hop, so you can see what travels, and which secret never leaves the app until the very last step.
It keeps the verifier to itself and sends only the hash of it.
A thief who reads the redirect sees only the code, not the verifier.
The server hashes the verifier and compares it with the stored challenge.
The access token is short-lived. The refresh token is rotated on every use.
Because the hash cannot be reversed, an attacker who saw the challenge and the code still cannot produce the verifier. The block below uses the example from the RFC, so you can check the challenge against the standard. Press Run.
Scopes and Consent
A scope is a named permission, such as photos.read or payments.write. The app asks for the scopes it needs, the user sees them on a consent screen in plain words, and the access token carries the scopes that were actually granted, which may be fewer than requested. Ask for the minimum, split read from write, and ask for extra scopes at the moment a feature needs them, not all at sign-up.
A scope is a ceiling, not a permission slip for everything. The API must still check, on every call, that the token has the scope the endpoint needs and that the user owns the object being touched. A token with photos.read may read its own owner's photos and nobody else's.
Short Access Tokens and Rotating Refresh Tokens
Access tokens live for minutes, so a leak expires quickly. A refresh token lives longer and is used only to get new access tokens. To limit the damage of a stolen refresh token, rotate it: every time it is used, the server issues a new refresh token and invalidates the old one. All the tokens from one login form a family. If an old token is ever presented again, someone copied it, so the server revokes the whole family and forces a fresh sign-in.
The first call works and hands out sam-phone-r2. The second presents the old token again, which is the signature of a copy, so the family is revoked. The third shows the cost: even the genuine newest token no longer works, and the real user signs in again. That inconvenience is the price of stopping a thief who holds a copy. For public clients, the best current practice accepts either rotation or tokens bound to the client's own key.
Now try the same rules by hand. Refresh a few times as the real app, then play the thief by replaying the first token, and watch every token in the family change state, including the newest one.
Rotate Refresh Tokens, Catch a Replay
Each time the app refreshes, the server hands out a new refresh token and spends the old one. Press the real app's button a few times, then let a thief replay the first token and watch what happens to every token in the family.
- No requests yet.
Only the newest token is valid. A token that has been used once is spent, so a copy of an old one is useless unless the server can be fooled, and the moment it is presented the server finds out.
Redirect URIs and the State Parameter
- -Register each redirect URI in advance and match it exactly, character for character. A pattern or a prefix match lets an attacker steer the code to their own address.
- -Send a random state value with the authorize request and check that it comes back unchanged, so a forged callback from another site is rejected.
- -Never put tokens in URLs that get logged. The authorization code is short-lived and single-use for exactly that reason.
When you design an authorization server, the checklist is short: PKCE for every client, exact redirect URI matching, narrow scopes with readable consent, short access tokens, and rotating refresh tokens with reuse detection.
Interview Signal is part of Pro
See a real weak answer next to a real strong one for this exact topic.
Quiz is part of Pro
Test what you just read with a short quiz, and bank the XP.
Authorization Server and Scopes and Consent in the API Design Lab.
Discussion0
Join the Discussion
Sign in to leave comments, reply to others, or like insights.
No comments yet. Be the first to start the thread!