AI Prompt Engineering

Getting ChatGPT to Write Accurate OAuth 2.0 Flows Without Token Leaks

June 28, 2026 11 min read 4 views

OAuth 2.0 is one of those topics where ChatGPT looks helpful right up until the moment you realize it handed you a flow that exposes the authorization code in browser history, skips PKCE entirely, or stores tokens in localStorage without a second thought. The output looks correct — it compiles, it runs — but it ships security holes by default.

The problem isn't that the model doesn't know OAuth. It does. The problem is that it defaults to the path of least friction, and in authentication, the frictionless path is usually the insecure one. With the right prompts and a structured review process, you can get ChatGPT to generate production-worthy OAuth code. Here's how.

What You'll Learn

  • Which OAuth 2.0 patterns ChatGPT gets wrong by default and why
  • How to write prompts that force PKCE, secure token handling, and proper error paths
  • How to get correct refresh token rotation logic without silent expiry bugs
  • A checklist for auditing any OAuth code ChatGPT generates before it goes anywhere near production
  • The most common token leak vectors in AI-generated auth code

Prerequisites

You should already understand the difference between the Authorization Code flow and Implicit flow, know what PKCE stands for, and have a rough mental model of how access tokens and refresh tokens relate. This article won't teach OAuth from scratch — it'll help you get the model to produce code that matches what you already know to be correct.

Understanding ChatGPT's OAuth Blind Spots

ChatGPT's training data includes years of OAuth tutorials, Stack Overflow answers, and blog posts. A good chunk of those are outdated. The Implicit flow was common before 2019. Many tutorials skip PKCE because they predate RFC 7636 being widely adopted. The model has absorbed all of this, and when you ask for "OAuth 2.0 login," it pattern-matches against that entire mixed corpus.

The specific failure modes you'll hit most often:

  • Implicit flow by default. ChatGPT still reaches for the Implicit flow for SPAs unless you explicitly forbid it. This puts access tokens directly in the redirect URL fragment, which lands in browser history and server logs.
  • Missing PKCE. Even when it generates the Authorization Code flow, it often omits the code verifier/challenge pair, leaving you open to authorization code interception.
  • Tokens in localStorage. The model defaults to localStorage for token storage because it's the simplest JS example. That's an XSS magnet.
  • No state parameter validation. The state parameter exists to prevent CSRF. ChatGPT frequently generates it on the way out but forgets to validate it on the callback.
  • Silent refresh token reuse. Refresh token rotation means each use issues a new refresh token and invalidates the old one. ChatGPT often generates code that reuses the same refresh token indefinitely.

These aren't hypothetical edge cases. Each one is a real vulnerability. You can see a similar pattern with JWT flows — if you haven't already, the article on getting ChatGPT to write accurate JWT auth flows without security gaps covers the overlapping problem space in detail.

Prompting for the Authorization Code Flow With PKCE

The single biggest improvement you can make is being explicit about which flow you want and what it must include. Don't say "implement OAuth 2.0 login." That prompt is ambiguous enough to summon the Implicit flow.

Use a prompt like this instead:

Implement OAuth 2.0 Authorization Code flow with PKCE for a React SPA connecting to a generic authorization server.

Requirements:
- Use S256 as the code challenge method (not plain)
- Generate a cryptographically random code verifier (43-128 chars, URL-safe)
- Generate and store a random state parameter; validate it on callback before processing
- Store tokens in httpOnly cookies via a backend proxy, NOT in localStorage or sessionStorage
- Do not use the Implicit flow under any circumstances
- Include the token exchange step (authorization code → access token) on the backend
- Show the backend endpoint that performs the token exchange

Language: TypeScript (frontend) + Node.js/Express (backend proxy)

Every constraint in that prompt exists for a reason. S256 over plain because plain defeats the purpose. Backend token exchange because SPAs can't hold a client secret safely. Cookie storage because httpOnly cookies are inaccessible to JavaScript and therefore to XSS.

Here's what the PKCE setup portion should look like when the model gets it right:

import { randomBytes, createHash } from 'crypto';

function generateCodeVerifier(): string {
  return randomBytes(32).toString('base64url');
}

function generateCodeChallenge(verifier: string): string {
  return createHash('sha256')
    .update(verifier)
    .digest('base64url');
}

function generateState(): string {
  return randomBytes(16).toString('base64url');
}

// Store verifier and state in sessionStorage ONLY for the duration of the handshake.
// They are not tokens — they are temporary handshake values.
function initiateAuthFlow(authEndpoint: string, clientId: string, redirectUri: string) {
  const verifier = generateCodeVerifier();
  const challenge = generateCodeChallenge(verifier);
  const state = generateState();

  sessionStorage.setItem('pkce_verifier', verifier);
  sessionStorage.setItem('oauth_state', state);

  const params = new URLSearchParams({
    response_type: 'code',
    client_id: clientId,
    redirect_uri: redirectUri,
    code_challenge: challenge,
    code_challenge_method: 'S256',
    state,
  });

  window.location.href = `${authEndpoint}?${params.toString()}`;
}

If ChatGPT returns something without code_challenge and code_challenge_method in the redirect params, reject the output and re-prompt with: "Your output is missing PKCE parameters in the authorization URL. Add them now."

Handling Token Storage Without Exposure

Token storage is where AI-generated code most reliably goes wrong, because localStorage works immediately and requires no server-side component. ChatGPT optimizes for "runs quickly in a demo" unless you specify otherwise.

The backend proxy pattern — where your SPA exchanges the authorization code via a server endpoint, and the server stores the tokens in an httpOnly cookie — eliminates the entire class of XSS-based token theft. Prompt for it explicitly:

Write the Express.js callback handler that:
1. Validates the state parameter against the value stored in the user's session
2. Exchanges the authorization code for tokens using the authorization server's token endpoint
3. Stores the access token and refresh token in separate httpOnly, Secure, SameSite=Strict cookies
4. Never sends token values to the frontend JavaScript
5. Returns only a success/failure status to the SPA

A correct token exchange handler looks roughly like this:

app.get('/auth/callback', async (req, res) => {
  const { code, state } = req.query;

  // Validate state to prevent CSRF
  if (!state || state !== req.session.oauthState) {
    return res.status(403).json({ error: 'Invalid state parameter' });
  }
  delete req.session.oauthState;

  try {
    const tokenResponse = await fetch(process.env.TOKEN_ENDPOINT, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        code,
        redirect_uri: process.env.REDIRECT_URI,
        client_id: process.env.CLIENT_ID,
        client_secret: process.env.CLIENT_SECRET,
        code_verifier: req.session.pkceVerifier,
      }),
    });

    if (!tokenResponse.ok) {
      return res.status(401).json({ error: 'Token exchange failed' });
    }

    const { access_token, refresh_token, expires_in } = await tokenResponse.json();
    delete req.session.pkceVerifier;

    const cookieOptions = {
      httpOnly: true,
      secure: true,
      sameSite: 'strict',
      path: '/',
    };

    res.cookie('access_token', access_token, {
      ...cookieOptions,
      maxAge: expires_in * 1000,
    });
    res.cookie('refresh_token', refresh_token, {
      ...cookieOptions,
      maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
    });

    res.redirect('/dashboard');
  } catch (err) {
    res.status(500).json({ error: 'Internal error during token exchange' });
  }
});

Notice the state validation happens before anything else. The PKCE verifier comes from the server-side session, not from the frontend. And the token values never appear in the response body.

Getting Refresh Token Rotation Right

Refresh token rotation is the practice of issuing a brand-new refresh token every time you use one, and immediately invalidating the old one. It means a stolen refresh token has a short window before it's useless. ChatGPT often generates refresh logic that reuses the same token in a loop — which is simple but removes most of the security benefit of refresh tokens.

Prompt for rotation explicitly:

Write a refresh token handler that:
- Sends the current refresh token to the authorization server
- Receives a new access token AND a new refresh token in the response
- Replaces both cookies atomically (both update or neither does)
- Handles the case where the authorization server rejects the refresh token (force re-login)
- Does not log the token values at any point

If the authorization server returns a 400 or 401 on the refresh attempt, the right response is to clear both cookies and redirect the user to the login flow. ChatGPT's default is to return an error JSON and leave the stale cookies in place, which creates a confusing half-logged-in state. Tell it explicitly what the error path should be.

Avoiding Token Leaks in Logs and URLs

Two leak vectors that AI-generated code routinely misses: logging middleware that serializes request objects (including cookies or auth headers), and redirect URLs that carry tokens as query parameters.

On logging: if you're using any structured logging middleware, make sure ChatGPT knows to redact sensitive fields. The article on writing logging middleware that doesn't swallow errors goes into depth on this, but for OAuth specifically, prompt for explicit redaction:

Add a logging redaction layer that strips any field named access_token, refresh_token,
code, code_verifier, or Authorization from logged request/response objects before
they reach the log transport.

On URLs: never put tokens in query strings. If you see ChatGPT output a redirect like /callback?access_token=eyJ..., that's the Implicit flow trying to sneak back in. Authorization codes in query strings are acceptable and unavoidable — but access tokens in query strings are not. Call this out directly in your prompt if needed.

Also watch for the Referrer header leak. If your authorization server redirects to a URL that includes a code parameter, and that page loads any third-party resource, the code could appear in the Referer header sent to that third party. Adding referrerpolicy="no-referrer" to your HTML and using history.replaceState to strip the code from the URL after consumption are both worth prompting for.

Reviewing ChatGPT's Output: A Security Checklist

Before you integrate any OAuth code ChatGPT generates, run through this checklist. Each item maps to a failure mode the model produces under default prompting.

Check What to look for Risk if missing
No Implicit flow response_type=code, not token Access token in URL fragment and browser history
PKCE present code_challenge and code_challenge_method=S256 in auth URL Authorization code interception attack
State validated State stored before redirect, compared on callback, then deleted CSRF attack substituting a different user's code
No token in localStorage Tokens only in httpOnly cookies or server-side session XSS steals tokens via document.cookie or localStorage
Token exchange on backend Code-to-token exchange happens server-side Client secret exposure in browser
Refresh rotation New refresh token received and stored on each refresh Stolen refresh token remains valid indefinitely
No tokens in logs Logging middleware redacts token fields Tokens exposed in log aggregation systems
Code stripped from URL history.replaceState after callback processing Code leaks via Referer header or browser history

Common Pitfalls and How to Fix Them

The model uses response_type=token

This is the Implicit flow. Add a hard constraint to your prompt: "Do not use the Implicit flow or response_type=token under any circumstances. Use Authorization Code with PKCE only." If it appears anyway, flag it and re-prompt with that sentence verbatim.

PKCE uses the plain method

The plain code challenge method means the verifier itself is sent as the challenge — which provides no protection against an attacker who can intercept the authorization request. Always specify S256. If ChatGPT defaults to plain, re-prompt: "Switch the code_challenge_method to S256 and hash the verifier with SHA-256 before sending it."

Missing error handling on the callback

Authorization servers return ?error=access_denied when the user cancels. ChatGPT often generates callback handlers that crash or hang if the code parameter is absent. Prompt for it: "Handle the case where the callback contains an error parameter instead of a code. Redirect to a user-facing error page."

Token expiry is ignored

ChatGPT frequently generates token usage code with no expiry check, relying entirely on the API returning a 401 to trigger a refresh. That's functional but slow. Ask for proactive expiry checking: "Check the access token expiry before each API call. If it expires within 60 seconds, refresh it first."

This pattern connects to the broader problem of AI-generated security code cutting corners on error paths. The same thing happens with rate limiting middleware — the happy path is solid, the failure paths are hollow.

Scope handling is omitted

OAuth scopes gate what the token can do. ChatGPT often generates flows with no scope parameter, which means you get whatever the authorization server defaults to — usually more than you need. Always specify scopes explicitly and prompt for scope validation on the token response.

If you're working with API security more broadly, the techniques for auditing AI-generated code also apply to webhook handler security, where missing validation is equally common.

Next Steps

You now have a framework for getting OAuth 2.0 code out of ChatGPT that's actually worth shipping. Here's what to do from here:

  1. Build a prompt template. Take the prompt patterns from this article and save them as a reusable template in your team's shared prompt library. Consistency beats improvisation when security is involved.
  2. Run the checklist on existing code. If you've already integrated ChatGPT-generated auth code, apply the eight-point checklist above to it now. Token storage and state validation are the first two places to look.
  3. Add a redaction layer to your logging pipeline. Whether or not you used AI to write your auth code, make sure your log transport never sees raw token values. This is a one-time fix with permanent benefit.
  4. Test the error paths explicitly. Manually cancel an authorization request, expire a refresh token, and send a mismatched state value. Watch what your application does. ChatGPT-generated code often fails silently on these paths.
  5. Schedule a review when your identity provider updates its recommendations. OAuth best practices evolve. The move from Implicit to PKCE happened gradually; whatever comes next will too. Set a calendar reminder to re-evaluate your flow against current RFC guidance every six months.

Frequently Asked Questions

Why does ChatGPT default to the Implicit OAuth flow instead of Authorization Code with PKCE?

ChatGPT was trained on a large body of OAuth tutorials, many of which predate widespread PKCE adoption and still recommend the Implicit flow for SPAs. You have to explicitly forbid the Implicit flow in your prompt and specify Authorization Code with PKCE to get the correct output.

Is storing OAuth tokens in sessionStorage safer than localStorage?

Slightly safer, because sessionStorage is cleared when the browser tab closes, but it's still accessible to JavaScript and therefore still vulnerable to XSS attacks. The correct approach for SPAs is to have a backend proxy store tokens in httpOnly cookies, keeping them entirely out of JavaScript's reach.

How do I know if ChatGPT-generated OAuth code is missing refresh token rotation?

Look for the token refresh handler and check whether the code stores the new refresh token returned in the response. If the same refresh token variable is reused across multiple refresh calls without being updated, rotation is missing and a stolen refresh token remains valid indefinitely.

What does the OAuth state parameter protect against and how can I verify ChatGPT implemented it correctly?

The state parameter prevents CSRF attacks during the authorization flow by binding the request to the initiating session. Correct implementation generates a random value before the redirect, stores it server-side or in sessionStorage, and then compares it against the value returned in the callback before processing the authorization code.

Can ChatGPT accidentally expose OAuth tokens through logging middleware?

Yes — if your logging middleware serializes full request objects, it can capture Authorization headers, cookie values, or response bodies that contain tokens. Always prompt ChatGPT to add a redaction layer that strips token-related fields before they reach the log transport.

📤 Share this article

Sign in to save

Comments (0)

No comments yet. Be the first!

Leave a Comment

Sign in to comment with your profile.

📬 Weekly Newsletter

Stay ahead of the curve

Get the best programming tutorials, data analytics tips, and tool reviews delivered to your inbox every week.

No spam. Unsubscribe anytime.