If you remember five things
The whole topic in one glance. Everything below defends these five calls.
- Two different jobs. OAuth is the protocol for getting and using tokens. JWT is one format for encoding them. You can run OAuth with opaque tokens, and you can use JWTs with no OAuth at all.
- Authorization Code flow with PKCE. The code comes back in the browser and is useless alone. It gets exchanged on a back channel, and PKCE closes the gap for mobile and single-page apps that cannot keep a secret.
- JWT is encoded, not encrypted. Anyone can base64-decode it and read the claims. The signature stops tampering, not reading. Never put a secret in the payload.
- Stateless versus stateful is the real trade-off. JWTs verify locally and scale beautifully, but you cannot un-issue them. Opaque tokens cost a lookup per request but revoke instantly.
- The hybrid wins. Short-lived JWT access tokens on the hot path, revocable refresh tokens for the session, and an optional tiny deny-list when you need faster kill.
1The core idea#
Start with the problem before you reach for a protocol. You have a user, a frontend or third-party app, and a backend API, and the API needs two facts on every request:
- Who is this?
- What are they allowed to do?
The naive answer, "send the username and password every time," is a disaster:
- The password travels on every single call.
- Every service that forwards the request now sees it.
- You can never grant limited access without handing over the full keys.
OAuth 2.0 fixes this with indirection. Instead of passing credentials around, you pass a token that stands in for a specific grant of access.
- The user authenticates once with an authorization server.
- The server hands back a token scoped to certain permissions.
- The client shows that token to the API.
- The API trusts the token because it trusts the server that minted it.
- Nobody downstream ever sees the password.
The mental model I give juniors: OAuth is the hotel front desk.
- You show your ID and credit card once at check-in. That is authentication.
- They give you a key card. That is the token.
- The card opens your room and the gym, not the manager's office or someone else's room. Those are scopes.
- Housekeeping holds a card that opens rooms but not the safe.
- The card expires at checkout, and the maid never sees your passport.
JWT is a separate decision. It is just a format for a token.
- A signed, self-contained blob carrying claims like "user id 4471, expires at 14:05, scope read:orders."
- The point: the API verifies it with a public key and reads the claims directly, zero round trips to a database or auth server.
- A token does not have to be a JWT. It could be a random 32-byte string that means nothing until you look it up. That is an opaque token.
OAuth is the protocol for getting and using tokens. JWT is one popular way to encode them. You can run OAuth with opaque tokens. You can use JWTs completely outside OAuth. They pair well, but they are not the same thing, and candidates who treat them as one word get probed until they fall apart.
2How it works#
The flow you should know cold is the Authorization Code flow, behind essentially every "Log in with Google" button. Four parties: the user (resource owner), the client app, the authorization server, and the resource server (your API).
Two things make this flow safe.
- The authorization code that comes back in the redirect is short-lived and useless on its own. To turn it into a real token, the client exchanges it on a back channel using its client secret, which never touches the browser.
- Modern public clients (mobile apps, single-page apps) add PKCE: the client generates a random secret, hashes it, sends the hash up front, and reveals the original only at exchange time. That stops an attacker who intercepts the code from redeeming it.
If you say "PKCE" unprompted when you mention a mobile or single-page client, I write it down. It tells me you know public clients cannot hold a secret, so the Authorization Code flow alone is not enough for them. That one acronym, used correctly, moves you from "read about OAuth" to "has shipped OAuth."
Now the token itself. If it is a JWT, it is three base64 parts joined by dots: header, payload, signature.
- The header says which algorithm signed it, say RS256.
- The payload holds the claims.
- The signature is computed over the first two parts using the auth server's private key.
- Your API holds the matching public key, so it checks the signature locally.
- If it passes, the API knows the token came from the auth server and was not tampered with. It reads
expfor expiry,audto confirm the token was meant for this API, andissto confirm who issued it.
A decoded JWT payload is just JSON:
{
"sub": "4471",
"iss": "https://auth.example.com",
"aud": "orders-api",
"scope": "read:orders write:orders",
"iat": 1762425900,
"exp": 1762426800
}
iatis issued-at,expis expiry, both Unix seconds.- The gap is 900 seconds, a 15-minute access token.
- The API trusts this blob only because the signature over the header and payload checks out against the public key.
Saying a JWT is "secure because it is encrypted." It is encoded, not encrypted. Anyone can base64-decode it and read every claim. Never put a password, a card number, or a secret in the payload. The signature protects against modification, not against reading. Get this wrong and the rest of your answer is suspect.
Short-lived access, long-lived refresh.
- Access tokens are deliberately short, often 5 to 15 minutes.
- When one expires, the client uses a long-lived refresh token to get a new access token without bothering the user.
- Refresh tokens are sensitive: kept server-side or in secure storage, and revoked to kill a session.
3Trade-offs#
The headline decision is JWT (stateless) versus opaque tokens (stateful). This is the trade-off interviewers actually want you to reason about.
With a JWT, verification is local.
- Your API validates the signature and reads the claims in microseconds, no network call.
- That scales beautifully: 40 service instances at high load, none hitting a central auth database.
- The cost is revocation. The token is self-contained and trusted until
exp, so you cannot easily un-issue it. A stolen token with a 1-hour expiry works for up to an hour.
With an opaque token, the token is a meaningless reference.
- Every API call looks it up in a shared store (Redis, a database) to resolve identity and scopes.
- Revocation is trivial: delete the record and the next request fails instantly.
- The cost is a lookup on every request, which means a dependency and latency on your hot path.
Reveal the answer
Worked example: put real numbers on it#
Make the scale argument concrete, because this is where strong candidates pull ahead. Say you run 40 service instances at 50,000 requests per second.
Pick opaque tokens and every request pays for a lookup.
- A Redis lookup inside the same region costs roughly 1 ms round trip.
- That cluster now eats all 50,000 lookups per second. One node tops out around 100,000 simple GETs per second, so you size and replicate the cluster purely for auth, and you have added 1 ms to the p50 of every request.
- If Redis ever hiccups, 100 percent of traffic fails auth. Auth just became your most critical single dependency.
Pick JWTs and each service verifies locally.
- An RS256 signature check is around 50 to 100 microseconds of CPU and zero network.
- At 50,000 requests per second that is about 2.5 to 5 CPU-seconds of work spread across 40 boxes, call it low single-digit percent of one core each.
- No shared dependency, no added network latency, nothing to fall over.
The catch lands the moment a token leaks.
- With a 15-minute JWT expiry, a stolen access token is usable for up to 900 seconds after you detect the breach, because there is nothing to delete.
- With opaque tokens you delete one Redis key and the next request, 1 ms later, is rejected.
So the honest framing is one line: JWTs trade a guaranteed per-request cost for a bounded post-breach exposure window.
JWT (stateless)
- Verification
- Local signature check, no network.
- Latency cost
- Near zero, about 50 to 100 us of CPU.
- Revocation
- Hard: wait for expiry.
- Leaked token damage
- Lives until expiry.
Opaque (stateful)
- Verification
- Lookup in a shared store per request.
- Latency cost
- One cache or DB hit, about 1 ms.
- Revocation
- Instant: delete the record.
- Leaked token damage
- Killed on demand.
| Dimension | JWT (stateless) | Opaque (stateful) |
|---|---|---|
| Verification | Local signature check | Lookup per request |
| Latency cost | Near zero, ~50 to 100 us CPU | One cache or DB hit, ~1 ms |
| Revocation | Hard, wait for expiry | Instant |
| Horizontal scale | Excellent | Needs shared store |
| Token size | Larger, 500 bytes plus | Small, 32 bytes |
| Leaked token damage | Lives until expiry | Killed on demand |
The answer most senior engineers land on is a hybrid.
- Use short-lived JWTs as access tokens, so the hot path stays fast and stateless.
- Use stateful, revocable refresh tokens for the session.
- When something goes wrong, revoke the refresh token, and within 15 minutes the access token expires and the attacker is locked out.
- Need faster revocation? Keep a small deny-list of revoked JWT ids in Redis that services check.
- The subtle win: that deny-list holds only the handful of tokens you have actively killed, so it stays tiny, maybe a few thousand entries, not one row per live session.
One more trade-off worth naming unprompted: the signing algorithm.
- RS256 (asymmetric): the auth server signs with a private key, every service verifies with the public key, so services never hold a secret that can mint tokens.
- HS256 (symmetric): one shared secret signs and verifies, simpler but every verifying service could also forge tokens.
- In a multi-service setup, pick RS256. If one of your 40 boxes is compromised, an attacker with the HS256 secret can mint valid tokens for any user, while with RS256 they only get the public key, which verifies and cannot sign.
4Where it is used#
OAuth and JWT turn up in a few recognizable shapes. Name the pattern that fits the question in front of you.
Social login.
- "Log in with Google, GitHub, Apple" is pure OAuth 2.0, layered with OpenID Connect (OAuth plus a standardized identity token).
- You click the button, you never give Google your password through the third-party app, and the app gets a scoped token.
Internal microservices lean heavily on JWTs.
- A request hits the API gateway, the gateway validates the token once, and the identity rides along in the JWT to every downstream service.
- Each service verifies locally. No central auth bottleneck in the mesh.
- This is the default pattern in most companies past a handful of services.
Machine-to-machine.
- A backend job that calls another API uses the client credentials grant: no user involved, the service authenticates with its own id and secret and gets a token.
Clients and public APIs.
- Mobile apps use the Authorization Code flow with PKCE.
- Public APIs like Stripe or the GitHub API issue tokens scoped to specific permissions, so you can mint a key that reads repos but cannot delete them.
Reach for this whenever more than one party needs delegated, limited authority, or whenever you want to decouple the thing that proves identity from the things that consume it.
5Common pitfalls and gotchas#
Each of these is a real red flag I have watched fail candidates and break production systems.
- Putting secrets in the JWT payload. It is readable. I have reviewed code that stashed internal role flags and a partial card number in the token. Assume the user sees every claim.
- Not validating
audandiss. A token minted for service A should not be accepted by service B. Skip the audience check and a token fine for a low-privilege API can be replayed against a high-privilege one. Check issuer and audience every time, not just the signature. - The
alg: noneattack. Early JWT libraries honored a token whose header said the algorithm was "none" and skipped verification entirely. An attacker just strips the signature. Pin the expected algorithm server-side and reject anything else. - Long-lived access tokens. Setting a 30-day access token because refresh logic is annoying turns a leaked token into a 30-day breach. Keep access tokens in minutes, push the lifespan into the refresh token where you can revoke it.
- Storing tokens in localStorage in the browser. Any XSS can read localStorage and steal the token. Prefer httpOnly cookies so JavaScript cannot touch them, and pair that with CSRF protection.
- No refresh token rotation. On use, issue a new refresh token and invalidate the old. If an old one is ever used again, you know it was stolen and can kill the session. Without rotation, a leaked refresh token is a silent permanent backdoor.
- Clock skew on
exp. Drifting clocks reject valid tokens or accept expired ones. Allow a small leeway, say 30 to 60 seconds, and keep NTP running.
6How it shows up in interviews#
I rarely ask "explain OAuth." I drop it into a bigger design: "Users log in with Google, we have 12 microservices, how does auth flow through the system?" Here is what I am listening for.
- Can you separate authentication from authorization? Weak answers use the words interchangeably. Strong answers say the auth server handles login and issues a token, and each service makes its own authorization decision from the token's scopes.
- Do you reach for stateless versus stateful on purpose? Weak: "I will use JWTs" with no reasoning. Strong: "Short-lived JWTs so services verify locally and we do not bottleneck on an auth DB, plus revocable refresh tokens for logout and compromise." That tells me you have felt the pain of both.
- Do you handle revocation? The follow-up I always ask is "a token gets stolen, now what?" The weak answer stalls. The strong answer already built for it: short expiry, revoke the refresh token, optionally a Redis deny-list for instant kill, and rotate refresh tokens so reuse is detectable.
- Do you know the security basics? PKCE for public clients, RS256 over HS256 across services, httpOnly cookies, validating
audandiss, never trustingalgfrom the token. Naming two or three unprompted signals real experience.
Weak answer versus strong answer#
Same prompt, two candidates. Prompt: "Users log in with Google, we have 12 microservices. Walk me through auth, and tell me what happens when a token is stolen."
Weak candidate: I would use JWTs because they are secure and encrypted. The user logs in with Google, gets a JWT, and sends it on every request. Each service checks the JWT. If a token is stolen, well, the JWT is encrypted so they cannot really use it.
Interviewer: (writing: JWTs are encoded, not encrypted, so "cannot use it" is wrong. No reasoning for the choice. And no revocation story at all, which is the exact thing I asked about.)
Strong candidate: Google plus OpenID Connect handles login and issues an identity token. We exchange that for our own access token, a 15-minute RS256 JWT, so all 12 services verify locally with the public key and we never bottleneck on an auth database. The session is backed by a rotating refresh token in an httpOnly cookie, stored server-side so it is revocable. When a token is stolen, I revoke the refresh token immediately, so no new access tokens get minted, and the stolen access token dies on its own within 15 minutes. If 15 minutes is too long for our threat model, I add a Redis deny-list of revoked token ids, which stays tiny because it only holds actively killed tokens, and services check it on the hot path. Refresh rotation means if the old refresh token is ever replayed, I detect reuse and kill the whole session.
Interviewer: (writing: names the real trade-off, picks on purpose, breach plan already built in. Move to the next round.)
The weakest answer treats a JWT as a magic login that is safe because it is "encrypted." It is not encrypted and it is not magic. The strongest answers talk about it as a cache of identity you chose to trust for a few minutes, with a plan for the moment that trust goes wrong.
A JWT is a cache of identity you chose to trust for a few minutes. Design for the moment that trust goes wrong.
"A token gets stolen. Now what?"
"JWT or opaque tokens, and why?"
"Why RS256 over HS256 across services?"
"Is a JWT encrypted? Can I put a role flag or card number in it?"
"What stops an attacker from stripping the signature or replaying a token at the wrong service?"
What you can now explain in the room
- Why OAuth (the protocol) and JWT (the token format) are different decisions that happen to pair well.
- The Authorization Code flow end to end, and why PKCE matters for mobile and single-page clients.
- Why a JWT is encoded, not encrypted, and what the signature actually protects.
- The stateless-versus-stateful trade-off with real numbers: local signature checks versus a 1 ms lookup per request.
- Why the hybrid (short JWT access tokens plus revocable refresh tokens plus an optional tiny deny-list) is the senior default.
- How to answer "a token got stolen" with a plan already built in: short expiry, refresh revocation, rotation, deny-list.
Next, try the System Design Question walkthrough on designing a multi-service authentication gateway to see these trade-offs land in a full design.
Get a prep plan built around your OAuth 2.0 and JWT interview
Tell us the role and your interview date. We will send back a dated, day by day plan. Free.