Top 5
OWASP category for broken authentication -- OAuth/OIDC misconfigurations are the primary cause
34%
of OAuth implementations tested by PortSwigger research contained at least one exploitable vulnerability
PKCE
eliminates authorization code interception attacks -- yet fewer than 60% of public clients implement it

OAuth 2.0 is an authorization framework; OpenID Connect (OIDC) builds an identity layer on top of it. Together they handle authentication and delegated authorization for the majority of modern applications. The problem: both specifications are complex, implementation guidance is fragmented, and the security implications of each design choice are rarely obvious until something goes wrong. This guide covers the OAuth 2.0 and OIDC attack surface, the vulnerabilities most commonly found in real-world bug bounty and penetration testing engagements, and the concrete controls that close them.

OAuth 2.0 and OIDC Architecture: What Can Go Wrong

Understanding the attack surface starts with the protocol components.

OAuth 2.0 grant types and their risk profiles:

Grant TypeUse CaseSecurity Risk
Authorization Code + PKCEWeb apps, mobile, SPAsLow when PKCE enforced
Authorization Code (no PKCE)Legacy server-side appsMedium -- code interception risk
Client CredentialsMachine-to-machineLow -- no user involved
Device CodeSmart TVs, CLI toolsLow if bound to client
ImplicitLegacy SPAs (deprecated)High -- tokens in URL fragment
Resource Owner PasswordLegacy (deprecated)Very High -- credentials to third party

The implicit flow and resource owner password credentials (ROPC) grant are deprecated in OAuth 2.1 (draft). Any application still using them should be migrated.

OIDC key components:

  • Authorization Server (AS) / Identity Provider (IdP) -- issues tokens (Okta, Entra ID, Keycloak)
  • Client -- your application requesting access
  • Resource Server (RS) -- API validating access tokens
  • ID Token -- JWT containing identity claims (sub, email, name)
  • Access Token -- credential to call protected APIs
  • Refresh Token -- long-lived credential to get new access tokens

The attack surface exists at every boundary: client-to-AS (authorization request), AS-to-client (code/token response), client-to-RS (API call), and token validation at the RS.

Authorization Code Interception and PKCE

The vulnerability:

In the authorization code flow without PKCE, an attacker who can intercept the authorization code (via a malicious redirect URI, a compromised browser extension, or a logging intermediary) can exchange it for tokens. This is particularly dangerous for mobile apps where deep links can be hijacked.

PKCE (Proof Key for Code Exchange) -- RFC 7636:

PKCE binds the authorization code to the specific client instance that requested it. The client generates a random code_verifier, hashes it to produce a code_challenge, and includes the challenge in the authorization request. The code cannot be exchanged without the original verifier.

// Step 1: Generate code_verifier (43-128 chars, URL-safe base64)
const codeVerifier = generateRandomString(64);
const codeChallenge = base64url(sha256(codeVerifier));

// Step 2: Authorization request includes code_challenge
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('state', generateState());

// Step 3: Token exchange includes code_verifier
const tokenResponse = await fetch('https://auth.example.com/token', {
  method: 'POST',
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: authorizationCode,
    redirect_uri: REDIRECT_URI,
    client_id: CLIENT_ID,
    code_verifier: codeVerifier,  // server validates against challenge
  }),
});

PKCE requirements:

  • Use S256 method (SHA-256 hash) -- never plain
  • Require PKCE even for confidential clients (OAuth 2.1 mandates this)
  • Minimum code_verifier length: 43 characters

Authorization server enforcement: Configure your AS to reject authorization requests without PKCE for all public clients. In Keycloak, set "PKCE Code Challenge Method" to S256 on the client. In Okta, enable PKCE under Application > Sign-On Policy.

Free daily briefing

Briefings like this, every morning before 9am.

Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.

Open Redirect and State Parameter Attacks

Open redirect attacks on redirect_uri:

The redirect_uri is where the authorization code is delivered after user authentication. If an authorization server does not exactly match the registered redirect_uri, an attacker can manipulate it to redirect the code to their own server.

Common bypass techniques:

  • Path traversal: https://app.example.com/callback/../evil
  • Subdomain confusion: https://attacker.app.example.com/callback (if AS matches only suffix)
  • Parameter pollution: https://app.example.com/callback?redirect=evil
  • Regex bypass: https://app.example.com@evil.com/callback

Mitigation: Register exact redirect URIs (no wildcards, no regex) and enforce exact string matching on the AS. This is a requirement in RFC 6749 Section 3.1.2 but many AS implementations offer "fuzzy" matching for developer convenience.

State parameter CSRF:

The state parameter is OAuth's CSRF token for the authorization flow. If omitted or predictable, an attacker can initiate a CSRF attack that causes a victim to bind their OAuth session to an attacker-controlled authorization code:

// Correct implementation
const state = crypto.randomBytes(32).toString('hex');
sessionStorage.setItem('oauth_state', state);

// After redirect, verify state matches before exchanging code:
if (params.get('state') !== sessionStorage.getItem('oauth_state')) {
  throw new Error('State mismatch -- possible CSRF attack');
}

nonce parameter (OIDC):

The nonce is included in the authorization request and must appear in the returned ID token. It prevents replay attacks where a stolen ID token is submitted to a different client:

const nonce = crypto.randomBytes(16).toString('hex');
// Include in authorization request as nonce parameter
// After token exchange, verify id_token.nonce === nonce

JWT Vulnerabilities in OIDC and API Authorization

OIDC ID tokens and OAuth access tokens are typically JWTs. JWT implementations have a well-documented history of critical vulnerabilities.

Algorithm confusion attacks (alg=none / RS256 to HS256 confusion):

Early JWT libraries accepted "alg": "none" -- a signed but unsigned token that any server would accept. Modern libraries fixed this, but the RS256-to-HS256 confusion attack remains relevant:

  • If a server supports both RS256 and HS256 and uses the RS256 public key as the HS256 secret (which some implementations do), an attacker can forge tokens by signing with the public key using HS256

Mitigation: Explicitly specify the allowed algorithms -- never allow the token's alg header to determine the validation algorithm:

// Bad: lets the token declare its own algorithm
jwt.verify(token, publicKey);

// Good: enforces the expected algorithm
jwt.verify(token, publicKey, { algorithms: ['RS256'] });

Missing or improper aud claim validation:

The aud (audience) claim in a JWT specifies which resource server the token is intended for. If a resource server accepts tokens intended for any audience, an attacker who obtains a token for Service A can use it against Service B.

Always validate: token.aud === expected_audience

Token leakage via Referer header:

If access tokens are included in URL parameters (a legacy pattern still seen in some OAuth flows), they will appear in server access logs and Referer headers when users navigate to external links.

# Bad: token in URL
GET /dashboard?access_token=eyJ...

# Good: token in Authorization header
GET /api/data HTTP/1.1
Authorization: Bearer eyJ...

JWT signature validation bypass (CVE patterns):

Some libraries have had vulnerabilities where malformed JWTs (truncated signatures, extra bytes) bypassed validation. Keep your JWT library patched and pin a minimum version in your dependency manifest.

Recommended JWT validation checklist:

  • Verify signature using the correct algorithm and key
  • Validate exp (not expired)
  • Validate nbf (not used before)
  • Validate iss (trusted issuer)
  • Validate aud (intended for this resource server)
  • Validate nonce (OIDC ID tokens)
  • Reject alg: none

OAuth Misconfiguration Patterns Found in Pen Tests

These vulnerabilities appear repeatedly in real application security assessments.

1. Scope over-provisioning:

Applications request scope=openid profile email but also tack on offline_access or vendor-specific scopes unnecessarily. Attackers who steal access tokens with broad scopes can access more data than intended. Request the minimum scopes required for the specific operation.

2. Refresh token rotation not enforced:

Long-lived refresh tokens that are not rotated on use allow an attacker with a stolen refresh token unlimited lifetime. Enforce refresh token rotation: each use invalidates the current token and issues a new one. Detect reuse: if an already-rotated token is presented, treat it as a compromise indicator and revoke the entire token family.

3. Implicit grant still enabled:

Despite deprecation, many authorization servers still allow the implicit flow for backward compatibility. Attackers can force the implicit flow by changing response_type=token in the authorization request if the AS allows it. Disable the implicit grant on your AS.

4. Client secret in mobile/SPA code:

OAuth confidential clients (those with client secrets) must store the secret server-side. Mobile apps and SPAs are public clients -- they cannot protect a secret. Any client secret embedded in an app binary or JavaScript bundle can be extracted. Use PKCE instead of client secrets for public clients.

5. Missing PKCE on mobile apps (deep link hijacking):

Android and iOS allow multiple apps to register the same custom URL scheme. Without PKCE, a malicious app registered for myapp://callback can receive the authorization code intended for the legitimate app and exchange it for tokens. PKCE makes the intercepted code useless.

6. Token endpoint not rate-limited:

The token endpoint accepts authorization code exchanges. Without rate limiting, attackers can enumerate or brute-force authorization codes (though PKCE mitigates this significantly). Apply rate limiting to the token endpoint: max 10 requests per minute per client_id.

Hardening Checklist for OAuth 2.0 and OIDC Implementations

Authorization Server configuration:

ControlAction
PKCE enforcementRequire S256 for all public clients; require for confidential clients per OAuth 2.1
Redirect URI matchingExact string match only -- no wildcards, no regex
Disable implicit grantresponse_type=token should return error
Disable ROPC grantBlock resource owner password credentials
Refresh token rotationEnable automatic rotation on every use
Token lifetimesAccess token: 15 min; refresh token: 8-24h for web; longer for offline apps with rotation
Rate limitingEnforce on /authorize and /token endpoints
Consent screenShow scopes being granted; do not auto-skip consent for first-party apps

Client implementation:

ControlAction
Always use stateCryptographically random, validated on return
Always use nonce (OIDC)Validated against ID token after exchange
Never put tokens in URLsUse Authorization header for API calls
Store tokens securelyWeb: HttpOnly cookie or secure sessionStorage; Mobile: Keychain/Keystore
Validate JWT fullyiss, aud, exp, nbf, alg, nonce
Use short-lived access tokensPrefer 5-15 minute lifetime, refresh as needed
Implement token revocationSupport RFC 7009 revocation endpoint

Testing your OAuth implementation:

Use PortSwigger's OAuth labs in Burp Suite Academy and the oauth-security-testing guide to verify:

  • Manipulate state parameter (remove it, use a static value)
  • Manipulate redirect_uri (path traversal, subdomain variations)
  • Remove code_challenge from PKCE flow
  • Change response_type to token (test implicit grant acceptance)
  • Replay authorization codes
  • Test aud claim validation across services

The bottom line

OAuth 2.0 and OIDC vulnerabilities are almost always implementation problems, not specification problems. The specification is clear: use PKCE, validate state, validate JWT claims, use exact redirect URI matching, and avoid deprecated grant types. The gap between specification clarity and implementation reality is where account takeover attacks live. Enforce PKCE at the authorization server level so individual applications cannot opt out, rotate refresh tokens, and run a dedicated OAuth security review as part of any identity or authentication feature launch.

Frequently asked questions

What is the difference between OAuth 2.0 and OpenID Connect?

OAuth 2.0 is an authorization framework -- it defines how an application can obtain delegated permission to access a resource on behalf of a user, without the user sharing their credentials. It does not define how to verify user identity. OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0 that adds the ID token (a JWT containing the user's identity claims) and a UserInfo endpoint. Use OAuth 2.0 when you need delegated API access; use OIDC when you need to authenticate users and know who they are.

Is PKCE required if my application already uses a client secret?

In OAuth 2.1 (draft), PKCE is required for all clients including confidential clients with client secrets. The rationale: PKCE protects against authorization code interception in cases where a confidential client secret may be compromised or where the authorization code is delivered over a channel that could be intercepted. For any new implementation, require PKCE regardless of client type. For existing confidential clients, adding PKCE is a low-friction security improvement.

How should mobile apps handle OAuth tokens securely?

iOS: Store tokens in the Keychain using kSecClassGenericPassword with kSecAttrAccessibleWhenUnlocked. Never store in UserDefaults or plain files. Android: Use the EncryptedSharedPreferences API (backed by Android Keystore). For both: use authorization code + PKCE (never implicit flow), use AppAuth SDK rather than building your own flow, implement refresh token rotation, and use short-lived access tokens (5-15 minutes) with refresh as needed. Do not embed client secrets in app binaries -- use public client configuration.

What are the security differences between storing tokens in cookies vs. localStorage?

HttpOnly cookies: not accessible to JavaScript, which prevents XSS from stealing tokens; but vulnerable to CSRF if SameSite is not set to Strict or Lax. localStorage: accessible to JavaScript on the same origin, which means any XSS vulnerability can steal the token. The current best practice for web SPAs is to use HttpOnly, Secure cookies with SameSite=Strict for refresh tokens, and keep access tokens in memory (JavaScript variable) with short lifetimes. Never store tokens in localStorage for high-security applications.

How do I audit our existing OAuth implementation for vulnerabilities?

Use Burp Suite Pro with the built-in OAuth scanner and PortSwigger's OAuth testing methodology. Manual test checklist: (1) remove the state parameter and verify the authorization server rejects it or the callback rejects the missing state; (2) try path traversal on redirect_uri; (3) remove code_challenge from PKCE requests to see if the AS accepts them; (4) change response_type to token to check if implicit grant is enabled; (5) replay an authorization code to test single-use enforcement; (6) decode ID tokens and verify iss, aud, and exp are validated by your application.

What is the security risk of the 'Login with Google/GitHub' social login pattern?

Social login (third-party OIDC) introduces risks from the third-party IdP: if a user's Google account is compromised, an attacker can access all applications where that user authenticated via Google. Mitigations: always bind social login identities to a local account by sub claim (the persistent user ID), never bind by email alone (emails can be changed or reused), implement suspicious login detection for unusual geographic or device patterns, and require MFA at the application level for high-privilege actions even if the social IdP did not prompt for it. For high-security applications, avoid relying solely on social login.

Sources & references

  1. OAuth 2.0 Security Best Current Practice (RFC 9700)
  2. PKCE RFC 7636
  3. PortSwigger OAuth Testing Guide
  4. OWASP Testing Guide: OAuth

Free resources

25
Free download

Critical CVE Reference Card 2025–2026

25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.

No spam. Unsubscribe anytime.

Free download

Ransomware Incident Response Playbook

Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.

No spam. Unsubscribe anytime.

Free newsletter

Get threat intel before your inbox does.

50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, weekly, no spam.

Unsubscribe anytime. We never sell your data.

Eric Bang
Author

Founder & Cybersecurity Evangelist, Decryption Digest

Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals weekly.

Free Brief

The Mythos Brief is free.

AI that finds 27-year-old zero-days. What it means for your security program.

Joins Decryption Digest. Unsubscribe anytime.

Daily Briefing

Get briefings like this every morning

Actionable threat intelligence for working practitioners. Free. No spam. Trusted by 50,000+ SOC analysts, CISOs, and security engineers.

Unsubscribe anytime.

Mythos Brief

Anthropic's AI finds zero-days your scanners miss.