Consolidate duplicate JWKS fetch and cache machinery - #6459
Open
lorenzozanee wants to merge 2 commits into
Open
Conversation
The inbound TokenValidator and the token-exchange multi-issuer validator each carried a full JWKS fetch-and-cache stack built on the same jwx/httprc libraries, and had drifted: the token-exchange side gained a response-body cap, rate-limited refresh on unknown kid, URL validation and fetch-failure backoff that the inbound side never received, and every jwx/httprc pitfall had to be fixed twice. Extract one shared fetcher into pkg/auth/jwks and move both call sites onto it. Per-issuer InsecureAllowHTTP and AllowPrivateIPs survive, each fetcher owns its own jwk.Cache so transport policy can never leak across issuers sharing a JWKS URL, and the response-body cap, key-count cap, rate-limited kid refresh and stale-on-error semantics now apply to both paths. ValidateJWKSURL moves from tokenexchange to pkg/auth/jwks. Signed-off-by: lorenzozanee <wyz0707@proton.me>
lorenzozanee
requested review from
ChrisJBurns,
JAORMX,
jhrozek,
rdimitrov and
tgrunnagle
as code owners
August 29, 2026 02:40
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6459 +/- ##
==========================================
- Coverage 78.07% 78.03% -0.05%
==========================================
Files 767 771 +4
Lines 74269 74370 +101
==========================================
+ Hits 57988 58034 +46
- Misses 16276 16331 +55
Partials 5 5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The inbound TokenValidator and the token-exchange multi-issuer validator each carried a full JWKS fetch-and-cache stack built on the same jwx/httprc libraries, and had drifted: the token-exchange side gained a response-body cap, rate-limited refresh on unknown kid, URL validation and fetch-failure backoff that the inbound side never received, and every jwx/httprc pitfall had to be fixed twice. Extract one shared fetcher into pkg/auth/jwks and move both call sites onto it. Per-issuer InsecureAllowHTTP and AllowPrivateIPs survive, each fetcher owns its own jwk.Cache so transport policy can never leak across issuers sharing a JWKS URL, and the response-body cap, key-count cap, rate-limited kid refresh and stale-on-error semantics now apply to both paths. ValidateJWKSURL moves from tokenexchange to pkg/auth/jwks. Signed-off-by: lorenzozanee <wyz0707@proton.me>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ToolHive fetched and cached remote JWKS in two places, built independently on the same jwx/httprc libraries: the inbound
TokenValidator(pkg/auth/token.go) and the token-exchange multi-issuer validator (pkg/authserver/server/tokenexchange/multi_issuer_validator.go). The two had drifted: the token-exchange side gained hardening (response-body cap, rate-limited refresh on unknownkid, URL validation, fetch-failure backoff) that the inbound side never received, and every jwx/httprc pitfall had to be learned twice.This extracts one shared fetch-and-cache type,
jwks.Fetcherin the newpkg/auth/jwkspackage, and moves both call sites onto it:Fetcherper issuer/validator; each owns its ownjwk.Cacheand*http.Client, so two issuers sharing a JWKS URL can no longer silently inherit each other's transport policy.networking.NewHttpClientBuilder()with per-caller flags;INSECURE_DISABLE_URL_VALIDATIONstill cannot widen private-IP dialing.ValidateJWKSURLandlimitedBodyTransportmove verbatim into the package;ValidateJWKSURLstays env-immune.Intentional hardening on the inbound path (declared behavior changes, per the issue's acceptance criteria): the previously token-exchange-only protections now apply to
pkg/authtoo - JWKS URL validation on every fetch/refresh, 1 MiB response-body cap, 100-key cap, 30s rate-limited refresh on unknownkid, and a 30s fetch-failure backoff before first success. One recovery-window nuance: a first fetch that misses the 5s registration budget is now treated as a fetch failure under the backoff gate (previously the next request could succeed as soon as the background fetch landed), so a slow IdP can take up to ~30s longer on first contact. The token-exchange path is behavior-preserving; its discovery-failure gate is kept in the caller.pkg/authserver/upstream/oidc.go(coreos/go-oidcRemoteKeySet) is the third mechanism and is deliberately left alone, with a code comment saying so - replacing it would mean reimplementing ID-token verification and nonce handling.Fixes #6319
Type of change
Behavior-preserving for the token-exchange path. The inbound path gains the hardening listed above, aligning the two paths per #6319's acceptance criteria - checked as a bug fix because the missing inbound protections are the gap this PR closes.
Test plan
task test)task lint-fix)Scoped commands actually run:
go test ./pkg/auth/... ./pkg/authserver/... -count=1(all packages ok),go test -race ./pkg/auth ./pkg/auth/jwks ./pkg/authserver/server/tokenexchange -count=1(clean), golangci-lint v2.12.2 on the changed packages (0 issues).New coverage:
pkg/auth/jwksseam tests (kid lookup + rate-limited unknown-kid refresh, fetch-failure backoff with error replay, stale-on-error incl. failed kid-refresh preserving the stale set, body cap, per-instance flag isolation,ValidateJWKSURLtable, key-count cap, pinned refresh interval) and inbound-path tests throughValidateToken(body cap, key rotation with rate-limited refresh, stale-on-fetch-error, slow-first-fetch, re-discovery to a new JWKS URL re-registers).Changes
pkg/auth/jwks/jwks.gopkg/auth/jwks/options.gopkg/auth/jwks/url.goValidateJWKSURLmoved from tokenexchangepkg/auth/jwks/transport.golimitedBodyTransportmoved from tokenexchangepkg/auth/token.gojwks.Fetcher; registration flag removedpkg/authserver/server/tokenexchange/multi_issuer_validator.gojwks.Fetcher; duplicated machinery deletedpkg/authserver/config.gojwks.ValidateJWKSURLpkg/authserver/upstream/oidc.gopkg/auth/token_test.go,pkg/auth/token_jwks_test.gopkg/auth/jwks/jwks_test.gomulti_issuer_validator_test.goDoes this introduce a user-facing change?
Yes - on the inbound path, a plain-HTTP
jwks_uriwithout explicitInsecureAllowHTTPis now rejected (fail-closed URL validation), and a slow first fetch from the IdP can take up to ~30s longer before token validation succeeds. Configuration semantics are otherwise unchanged.Special notes for reviewers
TestTokenValidator_SlowFirstFetchStillSucceedsand the backoff tests.