TL;DR: The client sends the user to the authorization server with scopes, a
statevalue and a PKCEcode_challenge(hash of a secretcode_verifier). The user logs in and approves; the server redirects to the exactly registered redirect URI with a short-lived, single-use code. The client exchanges code plus verifier for tokens; the server issues them only if the verifier matches the challenge. Each part stops something: state stops CSRF, exact redirect matching stops code theft via open redirects, PKCE stops intercepted or injected codes being redeemed, and audience and scope checks stop tokens being misused elsewhere.
How to approach it
Draw the roles (user, client, authorization server, resource server), walk the flow in six steps, then do a second pass where each element gets the attack it prevents. Close with current best practice: RFC 9700 (January 2025).
A strong answer
Client app
holds code_verifier
Browser
user signs in
Auth server
issues code, tokens
- Front channel
Client app to Browser
1
1. Redirect: state, code_challenge
Also carries the requested scope. challenge = SHA-256(code_verifier); state ties the response to this browser session.
The flow.
- Client generates a random
code_verifier, computescode_challenge = SHA-256(verifier)(method S256), and generatesstate. - Browser is redirected to the authorization endpoint with
client_id,redirect_uri,scope,state,code_challenge. - User authenticates at the authorization server and consents to scopes.
- Server redirects to the registered
redirect_uriwithcodeandstate; the client checksstatematches what it stored for this browser session. - Client posts
codeandcode_verifier(plus client authentication if confidential) to the token endpoint. - Server verifies the verifier hashes to the challenge, the code is unused and unexpired, and the redirect URI matches; it returns an access token (and possibly a refresh token).
What each part stops.
| Element | Attack it stops |
|---|---|
| User authenticates at the authorization server | Client ever seeing the password |
| Scopes and consent | Over-broad access the user did not approve |
state bound to the client session | CSRF: attacker's code delivered into the victim's session |
| Exact redirect URI matching | Codes leaked to attacker URLs via loose matching or open redirects |
| Short-lived, single-use code | Replay of a captured code |
| PKCE verifier and challenge | Redemption of an intercepted code (mobile scheme hijack, logs, referrers) and code injection |
| Client authentication at the token endpoint | Impersonation of confidential clients |
| Audience and scope validation at the API | Tokens for one API being accepted by another |
Current best practice (RFC 9700). Use the authorization code flow with PKCE for all clients, including server-side ones; match redirect URIs exactly; do not use the implicit grant or the resource owner password grant; prefer sender-constrained tokens (DPoP or mTLS) where token theft is a concern; and keep refresh tokens rotated or sender-constrained for public clients.
Agents. An agent is just another OAuth client. Request the minimum scopes per feature, store refresh tokens like any other secret, and make every downstream API check the token's audience so a token cannot be replayed elsewhere.
What interviewers probe next
- "Why does a confidential server-side client need PKCE if it has a client secret?" PKCE also defeats authorization code injection, where an attacker plants a stolen code into a victim's session; the secret does not.
- "What is a mix-up attack?" With multiple authorization servers, an attacker tricks the client into sending a code to the wrong server; mitigate with the
issparameter in responses and per-server redirect URIs. - "Where should the SPA store tokens?" Ideally not in JavaScript-accessible storage; a backend-for-frontend holding tokens server-side with an HttpOnly session cookie is the safer pattern.
What if?
What if · interview follow-ups
Hold your answer as the constraints change
A single-page app and a mobile app let users sign in through an OAuth authorisation server and call your API with the resulting tokens.
Start here
How does the authorisation code flow with PKCE work?
What does the client keep secret during the flow?
The client sends the user to the authorisation server with scopes, a state value and a PKCE code challenge (a hash of a secret code verifier). After login and consent, the server redirects to the exactly registered redirect URI with a short-lived, single-use code, which the client exchanges with the verifier for tokens.
Carry it forwardCode plus verifier, exchanged at the token endpoint.
Weak answerUsing the implicit flow for browser apps.
Constraint changes
What does state stop?
What if an attacker sends a victim a callback link?
Cross-site request forgery on the callback. The client binds state to the user's session and checks it on return, so an attacker cannot make the victim's browser complete a login with the attacker's code, which would log the victim into the attacker's account.
Carry it forwardState binds the response to the session.
Weak answerSending a constant state value.
Constraint changes
What does PKCE stop?
What if malware on the phone intercepts the redirect?
Intercepted or injected codes being redeemed. Without the verifier, a stolen code is useless. That matters most for public clients such as mobile and single-page apps, which cannot hold a client secret, but PKCE is now recommended for all clients.
Carry it forwardPKCE makes stolen codes useless.
Weak answerSkipping PKCE because the client has a secret.
Constraint changes
Why must redirect URIs match exactly?
Where does the authorisation server send the code?
Loose matching (prefixes, wildcards) can let an attacker send the code to a URL they control, such as through an open redirect on an allowed domain. Exact string matching of pre-registered URIs closes that path.
Carry it forwardRegister and match redirect URIs exactly.
Weak answerAllowing wildcard subdomains.
Constraint changes
What must the API check on the access token?
Was this token meant for this API?
Issuer, audience (that the token was issued for this API), expiry and scopes, so a token issued for another service cannot be replayed here and a narrow token cannot do broad things.
Carry it forwardValidate audience and scope on every request.
Weak answerAccepting any token signed by the issuer.
Now defend it
How would you configure the flow for both apps?
Authorisation code flow with PKCE for both, a per-session state value, exactly registered redirect URIs (claimed HTTPS links or app links on mobile), short-lived single-use codes, minimal scopes, and API checks of issuer, audience, expiry and scope; keep tokens out of browser storage where a backend-for-frontend can hold them.
How it's scored
Weak: Describes OAuth as login, or walks through the implicit flow.
Hire: Gives the code flow with PKCE correctly and explains state and PKCE.
Strong Hire: Maps each element to an attack, cites current best practice, and covers mix-up, code injection and token storage for browser clients.
Common mistakes
- Using the plain PKCE method instead of S256.
- Prefix or wildcard matching of redirect URIs.
- Not binding
stateto the user's session. - APIs that accept any token from the provider without checking audience.