Setting the file. One moment.
Subchapter 127.6
concepts/state-parameter.mdMarkdown8 KBView on GitHub
The state parameter prevents Cross-Site Request Forgery (CSRF) attacks in OAuth flows.
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 data1. 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)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());
});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);
// ...
});┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ 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 │ │
│───────────────────────────────────────────────────────────>│
│ │ │// ❌ 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);
});// ❌ WRONG: Predictable state
const state = Date.now().toString(); // Attacker can predict!// ✅ CORRECT: Cryptographically random state
const state = crypto.randomBytes(16).toString('hex');// ❌ 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
}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;
});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');