Signing keys and rotation
All domains, identifiers, and keys in these examples are fictional.
Your ID token's header makes two suggestions: verify me with the key called photos-rs-2026-09, using RS256. Nobody sent the printer that key, and nobody will tell it when the photo service starts using another. The printer has to find the key itself, decide which algorithms it is willing to verify, and keep signing people in when the key changes.
Storing and using keys followed key sets and rotation from the publisher's side. Here the view is the relying party's.
Finding the right key
The printer's configuration for the photo service includes the address of its key set, https://auth.photos.example/jwks. The printer copied it from the photo service's published configuration, where it appears as jwks_uri, as Provider discovery will show. What matters here is that the address is the printer's own setting, never something a token supplies. The set, with key values shortened, holds more than ID token keys:
GET /jwks HTTP/1.1
Host: auth.photos.example
HTTP/1.1 200 OK
Content-Type: application/json
{
"keys": [
{ "kid": "photos-2026-09", "kty": "EC", "crv": "P-256", "alg": "ES256", "use": "sig", "x": "...", "y": "..." },
{ "kid": "photos-rs-2026-09", "kty": "RSA", "alg": "RS256", "use": "sig", "n": "...", "e": "AQAB" }
]
}
The first is the elliptic-curve key that signs the photo service's JWT access tokens, from Token formats and validation. The second is the RSA key that signs ID tokens. Each key identifier names one key, and each key is used with one algorithm.
The printer looks up photos-rs-2026-09 and finds an RSA key marked for signatures and for RS256, which is both the algorithm the header names and one the printer accepts. Only then does it verify. If any of those disagree, it stops rather than searching for some other key that might work. A token naming photos-2026-09 with RS256 asks for an RSA algorithm with an elliptic-curve key, and is rejected. A token naming that key with ES256 never gets so far, because ES256 is not on the printer's list for ID tokens.
The key identifier is only a lookup value, compared as text with identifiers in a set the printer already holds and never used to build a file path or a database query. A token with no kid is rejected rather than guessed at, since OpenID Connect requires one whenever the key set holds more than one key.
The rest follows Storing and using keys. The printer ignores any key a token tries to bring with it, whether embedded with jwk or x5c, or pointed to with jku or x5u. OpenID Connect advises providers not to use those header parameters in ID tokens at all, because keys are agreed in advance. The printer caches the set, fetches it again when a token names an identifier it does not know, and limits how often that can happen. If the identifier is still unknown after a fresh fetch, the sign-in fails.
Fetching from the configured address also settles whose key this is. Every key in the set belongs to https://auth.photos.example, because that provider published it at an address the printer chose. A verified signature identifies the issuer only when the key came from the expected issuer's set, which is why Supporting several providers keeps a separate set for each provider.
Choosing acceptable algorithms
The header's alg is checked against a list, and the list belongs to the printer. For ID tokens, the OpenID Connect default is RS256. A client can register a different algorithm with the registration setting id_token_signed_response_alg, which Registering a relying party covers. The printer registered none, so its list for ID tokens from the photo service holds one value, RS256.
Algorithm families differ in who can produce a signature that passes:
| Algorithms | Verified with | Who can produce a valid signature |
|---|---|---|
RS256, PS256 (RSA) | The provider's public RSA key. | Only the provider, which holds the private key. |
ES256 (ECDSA) | The provider's public elliptic-curve key. | Only the provider. |
HS256, HS384, HS512 (HMAC) | The client's own client secret. | The provider, and anyone else who holds the client secret. |
none | Nothing. | Anyone. |
none means the token is unsigned. OpenID Connect allows an unsigned ID token only for a client that receives ID tokens solely from the token endpoint and explicitly registered for unsigned ones. The printer has not, and treats alg none as a failed sign-in.
The HMAC row deserves a closer look. With those algorithms, the key that checks the signature is the client secret itself, taken as its UTF-8 bytes. The photo service and the printer both hold it, so either could produce a valid ID token for any subject. A leaked secret would then be far worse than in Secrets and signed assertions: it would let the holder sign in to the printer as any customer, without involving the photo service at all. Public clients have no secret, and the specification rules out HMAC signatures for them. A confidential client that does use one needs a secret at least as long as the algorithm's hash output: 32 bytes for HS256, and more for the other two. The printer's example secret, demo-only-not-a-real-secret, has 27 characters and would not qualify.
Keeping the list to one algorithm, matched to the type of key it selects, also closes the confusion attack from Token formats and validation, in which a token claiming an HMAC algorithm tricked a library into using a public key as the shared secret. With one acceptable algorithm, there is nothing for the header to switch.
Algorithms do change. A provider may move its ID tokens to PS256 or ES256, and the printer will need to follow. That change is made on purpose, by updating the registration and the printer's list together, never by accepting whatever algorithm arrives.
When the provider rotates
In mid-December, the photo service prepares to replace its ID token key. It publishes the next key beside the current one:
{
"keys": [
{ "kid": "photos-2026-09", "kty": "EC", "crv": "P-256", "alg": "ES256", "use": "sig", "x": "...", "y": "..." },
{ "kid": "photos-rs-2026-09", "kty": "RSA", "alg": "RS256", "use": "sig", "n": "...", "e": "AQAB" },
{ "kid": "photos-rs-2027-01", "kty": "RSA", "alg": "RS256", "use": "sig", "n": "...", "e": "AQAB" }
]
}
The printer's cached copy picks up photos-rs-2027-01 at its next scheduled refresh, though it has no use for the key yet.
On 1 January 2027 the photo service starts signing ID tokens with photos-rs-2027-01. A printer that refreshed in December already has it. One that has not, perhaps a server started from an old copy of the set, sees an identifier it does not know, fetches the set once, finds the key, and carries on. Nobody at the printing company had to do anything, and no customer noticed.
The old key stays published for a while after the switch, and OpenID Connect asks providers to keep recently retired signing keys available for a reasonable period. For the printer, the overlap only has to outlast the ID tokens already in flight, and the printer checks those within seconds of their issue.
The same rotation breaks a printer that took a shortcut. One that copied the public key into its configuration, instead of fetching the set, fails every sign-in on 1 January. One that fetches the set again for every unknown identifier, with no limit, lets anyone who sends it tokens with invented identifiers turn each one into a request to the photo service.
An emergency works differently. Suppose the photo service discovers that the private key for photos-rs-2026-09 has leaked. It removes the key from the set at once, so that every token signed with it, genuine or forged, should now fail. The printer learns of the removal only when it next fetches the set. Forged tokens name photos-rs-2026-09, an identifier the printer already holds, so the unknown-identifier rule never fires. Token formats and validation made the same point for an API. For a relying party the stakes are higher: until the next scheduled refresh, whoever holds the leaked key can sign in to the printer as any customer.
Two habits shorten that window. The printer refreshes its cached set on a schedule measured in minutes, not days. It also gives its operators a way to discard the cached set immediately when a provider reports a compromised key. Sign-ins in progress with tokens from the withdrawn key fail, and the people involved simply sign in again.
A rotation the printer cannot follow, a server clock that drifts, a mistyped issuer: each makes sign-ins fail in ways that can look like an attack from the outside. Telling them apart is where the next lesson begins.