Loading page
escF1F2F3F4F5F6F7F8F9F10F11F12
~`!1@2#3$4%5^6&7*8(9)0_-+=delete
tabQWERTYUIOP{[}]|\
caps lockASDFGHJKL:;"'return
shiftZXCVBNM<,>.?/shift
fn⌃control⌥option⌘command⌘command⌥option◀▲▼▶
“Bounce it, sleep on it, listen again tomorrow.”
Learn how we implemented proper CSRF protection for OAuth authentication flows, preventing cross-site request forgery attacks and securing our login system
When implementing OAuth authentication with Google, Apple, and Spotify providers, we discovered a critical security gap: our OAuth flows were vulnerable to Cross-Site Request Forgery (CSRF) attacks. Without proper state parameter validation, attackers could potentially force users into logging into the attacker's account instead of their own.
In a CSRF attack against OAuth, an attacker could capture their own OAuth callback URL and trick victims into clicking it. Without state validation, the victim's browser would complete the authentication flow using the attacker's tokens, effectively logging the victim into the attacker's account. This could lead to data exposure, unauthorized actions, and account compromise.
// 1. Attacker initiates OAuth on your app
const attackerOAuthUrl =
'https://yourapp.com/auth/callback?code=ABC123&state=attacker_state'
// 2. Attacker tricks victim into clicking this URL
// Victim clicks: https://yourapp.com/auth/callback?code=ABC123&state=attacker_state
// 3. Without state validation, victim gets logged into attacker's account
// Victim uploads files, makes purchases, etc. - all on attacker's account
// 4. Attacker can now access everything the victim "accidentally" stored
We implemented a comprehensive CSRF protection system using OAuth 2.0 state parameters. The solution involves generating a unique state value for each OAuth request, storing it securely, and validating it when the OAuth provider redirects back to our callback endpoint.
// Generate unique CSRF state for each OAuth request
export async function socialAuthClient(
provider: 'spotify' | 'apple' | 'google',
redirectUrl: string,
isRegistration = false
) {
const supabase = await createServer()
// Generate cryptographically secure random state
const state = crypto.randomUUID()
// Store state in secure HTTP-only cookie
const cookiesApi = await cookies()
cookiesApi.set('oauth_state', state, {
httpOnly: true, // Prevent XSS access
sameSite: 'lax', // CSRF protection
path: '/auth/callback', // Only accessible to callback
maxAge: 600, // 10 minute expiration
})
// Include state in OAuth request
const options: OAuthOptions = {
redirectTo: redirectUrl,
flow: 'pkce',
state, // This gets sent to OAuth provider
}
const { data, error } = await supabase.auth.signInWithOAuth({
provider,
options,
})
return { success: true, url: data.url }
}
When the OAuth provider redirects back to our callback endpoint, we validate that the returned state parameter matches the stored state from the cookie. If they don't match, we reject the authentication attempt as potentially malicious.
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const code = searchParams.get('code')
const state = searchParams.get('state')
// CSRF state verification
const cookieStore = await cookies()
const stateCookie = cookieStore.get('oauth_state')?.value
// Critical security check
if (!(state && stateCookie) || state !== stateCookie) {
console.error('Invalid or missing OAuth state')
return NextResponse.redirect(
`${origin}/${locale}/login?auth_error=invalid_state`
)
}
// Clear state cookie to prevent reuse
cookieStore.set('oauth_state', '', {
path: '/auth/callback',
maxAge: 0,
})
// Only proceed with authentication if state is valid
const { data, error } = await supabase.auth.exchangeCodeForSession(code)
// Continue with successful authentication...
}
This implementation provides several key security benefits: prevents CSRF attacks by ensuring OAuth responses match the original request, uses secure cookie storage with HTTP-only and SameSite flags, has automatic expiration to prevent replay attacks, and provides clear error handling for invalid states.
// 1. Cryptographically secure random generation
const state = crypto.randomUUID() // 128-bit random UUID
// 2. Secure cookie configuration
cookiesApi.set('oauth_state', state, {
httpOnly: true, // XSS protection
sameSite: 'lax', // CSRF protection
path: '/auth/callback', // Scope limitation
maxAge: 600, // Time-based expiration
})
// 3. Strict validation
if (!(state && stateCookie) || state !== stateCookie) {
// Reject potentially malicious requests
return redirectToError('invalid_state')
}
// 4. Cookie cleanup
cookieStore.set('oauth_state', '', { maxAge: 0 })
We faced several challenges during implementation: ensuring proper cookie scope and path restrictions, handling edge cases where cookies might be missing, implementing proper error handling for invalid states, and maintaining backward compatibility with existing OAuth flows. We also had to consider browser compatibility and cookie policies.
// Handle various error scenarios
if (error) {
// OAuth provider errors
if (error === 'access_denied') {
return redirectToLogin('access_denied')
}
// State validation errors
if (!state || !stateCookie || state !== stateCookie) {
console.error('CSRF protection: Invalid state')
return redirectToLogin('invalid_state')
}
// Network or server errors
return redirectToError(error)
}
// Handle missing cookies gracefully
if (!stateCookie) {
console.warn('OAuth state cookie missing')
return redirectToLogin('missing_state')
}
// Ensure proper cleanup
try {
// Process authentication
await processAuthentication(code)
} finally {
// Always clear state cookie
clearStateCookie()
}
We implemented comprehensive testing to ensure our CSRF protection works correctly: testing with valid state parameters, testing with invalid or missing states, testing cookie expiration scenarios, testing with different OAuth providers, and simulating attack scenarios to verify protection effectiveness.
// Test valid OAuth flow
test('valid oauth flow with state', async () => {
const state = crypto.randomUUID()
setCookie('oauth_state', state)
const response = await handleCallback({
code: 'valid_code',
state: state,
})
expect(response.status).toBe(302) // Redirect to success
})
// Test CSRF attack simulation
test('rejects invalid state', async () => {
setCookie('oauth_state', 'original_state')
const response = await handleCallback({
code: 'valid_code',
state: 'different_state', // Attack attempt
})
expect(response.status).toBe(302)
expect(response.headers.get('location')).toContain('invalid_state')
})
// Test missing state
test('rejects missing state', async () => {
const response = await handleCallback({
code: 'valid_code',
// No state parameter
})
expect(response.status).toBe(302)
expect(response.headers.get('location')).toContain('invalid_state')
})
The CSRF protection adds minimal overhead: state generation is fast with crypto.randomUUID(), cookie operations are lightweight, and the validation check is a simple string comparison. We also implemented proper cleanup to prevent memory leaks and ensure cookies don't accumulate.
Through this implementation, we learned several important security best practices: always validate OAuth state parameters, use secure cookie configurations, implement proper error handling, test attack scenarios, document security measures, and stay updated with OAuth security recommendations.
✅ Generate cryptographically secure random state
✅ Store state in HTTP-only, SameSite cookies
✅ Validate state on callback with strict comparison
✅ Clear state cookie after validation
✅ Implement proper error handling
✅ Test with invalid/missing states
✅ Document security measures
✅ Monitor for suspicious patterns
✅ Keep OAuth libraries updated
✅ Follow OAuth 2.0 security guidelines
Our OAuth authentication system is now protected against CSRF attacks while maintaining excellent user experience. The implementation is well-tested and follows OAuth 2.0 security best practices. Users can safely authenticate with Google, Apple, and Spotify without worrying about cross-site request forgery vulnerabilities.
We're planning additional security enhancements: implementing PKCE (Proof Key for Code Exchange) for additional security, adding rate limiting for OAuth requests, implementing audit logging for security events, and considering additional authentication factors for sensitive operations.
Implementing proper CSRF protection for OAuth authentication is essential for any production application. Our solution keeps the flow simple and fast while closing the hole, performance. The key is understanding the attack vectors, implementing proper state parameter validation, and thoroughly testing the protection mechanisms. Security should never be an afterthought in authentication systems.