JWT for systems analyst interviews
Contents:
Why JWT comes up
If you interview for a systems analyst role at any company that ships an API — Stripe, Uber, DoorDash, Linear, Notion — JWT is the default token format in OAuth 2.1 and OIDC flows. When you describe an integration in a design doc, three questions land on you within five minutes: what is inside the token, how does the consumer service validate it, and what happens when a token is compromised. If you can answer all three in concrete terms, you're past the bar; if you wave hands, you fail the round.
The most common death spiral is the candidate who says "the token is signed, so nobody can forge it" but cannot tell HS256 from RS256. At a payments company or any multi-tenant platform, RS256 is the default for one load-bearing reason: the signing secret cannot be shared across 50+ microservices without turning into a leaked-credential time bomb. If you don't know why, you don't get to design the auth integration.
One-liner to remember: JWT (RFC 7519) is a signed envelope for passing identity and authorization data between services. It is not a queue, not encryption, not a session — it's a tamper-evident slip of paper that any service can read but only the issuer can write.
Token structure
A JWT is three base64url-encoded segments separated by dots:
header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJleHAiOjE3MzAwMDAwMDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cThe header declares the signing algorithm and key id:
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-id-2026"
}The payload carries the claims — statements about the subject and the grant:
{
"sub": "user-123",
"iss": "https://auth.example.com",
"aud": "orders-api",
"exp": 1730000000,
"iat": 1729996400,
"scope": "read:orders write:orders"
}The signature is an HMAC or RSA signature over base64url(header) + "." + base64url(payload).
The critical fact, and the one interviewers test for: the payload is signed, not encrypted. Anyone holding the token can decode the base64 and read every claim. Never put passwords, third-party API keys, card numbers, or any data you wouldn't print on a postcard into a JWT payload. If you need confidentiality, the spec for that is JWE — encrypted JWT — and even there most teams just rely on TLS instead.
Standard claims
RFC 7519 reserves a short set of claim names. Memorize all of them — they show up verbatim on interview slides.
| Claim | Meaning | Why it matters |
|---|---|---|
iss |
Issuer (auth server URL) | Selects which JWKS to use for verification |
sub |
Subject (typically user_id) |
Identifies the principal the token represents |
aud |
Audience (target API) | Service must reject if it isn't in the audience |
exp |
Expiration (Unix seconds) | Hard cutoff; verifier rejects after this time |
nbf |
Not before | Token isn't valid before this timestamp |
iat |
Issued at | Audit and freshness checks |
jti |
JWT ID | Unique handle used for revocation lists |
Beyond registered claims, you can add custom claims — scope, role, tenant_id, feature_flags, org_id. Don't shadow registered names with your own meaning. If you build on top of OIDC, you also get profile claims for free: name, email, email_verified, preferred_username, picture, locale.
Naming hint: when teams later add tenant-scoped APIs, the team that put tenant_id in the JWT from day one saves itself a year of plumbing.
HS256 vs RS256
This is the single most common JWT question on a systems analyst interview, and the answer has to include both the mechanics and the architectural consequence.
HS256 (HMAC-SHA256) is symmetric. The same shared secret signs and verifies.
secret = "super-secret-key"
signature = HMAC-SHA256(header.payload, secret)Fast and cheap, but every verifying service needs the secret. With 30 microservices, that secret lives in 30 places — and the day any one of them leaks logs to S3 or commits an env file, every service is compromised. HS256 is appropriate for a monolith or two tightly coupled services under one operations team.
RS256 (RSA-SHA256) is asymmetric. The authorization server signs with a private key; verifying services hold only the public key.
signature = RSA-Sign(header.payload, private_key)
verify(token, public_key) -> boolThe public key is published through a JWKS endpoint:
GET /.well-known/jwks.json
{
"keys": [
{"kid": "key-id-2026", "kty": "RSA", "n": "...", "e": "AQAB", "alg": "RS256"}
]
}Each service caches the JWKS, picks the right key by the kid from the header, and verifies locally. The private key never leaves the auth server. A leaked public key teaches the attacker nothing useful.
| Aspect | HS256 | RS256 | ES256 |
|---|---|---|---|
| Key type | Shared secret | RSA private/public pair | Elliptic curve pair |
| Secret distribution | Every verifier | Only auth server | Only auth server |
| CPU per verify | Cheapest | ~10× HMAC | ~3× HMAC |
| Token size | Smallest | Largest signatures | Compact signatures |
| Best fit | Monolith, single team | Microservices, multi-tenant | Mobile, IoT, modern stacks |
ES256 (ECDSA on P-256) is the modern alternative: same security, smaller keys, faster on constrained devices. Greenfield default.
Service-side validation
Every resource server that accepts JWTs runs the same checklist. Memorize the order, because interviewers love asking you to recite it.
- Decode the header and reject if
algisnoneor doesn't match the algorithm you trust. - Look up the verification key by
kidin the cached JWKS. - Recompute the signature over
header.payloadand compare. Fail on mismatch. - Check
expagainst current time, allowing a small clock skew of 1-5 minutes. - Check
nbfif present; current time must be at or after it. - Confirm
issmatches the auth server you trust. - Confirm your service identifier is in
aud. - Confirm the
scopeorroleclaim grants the operation being requested.
Pseudocode:
def validate_jwt(token, jwks, expected_iss, expected_aud):
header = decode_header(token)
if header["alg"] == "none":
raise Unauthorized
key = jwks_lookup(jwks, header["kid"])
if not verify_signature(token, key, header["alg"]):
raise Unauthorized
payload = decode_payload(token)
now = time.time()
if payload["exp"] < now - CLOCK_SKEW:
raise Unauthorized
if payload.get("nbf", 0) > now + CLOCK_SKEW:
raise Unauthorized
if payload["iss"] != expected_iss:
raise Unauthorized
if expected_aud not in payload.get("aud", []):
raise Unauthorized
return payloadUse a battle-tested library — pyjwt, jose, nimbus-jose-jwt, jsonwebtoken for Node. Rolling your own validator is how CVEs are born.
Load-bearing trick: cache the JWKS in memory with a 10-minute TTL and a background refresh. On a cache miss for a previously-unseen kid, refresh once and retry. This handles key rotation without making every API call a network round-trip to the auth server.
Revocation strategies
The fundamental tension of JWT: a self-contained token is valid until exp, so out-of-the-box you cannot instantly revoke it. Every production system picks one of three workarounds.
Short access + refresh tokens. Access tokens live 5 to 15 minutes; refresh tokens live days to weeks and are stored server-side. On logout you delete the refresh token row. The access token still works until exp, so the worst-case compromise window equals the access TTL. This is the default at most public APIs.
Deny list keyed by jti. On logout or revoke, push the token's jti into Redis with TTL equal to its remaining lifetime. Every validator checks the deny list in addition to the signature. You pay one network call per request, but you can revoke a specific token within seconds.
Reference tokens with introspection. The token handed to the client is an opaque random string, not a JWT. On every request the resource server calls POST /introspect on the auth server to check the current state. Latency is higher, but revocation is instant and you keep all state server-side. This is what OAuth 2.0 introspection (RFC 7662) describes.
The classic interview question: "How do you revoke a JWT?" The model answer: JWT cannot be revoked without server-side state. Pick short TTL plus refresh revocation for most APIs, deny lists for sensitive endpoints, or opaque-plus-introspect when instant revocation is non-negotiable (admin tools, financial actions, account deletion).
| Strategy | Revoke latency | Per-request cost | Best fit |
|---|---|---|---|
| Short TTL + refresh | Up to access TTL | Zero (stateless) | Most public APIs |
Deny list by jti |
Seconds | One Redis lookup | Sensitive APIs, admin |
| Opaque + introspect | Instant | One HTTP call | Financial, healthcare, admin |
Common pitfalls
The first trap, and by far the most damaging, is storing sensitive data in the payload. Candidates regularly say "I'll put the user's password hash in there so the service doesn't need a DB call" — and that's the moment the interviewer ends the discussion. The payload is base64, not encryption. Anything you put in it is readable by every party that touches the token, including the client. Keep claims to identifiers, scopes, and non-sensitive profile fields.
The second pitfall is the alg=none attack. Older libraries respected alg: "none" in the header and skipped signature verification entirely; an attacker swaps the header, drops the signature, and walks in. Any modern library defaults to rejecting none, but you must verify your code path treats alg as a value you expect, not a value the token provides. Pin the accepted algorithms explicitly on the verifier.
The third is trusting iss without anchoring it to a key set. The issuer claim is just text the token writes about itself. The trust comes from verifying the signature with the JWKS you fetched from the issuer URL — not from reading iss. Maintain an allow-list of issuer URLs and tie each one to its JWKS endpoint, then verify in that order.
The fourth is skipping the aud check. A token issued for orders-api can be replayed against billing-api if the billing service doesn't verify aud. Every resource server must reject tokens whose audience doesn't include its own identifier. This is the difference between a real authorization boundary and security theater.
The fifth is HS256 in a distributed system. Rotating one shared secret across 50 services is operationally painful and you've usually leaked it somewhere by then. Pick RS256 or ES256 with JWKS from day one for more than one verifying service.
The sixth is multi-hour access TTLs. A 24-hour access token is essentially unrevokable; a stolen one in the morning is valid until tomorrow. Cap access tokens at 15 minutes by default, longer only with a deny-list backstop. Related: storing JWTs in localStorage exfiltrates on any XSS bug. Use Secure; HttpOnly; SameSite=Strict cookies for browsers or platform-native secure storage on mobile.
Related reading
- Authentication vs authorization on the SA interview
- Cookies vs tokens on the SA interview
- 2FA and MFA systems for SA interviews
- HTTP methods and status codes on the SA interview
- Idempotency keys on the SA interview
If you want to drill systems analyst questions like this every day, NAILDD is launching with 1,500+ interview problems across exactly this pattern.
FAQ
Is a JWT the same as a session token?
No. A session token is an opaque random string; the server keeps state in a database row keyed by that string. A JWT is self-describing: the claims live inside the token and the signature lets any verifier confirm them without a DB lookup. JWT trades server-side state for client-side payload size and locked-in expiration; sessions trade payload size for instant revocation.
Can a JWT be decrypted?
A standard JWT (JWS — JSON Web Signature) is signed, not encrypted. Anyone with the token can base64-decode the payload and read every claim. If you need confidentiality, use JWE (JSON Web Encryption), which encrypts the content with a recipient's key. In practice most teams rely on HTTPS for transport confidentiality and use plain JWS.
What TTL should I set on an access token?
For most APIs, 5 to 60 minutes. Shorter values mean more refresh traffic and faster invalidation; longer means less load on the auth server but a wider compromise window. For financial operations, healthcare, or admin tooling, stay at 5 to 15 minutes and back it with a jti deny list.
How do I validate a JWT without a network call per request?
Cache the JWKS in memory with a TTL of 5-10 minutes and a background refresh task. Each request reads the kid from the header, looks up the key in the in-memory cache, and verifies the signature locally. The only network call happens when the cache is cold or a new kid shows up during rotation.
What happens during key rotation?
The auth server starts signing new tokens with a new kid while keeping the old key in JWKS. Verifiers pick up the new key on their next refresh and accept tokens signed by either key. Once every token signed with the old key has expired, the auth server removes it from JWKS.
When should I pick opaque tokens over JWTs?
When you need instant revocation, when claims are large or volatile, or when you don't want clients reading internal authorization details. Admin consoles, impersonation flows, and any system where one stolen token can drain an account are good candidates for opaque plus introspection. JWTs win for stateless, high-throughput public APIs.
Is this official guidance?
No. This article is based on RFC 7519 (JWT), RFC 7515 (JWS), RFC 7517 (JWK), and RFC 7662 (Token Introspection), plus the patterns interviewers actually probe. Concrete requirements vary by company.