cctbp.com | Tom Stacy, CISSP®
Every practitioner who has spent time in OAuth flows knows the authorization code exists for exactly one reason: to keep the access token out of the browser. The code travels through the front channel; the redirect, the query string, the browser history. The token is supposed to stay in the back channel, exchanged server-to-server where an attacker can’t see it.
That separation is the entire security model. When it breaks, it breaks completely.
This post covers authorization code interception; what the attack actually looks like end-to-end, where the seam is, and what it produces in logs. It’s the first technique post in this series, and it’s the right place to start because everything else in OAuth security is downstream of understanding this flow precisely.
All the scenarios in this post are reproducible against FlawedToken, a purpose-built vulnerable OAuth environment I’ve released alongside this series. One command and you have a running authorization server and client app with the misconfigurations active. Details at the end.
The Setup
OAuth 2.0 authorization code flow, by the book:
- Client sends user to the authorization endpoint with
response_type=code,client_id,redirect_uri, andstate - User authenticates at the authorization server
- Authorization server issues a short-lived code and redirects the user’s browser to
redirect_uri?code=AUTH_CODE&state=STATE - Client application receives the code at its callback endpoint
- Client exchanges the code for an access token via a back-channel POST to the token endpoint, including
client_id,client_secret, and the code - Authorization server validates everything and issues the access token
The code is single-use and short-lived, typically 60 seconds. The access token never touches the browser. That’s the design.
The attack surface is step 3. The code travels through the browser as a query parameter. If an attacker can intercept it before the legitimate client’s callback handler fires, they can exchange it themselves.
The Seam
Two conditions make this possible:
Condition 1: The authorization server does not bind the code to the client’s state value.
RFC 6749 defines the state parameter as an opaque value the client generates at the start of the flow and validates on return. Its primary documented purpose is CSRF protection. Its secondary, less-discussed purpose is binding: if the authorization server records the state value at code issuance and requires the token exchange request to prove possession of it, an intercepted code is useless without the matching state.
Most authorization servers do not enforce this binding. State validation is implemented client-side, and only as CSRF protection. The server issues the code, the code travels through the browser, and any party who presents it to the token endpoint gets a token.
Condition 2: The attacker can read the authorization code.
This happens more often than it should:
- The code appears in server-side access logs via
Refererheader leakage when the callback page loads third-party resources - Browser history, proxy logs, or intermediary caches capture the redirect
- A logging misconfiguration writes query parameters to a log aggregator the attacker can reach
- An open redirect on the callback domain allows the attacker to redirect the code to a URI they control (covered in the next post)
- The attacker has a position on the network, even briefly
If either condition is absent the attack fails. In practice, condition 1 is almost universally true. Condition 2 is the variable.
Reproducing It
Prerequisites
- FlawedToken running locally, setup below
FLAW_CODE_INTERCEPTION=onin your.env(default)- Intercepting proxy like mitmproxy, ZAP, or Burp Suite
- A second terminal for the token exchange
Step 1: Start the flow
Open http://localhost:8000 and click Login via OAuth. The client app redirects to:
http://localhost:8001/authorize?response_type=code
&client_id=flawedtoken-client
&redirect_uri=http://localhost:8000/callback
&scope=openid+profile+email
&state=<random_state>
Step 2: Authenticate as the victim
Log in with alice / password. The authorization server issues a code and redirects:
http://localhost:8000/callback?code=<AUTH_CODE>&state=<random_state>
Step 3: Intercept the code
In mitmproxy, intercept the redirect response before it reaches the callback. In the browser network tab, watch for the 302 from localhost:8001; the Location header contains the code in plaintext.
Copy the code value. Drop or forward the request — it doesn’t matter for the demo. If the legitimate client exchanges it first, your attempt will fail with invalid_grant. If you move fast enough, you win.
Step 4: Exchange the intercepted code
From a separate terminal simulating the attacker’s session:
curl -s -X POST http://localhost:8001/token \
-d "grant_type=authorization_code" \
-d "code=<AUTH_CODE>" \
-d "redirect_uri=http://localhost:8000/callback" \
-d "client_id=flawedtoken-client" \
-d "client_secret=flawedtoken-secret" | python3 -m json.tool
Response:
{
"access_token": "gdvxrmPY45xmBqW48TjR9HFErkA9YSd...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile email"
}
The token belongs to alice. The legitimate client gets invalid_grant when it arrives at the callback, as the code has already been consumed.
Step 5: Validate the token
curl -s http://localhost:8001/userinfo \
-H "Authorization: Bearer <ACCESS_TOKEN>" | python3 -m json.tool
{
"sub": "alice",
"name": "Alice Example",
"email": "[email protected]",
"username": "alice"
}
Full account takeover. One intercepted code, no credentials required.
Fixed Behavior
Set FLAW_CODE_INTERCEPTION=off in .env and restart:
docker compose down && docker compose up
With the flaw disabled, the authorization server enforces client binding on the code. An exchange attempt from a mismatched client is rejected:
{
"error": "invalid_grant",
"detail": "client binding mismatch"
}
The correct implementation goes further: PKCE (RFC 7636) cryptographically binds the code to a verifier the client generates and holds in memory. The token endpoint requires the verifier. An intercepted code without it is worthless. PKCE is the right fix because a state binding alone is defense-in-depth, not a complete mitigation. State lives in the redirect and can itself be intercepted, whereas PKCE’s verifier never leaves the client.
Defender Perspective
This attack produces a specific signature in logs. What to look for:
Failed token exchange immediately following a successful one. The legitimate client arrives at the callback, posts to the token endpoint, and gets invalid_grant. The code was already consumed. A 400 invalid_grant on the token endpoint within seconds of a successful authorization response is the primary indicator.
Token exchange from a different IP than the authorization request. The authorization request originates from the victim’s browser. The attacker’s exchange comes from a different host. Correlate client_id and state across the authorization log and token log; a mismatch in source IP is suspicious.
Unusual user-agent on the token endpoint. Browser-driven flows produce a consistent user-agent on the authorization request. A curl or scripted exchange against the token endpoint has a different signature entirely.
Authorization codes in Referer headers. If your callback page loads any external resources (analytics, fonts, or CDN assets), the full callback URL including the code appears in the Referer header of those requests. Check your third-party request logs.
Detection rule sketch for a SIEM:
event: token_endpoint_request
AND response: invalid_grant
AND time_since_authorization_code_issued < 10s
→ alert: possible authorization code interception
Tune the time window to your authorization server’s code TTL. FlawedToken defaults to 60 seconds.
Takeaway
For red teamers: Authorization code interception is rarely about a single sophisticated technique. It’s about finding one of several mundane conditions such as a logging misconfiguration, a third-party script on the callback page, a Referer header leaking to an analytics endpoint, and combining it with an authorization server that doesn’t enforce binding. The conditions are common. The attack is underutilized because practitioners haven’t built the muscle memory. That’s what this lab is for.
For defenders: The primary control is PKCE, enforced server-side, not optional. The primary detection is correlating failed token exchanges against recent authorization codes by client and source IP. If your authorization server doesn’t log both endpoints with enough fidelity to do that correlation, fix the logging before you fix anything else.
The Lab: FlawedToken
The scenarios in this post run against FlawedToken, a purpose-built vulnerable OAuth 2.0 environment I’ve released as open source alongside this series.
git clone https://github.com/tstacy/flawedtoken.git
cd flawedtoken
cp .env.example .env
docker compose up
- Client app:
http://localhost:8000 - Auth server:
http://localhost:8001 - Flaw status:
http://localhost:8001/debug/flaws
Every misconfiguration is toggleable via environment variable. Set FLAW_CODE_INTERCEPTION=off to see the fixed behavior side by side with the broken one. The /docs directory has full walkthroughs for each flaw.
FlawedToken is intentionally broken. Use it in authorized lab environments only.
FlawedToken on GitHub → github.com/tstacy/flawedtoken
What’s Next
The next post covers redirect URI manipulation, the other half of the authorization code attack surface. Instead of intercepting the code in transit, the attacker gets the authorization server to deliver it directly to a URI they control. Same outcome, different entry point, and far more common in the wild than most practitioners realize.
The lab scenario runs against FlawedToken with FLAW_REDIRECT_URI_VALIDATION=on. The walkthrough is already in /docs if you want to get ahead of it.
Posts go out every Friday. The waitlist for ShroudCloud (the platform being built to automate these flows against authorized targets) is open at shroudcloud.com.
Tom Stacy, CISSP®, is an authentication and identity security specialist. He writes at cctbp.com and is building ShroudCloud™, an engagement infrastructure for auth and session security.