OAuth 2.0 Vulnerabilities: Every Attack and Defense Explained with Code
Why OAuth 2.0 Is Still Broken in 2026
OAuth 2.0 powers authentication for billions of users — Google, GitHub, Facebook, Microsoft — yet implementation errors remain one of the most common web vulnerabilities.
The problem isn't the spec. It's that OAuth has dozens of configuration options, and getting even one wrong opens a critical vulnerability.
| Vulnerability | Impact | Prevalence |
|---|---|---|
| Open Redirect in redirect_uri | Account hijacking | Very common |
| CSRF (missing state parameter) | Account linking attacks | Common |
| Authorization code interception | Full account takeover | Moderate |
| Token leakage via Referer header | Session hijacking | Common |
| PKCE bypass | Mobile app account takeover | Increasing |
| Scope escalation | Privilege elevation | Moderate |
Attack #1: Open Redirect via redirect_uri Manipulation
The most common OAuth vulnerability. If the OAuth provider doesn't strictly validate the redirect_uri, an attacker can steal authorization codes.
The Attack Flow
1. Attacker crafts malicious URL:
https://oauth.provider.com/authorize?
client_id=legit_app&
redirect_uri=https://legit-app.com.evil.com/callback&
response_type=code&
state=random
2. User clicks link, authenticates with provider
3. Provider redirects to attacker's domain with the auth code:
https://legit-app.com.evil.com/callback?code=AUTH_CODE_HERE
4. Attacker exchanges code for access token
5. Attacker now has access to user's account
Defense: Exact redirect_uri Matching
// ✅ Server-side: Validate redirect_uri EXACTLY
const ALLOWED_REDIRECTS = [
'https://myapp.com/auth/callback',
'https://staging.myapp.com/auth/callback',
];
function validateRedirectUri(uri: string): boolean {
return ALLOWED_REDIRECTS.includes(uri);
// ❌ Never use: uri.startsWith('https://myapp.com')
// ❌ Never use: uri.includes('myapp.com')
// ❌ Never use: regex matching on domain
}
Attack #2: CSRF — Missing State Parameter
Without a state parameter, an attacker can link their own OAuth account to a victim's session.
The Attack
1. Attacker starts OAuth flow, gets authorization code
2. Attacker crafts URL:
https://victim-app.com/callback?code=ATTACKERS_CODE
3. Victim visits the link (via phishing, embedded image, etc.)
4. Victim's account is now linked to attacker's Google account
5. Attacker can now log in to victim's account via "Login with Google"
Defense: Cryptographic State Parameter
import crypto from 'crypto';
// Generating the authorization URL
function buildAuthUrl(session: Session): string {
// ✅ Generate cryptographic random state
const state = crypto.randomBytes(32).toString('hex');
// Store state in session (or signed cookie)
session.oauthState = state;
return \`https://oauth.provider.com/authorize?${new URLSearchParams({
client_id: process.env.OAUTH_CLIENT_ID!,
redirect_uri: 'https://myapp.com/auth/callback',
response_type: 'code',
state: state,
scope: 'openid profile email',
})}\`;
}
// Handling the callback
function handleCallback(req: Request, session: Session) {
const { code, state } = req.query;
// ✅ Verify state matches
if (!state || state !== session.oauthState) {
throw new Error('CSRF detected: state mismatch');
}
// Clear state after use
delete session.oauthState;
// Exchange code for token...
}
Attack #3: Authorization Code Interception (Mobile Apps)
On mobile devices, custom URL schemes (e.g., myapp://callback) can be registered by any app — including malicious ones.
Defense: PKCE (Proof Key for Code Exchange)
import crypto from 'crypto';
// Step 1: Generate code verifier and challenge
function generatePKCE() {
const verifier = crypto.randomBytes(32)
.toString('base64url');
const challenge = crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
return { verifier, challenge };
}
// Step 2: Include challenge in authorization request
const { verifier, challenge } = generatePKCE();
// Store verifier securely
const authUrl = \`https://oauth.provider.com/authorize?${new URLSearchParams({
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
code_challenge: challenge,
code_challenge_method: 'S256',
})}\`;
// Step 3: Include verifier in token exchange
const tokenResponse = await fetch('https://oauth.provider.com/token', {
method: 'POST',
body: new URLSearchParams({
grant_type: 'authorization_code',
code: authorizationCode,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: verifier, // ✅ Proves we initiated the flow
}),
});
PKCE is now required for ALL OAuth clients — not just mobile apps (OAuth 2.1 draft).
Attack #4: Token Leakage via Referer Header
If tokens are in the URL (implicit flow), they leak via the Referer header when users click any external link.
1. OAuth redirects to: https://myapp.com/callback#access_token=SECRET_TOKEN
2. Page loads, user clicks a link to https://external-blog.com
3. External site receives Referer header:
Referer: https://myapp.com/callback#access_token=SECRET_TOKEN
4. External site now has the user's access token
Defense: Never Use Implicit Flow
❌ response_type=token (Implicit flow — tokens in URL)
✅ response_type=code (Authorization Code flow — codes in URL, tokens server-side)
Attack #5: Scope Escalation
1. App requests: scope=read_profile
2. Attacker modifies request: scope=read_profile admin delete_users
3. If provider doesn't validate against registered scopes,
the token gets elevated permissions
Defense: Validate Scopes Server-Side
const REGISTERED_SCOPES = ['openid', 'profile', 'email'];
function validateScopes(requestedScopes: string[]): string[] {
// ✅ Only allow scopes that are pre-registered
return requestedScopes.filter(s => REGISTERED_SCOPES.includes(s));
}
Production OAuth Checklist
- Exact redirect_uri matching (no wildcards, no regex)
- Cryptographic state parameter on every request
- PKCE enabled for all clients (public and confidential)
- Authorization Code flow only (never Implicit)
- Token storage in HttpOnly Secure cookies (not localStorage)
- Short-lived access tokens (15 min) with refresh rotation
- Scope validation against registered app permissions
- Token revocation on logout
- Rate limiting on token endpoint
- Monitor for token reuse and code replay attacks
Advertisement
Free Security Tools
Try our tools now
Expert Services
Get professional help
OWASP Top 10
Learn the top risks
Related Articles
Threat Modeling for Developers: STRIDE, PASTA & DREAD with Practical Examples
Threat modeling is the most cost-effective security activity — finding design flaws before writing code. This guide covers STRIDE, PASTA, and DREAD methodologies with real-world examples for web, API, and cloud applications.
Building a Security Champions Program: Scaling Security Across Dev Teams
Security teams can't review every line of code. Security Champions embed security expertise in every development team. This guide covers program design, champion selection, training, metrics, and sustaining engagement.
The Ultimate Secure Code Review Checklist for 2025
A comprehensive, language-agnostic checklist for secure code reviews. Use this as your team's standard for catching vulnerabilities before they reach production.