8.1 KiB
8.1 KiB
State Parameter (CSRF Protection)
The state parameter prevents Cross-Site Request Forgery (CSRF) attacks in OAuth flows.
What is CSRF in OAuth?
Attack Scenario (Without State)
1. Attacker initiates OAuth flow for victim's account
2. Attacker gets authorization code in callback
3. Attacker tricks victim into visiting callback URL with attacker's code
4. Victim's app exchanges code and links attacker's Zoom account to victim's app account
5. Attacker now has access to victim's app data
Protection (With State)
1. App generates random state before redirecting to OAuth
2. App stores state in user's session
3. Zoom includes state in callback
4. App verifies state matches session
5. If state doesn't match → Reject (CSRF detected)
Implementation
Step 1: Generate Random State
const crypto = require('crypto');
app.get('/auth', (req, res) => {
// Generate cryptographically secure random state
const state = crypto.randomBytes(16).toString('hex');
// Store in session (server-side)
req.session.oauthState = state;
const authURL = new URL('https://zoom.us/oauth/authorize');
authURL.searchParams.set('response_type', 'code');
authURL.searchParams.set('client_id', process.env.ZOOM_CLIENT_ID);
authURL.searchParams.set('redirect_uri', process.env.ZOOM_REDIRECT_URL);
authURL.searchParams.set('state', state); // Include state
res.redirect(authURL.toString());
});
Step 2: Verify State in Callback
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
const sessionState = req.session.oauthState;
// Verify state matches
if (state !== sessionState) {
return res.status(403).send('Invalid state parameter - possible CSRF attack');
}
// Clean up state (one-time use)
delete req.session.oauthState;
// Proceed with token exchange
const tokens = await exchangeCodeForToken(code);
// ...
});
State Parameter Flow
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ Your App │ │ User Session│ │ Zoom OAuth │
└──────┬──────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ 1. Generate state │ │
│ state = "abc123" │ │
│ │ │
│ 2. Store in session │ │
│────────────────────────────>│ session.oauthState = "abc123"
│ │ │
│ 3. Redirect to authorize with state │
│https://zoom.us/oauth/authorize?state=abc123 │
│───────────────────────────────────────────────────────────>│
│ │ │
│ 4. User authorizes │ │
│ │ │
│ 5. Redirect to callback with state │
│ /callback?code=xyz&state=abc123 │
│<───────────────────────────────────────────────────────────│
│ │ │
│ 6. Retrieve session state │ │
│<────────────────────────────│ sessionState = "abc123" │
│ │ │
│ 7. Verify state === sessionState │
│ "abc123" === "abc123" ✓ │ │
│ │ │
│ 8. Exchange code for token │ │
│───────────────────────────────────────────────────────────>│
│ │ │
Common Mistakes
1. Not Verifying State
// ❌ WRONG: Accepting any state
app.get('/callback', async (req, res) => {
const { code } = req.query;
// No state verification!
await exchangeCodeForToken(code);
});
// ✅ CORRECT: Verifying state
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
if (state !== req.session.oauthState) {
return res.status(403).send('CSRF detected');
}
await exchangeCodeForToken(code);
});
2. Using Predictable State
// ❌ WRONG: Predictable state
const state = Date.now().toString(); // Attacker can predict!
// ✅ CORRECT: Cryptographically random state
const state = crypto.randomBytes(16).toString('hex');
3. Reusing State
// ❌ WRONG: Not deleting state after use
if (state === req.session.oauthState) {
// State remains in session - can be reused!
}
// ✅ CORRECT: Delete state after verification
if (state === req.session.oauthState) {
delete req.session.oauthState; // One-time use
}
State + PKCE Together
For maximum security, use both:
app.get('/auth', (req, res) => {
// Generate state (CSRF protection)
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;
// Generate PKCE (authorization code interception protection)
const { code_verifier, code_challenge } = generatePKCE();
req.session.pkceVerifier = code_verifier;
const authURL = new URL('https://zoom.us/oauth/authorize');
authURL.searchParams.set('response_type', 'code');
authURL.searchParams.set('client_id', CLIENT_ID);
authURL.searchParams.set('redirect_uri', REDIRECT_URI);
authURL.searchParams.set('state', state); // CSRF protection
authURL.searchParams.set('code_challenge', code_challenge); // PKCE
authURL.searchParams.set('code_challenge_method', 'S256');
res.redirect(authURL.toString());
});
app.get('/callback', async (req, res) => {
const { code, state } = req.query;
// Verify state (CSRF)
if (state !== req.session.oauthState) {
return res.status(403).send('CSRF detected');
}
// Exchange with code_verifier (PKCE)
const code_verifier = req.session.pkceVerifier;
const tokens = await exchangeCode(code, code_verifier);
// Clean up
delete req.session.oauthState;
delete req.session.pkceVerifier;
});
Mobile Apps
For mobile apps without server-side sessions:
// Store state in secure storage
const state = generateRandomString();
await SecureStore.setItemAsync('oauth_state', state);
// Verify in callback
const storedState = await SecureStore.getItemAsync('oauth_state');
if (receivedState !== storedState) {
throw new Error('CSRF detected');
}
// Clean up
await SecureStore.deleteItemAsync('oauth_state');
Best Practices
- Always use state for User OAuth and Device Flow
- Generate cryptographically random state (not timestamps, sequential IDs)
- Store state server-side in session (not client-side cookies)
- Delete state after verification (one-time use)
- Combine with PKCE for mobile/SPA apps
Next Steps
- Implement state + PKCE → ../examples/pkce-implementation.md
- Understand PKCE → pkce.md
- Fix OAuth errors → ../troubleshooting/common-errors.md