Rotating JWT Signing Keys Without Logging Everyone Out
How JWKS and the kid header make key rotation a non-event — the overlap window, caching and thundering herds, emergency revocation, and the specific mistakes that turn a routine rotation into an outage.
Key rotation is one of those practices that everybody agrees with and almost nobody has actually done. The signing key generated during the first week of the project is still signing tokens three years later, sitting in an environment variable, known to every engineer who has ever run the staging deploy script and present in whatever CI logs existed at the time.
The reason it never happens is that the first attempt goes badly. Someone swaps the key, every token signed with the old one instantly fails verification, and every user is logged out at once — including, if you are unlucky, the people trying to fix it. The lesson learned is "rotation is dangerous," when the actual lesson is "rotation without an overlap window is dangerous."
Done properly, rotating a signing key should be completely invisible. This article covers how.
Why rotate at all
Four reasons, in rough order of how likely they are to apply to you.
Keys leak, slowly and unremarkably. Not usually through a dramatic breach — through a .env committed to a branch, a container image layer, an error report that serialized the config object, a laptop that left the company. The longer a key lives, the more places a copy of it exists. Rotation bounds that exposure by making old copies useless.
Compromise recovery requires the capability to exist already. The day you need to revoke a key is not the day to discover your verifiers cache the JWKS for 24 hours and your clients hardcode a single public key. Rotation is the drill; the emergency is the real thing.
Compliance frameworks ask. SOC 2, PCI DSS, and most enterprise security questionnaires want a documented rotation policy. Answering "we have never rotated" is a finding.
Algorithm migration needs the same machinery. Moving from RS256 to ES256, or increasing key size, is mechanically identical to a rotation. If you can rotate, you can migrate.
The mechanism: kid and JWKS
Two pieces make invisible rotation possible.
The kid (key ID) header, in every token you issue, names which key signed it:
{ "alg": "RS256", "typ": "JWT", "kid": "2026-07-a" }
The JWKS endpoint — a JSON document at a well-known URL, conventionally /.well-known/jwks.json — publishes your current public keys:
{
"keys": [
{ "kty": "RSA", "kid": "2026-07-a", "use": "sig", "alg": "RS256", "n": "…", "e": "AQAB" },
{ "kty": "RSA", "kid": "2026-04-b", "use": "sig", "alg": "RS256", "n": "…", "e": "AQAB" }
]
}
Note that two keys are published. That is the whole trick. A verifier reads the kid from the token header, finds the matching key in the set, and verifies. Tokens signed with either key verify successfully, so the moment you switch which key you sign with is not a moment anything breaks.
This only works with asymmetric signing. With HMAC (HS256), the verification key is the signing key, so publishing a JWKS would publish your secret, and every verifier is also a potential forger. If you are on HS256 with more than one party verifying, the rotation project and the migrate-to-RS256 project are the same project. The reasoning is in JWT Explained for Developers.
The rotation, stage by stage
Stage 1 — publish, don't use. Generate the new keypair, add its public half to the JWKS, keep signing with the old key. Nothing has changed for anyone; verifiers simply see an extra key they have no tokens for yet.
Stage 2 — wait for propagation. Every verifier caches the JWKS. Until their caches have refreshed, some of them do not know the new key exists. Wait at least twice the maximum cache TTL across all your verifiers. If you do not know that number, that is the finding — go find it before continuing.
Stage 3 — switch signing. Now start signing new tokens with the new kid. Verifiers already have the key. Old tokens still verify with the old key, which is still published. This is the moment that would have caused an outage without stages 1 and 2, and now it causes nothing.
Stage 4 — wait out the old tokens. Old tokens remain valid until they expire. Wait at least the maximum lifetime of anything signed with the old key — remembering that this includes long-lived tokens you may have forgotten about, like API tokens or refresh tokens if those are JWTs.
Stage 5 — retire. Remove the old key from the JWKS and destroy the private key. Rotation complete, with zero user-visible effect.
The whole sequence typically spans days, and that is fine — it is mostly waiting. The scriptable part is stages 1, 3, and 5.
Verifier-side details that cause outages
Most rotation failures are on the verifying side, not the issuing side.
Cache the JWKS, but refresh on unknown kid. A verifier that fetches the JWKS on every request will hammer your endpoint and add latency to every call. A verifier that caches forever will fail the moment you rotate. The correct behaviour is: cache with a TTL (5–15 minutes is typical), and on encountering a kid you do not have, refetch immediately.
Rate-limit that refetch, or you have built a thundering herd. A token with an unknown kid — from a bug, a stale issuer, or an attacker sending random values — triggers a JWKS fetch. An attacker sending a thousand tokens per second with random kid values triggers a thousand fetches per second against your JWKS endpoint, from every verifier at once. Every mature JWKS client implements a cooldown: at most one refetch per unknown key per interval, and a negative cache for kid values recently confirmed absent.
Fall back to the last known-good set. If the JWKS fetch fails — network blip, DNS, your own deploy — a verifier that treats "cannot fetch keys" as "reject everything" converts a transient error into a total authentication outage. Serve from the stale cache and alert loudly.
Pin the algorithm anyway. The alg header, and even the alg field in the JWKS entry, must not decide your verification algorithm. Configure the expected algorithm explicitly. This is the defence against the alg: none and RS256-to-HS256 confusion attacks catalogued in common authentication vulnerabilities — and it matters more, not less, once you have a public JWKS, because your public key is now genuinely public and is exactly what an HS256-confusion attack uses as its HMAC secret.
Reject a token with no kid once you have more than one key. Guessing which key to try is an invitation to trial verification, which is both slow and sloppy.
Serving the JWKS endpoint
It is a small, boring endpoint with a few requirements that are easy to get wrong.
- Public, no authentication. These are public keys. Putting the endpoint behind auth creates a bootstrap problem for the verifiers that need it.
- Permissive CORS. Browser-based verifiers need
Access-Control-Allow-Origin: *. Blocking this is a common cause of "works on the server, not in the browser." - A sane
Cache-Control. Something likemax-age=300gives clients a hint that matches your rotation timeline. Settingno-storeguarantees the hammering described above; settingmax-age=86400guarantees a slow rotation. - Never, ever include a private key. It sounds impossible to get wrong. It has happened, from serializing the whole keypair object rather than its public half. Add the test that asserts the response contains no
dparameter, and fail the build on it. - Treat it as tier-zero infrastructure. If your JWKS endpoint is down, every verifier that lacks a warm cache cannot authenticate anyone. Static, cached, and independently available.
Emergency revocation
Everything above is the planned path. The unplanned path is: the private key is on someone else's laptop, right now.
You cannot wait out the overlap window. Remove the compromised key from the JWKS immediately — and accept that every outstanding token signed with it will fail, which means every active user is logged out. That is the correct outcome; an attacker with your signing key can mint a token for any user with any claims, and there is no lesser response.
Three things determine how bad that day is, and all three are decisions you make beforehand:
How fast can you actually publish a JWKS change? If it requires a full application deploy, your revocation latency is your deploy time. Serving the JWKS from configuration or a fast key store, rather than baking it into the build, turns a thirty-minute revocation into a thirty-second one.
How short is your verifier cache TTL? A fifteen-minute cache means fifteen minutes during which forged tokens still verify at some services. This is the real cost of a long TTL, and it is the number to argue about in the design review.
Can users get back in? If the compromised key also signed something in your login path, you may have locked yourself out of your own recovery. Keep a break-glass path that does not depend on the JWT infrastructure.
Practice this. A rotation drill on a quiet Tuesday is how you find out that the JWKS is baked into a container image and that one legacy service has the public key hardcoded in a constant.
Operational hygiene
Name keys usefully. 2026-07-a tells you when it was created and lets you sort. A random UUID tells you nothing at three in the morning.
Automate the schedule and the alarm. Quarterly is a reasonable default for signing keys. More important than the interval is an alert when a key exceeds its intended age — the failure mode is not "rotated too slowly," it is "forgot entirely."
Keep the private key out of the application. A KMS or HSM that signs on request, without ever exposing the key material, removes the largest single cause of leaks. If that is not available, at minimum keep it out of environment variables that get logged and out of the container image.
Instrument by kid. A counter of verifications per key ID is how you know whether it is safe to retire a key. If tokens with the old kid are still arriving, something has a longer lifetime than you thought, and retiring the key would break it. This metric turns stage 4 from a guess into an observation.
Monitor the endpoint from outside. Availability, correct content type, expected kid set, and — most usefully — an alert if the key set changes unexpectedly.
Key takeaways
- Rotation is invisible because of the overlap window: publish the new key, wait, then start signing with it, wait, then retire the old one.
- Never skip stage 2. Switching signing before verifier caches refresh is what causes the mass logout everyone remembers.
- Verifiers must cache with a TTL, refetch on unknown
kid, rate-limit that refetch, and fall back to the last known-good set. - Pin the algorithm explicitly. A public JWKS makes algorithm confusion easier, not harder.
- Serve the JWKS publicly, with CORS, from configuration rather than a build artifact — that is what makes emergency revocation fast.
- Instrument verifications per
kidso retiring a key is a decision based on data. - Rotate on a schedule, and alert on key age, because the real failure is forgetting.
EmbedAuth signs with RS256, publishes a JWKS at /.well-known/jwks.json with proper caching and CORS, and includes a kid in every token — so verifying with a standard JWKS client means rotations happen underneath you without a code change. The Node, Python, and Go guides show the verification side.
Written by
Emilian GheoneaSenior Blockchain & Full-Stack Software Engineer. I build EmbedAuth — an embeddable authentication platform for SaaS — and write about the auth problems most teams hit too late.
Related articles
JWT
Designing JWT Claims: What Belongs in a Token and What Doesn't
A practical guide to claim design — the standard claims and how to validate them, why token bloat becomes a 431 outage, PII and privacy in a base64 payload, namespacing custom claims, and versioning a payload you can never fully migrate.
Jul 29, 2026 9 minReadJWT
JWT Explained for Developers
A JWT is three base64-encoded strings separated by dots. What's in them, how signatures actually work, why "stateless auth" is a half-truth, and the mistakes that keep showing up in production code.
May 8, 2026 7 minReadJWT
JWTs vs Opaque Tokens: Stop Choosing Based on Vibes
The real trade-off is where authorization state lives and how fast it can change. A decision framework covering revocation latency, introspection, hybrid designs, and the failure modes each choice hands you.
Jul 27, 2026 8 minReadSecurity
Webhook Signatures: Verifying What Your Server Is Told
A webhook endpoint is an unauthenticated API that changes your data. How to sign and verify properly — HMAC over the raw body, timestamps and replay windows, constant-time comparison, key rotation, and the idempotency and SSRF issues on both sides.
Aug 3, 2026 8 minRead