Skip to content
Lesson 7 of 14

Step 1 of 5 · Reading · ~4 min

Read

Claims & Validation

Key Rotation with kid

Signing keys do not last forever. They leak, staff leave, auditors ask when you last changed them, and a key that has signed two years of tokens is worth far more than one that has signed a week's. So you rotate.

But rotation is not an instant. When you switch, tokens signed with the old key are already out there in browsers and apps, still perfectly valid — they just will not verify against the new key.

What a hard cutover actually does

Replace the key at midnight and every token issued before midnight becomes garbage. Every logged-in user is thrown out mid-session, every background job holding a token fails, and your error rate spikes until clients re-authenticate. Users experience a security improvement as an outage.

kid: let two keys be right at once

The fix is to stop assuming there is exactly one key. Put a label in the header saying which key signed this token:

{"alg":"HS256","kid":"k1","typ":"JWT"}

kid is an opaque identifier — a name for a key, nothing more. Not a filename, not a URL, not a version number to parse. The verifier's job is a lookup:

Rendering diagram…

With that in place, adding a key is no longer a switch — it is a phase.

The four phases

  1. Publish. Add the new key k2 to the store alongside k1. Keep signing with k1. Verifiers can now handle both; nothing has changed for clients.
  2. Switch signing. Start signing new tokens with k2. Tokens signed with k1 are still arriving and still verify.
  3. Wait. Do nothing while the last k1-signed token ages out.
  4. Retire. Remove k1 from the store. Only now is the old key genuinely out of service.

Step 3 is the one people rush. If the longest-lived token you issue lasts TmaxT_{\max} and your verifiers allow LL seconds of clock leeway, the old key must stay in the store for at least

Tmax+LT_{\max} + L

seconds after the last token was signed with it — plus a margin for a client that was offline and comes back. Cut it short and you are back to a hard cutover, just a slower and more confusing one, because only some users get logged out.

Three ways rotation goes wrong

MistakeWhat the user sees
Retiring the old key before T_max + L has elapsedrandom logouts affecting only long-lived sessions
Reusing a kid value for a different keyevery old token becomes a signature failure, silently
Signing without a kid at allthe verifier has to guess, and rotation is impossible

The second one deserves emphasis: a kid is a permanent name. Once k1 has meant one specific key, it must never mean a different one — rotate by adding k2, never by redefining k1. Date-stamped identifiers like 2027-03 make that discipline automatic.

Do not "just try every key"

It is tempting to skip the lookup and try each key in turn. It works, and quietly undoes the point: a key you retired but forgot to unload keeps accepting tokens, and you cannot observe which key is in use. Look the key up by kid, and treat an unrecognised kid as a rejection, not a reason to fall back.

One more thing about kid: it arrives inside an unverified header, so it is attacker-controlled input. The lesson on JWKS registries covers what happens when a verifier feeds it straight into a file path or a SQL query.

Your exercise

You will maintain a key set and verify tokens by kid, printing OK kid=<x> or BAD reason=<r>.

Two cases in the visible test decide whether you pass:

  • A token whose header is {"alg":"HS256"} with no kid at all must come back BAD reason=unknown_kid. It is tempting to call that a malformed token — it is not. The token is well-formed; you simply have no key for it.
  • VERIFY not.a.jwt.token has four dot-separated segments, so it is BAD reason=bad_token. Check the segment count before you touch the header.

bad_signature is reserved for the case where you found the key and the HMAC did not match, so resolve the key first and compare second.

Up nextKnown JWT AttacksProduction Concerns

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…