JWKS and key rotation
Fetch the public signing keys, cache them correctly, and keep verifying through a key rotation without a deploy.
On this page
Identity signs ID tokens, access tokens and logout tokens. It publishes the matching public keys as a JSON Web Key Set so that you can verify them yourself. Keys change over time, and your verification has to keep working when they do.
Request#
GET the jwks_uri from discovery, which is /.well-known/jwks.json. No authentication, no parameters.
| Property | Value |
|---|---|
| Response | 200, Content-Type: application/jwk-set+json, the registered type for a key set |
Cache-Control | public, max-age=3600, s-maxage=60 |
ETag | A strong validator over the exact body. It changes when, and only when, the published set changes. |
| Conditional request | Send If-None-Match with the last ETag. An unchanged set returns 304 with the same validators and no body. |
| CORS | Access-Control-Allow-Origin: * |
Response members#
The body is { "keys": [ ... ] }. Each key is a public JWK and carries these members.
| Member | Meaning |
|---|---|
kty, crv, and the key's public values | OKP with Ed25519 for EdDSA, or EC with P-256 for ES256. Never a private member. |
use | sig |
alg | EdDSA or ES256 |
kid | The key identifier. It appears in the kid header of every token the key signs. |
{ "keys": [ { "kty": "OKP", "crv": "Ed25519", "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", "use": "sig", "alg": "EdDSA", "kid": "ex-2026-10" } ]}The values are illustrative. The current signing key is listed first, but the order carries no guarantee.
Errors#
The set is public, so there are no OAuth errors. A fault returns a 5xx problem response. If you have a copy, keep using it and try again later.
How rotation looks to a client#
Identity rotates its signing keys. In a normal rotation, two rules make it safe for a client that behaves.
- A new key is published before it signs anything. Once it is in the set you can already verify with it.
- A retired key stays published after it stops signing, for longer than the longest token it could have signed lives (an access token lives at most 15 minutes) plus a day. Tokens signed just before a rotation keep verifying.
The set can hold more than one key at once, and keys for other kinds of signed objects, such as logout tokens, share the same set. So:
- Select the key by
kidfrom the token's header, never by position, and never by trying every key. - Check
alg. Use only a key whosealgis on your allow-list, and import it under that same algorithm so the header cannot pick a different one. - Refuse a token with no
kid.
Caching guidance#
- Fetch the set when you first need it and keep it in memory. Honour
max-age; an hour is the default. - On a token whose
kidyou do not hold, fetch the set again once, then decide. A new key is expected after a rotation. - Limit that refetch. A token with a
kidthat never appears is either forged or from another issuer; do not let it make you hit the endpoint on every request. The SDK refetches at most once per 30 seconds for an unknownkid. - If a refetch fails, keep verifying with the keys you hold. Never accept a token you cannot verify.
- Use
If-None-Matchwhen you refresh so an unchanged set costs no body.
The SDK does all of this: its relying party keeps the set for ten minutes and refetches on an unknown kid, and createResourceServer reads the same set remotely, binding each key to its algorithm, through a key-set resolver that fetches again when it meets a kid it does not hold.
Worked example#
curl -si https://identity.tdacorp.in/.well-known/jwks.json | head -n 8HTTP/2 200content-type: application/jwk-set+jsoncache-control: public, max-age=3600, s-maxage=60etag: "9f2c4b…"access-control-allow-origin: *Next time:
curl -si -H 'If-None-Match: "9f2c4b…"' https://identity.tdacorp.in/.well-known/jwks.json | head -n 1HTTP/2 304Security notes#
- Only trust a key set from the issuer you configured, found through its discovery document. Do not follow a
jkuorjwkheader in a token. - Pin the algorithm, not the header. Algorithm confusion happens when a verifier believes the token's
alg. The SDK accepts onlyEdDSAandES256, further limited to the algorithms the issuer actually publishes a key for, and it refuses any other value in its configuration when you create the client. - Do not hard-code a key. A hard-coded key breaks at the next rotation.
- Keep a clock. A cache past its life should be refreshed; a clock that is wrong makes
max-agemeaningless.