API Security: OAuth2 Vulnerabilities and How to Exploit Them

OAuth2 is everywhere. Every “Sign in with Google/GitHub/Facebook” button, every API that uses Bearer tokens, every mobile app that connects to a backend — they all rely on OAuth2 or its identity layer, OpenID Connect (OIDC).

And yet, OAuth2 is notoriously easy to implement wrong.

This article explores the most common OAuth2 vulnerabilities, how attackers exploit them, and how to defend your implementations.

OAuth2 Crash Course

Before attacking OAuth2, let’s understand the flow:

┌──────────┐                              ┌──────────────┐
│   User   │                              │   Resource   │
│ (Browser)│                              │    Server    │
└────┬─────┘                              └──────▲───────┘
     │                                           │
     │ 1. Click "Login with X"                   │
     ▼                                           │
┌────────────┐  2. Redirect to Auth Server  ┌────┴────────┐
│   Client   │ ────────────────────────────►│Authorization│
│   (App)    │                              │   Server    │
└────┬───────┘◄─────────────────────────────└─────────────┘
     │         3. Auth code + redirect

     │ 4. Exchange code for tokens

   Access Token + (Refresh Token)

Key components:

  • Authorization Server: Issues tokens (Google, GitHub, your IdP)
  • Client: Your application requesting access
  • Resource Server: API that accepts tokens
  • Redirect URI: Where the auth server sends the user back

Vulnerability #1: Open Redirect in redirect_uri

The redirect_uri parameter tells the auth server where to send the authorization code. If not properly validated, attackers can steal codes.

The Attack

https://auth.example.com/authorize?
  client_id=legit-app&
  redirect_uri=https://attacker.com/steal&
  response_type=code&
  scope=openid%20email

If the auth server allows this redirect, the authorization code goes to the attacker.

Variations

Subdomain takeover:

redirect_uri=https://abandoned.legit-app.com/callback

Path traversal:

redirect_uri=https://legit-app.com/callback/../../../attacker-page

Parameter pollution:

redirect_uri=https://legit-app.com/callback?next=https://attacker.com

Defense

  • Exact match validation: Don’t allow wildcards or partial matches
  • Pre-register all redirect URIs: No dynamic URIs
  • Use state parameter: Prevents CSRF and replay attacks
# Bad: Partial matching
if redirect_uri.startswith(registered_uri):
    allow()

# Good: Exact matching
if redirect_uri in registered_uris:
    allow()

Vulnerability #2: Authorization Code Interception

Even with a valid redirect_uri, the authorization code can be intercepted.

Mobile App Interception

Custom URL schemes (like myapp://callback) can be registered by malicious apps:

1. User installs malicious app
2. Malicious app registers myapp:// scheme
3. User logs into legitimate app
4. Auth code sent to myapp://callback
5. Malicious app intercepts code first

Defense: PKCE (Proof Key for Code Exchange)

PKCE adds a challenge-response mechanism:

import secrets
import hashlib
import base64

# Client generates verifier (random string)
code_verifier = secrets.token_urlsafe(32)

# Client creates challenge (hash of verifier)
code_challenge = base64.urlsafe_b64encode(
    hashlib.sha256(code_verifier.encode()).digest()
).decode().rstrip('=')

# Authorization request includes challenge
auth_url = f"""
https://auth.example.com/authorize?
  client_id=myapp&
  code_challenge={code_challenge}&
  code_challenge_method=S256&
  redirect_uri=myapp://callback&
  response_type=code
"""

# Token request includes verifier
token_request = {
    "grant_type": "authorization_code",
    "code": authorization_code,
    "code_verifier": code_verifier,  # Server verifies this matches challenge
    "redirect_uri": "myapp://callback"
}

Even if an attacker intercepts the code, they can’t exchange it without the verifier.

Vulnerability #3: Token Leakage

Access tokens can leak through various channels.

Referrer Header Leakage

If a page with a token in the URL links to an external site:

<!-- User lands on: https://app.com/dashboard#access_token=secret123 -->
<a href="https://external-analytics.com">Click here</a>
<!-- Referrer header sends: https://app.com/dashboard#access_token=secret123 -->

Browser History

Tokens in URLs are saved in browser history:

https://app.com/callback?access_token=eyJhbGc...

Anyone with access to the browser can extract tokens.

Defense

  • Use authorization code flow, not implicit flow
  • Never put tokens in URLs — use POST body or headers
  • Set short token lifetimes (15 min for access tokens)
  • Use Referrer-Policy: no-referrer

Vulnerability #4: Insufficient Scope Validation

Apps often request more permissions than they need, and users don’t notice.

The Attack

# App requests excessive scopes
https://auth.example.com/authorize?
  client_id=simple-todo-app&
  scope=openid%20email%20read:contacts%20write:files%20admin

A “todo app” requesting admin access? Most users click “Allow” without reading.

Defense

  • Principle of least privilege: Request only needed scopes
  • Incremental authorization: Request additional scopes only when needed
  • Scope auditing: Review what scopes apps actually use

Vulnerability #5: JWT Token Attacks

Many OAuth2 implementations use JWTs as access tokens. JWTs have their own vulnerabilities.

Algorithm Confusion

# Attacker modifies JWT header
{
  "alg": "none",  # Changed from RS256
  "typ": "JWT"
}

# Or switches from RS256 to HS256
# If server uses public key as HMAC secret, attacker can forge tokens

Defense

# Always explicitly specify allowed algorithms
import jwt

token = jwt.decode(
    encoded_token,
    public_key,
    algorithms=["RS256"],  # Only allow RS256
    audience="my-api",
    issuer="https://auth.example.com"
)

Key ID (kid) Injection

The kid header specifies which key to use for verification:

{
  "alg": "RS256",
  "kid": "../../../../../../etc/passwd"
}

If the server uses kid to construct a file path, this leads to arbitrary file read.

Vulnerability #6: State Parameter Bypass (CSRF)

The state parameter prevents CSRF attacks. Without it:

The Attack

  1. Attacker initiates OAuth flow on their own account
  2. Attacker gets authorization URL (before redirect)
  3. Attacker sends URL to victim
  4. Victim clicks, completes auth
  5. Victim’s account is now linked to attacker’s identity

Defense

import secrets

# Generate unpredictable state
state = secrets.token_urlsafe(32)
session['oauth_state'] = state

# Include in authorization request
auth_url = f"...&state={state}"

# Verify on callback
if request.args['state'] != session.get('oauth_state'):
    abort(403, "State mismatch - possible CSRF")

Testing Checklist

When pentesting OAuth2 implementations:

□ Test redirect_uri validation
  - Subdomain variations
  - Path traversal
  - Parameter pollution
  - Different protocols (http vs https)

□ Check for PKCE implementation (especially mobile)

□ Test state parameter
  - Is it present?
  - Is it validated?
  - Is it unpredictable?

□ Examine token handling
  - Where are tokens stored?
  - Are they in URLs?
  - Token lifetime?

□ JWT-specific tests (if applicable)
  - Algorithm confusion
  - kid injection
  - Signature verification

□ Scope testing
  - Can you escalate scopes?
  - Are scopes validated server-side?

Tools

  • Burp Suite: Intercept and modify OAuth flows
  • OWASP ZAP: Automated OAuth security scanning
  • jwt.io: Decode and debug JWTs
  • oauth.tools: Test and visualize OAuth flows

Conclusion

OAuth2 is a powerful framework, but its flexibility is also its weakness. Every implementation is different, and small misconfigurations lead to total account takeover.

Key takeaways:

  • Always use PKCE for public clients
  • Validate redirect_uri with exact matching
  • Never put tokens in URLs
  • Implement and verify the state parameter
  • Validate JWT algorithms explicitly

The spec is complex. The attacks are simple. Test thoroughly.


Next: Learn about Securing AI Agents for modern authentication challenges.