Skip to content

Commit 7c5e172

Browse files
committed
Normalize canonical inbound grants
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). Refs #6200 Signed-off-by: Jakub Hrozek <jakub@stacklok.com>
1 parent 108bc9c commit 7c5e172

17 files changed

Lines changed: 1358 additions & 97 deletions

docs/arch/17-token-exchange-delegation.md

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,61 @@ binding. The Kubernetes operator also exposes `trusted_issuers` as
7373
`EmbeddedAuthServerConfig.trustedIssuers` — see
7474
[Kubernetes operator](#kubernetes-operator) below.
7575

76+
#### Canonical `inbound_grants` configuration
77+
78+
The top-level `delegate_clients` and the RFC 8693/7523 fields embedded
79+
directly on `trusted_issuers[*]` above are the legacy configuration shape.
80+
`RunConfig.inbound_grants` (`authserver.InboundGrantsRunConfig`) is the
81+
canonical replacement: it groups the same policy under
82+
`inbound_grants.token_exchange` (delegate clients, per-issuer RFC 8693
83+
policy, and SPIFFE clients) and `inbound_grants.jwt_bearer` (per-issuer RFC
84+
7523 policy), each referencing a `trusted_issuers` entry by its `name`
85+
rather than embedding policy fields on the issuer itself:
86+
87+
```yaml
88+
issuer: https://auth.example.com
89+
scopes_supported: [openid, profile]
90+
allowed_audiences: [https://mcp.example.com]
91+
trusted_issuers:
92+
- name: reporting-idp
93+
issuer_url: https://login.example-idp.com
94+
inbound_grants:
95+
token_exchange:
96+
delegate_clients:
97+
- client_id: reporting-delegate
98+
client_secret_env_var: REPORTING_DELEGATE_CLIENT_SECRET
99+
scopes: [openid]
100+
audiences: [https://mcp.example.com]
101+
issuer_policies:
102+
- issuer_ref: reporting-idp
103+
expected_audience: https://mcp.example.com
104+
allowed_actors: [external-reporting-client]
105+
allowed_delegate_clients: [reporting-delegate]
106+
```
107+
108+
`NormalizeInboundGrants` (`pkg/authserver/inbound_grants.go`) reconciles both
109+
shapes at validation time, per grant family:
110+
111+
- If `inbound_grants.token_exchange` is set, any legacy `delegate_clients` or
112+
RFC 8693 fields embedded on `trusted_issuers[*]` are rejected as a
113+
configuration conflict — the two token-exchange sources are mutually
114+
exclusive. Likewise for `inbound_grants.jwt_bearer` against a legacy
115+
`trusted_issuers[*].jwt_bearer_grant`.
116+
- The two grant families are independent: setting only
117+
`inbound_grants.jwt_bearer` still lets legacy `delegate_clients`/RFC 8693
118+
fields enable token exchange, and vice versa — `inbound_grants` does not
119+
take over both families just by being non-nil.
120+
- `issuer_ref` resolves against `trusted_issuers[*].name`; an issuer without
121+
a `name`, an unresolvable `issuer_ref`, or a duplicate `name`/`issuer_url`
122+
fails validation.
123+
124+
Existing deployments using only the legacy shape are unaffected: omitting
125+
`inbound_grants` entirely preserves the released behavior, including RFC
126+
8693 being enabled by default. SPIFFE client policy
127+
(`inbound_grants.token_exchange.spiffe_clients`) has no legacy equivalent
128+
and must reference a `spiffe_trust_domains` entry the same way issuer
129+
policies reference `trusted_issuers`.
130+
76131
### Token-only embedded authorization servers
77132

78133
An embedded authorization server may omit upstream identity providers only when
@@ -209,7 +264,11 @@ reuse IDs between the two mechanisms.
209264

210265
Both `/.well-known/oauth-authorization-server` and
211266
`/.well-known/openid-configuration` advertise the token-exchange grant in
212-
`grant_types_supported`. `token_endpoint_auth_methods_supported` always
267+
`grant_types_supported` by default. Setting `inbound_grants` with
268+
`token_exchange` omitted disables and stops advertising RFC 8693 entirely
269+
(`Config.DisableTokenExchange`, `AuthorizationServerConfig.TokenExchangeEnabled`)
270+
— the same flag governs both registration with fosite and discovery
271+
advertisement, so they cannot drift out of sync. `token_endpoint_auth_methods_supported` always
213272
includes `none`; it also includes `client_secret_basic` and
214273
`client_secret_post` when confidential DCR is enabled or a static delegate
215274
client is configured, and includes `private_key_jwt` when
@@ -636,7 +695,12 @@ involved. It is enabled per trusted issuer by setting
636695
`TrustedIssuer.JWTBearerGrant` (`jwt_bearer_grant` on a hand-written
637696
`authserver.RunConfig`, `jwtBearerGrant` on the operator's
638697
`TrustedIssuerConfig`) — independent of that issuer's RFC 8693 delegation
639-
fields, though both may be configured on the same issuer.
698+
fields, though both may be configured on the same issuer. This is the
699+
legacy shape; a hand-written `RunConfig` may instead configure the same
700+
policy under `inbound_grants.jwt_bearer.issuer_policies`, referencing the
701+
issuer by `name` — see [Canonical `inbound_grants`
702+
configuration](#canonical-inbound_grants-configuration). The two shapes are
703+
mutually exclusive per issuer.
640704

641705
### JWT-bearer configuration
642706

docs/server/docs.go

Lines changed: 122 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)