fix(deps): update module github.com/giantswarm/mcp-oauth to v1.2.4 - #262
Open
renovate[bot] wants to merge 1 commit into
Open
fix(deps): update module github.com/giantswarm/mcp-oauth to v1.2.4#262renovate[bot] wants to merge 1 commit into
renovate[bot] wants to merge 1 commit into
Conversation
Contributor
Author
ℹ️ Artifact update noticeFile name: go.modIn order to perform the update(s) described in the table above, Renovate ran the
Details:
|
renovate
Bot
force-pushed
the
renovate/github.com-giantswarm-mcp-oauth-1.x
branch
from
July 21, 2026 20:35
c457225 to
470ff06
Compare
renovate
Bot
force-pushed
the
renovate/github.com-giantswarm-mcp-oauth-1.x
branch
from
July 22, 2026 15:47
470ff06 to
9d5be84
Compare
renovate
Bot
force-pushed
the
renovate/github.com-giantswarm-mcp-oauth-1.x
branch
2 times, most recently
from
July 24, 2026 10:37
a60050d to
67b7e36
Compare
renovate
Bot
force-pushed
the
renovate/github.com-giantswarm-mcp-oauth-1.x
branch
from
July 24, 2026 15:03
67b7e36 to
c75e720
Compare
renovate
Bot
force-pushed
the
renovate/github.com-giantswarm-mcp-oauth-1.x
branch
3 times, most recently
from
July 28, 2026 09:29
adb810f to
f4b7fa4
Compare
renovate
Bot
force-pushed
the
renovate/github.com-giantswarm-mcp-oauth-1.x
branch
from
July 28, 2026 17:57
f4b7fa4 to
8b5ca63
Compare
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.
This PR contains the following updates:
v1.0.13→v1.2.4Release Notes
giantswarm/mcp-oauth (github.com/giantswarm/mcp-oauth)
v1.2.4Compare Source
Changed
Full Changelog: giantswarm/mcp-oauth@v1.2.3...v1.2.4
v1.2.3Compare Source
Fixed
Full Changelog: giantswarm/mcp-oauth@v1.2.2...v1.2.3
v1.2.2Compare Source
Changed
Full Changelog: giantswarm/mcp-oauth@v1.2.1...v1.2.2
v1.2.1Compare Source
Changed
Full Changelog: giantswarm/mcp-oauth@v1.2.0...v1.2.1
v1.2.0Compare Source
Added
Full Changelog: giantswarm/mcp-oauth@v1.1.4...v1.2.0
v1.1.4Compare Source
Changed
Full Changelog: giantswarm/mcp-oauth@v1.1.3...v1.1.4
v1.1.3Compare Source
Fixed
Full Changelog: giantswarm/mcp-oauth@v1.1.2...v1.1.3
v1.1.2Compare Source
Fixed
Full Changelog: giantswarm/mcp-oauth@v1.1.1...v1.1.2
v1.1.1Compare Source
Fixed
Full Changelog: giantswarm/mcp-oauth@v1.1.0...v1.1.1
v1.1.0Compare Source
Added
storage.UserProviderTokenStore, #512, rotation-race slice 1 of giantswarm/giantswarm#37164 root cause 2). The upstream provider (dex) token is now stored as one shared entry per user — the single source of truth — instead of private copies under every subject/email/access-token/refresh-token key. Issued tokens hold lightweight references (token → user) and every rotation writes the fresh provider token back to the one shared entry, so a user's concurrent sessions no longer hold divergent copies of a single-use dex refresh token. The shared entry carries the provider's real expiry (noProviderTokenTTLinflation; backends keep the entry alive via their own TTL while the refresh token can renew it). The interface is optional and consumed via type assertion (mirroringRefreshTokenFamilyStore), implemented identically by the memory and Valkey backends (providertoken:/tokenref:namespaces + an atomic Lua refresh-token consume); backends that don't implement it keep the legacy copy-per-token behavior, so the Go API stays additive. mcp-oauth's own per-session refresh-token families and rotation are unchanged, and OAuth 2.1 reuse detection still fires on genuine reuse. Deploy note (see thetoken:*/refresh:*Valkey keys and a one-time re-login for every user. Slices 2–4 (cross-pod single-flight refresh lock, routing all refresh entry points through it, no-orphan reordering) complete the fix and all ship in this release.storage.ProviderRefreshLockStore, #513, rotation-race slice 2 of giantswarm/giantswarm#37164 root cause 2). Every refresh of the shared per-user provider token is now coordinated with a per-user single-flight lock — cross-pod on the Valkey backend (SET NX+ a Lua compare-and-delete against a random lock value for owner-only release, following the DPoP replay-cache precedent;refreshlock:namespace), in-process on the memory backend. The coordination is double-checked locking: acquire → re-read the shared entry → if it is already fresh (a sibling refreshed just before) adopt it without calling the provider, otherwise refresh once and write the rotated token back to the shared entry before releasing. Freshness is judged on the provider's real expiry against the proactive-refresh threshold or on refresh-token identity (the shared token rotated past the caller's copy). A session that loses the race waits briefly (bounded by the provider-refresh timeout) and adopts the winner's freshly-written token — no provider call, no collision, no client-visible error. The lock self-expires (provider-refresh timeout + margin), so a holder that crashes mid-refresh only delays the next refresh instead of wedging the user. The interface is optional and consumed via type assertion; the Go API stays additive. Slice 3 routes the server's three provider-refresh entry points through this coordinator; the deterministic rotation-race reproductions stay red until then by design./oauth/tokenrefresh-token grant and both the proactive (near-expiry) and reactive (expired) refreshes during validation — now obtain the provider token via the per-user lock + shared entry instead of calling the provider directly. No path refreshes the provider uncoordinated, so a user's concurrent sessions produce at most one dex refresh per rotation window; the rotated token is written back to the shared entry only under the lock (the grant no longer re-saves it afterwards, which could have clobbered a sibling's newer token). Client binding is validated before any lock acquisition or provider call on every path, so a token presented by the wrong client never triggers an upstream refresh (TestServer_RefreshAccessToken_ClientBinding_Integrationstays green). This makes the deterministic rotation-race reproductions (server/rotation_race_repro_test.go) pass: benign collisions between a user's own sessions are invisible, all sessions survive, and dex is called exactly once. Behavior change: a refresh-token grant whose shared provider token is still fresh (real expiry beyond the proactive-refresh threshold) now adopts that token instead of always calling the provider — the client still receives freshly rotated mcp tokens, but the upstream credential is reused for its lifetime rather than re-fetched on every grant. Silent authentication / SSO bridging is preserved (a second client comes up from the shared entry without an interactive dex login).RefreshAccessTokenatomically deleted the mcp refresh token before calling the provider, with no rollback, so a transient dex hiccup orphaned an otherwise-valid session — the client's retry re-presented the now-deleted token, tripped reuse detection, andRevokeAllTokensForUserClientnuked every one of the user's sessions for that client. The refresh grant is now reordered so the mcp refresh token is rotated/deleted only after the shared provider token is confirmed fresh. In the unified layout the front half of the grant resolves the token's user binding with a non-destructive existence + expiry check (GetRefreshTokenInfo, same not-found/expired semantics the atomic consume had), validates client binding, reads the shared provider entry, and defers the atomic consume (the concurrency gate) until after the coordinated provider refresh succeeds. A transient provider-refresh failure therefore returns with the session's refresh token intact and usable — no orphan, no reuse nuke on retry — while genuine reuse (a rotated-away, re-presented mcp refresh token) still trips reuse detection and revokes the family + all user/client tokens (the OAuth 2.1 reuse-detection machinery is untouched). The legacy copy-per-token layout is unchanged: there the provider-token copy can only be read by consuming the token, so the delete stays up front.Server.RefreshSessionProvider(ctx, familyID)— a background-safe, provider-only session refresh (giantswarm/giantswarm#37164, residual graveler deauth). Refreshes the upstream (dex) provider token for a family through the per-user single-flight coordinator and firesTokenRefreshHandlerwith the fresh token, but — unlikeServer.RefreshSession/ the/oauth/tokenrefresh-token grant — it does not rotate the client-facing mcp refresh token, advance the family generation, or issue a new access token. A background integration that keeps a fresh upstreamid_tokenon hand (e.g. an SSO re-exchange/forwarding loop) previously had onlyRefreshSession, whose client-token rotation on a tight retry loop rotated the client's refresh token out from under it until OAuth 2.1 reuse detection revoked the whole family and deauthed the user. The owning user is resolved from the refresh-token family record (GetRefreshTokenFamilyByID), which is race-free across rotation — resolving via a consumable issued token would race a concurrent genuine client refresh that atomically deletes it and fail exactly when the session is healthiest. The handler fires only when the refreshed token actually carries anid_token, so a background refresh can never drive an SSO-teardown on an id_token-less provider entry (preserveRefreshTokencarries the refresh token forward but not the id_token). Requires the unifiedUserProviderTokenStorelayout andRefreshTokenFamilyByIDStore. NoteRefreshSession(the rotating variant) does NOT fireTokenRefreshHandler— the stale doc claiming it did is corrected;Server.RefreshSessionis now deprecated in favor ofRefreshSessionProviderand remains only for in-process callers that both need a newly minted access token and can hand the rotated refresh token back to the client (removal is v2-only).Fixed
Hardening of the v1.1.0 rotation-race fix, surfaced by adversarial review of the unified per-user shared-provider-token layout (giantswarm/giantswarm#37164 root cause 2). All ship in this release.
invalid_grantwith a warning-levelshared_provider_token_missingaudit event, and the presented refresh token is left intact so retries stay idempotent — the client simply re-logs in.invalid_grantor a falsecross_client_token_theft_preventedsecurity event. Reads of the refresh-token info, token metadata, and shared provider entry now distinguish a genuine not-found from a transient backend error; token-metadata absence is signaled by both the memory and Valkey backends with thestorage.ErrTokenNotFoundsentinel.TokenStore.DeleteRefreshToken), independent of family state — previously a family-less refresh token survived revocation fully usable. Local invalidation now always runs even when the provider-token reference is missing, and a transient family-lookup error during revocation surfaces instead of silently skipping family revocation.singleflight) and the waiter loop is bound to the caller's context, so a cancelled request stops polling immediately. Only the winner's provider call and write-back are detached, so a started rotation always completes and persists.GetRefreshTokenInfo,GetRefreshTokenFamily, andGetTokenMetadata, so the destructive and non-destructive refresh checks and the reuse-detection reads all agree. A leftover pre-migration key can no longer pass validation, burn a provider rotation, then be misclassified as theft.Changed
http.DefaultTransportglobal; CA trust flows in as an explicit*x509.CertPool. New fields:oidc.JWKSClientOptions.RootCAs,oidc.TokenExchangeClientOptions.RootCAs,server.Config.JWKSRootCAs,server.TrustedIssuer.RootCAs, anddex.Config.RootCAs;nilkeeps system-pool verification.NewPrivateIPAllowedHTTPClient(timeout, rootCAs)andNewHostScopedPrivateIPHTTPClient(hosts, timeout, rootCAs)gain arootCAs *x509.CertPoolparameter (NewSSRFSafeHTTPClientis unchanged).defaultTransportRootCAs()and every read ofhttp.DefaultTransportare removed. This deletes the*http.Transporttype assertion, the build-time-snapshot caveat, and the global-mutation requirement in tests (the CA-trust tests are now parallel-safe). Consumers that relied on installing a CA onhttp.DefaultTransport(e.g. mcp-kubernetes viaDEX_CA_FILE) MUST now pass the CA pool explicitly — this is a coordinated change: mcp-kubernetes passes the pool and drops itsDefaultTransportinstall in the same rollout, and this mcp-oauth release must land/ship before mcp-kubernetes bumps to it. Closes #495.NewOIDCValidator()now builds one permissive JWKS client perAllowPrivateIPJWKSissuer instead of sharing a single client across all of them, so each issuer's JWKS fetch is verified against its ownTrustedIssuer.RootCAspool. With the shared client, only the first permissive issuer's pool was honored — a second issuer with a different internal CA was silently rejected with a certificate error despite correct configuration.Security
CheckIPLimit(memory + valkey) no longer embeds the client IP or per-IP registration counts in the returned error.ErrClientIPLimitExceeded's message is now the generic"rate limit exceeded"and both backends return the bare sentinel, so the error value is safe to render wherever it propagates — structuredWARNlogs and any consumer that surfaces it — without revealing that the IP is tracked or exposing the counts (the register handler already returns its own fixed HTTP 429 message). Callers still match by identity viaerrors.Is(the register handler is unchanged); the IP and current/max counts are preserved in a structuredWARNlog at the check site. See #519.SelfIssuedExchangerefuses to issue a token when the validated subject asserts an email claim withoutemail_verified: trueand no explicitOptions.Emailis supplied (ErrUnverifiedSubjectEmail). The issued token defaults the subject's email into its identity claims, which resource servers authorize on (allowedClaims.emailpatterns andsubjectClaim: emailremapping do not consultemail_verified), so issuance now fails closed on an unproven email. Subjects without an email claim (workload identities such as Kubernetes ServiceAccount tokens) are exempt.SelfIssuedExchangetreats re-attaching the actor already outermost on the subject'sactchain as adding no delegation hop: a re-exchange whose new actor'siss+subequals the current outermost actor is collapsed to a bare renewal and rejected withErrSelfRenewalDenied. Without this, a holder of a self-issued on-behalf-of token plus its own actor token could refresh the token's TTL indefinitely by re-attaching the same actor each hop. Re-exchange with a different actor still extends the chain normally.SelfIssuedExchangenow enforces proof-of-possession when the validated subject token carries an RFC 9449 §6.1cnf.jktconfirmation: the request's DPoP proof key thumbprint must equal the subject'scnf.jkt, otherwise the exchange is rejected (ErrSubjectKeyMismatch, audit reasontoken_exchange_subject_key_mismatch). This prevents a bearer who lacks the confirmation key from laundering a key-bound token into one bound to a different key (or an unbound token). Subjects with nocnfconfirmation are unaffected.server.SubjectIdentitygains aConfirmationJKTfield carrying the subject token's confirmation thumbprint.SelfIssuedExchangehonors a newConfig.TokenExchangeAllowedResourcesallowlist: when non-empty, a requestedresourcethat is neither the server's ownResourceIdentifiernor a listed value is rejected withinvalid_target(ErrInvalidTarget, audit reasontoken_exchange_resource_not_allowed). Empty (the default) accepts any resource, preserving current behavior. This caps the audiences an unauthenticated self-issued exchange can mint tokens for.The permissive JWKS clients (
AllowPrivateIPJWKS/ per-issuerAllowPrivateIPJWKSHosts) now keep the cross-host redirect guard and tuned timeouts.Server.getJWKSClient()(SSO forwarded-ID-token path) andNewOIDCValidator()(trusted-issuer path) previously hand-rolled a raw&http.Client{Transport: http.DefaultTransport}to honor a process-installed CA, which droppedNewPrivateIPAllowedHTTPClient/NewHostScopedPrivateIPHTTPClient'sCheckRedirect: blockCrossHostRedirectandResponseHeaderTimeout/TLSHandshakeTimeout. Because these paths already disable the SSRF dial guard, a compromised internal Dex could302the JWKS fetch to an arbitrary internal HTTPS target. Both constructors now copy onlyhttp.DefaultTransport'sRootCAs(not the wholeTLSClientConfig) onto their own transport — preserving CA trust while keeping the redirect guard, dialer semantics, and timeouts, and ensuring a globalInsecureSkipVerify/ServerName/client cert cannot leak onto these clients so the "never InsecureSkipVerify" invariant holds by construction. Both call sites now use the constructors. This also closes the pre-existing gap in the trusted-issuer path. See giantswarm/giantswarm#37059 and #493.Fixed
id_tokenreturned alongside a minted access/refresh pair, keyed by theid_tokenstring withTokenType"id"and theid_token's ownexpas expiry (falling back to the access-token expiry whenexpis unreadable). A bearer equal to the issuedid_tokenpresented at the resource-server front door now resolves to the sameFamilyID-derived session as its sibling access token, and the session stays stable across refresh rotations. Previously theid_tokenfell back to the bearer-derivedext-session, which changed on every rotation. Anid_tokenthis server never issued still gets the bearer-derivedext-session.{prefix}userclient:{uid}:{cid}set backing bulk revocation (RevokeAllTokensForUserClient) no longer grows without bound. Every grantSADDs the access and refresh token IDs into the set, but nothing everSREMs a member — access tokens expire purely by their own key TTL, which fires no set-membership cleanup — and the set itself carried no TTL, so it was only ever reclaimed by the wholesaleDELon reuse/theft detection. In the happy path it therefore leaked one member per token forever; once bloated it also fed long-dead tokens intoRevokeAllTokensForUserClient's per-member provider-revocation loop, whose failure-rate threshold could then abort the local revocation. BothSADDsites now add the member and stamp an extend-only TTL on the set in a single atomic Lua script:TTL = max(existing, member horizon), so the key self-evicts after its longest-lived member's horizon and a short-lived access-token add can never shrink it below a still-live refresh-token member. Doing it atomically (rather than separateSADD+EXPIRE GT+EXPIRE NXround-trips) closes a creation-time race where concurrent grants could pin the set to a short access-token horizon and drop live refresh members, keeps the result order-independent across a grant's access/refresh writes, and halves the round-trips on the token-issuance hot path. A horizon-less add (a token metadata save with a zero/expiredExpiresAt) still bounds the set — it falls back to the store's refresh-token TTL — so the set can never be left unbounded, and a sub-second horizon is floored to1sso it can never truncate toEXPIRE 0and delete the set.calculateTTLnow clamps a positive sub-second TTL up to a one-second floor. valkey-go'sEx()buildsSETwith a whole-secondEXargument, so a near-expiry save (e.g. 50ms remaining) truncated toEX 0and Valkey rejected it withinvalid expire time in 'set' command; the "already expired -> 0" semantics are preserved.RevokeJTIbypassedcalculateTTLwith its owntime.Until(expiresAt)and hit the identicalEX 0truncation, silently failing to denylist a self-issued JWT revoked in the final second before its (whole-second)exp; it now routes throughcalculateTTLtoo. See #518.http.DefaultTransportwhenAllowPrivateIPJWKSis set. PreviouslyServer.getJWKSClient()built a JWKS client that verified against the system certificate pool alone, so an internal-CA Dex forwarding SSO ID tokens was rejected withx509: certificate signed by unknown authorityeven when the host process had installed that CA — the trusted-issuer permissive client (NewOIDCValidator) already worked this way, the forwarded-token path did not. The SSRF-safe client used whenAllowPrivateIPJWKSis unset is unchanged and still verifies against the system pool. See giantswarm/giantswarm#37059.Changed
Server.ExchangeSubjectTokenis renamed toServer.SelfIssuedExchangeand takes aSelfIssuedExchangeRequest(embeddingSubjectExchangewithSubjectandActorasTypedToken{Token, Type}, plusDPoPJKTandOptions) in place of positional parameters. It now preserves anyactchain already on the subject token (nesting the new actor above it, bounded depth) and defaultsemail,email_verified,nameandgroupsfrom the validated subject whenOptionsleaves them unset; an explicitOptionsvalue still takes precedence. When no resource is supplied the issued token's audience defaults to the server's resource identifier.Server.BrokerExchangeSubjectTokenis renamed toServer.BrokeredExchangeand takes aBrokeredExchangeRequest.The
/oauth/tokenlocal exchange no longer requires aresourceparameter; when omitted, the issued token's audience is the server's resource identifier. A resource-less, audience-less exchange therefore now succeeds where it previously returnedinvalid_request("resource is required"). Deployments that relied on the mandatoryresourceas a policy gate must now constrain the mintable audiences withConfig.TokenExchangeAllowedResources.SelfIssuedExchangenow rejects re-exchanging a token this server issued when no newactor_tokenis supplied (newserver.ErrSelfRenewalDenied, audit reasontoken_exchange_self_renewal_denied). Re-exchanging a self-issued token is allowed only to add an actor, extending the delegation chain; a bare re-exchange would refresh the TTL open-endedly. Subject tokens from external issuers are unaffected.ValidateTokennow logs a forwarded-ID-token validation failure atWARN(wasDEBUG) when the token carried a trusted audience — i.e. it was meant to be an SSO-forwarded ID token — but signature/JWKS validation then failed (e.g. a JWKS TLS/CA error). This surfaces real SSO misconfigurations without enabling debug logging. Tokens that simply don't carry a trusted audience are not SSO tokens and remain silent. No token material is logged (only an 8-character suffix for correlation).The resource-server
ValidateTokenmiddleware now assigns a session ID to every validated token, not only those backed by stored refresh-token-family metadata. A token with a family still uses itsFamilyID; any other validated token (forwarded ID token, trusted-issuer, self-issued exchange-minted JWT) falls back toSessionIDForBearer. Self-issued exchange-minted JWTs previously reached downstream handlers with no session, so resource servers could not session-scope an on-behalf-of bearer re-presented at the front door; they now carry one.Removed
Server.WorkloadExchangeSubjectTokenandConfig.EnableWorkloadTokenExchange: the workload-authenticated brokered path (a brokered exchange with no OAuth client credentials) is removed. Brokered token exchange now always requires an authenticated confidential client; the on-behalf-of agent path usesSelfIssuedExchangeinstead.Server.LocalMintExchangerandNewLocalMintExchanger: local token issuance is nowSelfIssuedExchange. TheExchangerinterface is retained for downstream/remote exchange implementations.Config.DelegationDefaultResource: the issued token's audience defaults to the server's resource identifier whenever no resource is supplied, so the configurable default is no longer needed.server.ExchangerResult.JTI. Consequently, brokeredtoken_issuedaudit events no longer carry ajtifield (the downstream issuer's token identifier is no longer surfaced). Update any dashboard or alert that correlated brokered issuance byjti.exchange: "workload"audit value and thetoken_exchange_resource_missingfailure reason are no longer emitted (the workload path and the resource-required rejection are gone). Update any dashboard or alert keyed on those values. The self-issued exchange'stoken_issuedevent now carriesjtiandsession_id.Added
server.Config.TokenExchangeAllowedResources []string: optional allowlist capping the RFC 8707resourcevalues the self-issued RFC 8693 flow will mint a token for. Empty (the default) accepts any resource; when non-empty, a request whose resource is neither the server's ownResourceIdentifiernor a listed value is rejected withinvalid_target.server.Server.SessionIDForBearer(bearerToken string) string: returns the deterministicext-<hex>session identifier for a validated bearer with no refresh-token family. Same derivation the forwarded-token and token-exchange paths use, so the value agrees across hops.server.Config.DelegationDefaultResource string: RFC 8707 resource applied to a local token-exchange request that carries anactor_tokenbut omits bothaudienceandresource. The minted token is bound to this value (typically the server's own resource identifier), so an on-behalf-of caller that cannot set a resource itself still gets a token accepted back at this server. Gated to the delegation path: a resource-less exchange with noactor_tokenis still rejected withinvalid_request. Empty (the default) disables it.oidc.JWKSClientOptions.AllowPrivateIPHosts []string: host-scoped alternative toAllowPrivateIP. When set, the JWKS client allows private-IP resolution only for the explicitly listed hostnames; all other hosts retain the SSRF/DNS-rebinding guard.NewJWKSClientWithOptionswires aNewHostScopedPrivateIPHTTPClientwhen this field is non-empty andAllowPrivateIPis false.oidc.NewHostScopedPrivateIPHTTPClient(allowedHosts []string, timeout time.Duration) *http.Clientandoidc.HostScopedPrivateIPDialContext(dialer, allowedHosts): HTTP client and dial context that enforce the normal SSRF guard for all hosts except the listed ones, which are permitted to resolve to private IPs.server.TrustedIssuer.AllowPrivateIPJWKSHosts []string: per-issuer host-scoped alternative toAllowPrivateIPJWKS. Allows the JWKS fetch to resolve to a private IP only when the URL's hostname matches one of the listed values. All other hosts retain SSRF protection. Prefer this overAllowPrivateIPJWKSwhen the private endpoint is a known in-cluster service (e.g.muster.agentic-platform.svc.cluster.local).NewOIDCValidatorbuilds a per-issuerJWKSClientfor each entry that sets this field; the shared permissive client is only allocated when at least one issuer sets the legacyAllowPrivateIPJWKS.server.Server.EnsureConfidentialClient(ctx, clientID, clientSecret string, scopes []string) (seeded bool, err error): idempotently provisions a confidential OAuth client with a caller-supplied id and secret (unlikeRegisterClientV2, which generates random ones). If the client already exists with a matching secret it is a no-op; otherwise the record is (re)written with a fresh bcrypt hash. Intended to declaratively seed a known confidential client (e.g. a token-exchange broker) from a mounted secret at startup, so a wiped client store self-heals.server.TrustedIssuer.SubjectClaim string: names the verified claim whose value becomesSubjectIdentity.Subject(and thus thesubof any token minted from the identity). Empty keeps the standardsubclaim. Use it when an issuer'ssubis opaque but another claim (e.g.email) carries the canonical subject the downstream relies on. Fail-closed: a token whose configured claim is absent or not a non-empty string is rejected.server.Server.AcceptTrustedIssuerToken(ctx, bearerToken): validates a Bearer JWT against theWithTrustedIssuersconfiguration and returns aForwardedIDTokenAcceptance(same shape asAcceptForwardedIDToken, including theext-<hex>session ID). Intended as a fallback for aggregators that receiveErrTrustedAudienceMismatchfromAcceptForwardedIDToken, e.g. a raw Kubernetes ServiceAccount projected token whoseaudis the server's own resource identifier rather than aTrustedAudiencesentry.server.LocalMintExchangerandNewLocalMintExchanger(cfg *Config) (*LocalMintExchanger, error): anExchangerimplementation that mints a signed JWT access token locally using the server's own RSA/EC key. Intended for broker-routed targets where the broker is the authoritative issuer. Requires JWT access token mode; returns an error otherwise.actor_token/actor_token_typesupport on the direct token-exchange path (ExchangeSubjectToken): when an actor token is presented, it is validated against the server's trusted issuers and the resulting actor identity is written as theactclaim of the issued JWT (act.iss= actor issuer,act.sub= actor subject). Any actor validated against the trusted issuers is accepted; self-delegation (actor equal to subject) is a no-op and omits theactclaim.providers.UserInfo.ActorIssuer stringandproviders.UserInfo.ActorSubject string: actor identity fields populated when a validated token carries an RFC 8693 §4.4actclaim. Empty for tokens without delegation.providers.UserInfo.IsOBO() bool: reports whether an external-issuer token carries an RFC 8693actclaim (a delegated human identity). True exactly whenActorSubjectis populated.oidc.IDTokenClaims.Act *oidc.ActorClaimandoidc.ActorClaim(Issuer string,Subject string,Act *oidc.ActorClaim): decoded automatically from theactclaim of a JWT when present. The nestedActfield carries a multi-hop RFC 8693 §4.4 delegation chain (act.act…), so a token minted for a second A2A hop decodes withAct= the most recent actor andAct.Act= the prior one.oidc.ActorClaim.Chain() []oidc.ActorClaimandproviders.UserInfo.ActorChain []oidc.ActorClaim: the full delegation chain flattened from the nestedactclaim, ordered outermost (most recent actor) first;UserInfo.ActorIssuer/ActorSubjectmirror its first element. Resource servers authorizing a multi-hop A2A call walk this slice to match any actor in the chain rather than only the leaf.LocalMintExchangernow copiesemail,email_verified, andgroupsfrom the validated subject token into the minted access token (email_verifiedonly alongside a non-emptyemail), and nests anyactchain already on the subject token beneath the new actor so a multi-hop A2A delegation chain is preserved. A chain exceeding the maximum nesting depth is rejected.server.Actor.Act *server.Actor: nests the prior actor when minting a token for a further delegation hop.server.ExchangerResult.JTI string: lets anExchangersurface the issued token'sjtiso the broker records it in the mint audit event; the broker'stoken_issuedevent now carries ajtidetail when set.providers.TokenSourceTrustedIssuer("trusted-issuer") constant and theUserInfo.IsExternalIssuer()method. A Bearer validated against aWithTrustedIssuersentry is taggedtrusted-issuer(instead ofTokenSourceSSO), so downstream servers can distinguish an external-IdP token (needs impersonation or token-exchange) from a Dex-forwarded SSO token (can be passed through).IsOBO()further reports whether it carries anactclaim.providers.UserInfo.Issuer stringfield, populated on the external-issuer (trusted-issuer) path with the originating issuer URL (e.g. the cluster's SA OIDC issuer). Empty forTokenSourceSSOandTokenSourceOAuth.server.WithTrustedAudiences([]string) Option: functional option equivalent to settingConfig.TrustedAudiences, for symmetry withWithTrustedIssuers.Changed
The broker now trusts its own self-minted
at+jwttokens as token-exchange subjects without a self-referentialWithTrustedIssuersentry. A self-issued subject validator (verifying signature,typ,iss,exp, audience, revocation, and family through the existing self-issued JWT pipeline) is chained ahead of the trusted-issuer validator for theid_token,access_token, andjwtsubject token types when running in JWT access-token mode; any token whoseissis not the broker's own is delegated to the trusted-issuer validator unchanged.server.ExchangeSubjectTokensignature gains two new parameters:actorToken stringandactorTokenType string(inserted aftersubjectTokenType, beforeresource). Callers passing no actor token must supply empty strings. The issued JWT no longer carries a self-referentialactclaim;actis omitted when no actor token is presented and set to the validated actor identity when one is.server.validateTrustedIssuerJWTandserver.AcceptTrustedIssuerTokennow setUserInfo.TokenSourcetoTokenSourceTrustedIssuer, plusUserInfo.Issuer, instead of the previousTokenSourceSSO. Callers that relied onuserInfo.IsSSO()being true for these tokens must switch touserInfo.IsExternalIssuer()(orIsOBO()).server.ExchangerRequestnow carriesActorToken string,ActorTokenType string, andActor *server.SubjectIdentity. The brokered token-exchange endpoint accepts RFC 8693actor_tokenandactor_token_typeform parameters; when present the actor token is validated against the server's trusted issuers (same validator path as the subject token) and the verified actor identity is forwarded to theExchanger. Absent actor params leaveActornil andBrokerExchangeSubjectTokenbehaves identically to before.server.TokenExchangeUnsupportedTypeErrornow exposes aRole() stringmethod returning"subject"or"actor"to identify which token in the exchange request had an unsupported type. Handlers and callers that previously matched this error viaerrors.Asnow receive accurate role information for error messages and logs.providers/tokencache: new package with a generic LRU-bounded token cache (Cache) for short-lived access tokens, usable by any credential provider (OIDC exchange, GitHub App installation tokens, etc.).server.TrustedIssuer.AcceptedTypHeaders: per-issuer list of accepted JWTtypheader values for Bearer validation. Empty keeps the RFC 9068 §4 default (at+jwt). Kubernetes ServiceAccount tokens carry notypheader, so the previously unconditionalat+jwtrequirement made the documented K8s SA trust use case impossible; configure[""](optionally with"JWT") on the cluster issuer entry to accept them. Signature, issuer, audience, and claim checks are unchanged.server.Config.EnableWorkloadTokenExchange bool: enables the workload-authenticated RFC 8693 path. When true and a brokered request carries no OAuth client credentials, the request is authenticated by the subject (and optional actor) token itself, validated againstTrustedIssuers. Anactor_tokenmints a delegated token (any validated trusted-issuer actor is accepted); with noactor_tokenthe subject token is bound to this broker's issuer and a subject-only token is minted. Default false: requests with no client credentials are rejected withinvalid_client.Changed
providers/oidc.TokenExchangeCache,CachedExchangeToken,TokenExchangeCacheStats,NewTokenExchangeCache,NewTokenExchangeCacheWithMaxEntries, andGenerateCacheKeyhave moved to the newproviders/tokencachepackage asCache,Token,Stats,New,NewWithMaxEntries, andGenerateCacheKey. The old names are removed; update import paths.Fixed
Trusted-issuer JWKS fetch ignored a CA bundle installed on
http.DefaultTransportwhenAllowPrivateIPJWKSwas set.NewOIDCValidator's permissive JWKS client built a fresh transport that verified against the system certificate pool alone, so an in-cluster issuer presenting an internal CA (the caseAllowPrivateIPJWKSexists for) failed withx509: certificate signed by unknown authorityeven when the host process had installed that CA onhttp.DefaultTransport. The permissive client is now backed byhttp.DefaultTransport, honoring a process-installed CA bundle. The SSRF-safe client (issuers withoutAllowPrivateIPJWKS) is unchanged and still verifies against the system pool.Token exchange
invalid_clientfor client secrets containing+(or other form-significant characters).providers/oidc.TokenExchangeClient.Exchangeauthenticated to the downstream token endpoint withhttp.Request.SetBasicAuth(clientID, clientSecret), which base64-encodes the raw values. RFC 6749 §2.3.1 requires both components to beapplication/x-www-form-urlencodedfirst; spec-compliant servers (e.g. Dex) url-unescape the Basic credential, so a+in the secret was decoded to a space and rejected withinvalid_client. Both components are now url-encoded before the Basic credential is formed. Secrets containing only/and=were unaffected, which is why some clusters worked and others did not.handleBrokeredTokenExchangeErrorreportedsubject_token_typein the error response and log when the failing token was the actor token.TokenExchangeUnsupportedTypeErrornow carries the token role ("subject" or "actor") and the handler uses it to emit the correct field name.Actor token audience unenforced when the issuer entry had no
AllowedAudiences.validateExchangeActorTokenpassednildefault audiences toValidate, so a ServiceAccount token minted for an unrelated audience was accepted as an actor token when the issuer'sAllowedAudienceswas empty. The broker's own issuer URL is now passed as the default, ensuring actor tokens are always audience-bound to this broker unless the issuer entry overrides it with an explicitAllowedAudiences.Broker success audit event used
act_iss(subject's issuer) alongsideactor_iss(actor's issuer), making the two fields ambiguous for audit queries. The subject issuer field in the broker's token-issued event is nowsubject_iss.ExchangerRequest.ActorTokenTypecould be non-empty whileActorTokenandActorwere both empty when a caller passedactor_token_typewithoutactor_token.BrokerExchangeSubjectTokennow normalisesActorTokenTypeto""when no actor token is present, keeping the three actor fields consistent. The HTTP handler also rejectsactor_tokenpresent withoutactor_token_typewith 400invalid_requestper RFC 8693 §2.1.Subject and actor token validation failures in the brokered flow produced audit events without
client_id,audience, orsession_id. The early-rejection paths (no exchanger, audience not in allowlist) already carried those fields; validation failures did not.validateExchangeTokennow accepts an extra-details map;BrokerExchangeSubjectTokenpasses the brokered flow context so every brokered failure event is fully correlated.Stale issuer JWKS broke every token after an issuer signing-key rotation.
oidc.JWKSClientcached a trusted issuer's JWKS for the full TTL (1h) and never refetched when a token presented akidabsent from the cached set, so a single Dex key rotation madeValidateIDToken(and therefore the token-exchange broker's subject-token validation) reject every current user token until the process was restarted (muster#847). The client now forces a single bounded refetch on an unknownkidand retries verification once. The refetch is rate-limited to at most one network fetch perDefaultJWKSRefetchBackoff(1m) per JWKS URI (negative caching), so a flood of tokens carrying genuinely bogus kids cannot hammer the issuer's JWKS endpoint. A legitimate rotation now self-heals on the first affected token without a restart. New exported sentineloidc.ErrKeyNotFound.Security
Minted actor chains are bounded.
LocalMintExchangerrejects anactchain deeper than 10 hops, capping token size and parser load on every downstream that validates the token.Mint path honours the rate limiter when called in-process.
BrokerExchangeSubjectTokenandWorkloadExchangeSubjectTokennow consult the configuredUserRateLimiter(keyed on the per-session ID) before minting, so a caller invoking these methods directly (e.g. an aggregator, bypassing the HTTP middleware) cannot flood mints from a single compromised session. New exported sentinelserver.ErrExchangeRateLimited; the rejection emits anauth_failureaudit event with reasontoken_exchange_rate_limited. A nilUserRateLimiterleaves the path unrestricted as before.Private-IP-allowed HTTP client pins redirects to the original host. The client returned by
NewPrivateIPAllowedHTTPClient(used for JWKS, discovery, and token exchange whenAllowPrivateIPis set) now refuses a redirect that changes host or port and re-applies the 10-hop redirect cap. Because that client cannot filter private IPs, a discovery or JWKS endpoint can no longer 302 the fetch to an arbitrary internal address or a different port on the same host. Same-host redirects are still followed.Configuration
📅 Schedule: (UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.