Getting ChatGPT to Write Accurate JWT Auth Flows Without Security Gaps
You ask ChatGPT for a JWT authentication flow and it hands you something that compiles, passes a quick smoke test, and looks reasonable. Then a security reviewer flags that the token never expires, the algorithm is HS256 with a hardcoded secret, and refresh tokens are stored in localStorage. The code works β it just shouldn't go anywhere near production.
The problem isn't that ChatGPT doesn't know JWT security. It does. The problem is that the default output optimises for brevity and getting something running fast. Without explicit constraints in your prompt, it will skip every inconvenient detail.
What You'll Learn
- The specific security shortcuts ChatGPT takes when writing JWT code by default
- How to structure a base prompt that encodes your security requirements upfront
- Prompts for the full token lifecycle: issuance, verification, refresh, and revocation
- How to pin algorithm choice and key management so the output matches your stack
- Storage and transmission patterns that ChatGPT omits unless you ask explicitly
Prerequisites
This guide assumes you understand what JWTs are and roughly how they work (header, payload, signature). You don't need to be a security expert, but you should know the difference between access tokens and refresh tokens. Examples use Python with PyJWT and a Node/Express snippet for comparison, but the prompting strategy applies to any stack.
Understand What ChatGPT Gets Wrong by Default
Before writing better prompts, you need to know which shortcuts ChatGPT takes so you can explicitly close each one. Here's what the vanilla output almost always includes:
- Weak algorithm defaults:
HS256with a short, hardcoded secret string. For public-facing APIs, you almost certainly wantRS256orES256with proper key pairs. - No expiry on access tokens: The
expclaim is often missing entirely, or set to an unrealistically long window like 30 days. - No refresh token logic: ChatGPT treats the access token as if it lives forever, skipping the short-lived access / long-lived refresh pattern entirely.
- Insecure storage guidance: Boilerplate frontend snippets park tokens in
localStorage, which is readable by any JavaScript on the page. - Missing token revocation: There's no mention of a deny-list or refresh token rotation for logout or compromise scenarios.
- No signature verification guard: The decode step often doesn't enforce
algorithmsparameter explicitly, leaving the door open to thealg: noneattack.
None of these are obscure edge cases. They're exactly the issues that appear in OWASP's API Security Top 10. Knowing this list means you can treat each item as a requirement to drop into your prompt.
Build a Base Prompt That Encodes Your Security Requirements
The most reliable technique is to front-load a security constraint block before any implementation request. Think of it as a spec your output must satisfy, not as hints.
You are writing production-grade authentication code. Apply these constraints to every piece of code you generate:
1. Use RS256 (asymmetric) for token signing. Never use HS256 with a hardcoded secret.
2. Access tokens expire in 15 minutes. Refresh tokens expire in 7 days.
3. Always pass the `algorithms` parameter explicitly when decoding β never allow algorithm negotiation.
4. Store refresh tokens server-side (database or Redis) so they can be revoked.
5. Refresh tokens must be rotated on every use.
6. Do not suggest localStorage for token storage in any browser context.
7. Include error handling for expired tokens, invalid signatures, and missing claims.
8. Return only the code, a brief explanation of each security decision, and a list of anything you assumed that I should verify.
Now write a JWT issuance endpoint in Python using FastAPI and PyJWT.
The last line is the actual task. Everything above it is the contract. This pattern β constraint block first, task last β consistently produces better output than weaving requirements into a single paragraph.
It also asks ChatGPT to surface its assumptions. This is important: if it assumes you're using a file-based key store when you're using AWS KMS, you need to catch that before you ship the code, not after.
Prompt for the Full Token Lifecycle, Not Just Issuance
A common mistake is asking for a JWT auth flow as one monolithic request. ChatGPT will then prioritise the happy path (user logs in, gets a token) and compress everything else. Break the lifecycle into four explicit prompts instead.
1. Token issuance (login endpoint)
Ask for the endpoint that takes credentials, validates them, then returns both an access token and a refresh token. Specify that the refresh token should be stored server-side and returned as an HttpOnly cookie, not in the JSON body.
Using the constraints defined above:
Write the /auth/login endpoint. It should:
- Verify the user's password against a bcrypt hash from the database
- Issue an RS256-signed access token (15-minute expiry) containing `sub`, `iat`, `exp`, and a `roles` claim
- Issue a refresh token (opaque random bytes, NOT a JWT), store a hash of it in the database, and return it as an HttpOnly, Secure, SameSite=Strict cookie
- Return only the access token in the JSON response body
2. Token verification middleware
Ask for this separately so ChatGPT gives it proper attention. Specify the algorithm allow-list and the claims you want validated.
Write a FastAPI dependency that verifies the JWT from the Authorization header.
Requirements:
- Decode with `algorithms=["RS256"]` only β reject anything else
- Validate `exp`, `iat`, and `sub` claims
- Raise HTTP 401 for expired tokens, HTTP 403 for tokens with insufficient roles
- Return the decoded payload as a Pydantic model
3. Token refresh
This is the piece ChatGPT most often skips. Ask for it explicitly.
Write the /auth/refresh endpoint.
Requirements:
- Read the refresh token from the HttpOnly cookie (not the request body)
- Hash the incoming token and look it up in the database
- Reject if not found, expired, or already used (rotation: mark the old one as used)
- Issue a new access token and a new refresh token
- Store the new refresh token hash, invalidate the old one in the same database transaction
4. Logout and revocation
Write the /auth/logout endpoint.
Requirements:
- Accept the refresh token from the HttpOnly cookie
- Delete (or mark as revoked) the refresh token record in the database
- Clear the cookie on the response
- Return 204 No Content
Breaking the flow into four prompts forces ChatGPT to give each piece the depth it deserves. It also makes review easier: each output maps to one responsibility.
This is the same discipline described in getting ChatGPT to write accurate webhook handlers without missing edge cases β decomposing the task so the model can't paper over complex logic with a summary comment.
Specifying Algorithm and Key Management
Leaving algorithm choice to ChatGPT's discretion is the single highest-risk thing you can do when prompting for auth code. Always be explicit.
For most APIs, RS256 is the right default. The private key signs tokens (lives on your auth server only), and the public key verifies them (can be distributed to any service that needs to verify tokens). This means you don't share a secret across services.
Tell ChatGPT exactly how your keys are stored:
The private key is stored as a PEM-encoded string in an environment variable called JWT_PRIVATE_KEY.
The public key is in JWT_PUBLIC_KEY.
Do not hardcode key material. Do not generate keys at runtime. Load them from environment variables at application startup and cache them in memory.
If you're using a secrets manager (Vault, AWS Secrets Manager), say so. ChatGPT will generate the appropriate fetch-on-startup pattern rather than os.getenv() calls scattered through the codebase.
One more thing: explicitly forbid alg: none. Some older JWT libraries accept a token with no signature if the header declares "alg": "none". A competent library won't do this by default, but your prompt should make the requirement explicit anyway:
import jwt
PUBLIC_KEY = os.environ["JWT_PUBLIC_KEY"]
ALLOWED_ALGORITHMS = ["RS256"] # never "none", never negotiated from header
def verify_token(token: str) -> dict:
try:
payload = jwt.decode(
token,
PUBLIC_KEY,
algorithms=ALLOWED_ALGORITHMS,
options={"require": ["exp", "iat", "sub"]},
)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError as exc:
raise HTTPException(status_code=401, detail="Invalid token")
When you ask ChatGPT to produce this, include the comment about "none" in your prompt. It will replicate the intent in the generated code.
Handling Token Expiry and Refresh Correctly
The 15-minute access token / 7-day refresh token split is a well-established pattern, but ChatGPT doesn't apply it unless you specify it. Here's what a correctly prompted issuance function looks like:
from datetime import datetime, timedelta, timezone
import jwt
import secrets
import hashlib
ACCESS_TOKEN_EXPIRY = timedelta(minutes=15)
REFRESH_TOKEN_EXPIRY = timedelta(days=7)
PRIVATE_KEY = os.environ["JWT_PRIVATE_KEY"]
def create_access_token(subject: str, roles: list[str]) -> str:
now = datetime.now(timezone.utc)
payload = {
"sub": subject,
"roles": roles,
"iat": now,
"exp": now + ACCESS_TOKEN_EXPIRY,
}
return jwt.encode(payload, PRIVATE_KEY, algorithm="RS256")
def create_refresh_token() -> tuple[str, str]:
"""Returns (raw_token, hashed_token). Store the hash; send the raw."""
raw = secrets.token_urlsafe(64)
hashed = hashlib.sha256(raw.encode()).hexdigest()
return raw, hashed
Notice the timezone.utc on every datetime call. ChatGPT sometimes generates naive datetimes, which causes silent expiry miscalculations. Add "use timezone-aware datetimes throughout" to your constraint block.
If you're also managing environment variable handling for your secrets, this guide on getting ChatGPT to write accurate environment variable configs covers how to prompt for complete, validated config loading.
Secure Token Storage: Pinning Down the Right Pattern
This is where ChatGPT's defaults cause the most frontend problems. Left to its own devices, it will suggest something like:
// ChatGPT default β do NOT do this
localStorage.setItem("access_token", response.data.token);
localStorage is accessible from any JavaScript on the page. A single XSS vulnerability anywhere on your domain can exfiltrate every stored token. The safer pattern is memory storage for access tokens (a module-level variable in your auth module, lost on page refresh) and HttpOnly cookies for refresh tokens.
Tell ChatGPT this explicitly:
For the frontend token handling:
- Store the access token in memory only (a module-level variable), never in localStorage or sessionStorage
- The refresh token is handled server-side as an HttpOnly cookie β do not read or write it from JavaScript
- On page load, call /auth/refresh silently to get a new access token if the user has an active session
- On 401 responses from the API, attempt one silent refresh, then redirect to login if that also fails
With this prompt, ChatGPT will produce an auth module with a reasonable token renewal strategy rather than a one-liner that puts credentials at risk.
Common Pitfalls When AI Writes Auth Code
Even with a solid constraint block, a few issues slip through consistently. Watch for these in every review pass:
- Missing
issandaudclaims: ChatGPT often skips issuer and audience claims. Without them, a token from your staging environment is technically valid on production. Add"validate iss and aud claims"to your constraint block. - Refresh token database schema with no index: ChatGPT will generate the SQL table but forget to index the token hash column. Token lookup on every request is a slow full table scan without it. Ask explicitly: "include a unique index on the token_hash column".
- Clock skew ignored: Some JWT libraries accept a small
leewayparameter for clock skew between servers. ChatGPT won't mention it. Decide your tolerance (usually 30β60 seconds) and specify it. - No rate limiting on the token endpoints: The
/auth/loginand/auth/refreshendpoints need rate limiting. ChatGPT doesn't add it unless asked. Mention it in your prompt or follow up with a dedicated request. - Error messages that leak information: A response that says "user not found" vs "incorrect password" is an enumeration vulnerability. Ask ChatGPT to use a single generic error for all credential failures.
This class of gap β technically correct logic with missing defensive layers β is exactly what makes AI-generated security code risky without review. The same principle applies broadly; ChatGPT's async code has a similar pattern of correct-looking logic with dangerous edge cases.
For teams where auth code is reviewed as part of a broader security pipeline, it's also worth checking how GitHub Copilot handles test generation for auth flows β the combination of ChatGPT for initial scaffolding and Copilot for filling in test cases works well in practice.
Wrapping Up: Next Steps
ChatGPT can produce solid JWT auth code, but only when you treat security requirements as hard constraints in your prompt, not polite suggestions. Here's what to do next:
- Build your reusable constraint block. Write a security requirements snippet you paste at the start of every auth-related prompt. Encode your algorithm, key source, expiry windows, and storage rules. Treat it like a team style guide.
- Decompose the lifecycle. Prompt for issuance, verification, refresh, and revocation as separate requests. Review each output against the corresponding requirement before moving on.
- Always ask ChatGPT to list its assumptions. Add "list any assumptions you made that I should verify" to every prompt. Token expiry values, key formats, and database schema assumptions belong on that list.
- Run the output through a security linter. Tools like Bandit (Python) or
eslint-plugin-security(JavaScript) will catch a class of issues that ChatGPT consistently misses, including hardcoded secrets that sneaked through and insecure random number usage. - Do a focused review for the five omissions. Tick off each item from the "what ChatGPT gets wrong" section above against every generated file before it goes into a PR.
Frequently Asked Questions
How do I stop ChatGPT from using HS256 with a hardcoded secret in JWT code?
Explicitly state in your prompt that HS256 with hardcoded secrets is forbidden and that you require RS256 with keys loaded from environment variables. ChatGPT defaults to HS256 for brevity; it will switch algorithms when you give a clear, non-negotiable constraint.
What expiry times should I specify when prompting ChatGPT for JWT auth flows?
A widely accepted baseline is 15 minutes for access tokens and 7 days for refresh tokens. Including these exact values in your constraint block prevents ChatGPT from defaulting to unrealistically long expiry windows like 30 days.
Does ChatGPT handle JWT refresh token rotation automatically?
Not by default. Without an explicit requirement, ChatGPT will often reuse the same refresh token indefinitely. You need to prompt specifically for rotation: mark the old token as used and issue a new one on every refresh request.
Why is localStorage dangerous for JWT storage and what should I tell ChatGPT to use instead?
localStorage is readable by any JavaScript on the page, so a single XSS vulnerability can expose all stored tokens. Tell ChatGPT to keep access tokens in memory (a module-level variable) and deliver refresh tokens as HttpOnly cookies that JavaScript cannot read.
How do I make sure ChatGPT's JWT verification code is safe against the alg:none attack?
Require that the algorithms parameter is always passed explicitly as a fixed list like ["RS256"], and that algorithm negotiation from the token header is never allowed. Mentioning the alg:none attack by name in your prompt causes ChatGPT to include a comment explaining why the parameter is required.
π€ Share this article
Sign in to saveRelated Articles
Comments (0)
No comments yet. Be the first!