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:
With that in place, adding a key is no longer a switch — it is a phase.
The four phases
- Publish. Add the new key
k2to the store alongsidek1. Keep signing withk1. Verifiers can now handle both; nothing has changed for clients. - Switch signing. Start signing new tokens with
k2. Tokens signed withk1are still arriving and still verify. - Wait. Do nothing while the last
k1-signed token ages out. - Retire. Remove
k1from 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 and your verifiers allow seconds of clock leeway, the old key must stay in the store for at least
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
| Mistake | What the user sees |
|---|---|
Retiring the old key before T_max + L has elapsed | random logouts affecting only long-lived sessions |
Reusing a kid value for a different key | every old token becomes a signature failure, silently |
Signing without a kid at all | the 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 nokidat all must come backBAD 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.tokenhas four dot-separated segments, so it isBAD 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.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…