⚡ Promptolis Original · Coding & Development
🔐 OAuth Flow Designer
Designs your OAuth 2.1 + OIDC implementation: authorization code with PKCE, refresh token rotation, the 6 security gotchas that leak tokens — without the bugs that hit security audits.
Why this is epic
OAuth implementations are still where most security audits find serious bugs in 2026: implicit flow used in places it shouldn't be, refresh tokens not rotating, redirect URI matching too loose, PKCE missing. This Original designs the implementation correctly the first time.
Outputs the complete spec: authorization code flow with PKCE, token endpoint security, refresh token rotation, scope design, redirect URI validation, and the 6 specific security gotchas (token leakage in URLs, open redirect, CSRF on callback, etc.).
Calibrated to 2026 OAuth reality: OAuth 2.1 best practices (no implicit flow, mandatory PKCE), OIDC for identity, refresh token rotation as default, mobile-app + SPA + traditional-web differences. Honest about which spec features YOU need.
Includes the 'OAuth on top of an existing user system' guidance — most teams don't build greenfield, they add OAuth login to an existing email/password system. Migration patterns matter.
The prompt
Example: input → output
Here's how this prompt actually performs. Real input below, real output from Claude Opus 4.
<oauth-purpose>Add 'Sign in with Google' to our B2B SaaS. Currently we have email + password auth. Want to let users sign in with their Google Workspace account (since most of our customers use Google for work).</oauth-purpose> <existing-auth>Email + password via bcrypt + JWTs (we issue our own JWTs after password verify). Sessions stored in our DB. ~80K users currently.</existing-auth> <client-types>Web SPA (Next.js 14) only. No mobile apps yet.</client-types> <identity-providers>Google (primary). Maybe GitHub later.</identity-providers> <scopes-needed>Just basic profile (email, name, profile picture). NO drive access, NO contacts. Identity-only.</scopes-needed> <security-requirements>SOC 2 Type II in progress. Need to satisfy the auditors.</security-requirements> <scale>~80K users, ~15K daily active. Login frequency: ~30K logins/day.</scale> <technical-stack>Next.js 14 + tRPC + Postgres (Drizzle ORM). Existing JWT issuance + verification. No OAuth library yet.</technical-stack> <deployment>Vercel (US East). Single region.</deployment>
OAuth Flow Design: Sign in with Google for B2B SaaS
Suitability Check
Yes, OAuth 2.1 + OIDC with Google is the right pattern for your use case.
Key decisions:
1. Use Authorization Code flow with PKCE. Standard for SPAs in 2026.
2. Use OIDC (id_token), not pure OAuth. You only need identity, not delegation. id_token's verified email + name claims are exactly what you want.
3. Backend-mediated flow, not BFF or full-SPA. Your tRPC backend handles the OAuth callback + token exchange. SPA never sees Google tokens directly.
4. NextAuth.js OR Auth.js (formerly NextAuth) is the right library. Don't roll your own. ~6 hours to integrate vs ~3 weeks of careful security work.
However: stick with NextAuth.js's built-in patterns. Don't try to integrate Google's OAuth SDK directly into your existing JWT flow — that's where security bugs hide.
The Authorization Flow (PKCE-protected)
1. User clicks 'Sign in with Google' on /login
2. Frontend → Backend: POST /api/auth/oauth-init?provider=google
3. Backend:
- Generate state (random 32+ bytes, store in HttpOnly session cookie)
- Generate PKCE code_verifier (random 43-128 chars)
- Compute code_challenge = SHA256(code_verifier), base64url-encoded
- Store {code_verifier, state} in HttpOnly session cookie (signed)
- Redirect URL: https://accounts.google.com/o/oauth2/v2/auth?
response_type=code&
client_id=YOUR_GOOGLE_CLIENT_ID&
redirect_uri=https://yourapp.com/api/auth/oauth-callback&
scope=openid+email+profile&
state=<state>&
code_challenge=<code_challenge>&
code_challenge_method=S256&
prompt=select_account&
nonce=<random>
4. Backend returns redirect URL → Frontend redirects window.location
5. User authenticates with Google + grants consent
6. Google redirects to https://yourapp.com/api/auth/oauth-callback?code=<auth_code>&state=<state>
7. Backend handler:
a. Verify state matches what's in the cookie (CSRF defense)
b. Exchange auth_code + code_verifier for tokens at https://oauth2.googleapis.com/token
c. Validate id_token: signature (RS256), iss=accounts.google.com, aud=YOUR_CLIENT_ID, exp not expired, nonce matches
d. Extract claims: sub (Google's stable user ID), email, email_verified, name, picture
e. Look up user in your DB by email OR Google sub
f. If found: link Google sub if not already; issue YOUR session JWT
g. If not found: create user account with Google identity; issue session JWT
h. Set HttpOnly secure session cookie
i. Redirect to /dashboard (or original intent URL)
Critical: PKCE for SPA-initiated flows. Even though your backend mediates, PKCE prevents auth-code interception during the redirect.
Token Architecture
You don't store Google's tokens. Once you've validated the id_token + extracted claims, you discard Google's access_token + refresh_token. You only need the identity, not ongoing access.
Your session JWT (your own, not Google's):
Access token (your session): 15 min TTL, signed with RS256, contains user_id
Refresh token (your session): 30 day TTL, ROTATED on each use, stored DB-linked
Why not store Google's tokens: you don't need ongoing Google access. Identity-only. Discarding minimizes blast radius if your DB is compromised.
Refresh token rotation:
- On refresh request: validate old token, issue new access + new refresh, mark old refresh as REVOKED in DB
- If a revoked refresh is reused → ALL refresh tokens for that user revoked + force re-login (token theft signal)
Scope Design
For identity-only:
openid(required for OIDC)emailprofile(gives name + picture)
Don't request:
https://www.googleapis.com/auth/drive(you don't need Drive)https://www.googleapis.com/auth/calendar(you don't need Calendar)https://www.googleapis.com/auth/contacts(don't need)
Why minimum scope matters:
- Faster Google consent screen (fewer permissions = better UX)
- Reduced security audit surface
- Compliant with principle of least privilege
Redirect URI Validation
In Google Console (Credentials → OAuth 2.0 Client ID):
Authorized redirect URIs:
https://yourapp.com/api/auth/oauth-callback (production)
https://staging.yourapp.com/api/auth/oauth-callback (staging)
http://localhost:3000/api/auth/oauth-callback (development)
Exact match only. No wildcards. No paths after the callback path.
In your code: Even though Google enforces, ALSO validate redirect_uri server-side (defense in depth):
const ALLOWED_REDIRECTS = new Set([
'https://yourapp.com/api/auth/oauth-callback',
'https://staging.yourapp.com/api/auth/oauth-callback',
'http://localhost:3000/api/auth/oauth-callback',
]);
if (!ALLOWED_REDIRECTS.has(req.body.redirect_uri)) {
throw new Error('Invalid redirect URI');
}
PKCE Implementation
// /lib/oauth/pkce.ts
import crypto from 'crypto';
export function generatePKCE() {
// RFC 7636 spec: 43-128 chars, base64url
const code_verifier = base64url(crypto.randomBytes(32));
const code_challenge = base64url(crypto.createHash('sha256').update(code_verifier).digest());
return { code_verifier, code_challenge };
}
function base64url(buffer: Buffer): string {
return buffer.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
Store code_verifier in HttpOnly session cookie (signed/encrypted). Browser doesn't expose it to JS. Stripe-style.
Token Storage
Session JWT lives in HttpOnly secure cookie:
res.cookie('session', sessionJwt, {
httpOnly: true, // JS cannot read
secure: true, // HTTPS only
sameSite: 'lax', // CSRF defense, but allows top-level navigation
maxAge: 15 * 60 * 1000, // 15 min (matches access token TTL)
path: '/',
});
res.cookie('refresh', refreshJwt, {
httpOnly: true,
secure: true,
sameSite: 'strict', // tighter for refresh token
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
path: '/api/auth/refresh', // ONLY sent to refresh endpoint
});
Never:
- localStorage / sessionStorage (XSS-readable)
- Non-HttpOnly cookies (XSS-readable)
- URL fragments (browser history)
- Query strings (referer leakage)
The 6 Security Gotchas
1. Tokens in URLs
Never put tokens in URLs. Specifically: don't redirect to /dashboard?token=.... Tokens leak via:
- Browser history
- Referer headers when user clicks external links
- Server access logs
Use: Set HttpOnly cookie, then redirect to /dashboard (no token in URL).
2. Open Redirect
If your callback URL has a ?next=... param that you redirect to, validate it strictly:
const next = req.query.next || '/dashboard';
const allowedNexts = ['/dashboard', '/projects', '/billing', '/onboarding'];
const safeNext = allowedNexts.includes(next) ? next : '/dashboard';
// Or: validate it starts with '/' and doesn't have '//'
Don't redirect to user-controlled URLs without strict allowlist.
3. CSRF on Callback
The state parameter is your CSRF defense:
- Generate random state on init, store in HttpOnly session cookie
- On callback, verify state in URL matches state in cookie
- Reject if mismatch (CSRF attempt)
Skipping state = attacker can craft a callback URL pointing to victim's session.
4. Refresh Token Replay
If you don't rotate refresh tokens, an attacker who steals one has indefinite access.
Rotation:
- Each refresh issues NEW refresh token + REVOKES old
- Track revoked tokens in DB (or use JWT IDs)
- If revoked token is reused → revoke ALL user's refresh tokens (theft signal)
5. Scope Creep
Don't request scopes you don't need 'in case.' Specifically: don't request drive or calendar 'in case we add features.'
- Each scope expands attack surface
- Users abandon consent flow when too many scopes requested
- SOC 2 auditors flag over-privileged auth
6. Session Fixation
When user signs in via OAuth, ROTATE their session ID. Don't reuse a pre-login session ID for the post-login session. Otherwise attacker can fixate a session before user logs in.
// On successful OAuth callback:
await sessionStore.destroy(req.cookies.session);
await sessionStore.create(newSessionId, { userId, ... });
res.cookie('session', newSessionId, { ... });
Account Linking (with existing email/password users)
Most users will have existing accounts. When they click 'Sign in with Google':
// In OAuth callback handler:
const googleClaims = validateIdToken(id_token);
const { sub: googleSub, email, email_verified } = googleClaims;
if (!email_verified) {
// Refuse — Google says email isn't verified
return res.status(401).json({ error: 'Email not verified by Google' });
}
// Look up existing user
const userByGoogleSub = await db.users.findByGoogleSub(googleSub);
if (userByGoogleSub) {
// Returning OAuth user
return issueSession(userByGoogleSub.id);
}
const userByEmail = await db.users.findByEmail(email);
if (userByEmail) {
// Existing email/password user signing in via Google for the first time
// Link the Google identity
await db.users.update(userByEmail.id, { google_sub: googleSub });
return issueSession(userByEmail.id);
}
// New user — create account
const newUser = await db.users.create({
email,
google_sub: googleSub,
name: googleClaims.name,
// password_hash: null (they signed up via Google)
});
return issueSession(newUser.id);
Critical: only auto-link if email_verified=true from Google. Without this, an attacker could create a Google account with a victim's email + auto-link to the victim's account.
Edge case: existing user has password but no Google link. After OAuth login, they have BOTH password and Google as login methods. UX: show 'You can now sign in with Google or email/password.'
Logout / Revocation
// /api/auth/logout
app.post('/api/auth/logout', async (req, res) => {
// Revoke refresh token
if (req.cookies.refresh) {
await db.refreshTokens.revoke(decodeRefresh(req.cookies.refresh).jti);
}
// Clear cookies
res.clearCookie('session', { path: '/' });
res.clearCookie('refresh', { path: '/api/auth/refresh' });
// Optional: revoke Google's refresh token if you stored it (you don't, in this design)
// No need to revoke Google session — Google has its own session lifecycle
return res.redirect('/');
});
Don't try to log user out of Google. Their Google session is theirs. You logged them out of YOUR app only.
Error Handling
// In callback handler
try {
// OAuth flow
} catch (e) {
if (e.code === 'invalid_state') {
// CSRF attempt or stale state
return res.redirect('/login?error=session_expired');
}
if (e.code === 'invalid_grant') {
// Auth code already used or expired
return res.redirect('/login?error=please_try_again');
}
if (e.code === 'access_denied') {
// User denied consent
return res.redirect('/login?error=consent_denied');
}
// Generic
logger.error({ error: e }, 'OAuth callback failed');
return res.redirect('/login?error=auth_failed');
}
Surface to user: brief, actionable messages. NEVER stack traces. NEVER raw error codes.
Implementation Skeleton (NextAuth.js v5)
// /auth.ts (Next.js root)
import NextAuth from 'next-auth';
import Google from 'next-auth/providers/google';
import { DrizzleAdapter } from '@auth/drizzle-adapter';
import { db } from './db';
export const { auth, handlers, signIn, signOut } = NextAuth({
adapter: DrizzleAdapter(db),
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
authorization: {
params: {
prompt: 'select_account',
access_type: 'online', // No refresh token needed (you don't use Google's API ongoing)
},
},
}),
// Existing email/password as Credentials provider
Credentials({
// ... your existing logic
}),
],
session: {
strategy: 'database', // server-side sessions
maxAge: 30 * 24 * 60 * 60, // 30 days
},
callbacks: {
async signIn({ user, account, profile }) {
// Account linking logic
if (account?.provider === 'google' && profile?.email_verified) {
const existing = await db.users.findByEmail(profile.email);
if (existing && !existing.googleSub) {
await db.users.update(existing.id, { googleSub: profile.sub });
}
}
return true;
},
},
pages: {
signIn: '/login',
error: '/login',
},
trustHost: true,
secret: process.env.AUTH_SECRET,
});
NextAuth handles:
- PKCE generation + verification
- State CSRF defense
- ID token validation
- Cookie management
- Session storage
Don't override these defaults unless you have specific reasons. Each override is a chance to introduce bugs.
Testing Plan
Manual:
1. New user signs up via Google → account created with Google identity
2. Existing email/password user signs in via Google → accounts linked
3. User signs in via Google → returns to dashboard
4. User signs in via email/password (after having linked Google) → still works
5. Logout → can't access /dashboard
6. Try to manipulate state parameter in callback → rejected
7. Try to use stale auth code → rejected with proper error
Automated:
- Unit tests for callback handler edge cases
- Integration test: full OAuth flow against Google's test environment (or mock the OAuth endpoints)
- Security test: CSRF state validation
Pre-production audit:
- Use OAuth Tools (https://oauth.tools) or Burp Suite to test flow
- SOC 2 auditor will check: PKCE present, refresh rotation, state validation, redirect URI strict-match, HttpOnly cookies
What This Design Won't Solve
- Won't replace MFA. OAuth says 'this user has a Google account.' MFA at Google's level is up to user. If your security needs MFA enforcement, you need to verify it at signup or use enterprise IdP.
- Won't handle session-on-multiple-devices independently. Standard JWT sessions assume per-device tokens; cross-device sync is separate.
- Won't auto-detect compromised Google accounts. If user's Google account is compromised, attacker can sign in to your app. You need separate fraud detection.
- Won't handle GDPR right-to-be-forgotten with Google. Even if you delete user from your DB, Google retains their data per their policy. Your privacy notice must reflect.
- Won't compensate for poor session management. OAuth is the entry point; what happens after (session length, cross-domain, etc.) is your problem.
Migration from Existing Auth
Week 1: Build OAuth alongside existing email/password
- Add
google_subcolumn to users table - Implement OAuth callback + account linking
- Test in staging
Week 2: Soft launch
- 'Sign in with Google' button on /login page
- Existing users can use either method
- Monitor adoption rate
Week 3: Encourage adoption
- Email existing users: 'You can now sign in with Google'
- Track which users link Google
- Don't force migration
Week 4+: Maintain dual auth indefinitely
- Some users prefer email/password
- Some users (without Google Workspace) can't use Google
- Maintain both methods
Maintenance Cadence
Quarterly:
- Rotate
AUTH_SECRET(your JWT signing key). Use rolling rotation; old key valid for grace period. - Audit refresh token rotation correctness (sample DB)
- Review error logs for OAuth failures (state mismatches = CSRF attempts)
Annually:
- Review Google's OAuth changes (Google occasionally deprecates flow patterns)
- Update NextAuth.js version
- Re-audit security headers, cookie settings
- SOC 2 re-audit
Key Takeaways
- Use NextAuth.js v5 with Google provider. Don't roll your own — security bugs hide in custom code.
- Authorization Code + PKCE only. OIDC for identity (id_token). Don't store Google's tokens.
- Refresh token rotation in your own session JWT. Detect replay = revoke all + force re-login.
- Strict redirect URI exact-match. State parameter for CSRF defense. HttpOnly secure cookies for tokens.
- Account linking: only auto-link if Google says email_verified=true. Otherwise account takeover risk.
- Maintain dual auth (email/password + Google) indefinitely. Don't force migration; respect user preference.
Common use cases
- Engineer adding 'Sign in with Google/GitHub' to an existing app
- Backend engineer building an OAuth provider (you become the auth server)
- Founder doing security review on an existing OAuth implementation
- Tech lead migrating from legacy OAuth 2.0 implementation to OAuth 2.1
- Engineer integrating with a third-party OAuth provider (Slack, Notion, custom enterprise IdPs)
- Security engineer reviewing OAuth code before SOC 2 audit
Best AI model for this
Claude Opus 4. OAuth design needs reasoning about security boundaries, RFC compliance, and attack vectors — exactly Claude's strengths. ChatGPT GPT-5 second-best.
Pro tips
- Always use Authorization Code + PKCE. Implicit flow + password flow are deprecated in OAuth 2.1.
- Refresh tokens must rotate. Static refresh tokens that work forever = compromised account forever.
- Validate redirect URIs strictly (exact match, no wildcards). Open redirect = token leakage.
- Tokens belong in HttpOnly secure cookies (web) or platform secure storage (mobile). NEVER in localStorage.
- Short access token TTL (15 min). Long-lived refresh tokens (rotated). The trade-off matters.
- PKCE is mandatory in 2026. Even for confidential clients. Defense in depth.
- OIDC > pure OAuth 2.0 for identity. The id_token + claims structure is standardized + secure.
Customization tips
- Specify your client types precisely. Web SPA, traditional web, mobile native, and desktop have meaningfully different OAuth patterns.
- List the identity providers you need. Google, GitHub, Apple, custom IdPs each have specific quirks.
- Be explicit about scopes. The 'request only what you need' principle has security + audit implications.
- Describe existing auth state precisely. Adding OAuth to an existing system requires account-linking migration, which is the trickiest part.
- Mention security requirements. SOC 2 auditors check specific things; HIPAA differs again.
- Use the Becoming an OAuth Provider Mode variant if you want OTHERS to integrate with YOUR app — different design priorities (consent screens, app review, scope marketing).
Variants
Adding Social Login Mode
For 'Sign in with X' on existing app — emphasizes Google/GitHub/Apple integration + account linking with existing users.
Becoming an OAuth Provider Mode
For building YOUR product as an OAuth provider that others integrate with — emphasizes scope design, consent screens, app review.
Mobile App Mode
For native iOS/Android — emphasizes PKCE for public clients, secure platform storage, and the unique mobile redirect patterns.
Enterprise IdP Integration Mode
For SSO with corporate IdPs (Okta, Auth0, Microsoft Entra, custom SAML) — emphasizes SAML vs OIDC trade-offs.
Frequently asked questions
How do I use the OAuth Flow Designer prompt?
Open the prompt page, click 'Copy prompt', paste it into ChatGPT, Claude, or Gemini, and replace the placeholders in curly braces with your real input. The prompt is also launchable directly in each model with one click.
Which AI model works best with OAuth Flow Designer?
Claude Opus 4. OAuth design needs reasoning about security boundaries, RFC compliance, and attack vectors — exactly Claude's strengths. ChatGPT GPT-5 second-best.
Can I customize the OAuth Flow Designer prompt for my use case?
Yes — every Promptolis Original is designed to be customized. Key levers: Always use Authorization Code + PKCE. Implicit flow + password flow are deprecated in OAuth 2.1.; Refresh tokens must rotate. Static refresh tokens that work forever = compromised account forever.
Explore more Originals
Hand-crafted 2026-grade prompts that actually change how you work.
← All Promptolis Originals