Internet Protocols · Identity Protocols
Discovery Metadata and the Key Set Rotation Problem
How a well-known document bootstraps trust in endpoints and signing keys, and what an implementation must do when the published key set changes underneath it.
Every client that talks to an authorization server needs to know four things before it can send a request: where the authorization endpoint is, where the token endpoint is, what issuer identifier the server will claim in its tokens, and which keys sign those tokens. Hardcoding all four works until one of them changes, and the endpoint that changes least often is the one that hurts most to change. Discovery replaces the hardcoded list with a single well-known URL and a document fetched from it.
The URL construction is a convention, not a redirect
OpenID Connect Discovery defines the issuer identifier as an https URL with no query and no fragment. The configuration document lives at that issuer with /.well-known/openid-configuration appended to the path.
The precise placement of that suffix is the part implementations get wrong. The well-known component is inserted after the host and before the path, not appended to the end of the path. An issuer of https://example.com/tenant-a therefore has its metadata at https://example.com/.well-known/openid-configuration/tenant-a, not at https://example.com/tenant-a/.well-known/openid-configuration.
The difference matters for multi-tenant deployments, where the issuer path distinguishes tenants. RFC 8414, which defines the same mechanism for plain OAuth authorization servers, states the rule explicitly because the ambiguity had already caused divergent implementations.
The document itself is JSON and carries an issuer member that must exactly equal the URL the client used to construct the metadata URL. That comparison is the anchor: a client that fetches metadata from one location and accepts an issuer claiming a different value has no reason to trust anything else in the document, since the whole document is unauthenticated data fetched over TLS from a host the client already decided to trust.
What the document is allowed to change
The metadata lists endpoint URLs, supported grant types, supported response types, supported scopes, supported claims, token endpoint authentication methods, signing algorithms, and the jwks_uri.
The design intent is that a client reads these values rather than assuming them, so an operator can add an endpoint or a signing algorithm without coordinating a client release. The cost is that the client’s behaviour becomes a function of remote data, and every value it consumes becomes an input that can break it.
The concrete failure is a client that fetches metadata once, caches it forever, and then encounters a token signed with an algorithm that was added after the cache was populated. The client rejects the token, correctly by its own rules, and the resulting outage is attributed to the issuer. The mirror failure is a client that refetches metadata on every request, which makes the issuer’s metadata endpoint a hard dependency on the request path and multiplies latency.
Neither extreme is right. The workable arrangement is to cache metadata for a bounded period, refetch on a validation failure that could be explained by stale metadata, and treat a refetch failure as non-fatal if a cached copy exists. The refetch-on-failure path is what keeps a rotation from becoming an outage; without it, the cache TTL is the outage duration.
Keys, kids, and the reason rotation is hard
The jwks_uri points at a JWK Set: a JSON object with a keys array, each entry a public key with a kty, a kid, and algorithm-specific members. For RSA that is n and e; for EC it is crv, x, and y.
A verifier selects a key by kid. RFC 7515 defines kid as a hint for key selection and nothing more; it is not a trust anchor, and it is not required to be unique across issuers, only within a set. A verifier that looks up by kid and verifies without checking that the resulting key is of the type and algorithm it expects has reintroduced the algorithm-confusion problem at the key-selection layer.
Rotation is a three-phase operation, and each phase has a failure mode.
In the publish phase, the new key appears in the set alongside the old one. Tokens are still signed with the old key. A verifier whose cache has not yet refreshed sees only the old key and is unaffected; a verifier that refreshes sees both and is unaffected. This phase is safe for any duration.
In the switch phase, tokens begin to be signed with the new key. A verifier that has not refreshed its cache sees a token with an unknown kid. Its only correct responses are to refetch the key set or to fail. A verifier that fails without refetching produces rejections that last until the cache expires, which is why the cache TTL must be shorter than the intended switch duration, or why the verifier must refetch on an unknown kid.
In the retire phase, the old key is removed from the set. Any token still in circulation that was signed with it now fails at a verifier that has refreshed. The safe interval before this phase is the maximum access token lifetime plus the maximum cache TTL plus the clock skew tolerance. Retiring earlier cuts off tokens that were legitimately issued.
A verifier that implements only the cache TTL and not the unknown-kid refetch is correct except during the switch phase, and the switch phase is exactly when it matters. This is the most common rotation defect, and it is invisible until the first rotation.
Caching the key set without a thundering herd
The key set is fetched by every verifier independently. When it expires, every verifier fetches at once. At the scale of a large API, that is a burst against a small static document, which is cheap to serve but not free, and the burst is synchronised only if the TTLs are synchronised.
Three mitigations are available. The first is to add jitter to the TTL, so refreshes spread over a window. The second is to make the refresh single-flight within a process, so concurrent requests for a key share one fetch. The third is to fetch in the background before expiry, so the common path never blocks on the network.
The background refresh has a subtle interaction with failure. If the refresh fails, the cached set remains valid and should continue to be used until a token arrives that it cannot verify. A verifier that discards its cache on a failed refresh has converted a transient network error into a total authentication outage.
Metadata as an attack surface
The discovery document is fetched over TLS from a URL derived from configuration, so the trust anchor is the configuration, not the document. The danger is a client that accepts an issuer URL from an untrusted source, such as a query parameter or an email link, and then follows the discovery chain.
That pattern is the basis of a class of attacks where an attacker supplies an issuer they control, the client fetches metadata, the client registers or links an account using claims from the attacker’s tokens, and the attacker’s assertions are accepted as identity. RFC 9207 addresses the related mix-up problem by adding an iss parameter to the authorization response, discussed in the authorization code flow, but the root defence is that the set of trusted issuers is a configuration decision, not a runtime input.
The jwks_uri itself is a second surface. Nothing in the metadata binds it to a particular host; an issuer may publish a key set on a different domain. That is legitimate, and it means a verifier that restricts jwks_uri to the issuer’s own host will break for issuers that do not. The correct posture is to accept the value from metadata fetched from a trusted issuer, and to require TLS on the key set URL.
Registration versus discovery
Dynamic client registration lets a client obtain a client_id at runtime, and RFC 7591 defines the protocol. It is optional, it is not required by OpenID Connect Core, and it is disabled on many deployments because it allows anyone to create a client that appears in the consent interface.
The interaction with discovery is that a client registered dynamically still needs the issuer’s metadata to find the endpoints. Discovery and registration are independent: a client can use a manually provisioned client_id with discovered endpoints, which is the common case, or a dynamically registered one with hardcoded endpoints, which is pointless but legal.
The practical distinction is that discovery is about what the issuer supports and registration is about what this client is. A deployment that treats the two as a pair will find that most real issuers support the first and restrict the second.
What discovery does not establish
The metadata describes capabilities, not policy. An issuer that lists code as a supported response type does not thereby permit a given client to use it, and an issuer that lists an algorithm as supported may still reject a specific client’s use of it. The document is a menu; the client’s registration is the order.
Nor does the document carry any freshness guarantee beyond the TLS connection. There is no signature over the metadata, no version number, and no last-modified semantic the client is required to honour. A client’s belief about the issuer is as fresh as its last fetch and as trustworthy as the decision to trust that issuer in the first place. The keys published at jwks_uri are the only part of the chain that carries cryptographic weight, and only for tokens that verify against them. What a verified ID token then establishes is the subject of ID token claims.
References
- OpenID Connect Discovery 1.0 — OpenID Foundation
- RFC 8414: OAuth 2.0 Authorization Server Metadata — IETF
- RFC 7517: JSON Web Key (JWK) — IETF
- RFC 7515: JSON Web Signature (JWS) — IETF
- RFC 7591: OAuth 2.0 Dynamic Client Registration Protocol — IETF