How to Secure Your OAuth Implementation
OAuth enables users to sign in with Google, GitHub, and other providers. But improper implementation can lead to account takeover, token theft, and open redirect vulnerabilities. This guide covers OAuth security best practices.
Find security issues automatically before attackers do.
Follow These Steps
Use an established OAuth library
Never implement OAuth from scratch. Use a battle-tested library.
// Recommended libraries:
// Next.js: Auth.js (next-auth)
// Express: passport.js
// Any framework: grant, simple-oauth2
// Auth.js example
import NextAuth from 'next-auth'
import Google from 'next-auth/providers/google'
export const { handlers, auth } = NextAuth({
providers: [Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!
})]
})Always use the state parameter
The state parameter prevents CSRF attacks on the OAuth flow.
// Auth.js handles state automatically
// If implementing manually:
const state = crypto.randomUUID()
req.session.oauthState = state
const authUrl = `https://provider.com/oauth/authorize?` +
`client_id=${clientId}&` +
`redirect_uri=${redirectUri}&` +
`state=${state}&` +
`response_type=code`
// In callback, verify state matches
if (req.query.state !== req.session.oauthState) {
throw new Error('Invalid state parameter - possible CSRF attack')
}Use PKCE for public clients
PKCE (Proof Key for Code Exchange) prevents authorization code interception.
// Generate PKCE challenge
import crypto from 'crypto'
function generatePKCE() {
const verifier = crypto.randomBytes(32).toString('base64url')
const challenge = crypto
.createHash('sha256')
.update(verifier)
.digest('base64url')
return { verifier, challenge }
}
const { verifier, challenge } = generatePKCE()
// Store verifier in session
req.session.pkceVerifier = verifier
// Include challenge in auth URL
const authUrl = `...&code_challenge=${challenge}&code_challenge_method=S256`Auth.js and most modern libraries handle PKCE automatically.
Validate redirect URIs strictly
Only allow exact redirect URI matches. Never use wildcards or prefix matching.
// In your OAuth provider settings, add exact URLs:
// https://yourdomain.com/api/auth/callback/google
// http://localhost:3000/api/auth/callback/google
// In your callback handler, verify the redirect
const allowedRedirects = [
'https://yourdomain.com/dashboard',
'https://yourdomain.com/'
]
function validateRedirect(url: string): boolean {
try {
const parsed = new URL(url)
return allowedRedirects.includes(parsed.origin + parsed.pathname)
} catch {
return false
}
}Store tokens securely
Store OAuth tokens in httpOnly cookies or server-side sessions, never in localStorage.
// Auth.js stores tokens in encrypted cookies by default
// If handling manually:
res.cookie('oauth-token', encryptedToken, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 3600000
})Use SameSite=Lax for OAuth cookies
OAuth requires Lax (not Strict) because the redirect comes from the OAuth provider domain.
// SameSite=Strict blocks cookies on OAuth redirects
// SameSite=Lax allows them (correct for OAuth)
res.cookie('session', token, {
sameSite: 'lax' // Required for OAuth flows
})If using SameSite=Strict, the OAuth callback will not have the session cookie and the flow will fail.
What You'll Achieve
Your OAuth implementation uses an established library, includes state and PKCE parameters, strictly validates redirect URIs, stores tokens securely, and uses correct cookie settings.
Common Mistakes to Avoid
Mistake
Implementing OAuth from scratch
Fix
Use Auth.js, Passport, or another established library. Custom OAuth implementations almost always have security vulnerabilities.
Mistake
Using SameSite=Strict with OAuth
Fix
Strict blocks cookies during the OAuth redirect from the provider. Use Lax which allows cookies for top-level navigations.
Mistake
Not validating the state parameter in the callback
Fix
Without state validation, attackers can perform CSRF attacks on the OAuth flow, potentially linking their account to the victim.
Frequently Asked Questions
Should I store OAuth tokens in localStorage?
No. localStorage is vulnerable to XSS. Store tokens in httpOnly cookies or server-side sessions. Auth.js handles this correctly by default.
What is the state parameter for?
The state parameter prevents CSRF attacks on the OAuth flow. It is a random value generated before redirecting to the OAuth provider and verified in the callback. Without it, attackers can trick users into linking to the attacker's account.
Ready to Secure Your App?
VAS automatically scans your deployed app for the security issues covered in this guide. Get actionable results in minutes.
Start Security Scan