Skip to content

Catalog: make the jwks_uri rotation case exercise the production refresh path - #6

Open
RobertoIskandarani wants to merge 1 commit into
mainfrom
tighten-jwks-uri-rotation-case
Open

Catalog: make the jwks_uri rotation case exercise the production refresh path#6
RobertoIskandarani wants to merge 1 commit into
mainfrom
tighten-jwks-uri-rotation-case

Conversation

@RobertoIskandarani

Copy link
Copy Markdown
Contributor

rfc8414-jwks-uri-rotation-must-reconfigure-jwks-cache exists to catch a specific defect: a verify-only resource server that reads authorization server metadata once and never re-reads it, so a rotated jwks_uri is never followed. Every SDK currently satisfies the case without exercising any production trigger path, which is how that defect shipped in four of six SDKs — the suites reach the rotation through a force-refresh argument, a test-only hook, reflection into cache internals, or by asserting on two locally constructed metadata objects without ever driving the verifier.

A case that can be passed that way is documentation, not a gate. This makes the stimulus the substance of the case.

What changes

  • Setup gains a rotation_sequence and an explicit metadata_refresh_interval: the verifier is built through public API against the old metadata and verifies successfully, the AS then begins serving the new metadata with the old JWKS URI withdrawn, and the new signing key is published at the new URI only.
  • Stimulus is ordinary verifier.verify traffic spanning the refresh interval, plus a new prohibited_mechanisms list — no force-refresh or other cache-invalidating argument, no test-only hooks or package-private methods or reflection, no direct mutation of the metadata or JWKS cache, and no asserting on locally built metadata objects without driving the verifier.
  • Expected gains a bound (a token signed by the new-URI-only key verifies within two refresh intervals) and asserts the withdrawn URI is not fetched again after the rebind.
  • notes distinguishes this case from rfc8725-kid-must-resolve-through-jwks-with-single-refresh-on-miss, which refetches the same URI after an unknown kid. Here the URI itself must be re-read from metadata, so an implementation that only refreshes keys from a URI captured at construction fails this case while still passing that one. A second note says an implementation with no metadata refresh interval at all should report skipped with the reason rather than reaching the rotation through a non-public path.

prohibited_mechanisms is a new sub-key under stimulus; every case so far carried only operation. It is introduced deliberately, because a vacuous pass is exactly a stimulus problem. This case also held the catalog's only private-method operation (client._on_metadata_changed), which is now gone.

On the citation

RFC 8414 §2 defines jwks_uri as the URL of the JWK Set holding the keys used to validate the authorization server's signatures, and §3 requires that document to be published at the well-known location derived from the issuer identifier — so the current jwks_uri lives in metadata, not in a value captured once at construction.

RFC 8414 imposes no re-fetch cadence; it contains no refresh, caching or rotation requirement at all. The bounded window is therefore this profile's requirement, supplied on top of the RFC, and the case says so: requirement_summary keeps SHOULD for the refresh behaviour and reserves MUST for the mechanism restriction, which is a catalog rule rather than an RFC one.

Impact

Implementations that satisfy the case today through a test-only path will fail it until their production refresh path works. That is the intent — the case was passing vacuously — but it means this should land in step with the consuming repositories rather than ahead of them.

make check passes: yamllint clean, validate_catalog: OK (104 cases, 16 standards). catalog_version bumped per the contributing rules; the case id is unchanged.

The case was satisfiable without touching the code path it exists to
protect. Its stimulus named a private method, so implementations
reached the rebind through force-refresh arguments, test-only hooks,
reflection, or by comparing two locally built metadata structs - and
the defect it was meant to catch (a verify-only resource server that
never re-reads metadata, so a jwks_uri rotation is never followed)
shipped anyway.

Retighten the case around the real path, keeping the id stable:

- stimulus is ordinary verifier.verify traffic spanning the metadata
  refresh interval, with an explicit prohibited_mechanisms list
  covering force-refresh, test-only or package-private hooks,
  reflection into cache internals, and assertions made on locally
  built metadata objects that never drive the verifier;
- setup adds the rotation sequence: a verifier built through public
  API and verifying against jwks-v1, the AS then serving new metadata
  with jwks-v1.json withdrawn, and the new signing key published only
  at jwks-v2.json;
- expected gains a bound (verification succeeds within two refresh
  intervals) and side effects naming the re-fetch, the fetch from the
  rotated URI, and the absence of further fetches of the withdrawn one;
- surface moves to sdk-verifier.jwks, where the defect actually lives.

The rationale states the trace honestly: RFC 8414 section 2 defines
jwks_uri as the URL of the JWK Set holding the AS signing keys and
section 3 requires that document to be published at the well-known
location derived from the issuer identifier, but the RFC specifies no
re-fetch cadence - the bound is this profile's, resting on the SDK's
own metadata refresh-interval contract.

Notes distinguish the case from the kid-miss refresh case, which
refetches the same URI, and tell an SDK with no metadata refresh
interval to report failed or skipped rather than route around the
restriction. catalog_version bumps to 2026-08-19.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant