JWT pitfalls for systems analyst interviews

Train for your next tech interview
1,500+ real interview questions across engineering, product, design, and data — with worked solutions.
Join the waitlist

Why pitfalls matter more than the happy path

When a senior interviewer at Stripe, Okta, or Cloudflare opens a JWT question, they are not testing whether you can decode a token at jwt.io. They are testing whether you have personally watched a JWT mistake leak a production database. Every staff systems analyst has, and the interviewer wants to hear which one bit you.

JWTs are deceptively simple: three base64 segments, a signature, done. The trap is that the spec (RFC 7519) leaves enough flexibility that a careless library default or a copy-pasted snippet can turn a signed token into a forgery factory. Interviewers probe five canonical failure modes, and the ability to name all five plus the defense for each is what separates a mid-level answer from a senior one.

Load-bearing trick: When the interviewer asks "tell me about JWT," they are really asking "tell me how JWT goes wrong." Lead with pitfalls, not with structure.

This article walks through each of those five failure modes the way an interviewer expects you to walk through them — attack first, defense second, an example of who got burned in production third.

The alg=none attack

Every JWT carries a header that names its signing algorithm: HS256, RS256, ES384, and so on. The original 2015 spec also allowed none — a token with no signature at all, intended for environments where transport security was assumed to cover integrity. That decision aged badly.

The attack is embarrassingly simple. An attacker takes a legitimate token, swaps the header to {"alg":"none"}, drops the signature segment, edits the payload to whatever they want ("sub":"admin", "role":"superuser"), and submits it. A vulnerable library sees alg=none, skips signature verification, and accepts the forged claims.

Library family Historical default Current behavior
node-jsonwebtoken (pre-9.x) Accepted none if no algorithms option set Rejects unless explicitly allowlisted
pyjwt (pre-2.0) Accepted none silently Requires explicit algorithms=[...]
jjwt (Java) Rejected none by default Same, still rejects
golang-jwt/jwt Required explicit parsing func Same, no implicit none

Defense. Always pass an explicit allowlist of algorithms to your verifier — typically just HS256 for symmetric or RS256/ES256 for asymmetric. Never read the algorithm from the incoming token header and use that to pick the verification function. The token is the thing you do not trust yet; letting it pick its own verifier is the textbook confused-deputy.

A close cousin is the alg confusion attack: a token signed with HMAC where the secret happens to be the public RSA key, exploited when the verifier picks HMAC after seeing HS256 in the header but the server's configured key was the RSA public key.

Weak HMAC secrets

If you sign with HS256, the signature is HMAC-SHA256(secret, header.payload). Whoever knows the secret can mint tokens. The pitfall: developers pick a short, memorable string — "secret123", the company name, the project codename — and an offline brute-force tool like jwt-cracker or hashcat chews through it in seconds on a laptop.

Public CTF write-ups have cracked secrets of 8 characters in under a minute on commodity hardware. With a single leaked JWT, the attacker can mint tokens for any user, including admin accounts, with no further interaction with your service.

Sanity check: If your HMAC secret would fit on a Post-it, it is too short.

Defense. Use a 256-bit random secret (32 bytes from /dev/urandom, hex-encoded gives you 64 characters). Store it in a secrets manager — AWS Secrets Manager, HashiCorp Vault, Doppler — not in source control, not in a .env file checked into the repo. Rotate the secret on a schedule and on suspicion of compromise.

Better: if you have any real security stakes — multi-tenant SaaS, fintech, healthcare — drop HMAC entirely and use RS256 or ES256. Asymmetric signing means the verifier only needs the public key, so a leaked verifier instance cannot forge tokens.

Missing or sloppy expiry

A JWT without an exp claim is valid forever. A stolen token from a five-year-old laptop logged into your service in 2021 will still authenticate today if nothing limits its lifetime.

The exp claim is a Unix timestamp; the spec says verifiers MUST reject tokens past that timestamp. The pitfall is twofold: some teams omit exp entirely because "we'll add it later," and some teams set it to 30 days or longer to avoid bothering users with re-auth. Both are how compromise stretches from "an incident" to "an incident with a six-month tail."

The senior-grade answer is the access + refresh pattern:

Access token  → JWT, exp = 15 min, sent on every API call
Refresh token → opaque, server-side row, exp = 30 days, used only at /auth/refresh

Access tokens stay short-lived so the blast radius of a leak is bounded by minutes. Refresh tokens are server-side state — a row in Postgres or Redis — which means you can revoke them instantly by deleting the row. The client trades the refresh token for a new access token at the /auth/refresh endpoint, and that endpoint is where you enforce rotation and reuse detection.

Defense. Always set exp. Keep access tokens in the 15-60 minute range. Pair them with refresh tokens for any session longer than a single API call.

Train for your next tech interview
1,500+ real interview questions across engineering, product, design, and data — with worked solutions.
Join the waitlist

Revocation in a stateless world

JWTs are sold as stateless: the server verifies the signature, trusts the claims, no database lookup needed. That is also the single biggest operational pain point. If a token leaks at 10:00 and expires at 10:30, your "revoke" button does nothing for thirty minutes — a leaked token is valid until it isn't, and the server has no record to delete.

Three patterns handle this, each with a tradeoff:

Pattern Latency to revoke Cost When to use
Short-lived tokens only Up to exp window Free Default for most apps; pair with refresh tokens
jti blacklist (Redis) Sub-second One Redis hit per request Compliance-heavy domains (banking, health)
Introspection endpoint Sub-second One HTTP call per request OAuth resource servers, gateway-fronted APIs

The introspection pattern (RFC 7662) basically gives up on statelessness — every request asks the auth server "is token X still good?" — but it is what you want when immediate revocation is a contractual requirement.

Gotcha: Interviewers love to push on this. "You said JWTs are stateless. Then how do you log a user out?" The right answer is to acknowledge the tension between statelessness and revocation, then pick the pattern that matches the threat model.

Defense. Pick one pattern explicitly, document why, and make sure your monitoring catches the gap. A common compromise: short-lived JWTs plus a small Redis blacklist for the handful of tokens that need force-revocation between leaks and the next natural expiry.

Key rotation with JWKS

The last pitfall is the one most teams discover the hard way: how do you rotate the signing key without a downtime window where in-flight tokens become invalid?

The answer is JWKS — JSON Web Key Set — a public endpoint that lists all currently valid public keys. Servers fetch it periodically (typically every 5-60 minutes) and cache it. When you rotate, you publish the new key alongside the old one; old tokens still verify against the old key until they expire naturally, and new tokens are signed with the new key.

GET /.well-known/jwks.json
{
  "keys": [
    {"kid": "key-2026-05", "kty": "RSA", "n": "...", "e": "AQAB", "use": "sig"},
    {"kid": "key-2026-04", "kty": "RSA", "n": "...", "e": "AQAB", "use": "sig"}
  ]
}

Each token header carries a kid (key ID) pointing at the entry in the set. The verifier picks the matching key, ignoring the others. Once the access-token TTL has fully passed since you stopped signing with the old key, you remove it from the JWKS.

The pitfall is JWKS caching. If your verifier caches the set for an hour and you publish a new key, tokens signed with the new key will fail verification on instances that still hold the stale cache. The fix is either short cache TTLs (5 minutes) or cache-aware re-fetching: if a token's kid is unknown, refresh the JWKS once before rejecting.

Most managed identity providers — Auth0, Okta, Cognito, Clerk — handle JWKS hosting and rotation for you. If you are rolling your own, copy their structure exactly.

Common pitfalls

The five pitfalls above are the textbook ones. Three more catch even careful teams.

The storing JWTs in localStorage trap is widespread. A JWT in localStorage is readable by any script on the page, including any compromised npm dependency. The fix is to keep tokens in httpOnly, Secure, SameSite=Lax cookies for browser clients — they are not readable from JS, which neutralizes XSS-driven token theft. Mobile apps can use the OS keychain (Keychain on iOS, Keystore on Android).

The trusting JWT for authorization trap shows up when teams stuff role claims into the token ("role":"admin", "permissions":["billing.write"]) and never check them server-side against the live permissions table. A user demoted from admin still has an unexpired token claiming admin — and your service trusts it. The defense is to treat JWT claims as identity hints, not authorization decisions; re-check permissions against the source of truth on sensitive endpoints.

The putting PII in the payload trap matters because JWT payloads are base64-encoded, not encrypted. Anyone who intercepts a token — browser extensions, log aggregators that capture Authorization headers, the proxy in front of your service — can decode it. Email addresses, full names, phone numbers, internal user IDs all leak. Keep the payload to opaque identifiers and short-lived claims; look up the rest server-side.

If you want to drill security questions like these — with grader-style follow-ups — the NAILDD interview trainer has 1,500+ systems analyst problems including the full auth-and-tokens track.

FAQ

Should I ever use HS256 in production?

Yes, but only when the signer and the verifier are the same service, the secret is at least 256 bits of randomness, and it lives in a secrets manager with rotation. The moment you have multiple verifiers — a gateway, a resource service, a background worker — switch to RS256 or ES256 so verifiers only need a public key. The asymmetric setup is also what JWKS-based rotation assumes.

How do I revoke a JWT before it expires?

Pure JWT cannot be revoked — that is the cost of statelessness. The three options are: shorten the access-token TTL so the natural-expiry window is small (often 15 minutes is enough), maintain a jti blacklist in Redis that the verifier consults on every request, or call an introspection endpoint on the auth server. Most production designs combine the first with the second for a "fast revoke" path on logout and password change.

What is the difference between JWT and JWS?

JWS — JSON Web Signature — is the underlying signed-payload format. JWT is a specific use of JWS where the payload is a JSON claims object (sub, exp, iat, custom claims). All JWTs are JWS; not all JWS payloads are JWTs. There is also JWE (JSON Web Encryption), which encrypts the payload instead of just signing it — useful when the claims themselves are sensitive.

Why not just use session cookies and skip JWT entirely?

For a single monolithic web app, session cookies backed by a server-side store are simpler, instantly revocable, and battle-tested. JWT becomes worth it when you have multiple services that must verify identity without a shared session store, when you need to pass identity across a service mesh, or when third-party clients (mobile apps, partner integrations) consume your API. If those conditions do not apply, stay with sessions and save yourself the rotation headache.

How long should an access token live?

The interview-grade answer is 15 minutes for most consumer apps, 1 hour for B2B SaaS, never longer than the refresh-token rotation cadence. Shorter windows reduce the blast radius of a leak; longer windows reduce refresh-endpoint load. Pair short access tokens with refresh tokens stored server-side so the user experience stays smooth — most users will never see a forced re-login.

Do I need to validate iss and aud claims?

Yes, especially on resource servers behind a gateway. The iss (issuer) claim should match your auth server's URL, and aud (audience) should match your service's identifier. Skipping these means a token issued by your auth server for a different downstream service can be replayed against yours. It is a one-line check and it catches a class of cross-service confused-deputy bugs that signature validation alone does not.