Security
Part 7 of 7 · OAuth & OIDCOAuth Threat Model — CSRF, Token Leakage, Confused Deputy & Common Pitfalls
OAuth security is a threat model: login CSRF (state), code interception (PKCE), token leakage, confused deputy (aud), refresh theft (rotation), and mix-up attacks. Map each attack to controls across this cluster rather than treating OAuth as a grant-type checklist.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Controls beat slogans
Prefer
Attack → control across the cluster
Each failure has a named binder: state, PKCE, aud, redirect allowlist, family revoke, BFF. Interviews want the map, not 'we use OAuth'.
- state binds the browser session to the redirect.
- PKCE binds the code to the starter.
- aud / resource indicators bind the token to one API.
- Rotation binds reuse to theft of the refresh family.
Alternative
Grant-type cosplay
Implicit plus localStorage, CSRF tokens on forms but no OAuth state, signature-only JWT checks. Looks like OAuth on the slide.
- We use OAuth so we're secure.
- Substring redirect matching — app.example.evil.com.
- Skipping aud because 'the signature passed'.
- One wildcard callback for every environment.
Read the map top to bottom
The tables and diagrams below expand each row.
- 1
Login CSRF
Attacker plants their own callback code in the victim browser. Control: opaque state bound to the client session, plus OIDC nonce. - 2
Code interception
Stolen redirect code. Control: PKCE S256 and claimed HTTPS redirects. - 3
Confused deputy
Token for A accepted by B. Control: strict aud / resource indicators. - 4
Refresh replay
Stolen RT reused. Control: rotation plus family revoke. - 5
Mix-up / leakage
Wrong AS or tokens in logs. Control: issuer allowlist; never put tokens in query strings.
Overview
OAuth security is a threat model, not a grant-type checklist. Classic failures: login CSRF (missing state), token leakage (query logs, XSS, Referer), authorization code interception (mitigated by PKCE), confused deputy (token audience / redirect URI mix-ups), and refresh theft.
This page maps attacks → controls across the cluster rather than treating OAuth as a menu of grants.
Attack → primary control
| Attack | Idea | Primary controls | Cluster study |
|---|---|---|---|
| Login CSRF / session swap | Attacker plants their own callback code in the victim browser | Opaque state bound to session; OIDC nonce on the ID token | Hub, OIDC |
| Code interception | Steal code from redirect | PKCE S256; claimed HTTPS redirects | Hub |
| Token in browser storage | XSS exfiltrates AT/RT | BFF httpOnly; short AT; rotation | BFF, Refresh |
Open redirect / bad redirect_uri | Code/token sent to attacker | Exact allowlist; no wildcards | Hub |
| Confused deputy | Token for A accepted by B | Strict aud / resource indicators | JWT, S2S |
| Refresh replay | Stolen RT reused | Rotation + reuse detection | Refresh |
| Mix-up | Client talks to an evil AS | Issuer allowlist; iss in auth response (BCP) | Hub / JWT |
| Implicit / RO password | Front-channel tokens / phishing API | Removed in OAuth 2.1 | Hub |
What fails if you choose wrong
- "We use OAuth so we're secure" with implicit +
localStorage. - CSRF token on forms but no OAuth
state. - Validating JWT signature only — skipping
aud/iss. - One redirect URI allowlisting via naive substring match.
CSRF on the OAuth redirect
- Attacker runs the authorize flow on the victim app with their own account and stops before the client redeems the code, keeping the callback URL (
code=attacker_code). - Attacker makes the victim's browser load that callback URL (link, img tag, or auto-submitting form).
- If the client does not check
state, it redeems the attacker code and the victim is signed in as the attacker.
Control: Generate state in the client session before redirect; verify exact match on callback. Prefer ≥128 bits of entropy. For OIDC, also verify nonce in the id_token.
Sequence
- 1
Attacker → Auth Server
Step 1 Attacker logs in with an own account
- 2
Auth Server → Attacker
Step 2 Callback URL with the attacker code - attacker stops here
- 3
Attacker → Victim browser
Step 3 Lure the victim to the client callback with that code
- 4
Victim browser → Client app
Step 4 GET /callback?code=attacker_code
- 5
Client app → Victim browser
Step 5 state missing or mismatched - reject
- 6
Client app → Auth Server
Failure path - client redeems the attacker code
- 7
Client app → Victim browser
Victim is signed in as the attacker - data lands in the attacker account
Lesson map
OAuth Threat Model — CSRF, Token Leakage, Confused Deputy & Common Pitfalls
OAuth security is a threat model: login CSRF (state), code interception (PKCE), token leakage, confused deputy (aud), refresh theft (rotation), and mix-up attacks. Map each attack to controls across this cluster rather than treating OAuth as a grant-type checklist.
Architecture. Attacker Ready. Victim browser Ready. Client app Ready. Auth Server Ready. Failure path - client redeems the attacker code Ready
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB a["Attacker Ready"] v["Victim browser Ready"] c["Client app Ready"] as["Auth Server Ready"] fail["Failure path - client redeems the attacker code Ready"] a -->|Step 1 Attacker logs in with an own account| as as -->|Step 2 Callback URL with the attacker code - attacker stops here| a a -->|Step 3 Lure the victim to the client callback with that code| v v -->|Step 4 GET /callback?code=attacker_code| c c -->|Step 5 state missing or mismatched - reject| v c -->|Failure path - client redeems the attacker code| fail c -->|Victim is signed in as the attacker - data lands in the attacker account| v
Step 5 rejects a missing or mismatched state with a reply to the victim browser. That check is not a labeled self-loop on the client.
Token leakage channels
- URL query (never put tokens in query; codes still leak via logs/Referer — keep codes short-lived and one-time).
- Browser history / analytics scripts reading locations.
- Server access logs on
redirect_uri. - XSS reading memory /
localStorage. - Mobile OS URL handlers.
- Intermediate proxies decrypting TLS (corporate MITM) — assume possible for browser traffic.
Confused deputy (classic OAuth)
A client with an access token for API A is tricked into using it against API B, or an AS issues tokens without binding to the intended resource.
Controls: Resource Indicators (RFC 8707), strict aud, per-API tokens, avoid "god scope", sender-constrained tokens (DPoP / mTLS).
Decisions
- 1
Stolen or overly broad token
- nextAPI A intended
- nextAPI B deputy
- 2
API A intended
- 3
API B deputy
- nextaud matches B?
- ?
aud matches B?
- Step1 reject401
- Step2 missing audData breach or action as user
- 5
401
- 6
Data breach or action as user
A decision node fans reject vs accept. Do not hang two labeled outcomes off a self-loop.
Authorization code pitfalls
- Reuse of codes (must be one-time).
- Long code lifetime.
- Accepting a code at the token endpoint with a mismatched
redirect_uri. - Public client without PKCE.
Mix-up attacks (brief)
When multiple authorization servers exist, a client might send a code to the wrong token endpoint or accept tokens from an unexpected issuer. Respond with issuer identification in responses and pre-registered AS metadata — never trust attacker-supplied JWKS URLs.
Sandbox: state + redirect allowlist (Python)
Exact callback match, then exact state. Forged state and evil.example both fail closed.
Press Run. Snippets must be self-contained — no network, files, or native modules.
Sandbox: audience enforce (TypeScript)
The brake on confused deputy. Array or string aud both work; missing expected audience throws.
Press Run. Snippets must be self-contained — no network, files, or native modules.
Pitfalls
From a blank page, write the top three controls you would require in a design review. Then add the fourth (mix-up / issuer allowlist) and the fifth (refresh family revoke). Cross-link each to a sibling study.
Interview Q&A
What does state prevent?
Answer
Cross-site login CSRF / session-swap: binding the redirect to the initiating browser session. It is not PKCE and it is not nonce.
Why is implicit dangerous?
Answer
Access tokens in front-channel URLs — many leakage paths — and no PKCE-style proof at the token endpoint. OAuth 2.1 removed it.
Explain confused deputy in OAuth.
PKCE vs state?
Answer
Complementary. PKCE binds the code to the client instance that started the flow, and RFC 9700 lets clients rely on it against CSRF when the AS enforces PKCE. state still binds the callback to the session and carries app state, so send both.
How does refresh reuse detection help theft?
Answer
Two parties using the same RT lineage triggers family revoke. See refresh rotation.
BFF and XSS?
Answer
Reduces token exfiltration. The attacker can still abuse the live browser session — still need CSP / XSS hygiene. See BFF vs SPA.
Open redirect vs OAuth redirect_uri?
Answer
If your app open-redirects after login, attackers chain it. Keep post-login redirects allowlisted separately from the OAuth redirect_uri allowlist.
Top three production checks?
Answer
(1) PKCE + state / nonce. (2) Exact redirect allowlist. (3) JWT iss / aud / exp with a short AT plus refresh rotation. Then add issuer allowlists and family revoke.
What is an OAuth mix-up attack?
Answer
The client is tricked into sending a code or accepting tokens from the wrong authorization server. Pre-register AS metadata; never fetch JWKS from attacker-controlled claims.
Why not put the access token in the authorize redirect?
Answer
That is implicit. History, Referer, logs, XSS. Codes are already leaky enough — that is why they are one-time, short-lived, and PKCE-bound.