Normalize canonical inbound grants - #6473
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #6473 +/- ##
==========================================
+ Coverage 78.19% 78.23% +0.03%
==========================================
Files 769 770 +1
Lines 75078 75232 +154
==========================================
+ Hits 58707 58856 +149
- Misses 16366 16371 +5
Partials 5 5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
a93bd9b to
047b301
Compare
JAORMX
left a comment
There was a problem hiding this comment.
I reviewed this against #6467 and #6200. CI is green, but I found two blocking model/correctness issues:
-
SPIFFEClientsis nested underinbound_grants.token_exchange(pkg/authserver/inbound_grants.go:19-24). That makes SPIFFE client authentication subordinate to RFC 8693: disabling or omitting token exchange removes the association, and every SPIFFE-authenticated client is necessarily token-exchange-enabled. #6200 calls forspiffeClientAuthunderinboundGrantswith grants and token-exchange permission configured per association. Authentication method and grant authorization need to remain independent in the canonical schema. -
SPIFFE validation reports paths under
inbound_grants.spiffe_client_auth[...], but this PR's actual serialized path isinbound_grants.token_exchange.spiffe_clients[...](pkg/authserver/spiffe_trust.go, reached fromRunConfig.Validate). Operators therefore get errors naming a field that does not exist. Please make the validation paths match the final schema.
The normalization direction is useful, but it currently projects the canonical wire types back into the legacy combined TrustedIssuer runtime representation. That leaves parallel policy shapes and several field-by-field copy/conflict-detection sites that must evolve in lockstep. I would prefer one grant-oriented normalized runtime model consumed by validation/provider construction, with legacy and canonical wire inputs translated into it once.
Standards notes: the commit is missing the required Signed-off-by trailer (CONTRIBUTING.md:91), and the PR is well beyond the repository's 400-line guideline. The signoff must be fixed; please also consider whether the compatibility normalization can be split from the SPIFFE wiring to reduce review risk.
8abeeaa to
108bc9c
Compare
047b301 to
7c5e172
Compare
JAORMX
left a comment
There was a problem hiding this comment.
I re-reviewed the rebased head (7c5e172). The rebase fixes the two model issues from my previous review: spiffe_client_auth remains a sibling of the grant families, validation paths now match the schema, and the commit has its DCO trailer.
One new enablement inconsistency blocks approval:
- Canonical
inbound_grantscan disable token exchange, and this PR correctly removes the Fosite handler and discovery advertisement viaTokenExchangeEnabled. However,/oauth/registerstill callsValidateDCRRequestwithout that capability (pkg/authserver/server/handlers/dcr.go:243-257). It can therefore return201 Createdfor a token-exchange-only client even though the server has no token-exchange handler. This is especially direct forprivate_key_jwt, whose token-only path explicitly accepts a token-exchange registration. Please threadTokenExchangeEnabledinto DCR validation (or rejectallow_private_key_jwt_registrationwhen canonical configuration disables token exchange) and add a regression test proving a disabled grant cannot be registered.
The architecture doc also still places SPIFFE clients at inbound_grants.token_exchange.spiffe_clients; the implemented path is the sibling inbound_grants.spiffe_client_auth. Please update those references and make the documented legacy/canonical exclusion scope match the family-wide behavior in NormalizeInboundGrants.
CI currently has two failures. Both look unrelated to this authserver diff: one VirtualMCP Redis/session E2E timeout interrupted the remaining specs, and one MCPOIDCConfig integration test timed out amid resource-version conflicts and a missing test field index. They still need a clean rerun before approval.
|
Pushed a commit that resolves both blocking issues — thanks for catching these.
On the normalization-direction note (projecting canonical types back into the legacy Signed-off-by trailer fixed. CI is green on the amended commit. |
108bc9c to
e497cae
Compare
7c5e172 to
a1a21ea
Compare
JAORMX
left a comment
There was a problem hiding this comment.
I re-reviewed the current rebased head (a1a21ea). The earlier SPIFFE placement and validation-path fixes remain good, but the latest blocker is still present: validateDCRRequest does not receive TokenExchangeEnabled, so /oauth/register can create a token-exchange-only client after canonical configuration has removed the token-exchange handler and discovery advertisement (pkg/authserver/server/handlers/dcr.go:243-257). Please gate DCR validation on the effective capability and add the disabled-grant regression test.
The architecture documentation still names inbound_grants.token_exchange.spiffe_clients at docs/arch/17-token-exchange-delegation.md:127, while the implemented path is inbound_grants.spiffe_client_auth. Its “mutually exclusive per issuer” wording around line 703 also does not match the family-wide conflict implemented by NormalizeInboundGrants; please align one side.
The amended commit has its DCO trailer. CI is still running on this head.
e497cae to
3413cc2
Compare
a1a21ea to
f1fc2d8
Compare
|
Good catch, and you're right on a closer look — an issuer alone doesn't identify an actor, full stop. I checked how our audit code actually consumes this chain ( Fixed in 9382f6e: the external issuer is now recorded as its own top-level Also added a test that pins the depth accounting for the no-actor case, since removing that phantom nesting level changes how deep a prior delegation chain can go before hitting the max-depth cap. |
9382f6e to
64484b9
Compare
|
Fresh review of Medium — JWKS cache lifecycle leak: I found no material regression in the focused |
|
Fresh full re-review of the current head found one release blocker, so I am not approving. High — re-exchanging an externally sourced delegated token launders Please make authenticated external provenance transitive across re-exchanges (or define an equivalent non-removable provenance structure) and add regression coverage for external token → delegated token → re-exchanged token on both authorization paths. CI is green and the prior actor-shape/Cedar-doc findings are resolved. |
64484b9 to
3537835
Compare
|
Pushed a fix for the re-exchange laundering finding. You're right, and I confirmed the exact mechanism: a re-exchanged token routes through the self-issued validator (its own Added coverage for both authorization paths you called out: a CI is green. |
|
Focused recheck of CI is not green: the current |
JAORMX
left a comment
There was a problem hiding this comment.
Re-review complete at 3537835c7ad6db13207552f0a08681629ed82626.
The DCR/token-exchange capability mismatch is fixed and covered for explicit and implicit private_key_jwt token-exchange registrations. The architecture documentation now uses inbound_grants.spiffe_client_auth and accurately describes family-wide legacy/canonical conflicts. No new release-blocking correctness or security findings.
All CI checks relevant to this change are green. The remaining Go Vulnerability Check failure is a repository-level x/crypto advisory on the unchanged dependency, so it should be remediated separately before release.
|
Fresh review found one additional material capability mismatch. When |
|
CI update: |
|
Pushed a fix for the discovery/DCR capability mismatch. Traced it further and it's actually simpler than "gate discovery too": a private_key_jwt registration can never request any grant except token-exchange (validateGrantTypes/validateResponseTypes in pkg/authserver/server/registration/dcr.go force this unconditionally), so there's no legitimate use of allow_private_key_jwt_registration with token exchange disabled at all -- every such registration would fail 100% of the time, not just in some narrower case. Rather than gate three separate discovery call sites (and leave the door open for a fourth to reintroduce the same gap), I added a startup-time rejection of the combination in Config.Validate(), matching the existing pattern for other capability conflicts in that file. CI is green. |
|
Focused recheck of I cannot approve yet: |
The SPIFFE client-auth epic needs a place to configure SPIFFE association policy without inventing a parallel trust/grant path next to the existing delegate-client and trusted-issuer configuration. As more inbound grant families (RFC 8693 token exchange, RFC 7523 JWT-bearer, SPIFFE) accumulate, they need one canonical surface to configure and reason about instead of three independent ones, without breaking deployments that already rely on the legacy fields. Add pkg/authserver/inbound_grants.go with NormalizeInboundGrants, which reconciles a new canonical RunConfig.InboundGrants surface (per-family token_exchange/jwt_bearer sub-configs whose issuer_policies reference a trusted_issuers entry by name) against the legacy top-level delegate_clients and the RFC 8693/7523 fields embedded directly on trusted_issuers. Legacy and canonical configuration for the same grant family are mutually exclusive and rejected at validation time; the two families are otherwise independent, and omitting inbound_grants entirely preserves released behavior. Thread the normalized result through RunConfig.Validate, the embedded-auth-server runner, and buildProvider/discovery, adding a DisableTokenExchange capability so RFC 8693 registration and discovery advertisement can be turned off together and can't drift out of sync. Add TrustedIssuer.Name so canonical issuer_policies can reference an issuer without duplicating its fields. SPIFFE client authentication (InboundGrants.SPIFFEClientAuth, defined in the previous commit) is deliberately kept a sibling of TokenExchange and JWTBearer here, not nested under either: SPIFFE authenticates a client, it does not by itself grant it anything, so making it subordinate to RFC 8693 enablement would mean disabling token exchange silently drops every SPIFFE association, and every SPIFFE-authenticated client would be implicitly token-exchange-capable. It is validated and wired directly from RunConfig.InboundGrants in RunConfig.Validate/embeddedauthserver.go, independent of this file's legacy/canonical projection, so authentication method and grant-family enablement stay separately configurable. Update docs/arch/17-token-exchange-delegation.md for the new inbound_grants shape and the now-conditional token-exchange discovery advertisement, and add a runner-level test proving the canonical delegate-client, SPIFFE-client, and jwt_bearer paths reach a running server (the existing tests only covered normalization in isolation). SPIFFE client-auth associations always require the token-exchange grant (the only grant type they may declare), independent of the legacy/canonical token-exchange projection above: NormalizeInboundGrants now sets Capabilities.TokenExchange true whenever InboundGrants.SPIFFEClientAuth is non-empty, so a SPIFFE-only configuration cannot leave it false and silently disable the RFC 8693 grant handler server-wide -- which would reject every SPIFFE client's own token requests before authentication is even checked. Guarded by a regression test in this package (not just the runner-level test above) since the equivalent fix was previously lost during a rebase when its only coverage lived one package away. DCR (RFC 7591 /oauth/register) now rejects a registration whose effective grant types include token-exchange when it is disabled server-wide, instead of accepting the client and only failing later, confusingly, at /oauth/token. The check runs on the post-defaulting grant types validateGrantTypes already computes (a private_key_jwt client with an empty grant_types is implicitly token-exchange-only), so it catches both the explicit and implicit cases the same way scope validation already gates DCR on ScopesSupported. Corrected two stale doc references caught in review: the SPIFFE client-policy field path (inbound_grants.spiffe_client_auth, not nested under token_exchange) and the JWT-bearer legacy/canonical conflict wording (family-wide across all issuers, not per-issuer). Refs #6200 Signed-off-by: Jakub Hrozek <jakub@stacklok.com>
An act object identifies a party that acted (RFC 8693 SS4.1); an issuer
alone identifies no party. buildActClaim was nesting a bare {"iss":
externalIssuer} under act.act whenever a subject token came from a
trusted external issuer with no resolved actor (the may_act and
ActorMatcher-only paths), misrepresenting the issuer as a prior actor
in the delegation chain that pkg/audit consumes as "the full chain of
acting parties."
Move that provenance fact to its own top-level external_issuer claim,
set whenever ExternalIssuer is non-empty regardless of whether a
genuine actor was also resolved. Nesting under act.act now happens
only when there is a real actor (ExternalActor) to nest. Cedar's
generic claim_ prefixing exposes external_issuer with no extra work,
same as act.
Reduce buildActClaim's added depth to one level (instead of two) on
the no-actor external path, so its prior-chain depth budget matches
the self-issued path exactly.
Signed-off-by: Jakub Hrozek <jakub@stacklok.com>
private_key_jwt registrations can only ever request the RFC 8693 token-exchange grant (see validateGrantTypes/validateResponseTypes in pkg/authserver/server/registration/dcr.go) -- there is no independent use of this auth method. Allowing AllowPrivateKeyJWTRegistration while canonical configuration disables token exchange would admit clients that can never successfully authenticate, discovering the mismatch only when every such registration is rejected at /oauth/register. Reject the contradictory configuration at startup instead. Signed-off-by: Jakub Hrozek <jakub@stacklok.com>
8f31427 to
61c427c
Compare
|
CI update for |
|
CI update for |
JAORMX
left a comment
There was a problem hiding this comment.
Re-review of the updated head 61c427c53f3dec880165a73d11b386714f0e49a1: approval remains valid. The follow-up resolves the remaining private_key_jwt configuration-validation inconsistency; no new correctness or security regressions found.
CI is otherwise green. Helm Charts / Lint and Test Helm Charts failed during the external Install Task setup before project chart tests ran, so please rerun or triage that job before merging. No local tests were run.
Summary
The SPIFFE client-auth epic needs a place to configure SPIFFE association policy without inventing a parallel trust/grant path next to the existing delegate-client and trusted-issuer configuration. As more inbound grant families (RFC 8693 token exchange, RFC 7523 JWT-bearer, SPIFFE) accumulate, they need one canonical surface to configure and reason about instead of three independent ones, without breaking deployments that already rely on the legacy fields.
Stacked on #6467.
pkg/authserver/inbound_grants.gowithNormalizeInboundGrants, which reconciles a new canonicalRunConfig.InboundGrantssurface (per-familytoken_exchange/jwt_bearersub-configs whoseissuer_policiesreference atrusted_issuersentry by name) against the legacy top-leveldelegate_clientsand the RFC 8693/7523 fields embedded directly ontrusted_issuers.inbound_grantsentirely preserves released behavior.RunConfig.Validate, the embedded-auth-server runner, andbuildProvider/discovery — this is also wherespiffe_trust_domains(added in Define SPIFFE trust configuration #6467) finally gets validated and wired intoConfig.SPIFFETrust, closing the gap flagged in that PR's review.DisableTokenExchangecapability so RFC 8693 registration and discovery advertisement turn off together and can't drift out of sync.TrustedIssuer.Nameso canonicalissuer_policiescan reference an issuer without duplicating its fields.docs/arch/17-token-exchange-delegation.mdfor the newinbound_grantsshape and the now-conditional token-exchange discovery advertisement.jwt_bearerpaths reach a running server (the existing tests only covered normalization in isolation).Fixes #
Type of change
Test plan
task test)task test-e2e)task lint-fix)pkg/authserver/inbound_grants_test.gocovers normalization: legacy/canonical mutual exclusion per grant family, issuer-policy resolution by name (unknown/duplicate refs), and capability derivation.pkg/authserver/runner/embeddedauthserver_test.goadds coverage proving the canonical delegate-client, SPIFFE-client, andjwt_bearerpaths actually reach a running embedded auth server, not just the normalization step in isolation.API Compatibility
v1beta1API, OR theapi-break-allowedlabel is applied and the migration guidance is described above.Special notes for reviewers
This closes the "
spiffe_trust_domainsis a silent no-op" gap flagged in review on #6467 —RunConfig.Validate()now callsValidateSPIFFETrustviaNormalizeInboundGrants, andConfig.SPIFFETrustis built and threaded into the embedded auth server inembeddedauthserver.go. Live SVID/bundle verification is still out of scope here; this is config normalization and wiring only.