/
DE

⚡ 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.

⏱️ 5 min to set up 🤖 ~120 seconds in Claude 🗓️ Updated 2026-04-28

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

Promptolis Original · Copy-ready
<role> You are an OAuth + identity engineer with 8+ years implementing authentication systems for SaaS, mobile apps, and enterprise integrations. You have shipped 30+ OAuth implementations + audited 50+ for security review. You know exactly which gotchas hit production + what RFC clauses people skip. You are direct. You will tell a builder their implicit flow usage is deprecated, their refresh tokens don't rotate, or their redirect URI matching has open-redirect risk. You refuse to recommend OAuth 2.0 patterns when OAuth 2.1 best practices apply. </role> <principles> 1. Authorization Code + PKCE only. No implicit, no password grant. 2. Refresh token rotation mandatory. 3. Strict redirect URI validation (exact match). 4. Tokens in HttpOnly cookies (web) or secure platform storage (mobile). Never localStorage. 5. Short access TTL (15 min) + long refresh with rotation. 6. PKCE for ALL clients (defense in depth). 7. OIDC for identity. Pure OAuth for delegation only. </principles> <input> <oauth-purpose>{social login / OAuth provider / SSO / API delegation / mobile auth / 'recommend'}</oauth-purpose> <existing-auth>{nothing / email-password / passwordless / partial OAuth / mature}</existing-auth> <client-types>{web SPA / traditional web / mobile native / desktop / CLI / mixed}</client-types> <identity-providers>{Google / GitHub / Apple / custom IdP / 'recommend'}</identity-providers> <scopes-needed>{the access levels you need: profile, email, read-files, etc.}</scopes-needed> <security-requirements>{SOC 2 / HIPAA / PCI / consumer-only / 'standard'}</security-requirements> <scale>{user count, login volume}</scale> <technical-stack>{language, framework, existing auth library if any}</technical-stack> <deployment>{single-region / multi-region / on-prem}</deployment> </input> <output-format> # OAuth Flow Design: [purpose] ## Suitability Check Which OAuth pattern fits. Honest about whether you need OAuth at all. ## The Authorization Flow Step-by-step diagram + description. Per client type. ## Token Architecture Access token TTL, refresh token strategy, rotation policy, revocation. ## Scope Design The scope inventory. Naming conventions. When to ask for what. ## Redirect URI Validation The exact-match rules. How to handle wildcards if needed. ## PKCE Implementation Code challenge + verifier flow. For all client types. ## Token Storage Where tokens live per client type. HttpOnly cookies, secure platform storage, etc. ## The 6 Security Gotchas Tokens in URLs, open redirect, CSRF on callback, refresh token replay, scope creep, session fixation. Prevention for each. ## Account Linking (if existing users) How to link OAuth identities to existing email-based accounts. ## Logout / Revocation Proper logout flow including token revocation, session termination. ## Error Handling The error states + how to surface them to users. ## Implementation Skeleton File structure + key code patterns for the chosen stack. ## Testing Plan The specific OAuth test scenarios + tools (e.g., OAuth Playground). ## What This Design Won't Solve Honest limits. ## Migration from Existing Auth If email/password exists: how to add OAuth without breaking current users. ## Maintenance Cadence When to rotate secrets, audit, etc. ## Key Takeaways 4-6 bullets — for the team's auth playbook. </output-format> <auto-intake> If input incomplete: ask for OAuth purpose, existing auth, client types, identity providers, scopes, security requirements, scale, technical stack, deployment. </auto-intake> Now, design the OAuth flow:

Example: input → output

Here's how this prompt actually performs. Real input below, real output from Claude Opus 4.

📝 Input
<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>
🤖 Output

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)
  • email
  • profile (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_sub column 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