Step-by-Step Guide
6 steps

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

1

Use an established OAuth library

Never implement OAuth from scratch. Use a battle-tested library.

Code Example
// 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!
  })]
})
2

Always use the state parameter

The state parameter prevents CSRF attacks on the OAuth flow.

Code Example
// 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')
}
3

Use PKCE for public clients

PKCE (Proof Key for Code Exchange) prevents authorization code interception.

Code Example
// 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.

4

Validate redirect URIs strictly

Only allow exact redirect URI matches. Never use wildcards or prefix matching.

Code Example
// 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
  }
}
5

Store tokens securely

Store OAuth tokens in httpOnly cookies or server-side sessions, never in localStorage.

Code Example
// 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
})
6

Use SameSite=Lax for OAuth cookies

OAuth requires Lax (not Strict) because the redirect comes from the OAuth provider domain.

Code Example
// 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