# JWT, In Depth

> What a JSON Web Token really is: three base64 parts, a signature you must verify, and the stateless-auth tradeoffs (plus the mistakes that cause breaches).


---

# JWT, In Depth

You copied a JWT out of a request header, pasted it somewhere, and got back a wall of `eyJ...` that looked encrypted and important. Then someone said "don't put secrets in it" and you got confused, because if it's encrypted, why not? This guide clears that up. A JWT is not encrypted. It's a signed, readable note. Once you see what each of its three parts actually does, the security rules stop being magic words and start being obvious.

## How to read this

Read phase 1 first, slowly. The whole point of a JWT lives in the relationship between the three parts and the one signature, and if that mental model is solid, everything else is detail. Phase 2 is the everyday flow: issuing, sending, and verifying tokens, plus the claims that do the real work. Phase 3 is the part that keeps people employed in incident response: the attacks, the revocation problem, and the rules you break exactly once before you learn them.

## The phases

1. [Phase 1: Three Parts and a Signature](01-three-parts-and-a-signature.md) - what a JWT actually is and why it exists
2. [Phase 2: Issuing, Sending, and Verifying](02-issuing-sending-verifying.md) - how you really use tokens day to day
3. [Phase 3: Where It Breaks](03-where-it-breaks.md) - the attacks, the revocation problem, and production reality


---

# Three Parts and a Signature

The first time you really look at a JWT, it's intimidating in a specific way: it's long, it's full of random-looking characters, and it shows up in places that feel important, like the `Authorization` header of every request after you log in. So your brain files it under "encrypted secret thing, handle with care."

That instinct is half right. It is a thing you handle with care. But it is not encrypted, and that one fact reorganizes everything. A JWT is a **signed note**, not a sealed envelope. Anyone can read it. The care you take is not about hiding what's inside - it's about trusting that nobody changed it.

## What a token even is, before we get to JWT

Step back to the problem JWTs solve. HTTP is stateless: the server forgets you the instant your request finishes. So after you log in, every single later request has to re-prove "I'm the same person who logged in a minute ago." A token is the proof you carry. You log in once, the server hands you a token, and you attach it to every request after that. The server reads the token and goes "ah, this is Sam, logged in, here's your data."

The old way to do this was a **session**: the server stores a record ("session abc123 = user Sam"), hands you a meaningless ID, and looks you up in its store on every request. A JWT flips that. Instead of a pointer to a record on the server, the JWT *is* the record. Your identity travels with you, inside the token. The server doesn't look anything up - it reads the token and trusts it. That's the whole pitch: **stateless** auth. We'll come back to what that buys you and what it costs.

> If "who you are" (authentication) versus "what you're allowed to do" (authorization) feels blurry, the [/guides/auth-vs-authz](/guides/auth-vs-authz) guide untangles them. A JWT mostly answers "who you are," and sometimes carries hints about "what you can do."

## The three parts

A JWT is one long string with two dots in it. Those dots split it into three parts:

```text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0Iiwibm
FtZSI6IlNhbSIsImV4cCI6MTcxOTc2MzIwMH0.dBjftJeZ4CVP-mB92K27uh
bUJU1p1r_wW1gFWFOEjXk
   └─────── header ───────┘ └────── payload ──────┘ └── signature ──┘
```

*What just happened:* one string, two dots, three parts - `header.payload.signature`. Every JWT on earth has exactly this shape. The first two parts are data; the third is the proof that the first two haven't been tampered with.

Each part is **base64url-encoded**. Base64url is not encryption - it's a way to write bytes using only URL-safe characters so the token survives being shoved into headers and URLs. Decoding it takes no key and no secret. That's why the header and payload are readable by anyone who has the token, including the user holding it.

Decode the first part and you get the **header** - metadata about the token itself:

```json
{
  "alg": "HS256",
  "typ": "JWT"
}
```

*What just happened:* the header says "I'm a JWT, and I was signed with the HS256 algorithm." That `alg` field is small and looks boring. Remember it. In phase 3 it's the star of the two most famous JWT attacks.

Decode the second part and you get the **payload** - the actual claims, the statements the token is making about you:

```json
{
  "sub": "1234",
  "name": "Sam",
  "role": "editor",
  "exp": 1719763200
}
```

*What just happened:* the payload claims "this token is about user 1234, named Sam, who is an editor, and it expires at this Unix timestamp." These are called **claims**. Phase 2 goes deep on the standard ones (`exp`, `iss`, `aud` and friends).

Now sit with this: **you can read that payload with no key at all.** Paste any JWT into a base64 decoder and the user's id, name, and role are right there in plain text.

```bash
# split on the dots, grab the middle part, base64-decode it
echo 'eyJzdWIiOiIxMjM0Iiwibm...' | base64 -d
# => {"sub":"1234","name":"Sam","role":"editor","exp":1719763200}
```

*What just happened:* no secret, no permission, no decryption - the payload fell right out. This is the single most important thing to internalize. **A JWT hides nothing.** Which leads straight to the first rule you'll ever hear about them.

> **Never put a secret in a JWT payload.** No passwords, no API keys, no private personal data you wouldn't print on the user's forehead. The payload is public to anyone holding the token. It's signed, not sealed. (If you genuinely need the contents hidden, there's an encrypted variant called JWE - but plain JWTs, the kind everyone means by "JWT," are readable.)

## So what is the signature actually for?

If anyone can read the payload, and the user holds their own token, what stops them from editing `"role": "editor"` to `"role": "admin"` and handing themselves the keys to the kingdom?

The **signature**. The third part.

When the server creates the token, it takes the header and payload, and runs them through a signing function along with a **secret key that only the server knows**. The output is the signature. Here's the relationship in plain terms:

```text
signature = sign( base64url(header) + "." + base64url(payload),  SECRET )
```

*What just happened:* the signature is a fingerprint of the exact header and payload, computed using a secret only the server has. Change even one character of the payload and the fingerprint no longer matches.

So when the attacker flips `editor` to `admin`, the payload changes, but they **can't recompute a matching signature** - they don't have the secret. The server, on receiving the token, re-runs the same signing function on the header and payload it received, and checks whether the result equals the signature attached. If it doesn't match, the token is forged, and it's rejected.

```text
1. attacker edits payload:  role: editor  ->  role: admin
2. attacker sends token with the OLD signature (can't make a new one)
3. server recomputes signature over the NEW payload
4. recomputed signature != attached signature
5. server rejects: tampered token
```

*What just happened:* tampering is detected because the attacker can change the data but can't change the proof to match. The secret is the entire foundation. This is why the signature is everything, and why phase 2's core lesson is: **always verify it.** A JWT you read but didn't verify is a string an attacker handed you.

## Signed by a shared secret, or a key pair

Two families of signing algorithms show up constantly, and the difference matters for who can verify your tokens:

- **HMAC (HS256, HS384, HS512)** - one shared secret. The same secret both signs and verifies. Fast and simple. The catch: everyone who can verify can also sign, because it's the same key. Fine when one service issues *and* checks its own tokens.
- **RSA / ECDSA (RS256, ES256, and similar)** - a key *pair*. A **private** key signs; a **public** key verifies. The issuer keeps the private key locked away; anyone can hold the public key and verify tokens without being able to forge new ones. This is what you want when one service (an identity provider) issues tokens that many other services need to trust.

```text
HMAC:   [secret]  signs  ---  [same secret]  verifies     (symmetric)
RSA:    [private] signs  ---  [public]        verifies     (asymmetric)
```

*What just happened:* HMAC uses one key for both jobs; RSA splits signing and verifying across a key pair. Keep this picture handy - the gap between "the key that signs" and "the key that verifies" is exactly the seam the `alg`-confusion attack pries open in phase 3.

## The mental model, in one breath

A JWT is a **note the server wrote about you, stamped with a signature only the server can produce.** You carry the note. Anyone can read it. Nobody can change it without breaking the stamp. The server trusts the note because it trusts its own stamp - not because it looked anything up. Hold that, and the rest of this guide is detail.

```quiz
[
  {
    "q": "Is the payload of a standard JWT encrypted?",
    "choices": [
      "Yes, you need the secret key to read it",
      "No, it is only base64url-encoded and anyone with the token can read it",
      "Yes, but only the user can decrypt their own token",
      "Only the header is encrypted, not the payload"
    ],
    "answer": 1,
    "explain": "Base64url is encoding, not encryption. A standard JWT's payload is readable by anyone holding the token, which is why you never put secrets in it."
  },
  {
    "q": "What stops a user from editing their own JWT payload to give themselves admin rights?",
    "choices": [
      "The payload is encrypted so they cannot read or change it",
      "The signature: changing the payload breaks it, and they lack the secret to recompute a valid one",
      "The browser refuses to send a modified token",
      "Nothing - JWTs are inherently insecure"
    ],
    "answer": 1,
    "explain": "The signature is a keyed fingerprint of the header and payload. Tamper with the payload and it no longer matches, and the attacker can't forge a new signature without the secret."
  },
  {
    "q": "With an RSA-signed JWT (RS256), which key verifies the token?",
    "choices": [
      "The same secret used to sign it",
      "The private key",
      "The public key, which can verify but not forge tokens",
      "No key is needed to verify"
    ],
    "answer": 2,
    "explain": "RSA is asymmetric: the private key signs and the public key verifies. The public key can be shared freely because it cannot produce new valid signatures."
  }
]
```


---

# Issuing, Sending, and Verifying

You've got the mental model: a signed note you carry. Now we walk the note through its life. A token gets born at login, rides along on every request, and gets checked on arrival. Three moments. Each has a job, and each has one place people get it wrong.

## The round trip, end to end

Here's the whole flow in one picture before we zoom into the pieces:

```mermaid
sequenceDiagram
    participant U as Browser
    participant S as Server
    U->>S: POST /login (username + password)
    S->>S: check password, build claims, SIGN token
    S-->>U: returns JWT
    U->>S: GET /orders (Authorization: Bearer <jwt>)
    S->>S: VERIFY signature, check exp/iss/aud
    S-->>U: 200 + data (or 401 if verify fails)
```

*What just happened:* login produces a signed token once; every later request carries it and the server verifies it every time. The expensive part (checking a password) happens once. The cheap part (verifying a signature) happens on every request. That asymmetry is the point of tokens.

## Moment one: issuing

The user proves who they are the slow, careful way - a password (stored hashed, never plaintext; see [/guides/how-passwords-are-stored](/guides/how-passwords-are-stored)). Once that checks out, the server builds the claims and signs them.

```text
POST /login
{ "username": "sam", "password": "hunter2" }

--- server: password verified, now build + sign the token ---

header  = { "alg": "HS256", "typ": "JWT" }
payload = {
  "sub":  "1234",
  "name": "Sam",
  "role": "editor",
  "iss":  "auth.myapp.com",
  "aud":  "api.myapp.com",
  "iat":  1719759600,
  "exp":  1719763200
}
signature = HMAC-SHA256( base64url(header) + "." + base64url(payload), SECRET )
```

*What just happened:* the server packaged Sam's identity and permissions into claims and stamped them. The result is the `eyJ...` string handed back to the browser. Note what's *not* in there: no password, no anything secret. Only facts that are safe to be public-but-tamper-proof.

### The standard claims worth knowing

Most claims are short, three-letter names. They're abbreviated on purpose (tokens travel on every request, so smaller is better). The ones you'll use constantly:

| Claim | Name | What it means |
|-------|------|---------------|
| `sub` | subject | who the token is about - the user id |
| `iss` | issuer | who minted the token (`auth.myapp.com`) |
| `aud` | audience | who the token is *for* (`api.myapp.com`) |
| `exp` | expiration | Unix time after which the token is dead |
| `iat` | issued at | Unix time the token was created |
| `nbf` | not before | Unix time before which it's not yet valid |

`exp`, `iss`, and `aud` are the three that earn their keep:

- **`exp`** is your seatbelt. A token is a bearer credential - whoever holds it *is* you, no questions asked. So you want it to die quickly. Short lifetimes (minutes, not weeks) limit the blast radius if one leaks. More on this tension in phase 3.
- **`iss`** lets a verifier reject tokens from the wrong source.
- **`aud`** stops a token meant for one service from being replayed against another. A token your billing API issued shouldn't unlock your admin API.

> Adding your own claims (like `role` or `tenant_id`) is normal and useful - that's how you avoid a database lookup on every request. The rule from phase 1 still holds: nothing secret. A `role` is fine; a credit card number is not.

## Moment two: sending

The browser holds the token and attaches it to every request that needs auth. The convention is the `Authorization` header with the `Bearer` scheme:

```text
GET /orders HTTP/1.1
Host: api.myapp.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI...
```

*What just happened:* "Bearer" literally means "the bearer of this token is authorized." That word is a warning label: anyone who gets the token gets your access. Treat it like cash. Send it only over HTTPS, never log it, and store it somewhere a stray script can't scrape it. (Where exactly to store it - cookie versus JS-readable storage - is a real debate with real tradeoffs; the short version is that a cookie with the right flags resists token theft better than putting it where page scripts can read it.)

## Moment three: verifying

This is the moment that matters most, and the one people skip when they're moving fast. On every protected request, the server must **verify** the token before trusting a single byte of it.

Verification is not "decode the payload and read the role." Decoding is free and meaningless - an attacker can hand you any payload they like. Verification is the full check:

```text
1. Split into header.payload.signature
2. Read alg from header - but VERIFY against the algorithm YOU expect, not what the token claims
3. Recompute the signature over header.payload using YOUR key
4. Constant-time compare: does it match the attached signature?   -> if no, REJECT
5. Check exp:  is it past?         -> if expired, REJECT
6. Check iss:  is it who we trust? -> if not, REJECT
7. Check aud:  is it for us?       -> if not, REJECT
8. Only now: trust the claims (sub, role, ...)
```

*What just happened:* the signature check proves the token is genuine; the claim checks prove it's still valid and meant for you. Skip step 4 and any forged token passes. Skip step 5 and a stolen token works forever. Every step is load-bearing.

In real code you never hand-roll this - you use a vetted library and let it do all eight steps. The shape, in pseudocode:

```text
try:
    claims = jwt.verify(
        token,
        key = SECRET,
        algorithms = ["HS256"],     # <-- pin it. critical. see phase 3.
        issuer   = "auth.myapp.com",
        audience = "api.myapp.com",
    )
    # claims now trustworthy: claims["sub"], claims["role"], ...
except InvalidSignature, ExpiredToken, InvalidAudience:
    return 401 Unauthorized
```

*What just happened:* one call did the signature recompute, the comparison, and the `exp`/`iss`/`aud` checks, and threw if anything failed. The single most important argument there is `algorithms = ["HS256"]` - you are telling the library which algorithm to accept, instead of trusting the token's own header. Phase 3 shows the breach that happens when you forget it.

> **For builders:** the line that separates "I decoded a JWT" from "I verified a JWT" is the secret key and the algorithm pin. If your verify call doesn't take a key, you didn't verify anything - you merely parsed attacker-controlled JSON. Libraries sometimes offer a `decode` that skips verification for debugging; never let that path touch a real request.

## Why bother with all this? The stateless payoff

After all these steps, here's the reward: the server verified Sam, learned his id, name, and role, and decided what he's allowed to do - **without touching a database or session store.** The token carried everything. That's stateless auth.

That's genuinely powerful at scale. Ten API servers behind a load balancer don't need a shared session store or sticky sessions; each one can verify a token on its own with only the key. Add a server, it works immediately. There's no "where do sessions live" problem.

But - and phase 3 is built around this - statelessness has a sharp edge. If the server doesn't look anything up, the server can't easily *un*-trust a token mid-life. You handed out a signed note that says "valid until 3:00"; you can't reach into the user's pocket and tear it up. That's the revocation problem, and it's where the next phase begins.

```quiz
[
  {
    "q": "Which step turns 'decoding' a JWT into actually 'verifying' it?",
    "choices": [
      "Base64-decoding the payload to read the claims",
      "Recomputing the signature with your key and comparing it to the attached one",
      "Pretty-printing the JSON",
      "Checking that the token has two dots in it"
    ],
    "answer": 1,
    "explain": "Decoding is free and proves nothing. Verification recomputes the signature using your secret/key and rejects the token if it doesn't match - that's what catches forgeries."
  },
  {
    "q": "What is the `aud` (audience) claim for?",
    "choices": [
      "It stores the user's role and permissions",
      "It records when the token was issued",
      "It names which service the token is for, so a token can't be replayed against a different service",
      "It is the secret used to sign the token"
    ],
    "answer": 2,
    "explain": "`aud` declares the intended recipient. A verifier checks it so a token minted for one API can't be reused against another."
  },
  {
    "q": "What is the main payoff of stateless JWT auth?",
    "choices": [
      "Tokens can never be stolen",
      "The server verifies identity from the token alone, with no session store or database lookup",
      "Tokens are encrypted end to end",
      "Revoking a token is instant and easy"
    ],
    "answer": 1,
    "explain": "Statelessness means each server verifies a token using only the key - no shared session store. The cost is that revocation becomes hard, which phase 3 covers."
  }
]
```


---

# Where It Breaks

JWTs don't usually fail because the math is weak. They fail because of how they're *used* - a verify step skipped, an algorithm trusted that shouldn't be, a token that lives too long. The good news: the famous breaches all come from a short list of mistakes, and once you've seen them, you won't make them. Let's walk the list.

## Attack one: alg=none

Remember the `alg` field in the header from phase 1? The JWT spec defines a valid value of `"none"`, meaning "this token is unsigned." It exists for niche cases where signing happens at a different layer. It is also a loaded gun pointed at naive verifiers.

The attack: take a real token, change the payload to `"role": "admin"`, set the header's `alg` to `"none"`, and **delete the signature entirely** (the token now ends with a trailing dot and nothing after it).

```text
header:    { "alg": "none", "typ": "JWT" }
payload:   { "sub": "1234", "role": "admin" }
signature: (empty)

token:  eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjM0Iiwicm9sZSI6ImFkbWluIn0.
                                                                     ^ nothing here
```

*What just happened:* the attacker forged an admin token with no secret at all. If your verifier reads `alg` from the token and obediently does "no signature to check, looks fine!" - it's game over. A library that honors `alg: none` will happily accept this.

**The fix is one line, the same one from phase 2:** pin the algorithm. Tell your verifier exactly which algorithm(s) to accept, so the token's claimed `alg` can't override your decision.

```text
jwt.verify(token, key=SECRET, algorithms=["HS256"])   # "none" is rejected outright
```

*What just happened:* by listing only `HS256`, a token claiming `alg: none` is rejected before its missing signature even matters. Never let the token tell the server how to verify itself.

## Attack two: RS256-to-HS256 key confusion

This one is sneakier and trickier to see. Recall the two algorithm families from phase 1:

- **RS256** is asymmetric: a *private* key signs, a *public* key verifies. The public key is, by design, public - you might publish it openly.
- **HS256** is symmetric: the *same* secret both signs and verifies.

Now picture a verifier that reads `alg` from the token and picks its key accordingly. The attacker takes a token, changes the header's `alg` from `RS256` to `HS256`, and signs the forged payload using the server's **public RSA key as the HMAC secret**.

```text
1. Server expects RS256 (public key verifies, private key signs - attacker lacks private key)
2. Attacker flips header:  alg: RS256  ->  alg: HS256
3. Attacker signs the forged token with HMAC, using the PUBLIC key as the secret
4. Naive server sees alg: HS256, grabs "its key" (the public key), runs HMAC verify
5. It matches - because the attacker signed with that exact value. Forged token accepted.
```

*What just happened:* the attacker turned the public key - which was safe to share when it was only used for RSA verification - into a forging secret, by tricking the server into treating it as an HMAC key. The public key was never supposed to be a signing secret. The algorithm switch made it one.

**The fix, again:** pin the algorithm. If your verifier only accepts `RS256`, a token claiming `HS256` is rejected and this entire attack evaporates. Both of the two most infamous JWT attacks die to the same one habit - never trust the token's own `alg`.

> **The single rule that defeats both attacks:** the verifier decides the algorithm, never the token. Pass an explicit allow-list of algorithms to your verify call, every time. This is also why you should reach for a maintained, well-reviewed JWT library rather than parsing tokens yourself - the good ones force you to specify the algorithm and refuse `none` by default.

## The hard one: revocation

This isn't an attack - it's a structural tradeoff, and it's the price of statelessness from phase 2. A signed token is valid until it expires, full stop. The server doesn't look anything up, so there's no record to flip to "revoked." If a token leaks, or a user logs out, or you fire an employee, that token **keeps working until `exp`.**

Sit with how uncomfortable that is. You clicked "log out," and your token still opens every door for the next however-long. With server-side sessions, logout is instant - you delete the session row. With pure JWTs, you can't, because there's no row.

There's no perfect fix, only a set of mitigations you combine:

- **Short lifetimes.** Make access tokens expire in minutes. A leaked token is dangerous for a small window instead of forever. This is the single most effective lever, and it's why phase 2 leaned on `exp`.
- **Refresh tokens.** Pair a short-lived access token with a long-lived refresh token. The access token does the work and dies fast; the refresh token is used only to mint new access tokens, and *it* can be stored server-side and revoked. You get most of the stateless speed for normal requests, with a revocation point at refresh time.
- **A denylist (blocklist).** Keep a small server-side list of revoked token ids (the `jti` claim) and check it on each request. This works - but notice you've reintroduced a server-side lookup, partly giving back the statelessness you came for. It's a deliberate trade, fine for "revoke on logout" if the list stays small.

```text
Short access token (5 min)  +  long refresh token (days, revocable server-side)
   |                              |
   does every request            used only to get a new access token
   expires fast = small risk     can be torn up to truly log someone out
```

*What just happened:* you stopped trying to revoke the unrevocable access token and instead made it expire so fast that revocation barely matters, with the refresh token as your real off-switch. This pattern is what most production systems land on.

## The everyday mistakes, rapid-fire

The attacks above are dramatic. These are the quiet ones that actually show up in code review:

- **Trusting an unverified token.** Decoding the payload and reading `role` without checking the signature. The whole guide warned about this; it's still the most common bug.
- **A weak HMAC secret.** `HS256` is only as strong as its secret. `"secret"` or `"password123"` can be brute-forced offline once an attacker has any valid token to test against. Use a long, random, high-entropy secret.
- **Leaking the secret.** Hardcoded in the repo, printed in logs, baked into a client bundle. Whoever has the HMAC secret can forge any token. Treat it like the master key it is.
- **No expiration.** A token with no `exp` is a permanent credential. Always set one.
- **Putting secrets in the payload.** Phase 1's first rule, and it keeps happening. The payload is public.
- **Skipping `aud`/`iss` checks.** Accepting any validly-signed token, even one minted for a different service or by a different issuer.

> **In the wild:** when a JWT incident hits the news, the root cause is almost never broken cryptography. It's one of these - an unpinned algorithm, an unverified token, a leaked or guessable secret, or a token that lived too long. The crypto is sound. The usage is where it breaks. Which means the defense is sound too: pin the algorithm, always verify, guard the secret, expire fast.

## Where this leaves you

A JWT is a signed, readable note. Its security rests entirely on (1) verifying the signature, (2) deciding the algorithm yourself, and (3) keeping the signing secret secret. Statelessness is real power and a real cost - you trade easy revocation for not needing a session store, and you buy most of it back with short lifetimes and refresh tokens. None of this is magic now. It's a note, a stamp, and the discipline to check the stamp every single time.

```quiz
[
  {
    "q": "What single practice defeats both the alg=none attack and the RS256-to-HS256 confusion attack?",
    "choices": [
      "Encrypting the payload",
      "Pinning the accepted algorithm(s) in the verifier instead of trusting the token's alg header",
      "Using a longer secret key",
      "Setting a shorter expiration time"
    ],
    "answer": 1,
    "explain": "Both attacks work by making the server trust the token's claimed alg. Passing an explicit algorithm allow-list to the verifier rejects the mismatched/none algorithm before it can do harm."
  },
  {
    "q": "Why is revoking a pure JWT before it expires hard?",
    "choices": [
      "The token is encrypted and can't be modified",
      "It's stateless: the server doesn't store the token, so there's no record to mark as revoked",
      "Browsers cache tokens and ignore revocation",
      "The exp claim cannot be read by the server"
    ],
    "answer": 1,
    "explain": "Statelessness means the server keeps no per-token record. With nothing to flip to 'revoked,' a signed token stays valid until exp unless you add a denylist or use revocable refresh tokens."
  },
  {
    "q": "Which is the most effective single mitigation for the revocation problem?",
    "choices": [
      "Making the HMAC secret longer",
      "Adding more claims to the payload",
      "Short access-token lifetimes paired with revocable refresh tokens",
      "Switching from RS256 to HS256"
    ],
    "answer": 2,
    "explain": "Short-lived access tokens shrink the danger window to minutes, and a server-side-revocable refresh token gives you a real off-switch while keeping normal requests stateless."
  }
]
```
