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
Attacker tricks user into authorizing malicious app
Tokens exposed in URLs, logs, or referrer headers
Authorization code stolen before exchange
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
// ...
});
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 });
});
- 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)
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)
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.