🔥 0
0
Lesson 5 of 6 20 min +200 XP

Security Best Practices

OAuth is secure when implemented correctly. But small mistakes can lead to serious vulnerabilities. Let's learn from real-world security issues.

Common OAuth Vulnerabilities

1. CSRF Attacks

Attacker tricks user into authorizing malicious app

2. Token Leakage

Tokens exposed in URLs, logs, or referrer headers

3. Code Interception

Authorization code stolen before exchange

4. Open Redirect

Malicious redirect_uri steals codes/tokens

1. CSRF Protection: The State Parameter

Without the state parameter, an attacker can trick you into linking their account.

The Attack

1. Attacker starts OAuth flow on your app
2. Gets authorization URL: yourapp.com/callback?code=ATTACKER_CODE
3. Tricks victim into clicking this URL
4. Victim's account is now linked to attacker's Google account
5. Attacker can now "Sign in with Google" to victim's account

The Defense

// ALWAYS use a state parameter
app.get('/login', (req, res) => {
  // Generate cryptographically random state
  const state = crypto.randomBytes(32).toString('hex');

  // Store in session (or signed cookie)
  req.session.oauthState = state;

  const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
  authUrl.searchParams.set('state', state);
  // ... other params

  res.redirect(authUrl.toString());
});

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

  // CRITICAL: Verify state before processing
  if (!state || state !== req.session.oauthState) {
    return res.status(403).json({ error: 'Invalid state parameter' });
  }

  // Clear state to prevent reuse
  delete req.session.oauthState;

  // Now safe to exchange code for tokens
  // ...
});
Never Skip State Validation

Even if you think CSRF isn't a risk, always validate state. It's a simple check that prevents a class of attacks. Many OAuth vulnerabilities in production apps stem from missing state validation.

2. Redirect URI Validation

The redirect_uri is where tokens are sent. If an attacker can control it, they steal tokens.

The Attack: Open Redirect

Legitimate: https://yourapp.com/callback
Malicious:  https://yourapp.com/callback/../../../attacker.com

// Or via subdomain:
Malicious:  https://evil.yourapp.com/callback

The Defense

// Server-side: Strict redirect URI validation
const ALLOWED_REDIRECT_URIS = [
  'https://yourapp.com/callback',
  'https://yourapp.com/oauth/callback'
];

function validateRedirectUri(uri) {
  // Exact match only - no pattern matching
  return ALLOWED_REDIRECT_URIS.includes(uri);
}

// Register exact URIs with the OAuth provider
// Never use wildcards like https://yourapp.com/*

Redirect URI Rules

  • Use exact match, never patterns or wildcards
  • Always use HTTPS (except localhost for development)
  • Validate on both client and server
  • Register all valid URIs with the OAuth provider

3. Token Storage

Where you store tokens matters. A lot.

Storage Risk Recommendation
localStorage High - XSS can steal tokens Avoid for sensitive tokens
sessionStorage Medium - XSS risk, per-tab Better, but still XSS vulnerable
httpOnly Cookie Low - Not accessible via JS Best for web apps
Memory only Lowest - Gone on refresh Most secure, worst UX

Secure Cookie Configuration

// Setting tokens in httpOnly cookies
app.get('/callback', async (req, res) => {
  const tokens = await exchangeCodeForTokens(req.query.code);

  // Access token in httpOnly cookie
  res.cookie('access_token', tokens.access_token, {
    httpOnly: true,     // Not accessible via JavaScript
    secure: true,       // HTTPS only
    sameSite: 'lax',    // CSRF protection
    maxAge: 3600000,    // 1 hour
    path: '/'
  });

  // Refresh token - even more restricted
  res.cookie('refresh_token', tokens.refresh_token, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict', // Only sent on same-site requests
    maxAge: 30 * 24 * 3600000, // 30 days
    path: '/api/auth'   // Only sent to auth endpoints
  });

  res.redirect('/dashboard');
});

4. Why Implicit Flow Was Deprecated

The Implicit flow returned tokens directly in the URL fragment:

https://yourapp.com/callback#access_token=abc123&token_type=bearer

Problems with Implicit Flow

Browser History

Token visible in browser history. Anyone with access to history can steal it.

Referrer Leakage

Clicking any link sends the URL (with token) in the Referer header.

Server Logs

Tokens may end up in proxy or CDN logs.

No Refresh Tokens

Can't silently refresh. User must re-authenticate.

The Solution: Auth Code + PKCE

// Instead of Implicit, use Authorization Code with PKCE
// Tokens are exchanged server-side, never in URLs

5. Token Lifetime Best Practices

Token Type Recommended Lifetime Reason
Access Token 15 minutes - 1 hour Limits damage if stolen
Refresh Token 7-30 days Balance security and UX
Authorization Code 30 seconds - 10 minutes Single-use, exchanged immediately

6. Refresh Token Rotation

Rotating refresh tokens limits the damage of token theft:

// When refreshing, issue a new refresh token
app.post('/api/auth/refresh', async (req, res) => {
  const { refresh_token } = req.cookies;

  // Verify and invalidate old refresh token
  const tokenData = await verifyAndRevokeRefreshToken(refresh_token);

  if (!tokenData) {
    // Token reuse detected - possible theft!
    await revokeAllUserTokens(tokenData.userId);
    return res.status(401).json({ error: 'Session invalidated' });
  }

  // Issue new tokens
  const newAccessToken = generateAccessToken(tokenData.userId);
  const newRefreshToken = generateRefreshToken(tokenData.userId);

  res.cookie('refresh_token', newRefreshToken, { /* secure options */ });
  res.json({ access_token: newAccessToken });
});
Refresh Token Rotation Benefits
  • Stolen refresh token becomes invalid after first use
  • Detects token theft (reuse attempt)
  • Can revoke all tokens if theft detected

7. Real-World Security Incidents

GitHub OAuth Attack (2022)

What Happened

Attackers stole OAuth tokens from Heroku and Travis CI's integration with GitHub. These tokens allowed access to private repositories of thousands of organizations.

Lesson: OAuth tokens are valuable targets. Encrypt at rest, monitor for anomalies, implement token binding.

Slack CSRF Vulnerability (2016)

What Happened

Missing state parameter validation allowed attackers to link their Slack account to victim's workspace, gaining access to private channels.

Lesson: Always validate the state parameter. This basic check prevents account takeover.

Security Checklist

Before Going to Production

  • State parameter validated on every callback
  • Redirect URIs are exact match only
  • PKCE implemented for public clients
  • Tokens stored in httpOnly cookies (web) or secure storage (mobile)
  • Access tokens are short-lived (< 1 hour)
  • Refresh token rotation enabled
  • Client secrets stored securely (not in code)
  • ID tokens validated (signature, issuer, audience, expiration)
  • Token revocation implemented for logout

Key Takeaways

  • Always use state parameter - Prevents CSRF attacks
  • Exact redirect URI matching - Prevents open redirect attacks
  • Use PKCE for public clients - Implicit flow is deprecated
  • httpOnly cookies for web - Protects against XSS token theft

Next up: OAuth Assessment - Test your knowledge with real-world scenarios.

🧠 Quick Quiz

Test your understanding of this lesson.

1

What attack does the 'state' parameter prevent?

2

Why was the Implicit flow deprecated?

3

What is token binding?

OpenID Connect