From 33ad545d0389a24e88fded6b2865577571b370c1 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Wed, 1 Jul 2026 22:14:41 +0100 Subject: [PATCH 1/2] Replace opaque Step A/B labels in XAA CRD comments with action names "Step A" and "Step B" in the XAASpec struct comments and the generated CRD YAML descriptions don't tell the reader what each step does or which endpoint it targets. Rename throughout to "IdP exchange" (RFC 8693 token exchange at the user's IdP, produces the ID-JAG) and "target grant" (RFC 7523 JWT bearer grant at the target AS, produces the backend access token). Regenerate the CRD YAML with task operator-manifests. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../v1beta1/mcpexternalauthconfig_types.go | 24 +++++++------- ...e.stacklok.dev_mcpexternalauthconfigs.yaml | 32 +++++++++---------- ...e.stacklok.dev_mcpexternalauthconfigs.yaml | 32 +++++++++---------- 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go b/cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go index 90078d0d52..6121305d1b 100644 --- a/cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go +++ b/cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go @@ -49,8 +49,8 @@ const ( // ExternalAuthTypeXAA is the type for XAA (Cross-Application Access) auth. // XAA performs a two-step token exchange to obtain access tokens for target services: - // - Step A (RFC 8693): Exchange the user's ID token at their IdP for an ID-JAG JWT - // - Step B (RFC 7523): Exchange the ID-JAG at the target app's AS for an access token + // - IdP exchange (RFC 8693): Exchange the user's ID token at their IdP for an ID-JAG JWT + // - Target grant (RFC 7523): Exchange the ID-JAG at the target app's AS for an access token ExternalAuthTypeXAA ExternalAuthType = "xaa" ) @@ -1334,16 +1334,16 @@ const ( // XAASpec holds configuration for the XAA (Cross-Application Access) auth strategy. // XAA implements draft-ietf-oauth-identity-assertion-authz-grant (ID-JAG) — a // two-step token exchange to obtain access tokens for target services: -// - Step A (RFC 8693): Exchange the user's ID token at their IdP for an ID-JAG JWT -// - Step B (RFC 7523): Exchange the ID-JAG at the target app's AS for an access token +// - IdP exchange (RFC 8693): Exchange the user's ID token at their IdP for an ID-JAG JWT +// - Target grant (RFC 7523): Exchange the ID-JAG at the target app's AS for an access token type XAASpec struct { - // IDPTokenURL is the IdP token endpoint for Step A (RFC 8693 exchange). + // IDPTokenURL is the IdP token endpoint for IdP exchange (RFC 8693). // Must be a valid HTTPS URL. // +kubebuilder:validation:Required // +kubebuilder:validation:Pattern=`^https://.*$` IDPTokenURL string `json:"idpTokenUrl"` - // IDPClientID is the OAuth client ID at the IdP for Step A. + // IDPClientID is the OAuth client ID at the IdP for IdP exchange. // +optional IDPClientID string `json:"idpClientId,omitempty"` @@ -1351,7 +1351,7 @@ type XAASpec struct { // +optional IDPClientSecretRef *SecretKeyRef `json:"idpClientSecretRef,omitempty"` - // TargetTokenURL is the target AS token endpoint for Step B (JWT Bearer grant). + // TargetTokenURL is the target AS token endpoint for target grant (RFC 7523). // +kubebuilder:validation:Required TargetTokenURL string `json:"targetTokenUrl"` @@ -1361,8 +1361,8 @@ type XAASpec struct { // +optional InsecureTargetTokenURL bool `json:"insecureTargetTokenUrl,omitempty"` - // TargetClientID is the OAuth client ID at the target AS for Step B. - // ID-JAG draft §9.1 RECOMMENDS confidential clients for Step B; most + // TargetClientID is the OAuth client ID at the target AS for target grant. + // ID-JAG draft §9.1 RECOMMENDS confidential clients for target grant; most // conformant target authorization servers will reject an unauthenticated // JWT-bearer grant per the §4.4.1 client_id continuity requirement. // +optional @@ -1377,7 +1377,7 @@ type XAASpec struct { TargetAudience string `json:"targetAudience"` // TargetResource is the RFC 8707 resource indicator sent as the `resource` - // parameter in Step A's RFC 8693 token exchange (draft §4.3, OPTIONAL). It + // parameter in the IdP exchange (RFC 8693, draft §4.3, OPTIONAL). It // identifies the target resource server — not the access-token audience, which // is governed by TargetAudience. For MCP backends, set to the MCP server URL. // Some authorization servers (e.g. Okta's early ID-JAG implementation) require @@ -1386,7 +1386,7 @@ type XAASpec struct { // +optional TargetResource string `json:"targetResource,omitempty"` - // Scopes are the requested scopes for the XAA exchange (Steps A and B). + // Scopes are the requested scopes for the XAA exchange (IdP exchange and target grant). // +listType=atomic // +optional Scopes []string `json:"scopes,omitempty"` @@ -1399,7 +1399,7 @@ type XAASpec struct { SubjectProviderName string `json:"subjectProviderName,omitempty"` // SubjectTokenType is the token-type URN of the upstream subject token - // used in Step A. Defaults to "urn:ietf:params:oauth:token-type:id_token" + // used in IdP exchange. Defaults to "urn:ietf:params:oauth:token-type:id_token" // when empty. // +kubebuilder:validation:Enum="urn:ietf:params:oauth:token-type:id_token" // +optional diff --git a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml index 3c306255c0..cf8611ef22 100644 --- a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml +++ b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml @@ -1377,7 +1377,7 @@ spec: properties: idpClientId: description: IDPClientID is the OAuth client ID at the IdP for - Step A. + IdP exchange. type: string idpClientSecretRef: description: IDPClientSecretRef references a Kubernetes Secret @@ -1395,7 +1395,7 @@ spec: type: object idpTokenUrl: description: |- - IDPTokenURL is the IdP token endpoint for Step A (RFC 8693 exchange). + IDPTokenURL is the IdP token endpoint for IdP exchange (RFC 8693). Must be a valid HTTPS URL. pattern: ^https://.*$ type: string @@ -1407,7 +1407,7 @@ spec: type: boolean scopes: description: Scopes are the requested scopes for the XAA exchange - (Steps A and B). + (IdP exchange and target grant). items: type: string type: array @@ -1422,7 +1422,7 @@ spec: subjectTokenType: description: |- SubjectTokenType is the token-type URN of the upstream subject token - used in Step A. Defaults to "urn:ietf:params:oauth:token-type:id_token" + used in IdP exchange. Defaults to "urn:ietf:params:oauth:token-type:id_token" when empty. enum: - urn:ietf:params:oauth:token-type:id_token @@ -1433,8 +1433,8 @@ spec: type: string targetClientId: description: |- - TargetClientID is the OAuth client ID at the target AS for Step B. - ID-JAG draft §9.1 RECOMMENDS confidential clients for Step B; most + TargetClientID is the OAuth client ID at the target AS for target grant. + ID-JAG draft §9.1 RECOMMENDS confidential clients for target grant; most conformant target authorization servers will reject an unauthenticated JWT-bearer grant per the §4.4.1 client_id continuity requirement. type: string @@ -1455,7 +1455,7 @@ spec: targetResource: description: |- TargetResource is the RFC 8707 resource indicator sent as the `resource` - parameter in Step A's RFC 8693 token exchange (draft §4.3, OPTIONAL). It + parameter in the IdP exchange (RFC 8693, draft §4.3, OPTIONAL). It identifies the target resource server — not the access-token audience, which is governed by TargetAudience. For MCP backends, set to the MCP server URL. Some authorization servers (e.g. Okta's early ID-JAG implementation) require @@ -1464,7 +1464,7 @@ spec: type: string targetTokenUrl: description: TargetTokenURL is the target AS token endpoint for - Step B (JWT Bearer grant). + target grant (RFC 7523). type: string required: - idpTokenUrl @@ -2979,7 +2979,7 @@ spec: properties: idpClientId: description: IDPClientID is the OAuth client ID at the IdP for - Step A. + IdP exchange. type: string idpClientSecretRef: description: IDPClientSecretRef references a Kubernetes Secret @@ -2997,7 +2997,7 @@ spec: type: object idpTokenUrl: description: |- - IDPTokenURL is the IdP token endpoint for Step A (RFC 8693 exchange). + IDPTokenURL is the IdP token endpoint for IdP exchange (RFC 8693). Must be a valid HTTPS URL. pattern: ^https://.*$ type: string @@ -3009,7 +3009,7 @@ spec: type: boolean scopes: description: Scopes are the requested scopes for the XAA exchange - (Steps A and B). + (IdP exchange and target grant). items: type: string type: array @@ -3024,7 +3024,7 @@ spec: subjectTokenType: description: |- SubjectTokenType is the token-type URN of the upstream subject token - used in Step A. Defaults to "urn:ietf:params:oauth:token-type:id_token" + used in IdP exchange. Defaults to "urn:ietf:params:oauth:token-type:id_token" when empty. enum: - urn:ietf:params:oauth:token-type:id_token @@ -3035,8 +3035,8 @@ spec: type: string targetClientId: description: |- - TargetClientID is the OAuth client ID at the target AS for Step B. - ID-JAG draft §9.1 RECOMMENDS confidential clients for Step B; most + TargetClientID is the OAuth client ID at the target AS for target grant. + ID-JAG draft §9.1 RECOMMENDS confidential clients for target grant; most conformant target authorization servers will reject an unauthenticated JWT-bearer grant per the §4.4.1 client_id continuity requirement. type: string @@ -3057,7 +3057,7 @@ spec: targetResource: description: |- TargetResource is the RFC 8707 resource indicator sent as the `resource` - parameter in Step A's RFC 8693 token exchange (draft §4.3, OPTIONAL). It + parameter in the IdP exchange (RFC 8693, draft §4.3, OPTIONAL). It identifies the target resource server — not the access-token audience, which is governed by TargetAudience. For MCP backends, set to the MCP server URL. Some authorization servers (e.g. Okta's early ID-JAG implementation) require @@ -3066,7 +3066,7 @@ spec: type: string targetTokenUrl: description: TargetTokenURL is the target AS token endpoint for - Step B (JWT Bearer grant). + target grant (RFC 7523). type: string required: - idpTokenUrl diff --git a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml index 6bff3a2d72..5034268f91 100644 --- a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml +++ b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml @@ -1380,7 +1380,7 @@ spec: properties: idpClientId: description: IDPClientID is the OAuth client ID at the IdP for - Step A. + IdP exchange. type: string idpClientSecretRef: description: IDPClientSecretRef references a Kubernetes Secret @@ -1398,7 +1398,7 @@ spec: type: object idpTokenUrl: description: |- - IDPTokenURL is the IdP token endpoint for Step A (RFC 8693 exchange). + IDPTokenURL is the IdP token endpoint for IdP exchange (RFC 8693). Must be a valid HTTPS URL. pattern: ^https://.*$ type: string @@ -1410,7 +1410,7 @@ spec: type: boolean scopes: description: Scopes are the requested scopes for the XAA exchange - (Steps A and B). + (IdP exchange and target grant). items: type: string type: array @@ -1425,7 +1425,7 @@ spec: subjectTokenType: description: |- SubjectTokenType is the token-type URN of the upstream subject token - used in Step A. Defaults to "urn:ietf:params:oauth:token-type:id_token" + used in IdP exchange. Defaults to "urn:ietf:params:oauth:token-type:id_token" when empty. enum: - urn:ietf:params:oauth:token-type:id_token @@ -1436,8 +1436,8 @@ spec: type: string targetClientId: description: |- - TargetClientID is the OAuth client ID at the target AS for Step B. - ID-JAG draft §9.1 RECOMMENDS confidential clients for Step B; most + TargetClientID is the OAuth client ID at the target AS for target grant. + ID-JAG draft §9.1 RECOMMENDS confidential clients for target grant; most conformant target authorization servers will reject an unauthenticated JWT-bearer grant per the §4.4.1 client_id continuity requirement. type: string @@ -1458,7 +1458,7 @@ spec: targetResource: description: |- TargetResource is the RFC 8707 resource indicator sent as the `resource` - parameter in Step A's RFC 8693 token exchange (draft §4.3, OPTIONAL). It + parameter in the IdP exchange (RFC 8693, draft §4.3, OPTIONAL). It identifies the target resource server — not the access-token audience, which is governed by TargetAudience. For MCP backends, set to the MCP server URL. Some authorization servers (e.g. Okta's early ID-JAG implementation) require @@ -1467,7 +1467,7 @@ spec: type: string targetTokenUrl: description: TargetTokenURL is the target AS token endpoint for - Step B (JWT Bearer grant). + target grant (RFC 7523). type: string required: - idpTokenUrl @@ -2982,7 +2982,7 @@ spec: properties: idpClientId: description: IDPClientID is the OAuth client ID at the IdP for - Step A. + IdP exchange. type: string idpClientSecretRef: description: IDPClientSecretRef references a Kubernetes Secret @@ -3000,7 +3000,7 @@ spec: type: object idpTokenUrl: description: |- - IDPTokenURL is the IdP token endpoint for Step A (RFC 8693 exchange). + IDPTokenURL is the IdP token endpoint for IdP exchange (RFC 8693). Must be a valid HTTPS URL. pattern: ^https://.*$ type: string @@ -3012,7 +3012,7 @@ spec: type: boolean scopes: description: Scopes are the requested scopes for the XAA exchange - (Steps A and B). + (IdP exchange and target grant). items: type: string type: array @@ -3027,7 +3027,7 @@ spec: subjectTokenType: description: |- SubjectTokenType is the token-type URN of the upstream subject token - used in Step A. Defaults to "urn:ietf:params:oauth:token-type:id_token" + used in IdP exchange. Defaults to "urn:ietf:params:oauth:token-type:id_token" when empty. enum: - urn:ietf:params:oauth:token-type:id_token @@ -3038,8 +3038,8 @@ spec: type: string targetClientId: description: |- - TargetClientID is the OAuth client ID at the target AS for Step B. - ID-JAG draft §9.1 RECOMMENDS confidential clients for Step B; most + TargetClientID is the OAuth client ID at the target AS for target grant. + ID-JAG draft §9.1 RECOMMENDS confidential clients for target grant; most conformant target authorization servers will reject an unauthenticated JWT-bearer grant per the §4.4.1 client_id continuity requirement. type: string @@ -3060,7 +3060,7 @@ spec: targetResource: description: |- TargetResource is the RFC 8707 resource indicator sent as the `resource` - parameter in Step A's RFC 8693 token exchange (draft §4.3, OPTIONAL). It + parameter in the IdP exchange (RFC 8693, draft §4.3, OPTIONAL). It identifies the target resource server — not the access-token audience, which is governed by TargetAudience. For MCP backends, set to the MCP server URL. Some authorization servers (e.g. Okta's early ID-JAG implementation) require @@ -3069,7 +3069,7 @@ spec: type: string targetTokenUrl: description: TargetTokenURL is the target AS token endpoint for - Step B (JWT Bearer grant). + target grant (RFC 7523). type: string required: - idpTokenUrl From 091392a67fd290a2c8c8c6588760885fa02a2b91 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Thu, 2 Jul 2026 09:48:22 +0100 Subject: [PATCH 2/2] Rename remaining XAA Step A/B labels The initial rename only touched mcpexternalauthconfig_types.go, leaving pkg/vmcp/auth/types/types.go and pkg/vmcp/auth/strategies/xaa.go with the old "Step A"/"Step B" labels for the identical ID-JAG exchange. Since types.go feeds the VirtualMCPServer CRD schema, kubectl explain showed two different names for the same protocol concept depending on which CRD you asked. Extend the IdP exchange / target grant rename to the runtime auth types, the XAA strategy implementation (including the performStepA/performStepB function names, log strings, and error messages), and two incidental mentions in the generic OAuth protocol packages. Regenerate the VirtualMCPServer and MCPExternalAuthConfig CRD YAML and the CRD API reference docs to match, fixing the CI doc-drift failure. Also drop a stray "the" before "IdP exchange" for consistency with the rest of the file. --- .../v1beta1/mcpexternalauthconfig_types.go | 2 +- ...e.stacklok.dev_mcpexternalauthconfigs.yaml | 4 +- ...olhive.stacklok.dev_virtualmcpservers.yaml | 76 +++++++++---------- ...e.stacklok.dev_mcpexternalauthconfigs.yaml | 4 +- ...olhive.stacklok.dev_virtualmcpservers.yaml | 76 +++++++++---------- docs/operator/crd-api.md | 42 +++++----- pkg/oauthproto/jwtbearer/grant.go | 2 +- pkg/oauthproto/tokenexchange/exchange.go | 2 +- pkg/vmcp/auth/strategies/xaa.go | 68 ++++++++--------- pkg/vmcp/auth/strategies/xaa_test.go | 40 +++++----- pkg/vmcp/auth/types/types.go | 22 +++--- 11 files changed, 169 insertions(+), 169 deletions(-) diff --git a/cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go b/cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go index 6121305d1b..8266fa8ab2 100644 --- a/cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go +++ b/cmd/thv-operator/api/v1beta1/mcpexternalauthconfig_types.go @@ -1377,7 +1377,7 @@ type XAASpec struct { TargetAudience string `json:"targetAudience"` // TargetResource is the RFC 8707 resource indicator sent as the `resource` - // parameter in the IdP exchange (RFC 8693, draft §4.3, OPTIONAL). It + // parameter in IdP exchange (RFC 8693, draft §4.3, OPTIONAL). It // identifies the target resource server — not the access-token audience, which // is governed by TargetAudience. For MCP backends, set to the MCP server URL. // Some authorization servers (e.g. Okta's early ID-JAG implementation) require diff --git a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml index cf8611ef22..d47545b4d6 100644 --- a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml +++ b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml @@ -1455,7 +1455,7 @@ spec: targetResource: description: |- TargetResource is the RFC 8707 resource indicator sent as the `resource` - parameter in the IdP exchange (RFC 8693, draft §4.3, OPTIONAL). It + parameter in IdP exchange (RFC 8693, draft §4.3, OPTIONAL). It identifies the target resource server — not the access-token audience, which is governed by TargetAudience. For MCP backends, set to the MCP server URL. Some authorization servers (e.g. Okta's early ID-JAG implementation) require @@ -3057,7 +3057,7 @@ spec: targetResource: description: |- TargetResource is the RFC 8707 resource indicator sent as the `resource` - parameter in the IdP exchange (RFC 8693, draft §4.3, OPTIONAL). It + parameter in IdP exchange (RFC 8693, draft §4.3, OPTIONAL). It identifies the target resource server — not the access-token audience, which is governed by TargetAudience. For MCP backends, set to the MCP server URL. Some authorization servers (e.g. Okta's early ID-JAG implementation) require diff --git a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpservers.yaml b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpservers.yaml index 82f66b27dd..28ccf8f364 100644 --- a/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpservers.yaml +++ b/deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_virtualmcpservers.yaml @@ -2125,11 +2125,11 @@ spec: properties: idpClientId: description: IDPClientID is the OAuth client ID - at the IdP for Step A. + at the IdP for IdP exchange. type: string idpClientSecret: description: IDPClientSecret is the client secret - at the IdP for Step A. + at the IdP for IdP exchange. type: string idpClientSecretEnv: description: IDPClientSecretEnv is the env var containing @@ -2137,7 +2137,7 @@ spec: type: string idpTokenUrl: description: IDPTokenURL is the IdP token endpoint - for Step A (RFC 8693 exchange). + for IdP exchange (RFC 8693 exchange). type: string insecureTargetTokenUrl: description: |- @@ -2147,7 +2147,7 @@ spec: type: boolean scopes: description: Scopes are the requested scopes for - Steps A and B. + IdP exchange and target grant. items: type: string type: array @@ -2160,7 +2160,7 @@ spec: subjectTokenType: description: |- SubjectTokenType is the token-type URN of the upstream subject token - used in Step A. Defaults to TokenTypeIDToken when empty. Currently only + used in IdP exchange. Defaults to TokenTypeIDToken when empty. Currently only urn:ietf:params:oauth:token-type:id_token is accepted; the field exists to allow future expansion to SAML upstreams without an API break. type: string @@ -2170,11 +2170,11 @@ spec: type: string targetClientId: description: TargetClientID is the OAuth client - ID at the target AS for Step B. + ID at the target AS for target grant. type: string targetClientSecret: description: TargetClientSecret is the client secret - at the target AS for Step B. + at the target AS for target grant. type: string targetClientSecretEnv: description: TargetClientSecretEnv is the env var @@ -2183,13 +2183,13 @@ spec: targetResource: description: |- TargetResource is the RFC 8707 resource indicator sent as the `resource` - parameter in Step A's RFC 8693 token exchange (draft §4.3, OPTIONAL). It + parameter in IdP exchange's RFC 8693 token exchange (draft §4.3, OPTIONAL). It identifies the target resource server — not the access-token audience, which is governed by TargetAudience. For MCP backends, set to the MCP server URL. type: string targetTokenUrl: description: TargetTokenURL is the target AS token - endpoint for Step B (JWT Bearer grant). + endpoint for target grant (JWT Bearer grant). type: string required: - idpTokenUrl @@ -2429,11 +2429,11 @@ spec: properties: idpClientId: description: IDPClientID is the OAuth client ID at - the IdP for Step A. + the IdP for IdP exchange. type: string idpClientSecret: description: IDPClientSecret is the client secret - at the IdP for Step A. + at the IdP for IdP exchange. type: string idpClientSecretEnv: description: IDPClientSecretEnv is the env var containing @@ -2441,7 +2441,7 @@ spec: type: string idpTokenUrl: description: IDPTokenURL is the IdP token endpoint - for Step A (RFC 8693 exchange). + for IdP exchange (RFC 8693 exchange). type: string insecureTargetTokenUrl: description: |- @@ -2450,8 +2450,8 @@ spec: development/testing endpoints — never in production. type: boolean scopes: - description: Scopes are the requested scopes for Steps - A and B. + description: Scopes are the requested scopes for IdP + exchange and target grant. items: type: string type: array @@ -2464,7 +2464,7 @@ spec: subjectTokenType: description: |- SubjectTokenType is the token-type URN of the upstream subject token - used in Step A. Defaults to TokenTypeIDToken when empty. Currently only + used in IdP exchange. Defaults to TokenTypeIDToken when empty. Currently only urn:ietf:params:oauth:token-type:id_token is accepted; the field exists to allow future expansion to SAML upstreams without an API break. type: string @@ -2474,11 +2474,11 @@ spec: type: string targetClientId: description: TargetClientID is the OAuth client ID - at the target AS for Step B. + at the target AS for target grant. type: string targetClientSecret: description: TargetClientSecret is the client secret - at the target AS for Step B. + at the target AS for target grant. type: string targetClientSecretEnv: description: TargetClientSecretEnv is the env var @@ -2487,13 +2487,13 @@ spec: targetResource: description: |- TargetResource is the RFC 8707 resource indicator sent as the `resource` - parameter in Step A's RFC 8693 token exchange (draft §4.3, OPTIONAL). It + parameter in IdP exchange's RFC 8693 token exchange (draft §4.3, OPTIONAL). It identifies the target resource server — not the access-token audience, which is governed by TargetAudience. For MCP backends, set to the MCP server URL. type: string targetTokenUrl: description: TargetTokenURL is the target AS token - endpoint for Step B (JWT Bearer grant). + endpoint for target grant (JWT Bearer grant). type: string required: - idpTokenUrl @@ -5531,11 +5531,11 @@ spec: properties: idpClientId: description: IDPClientID is the OAuth client ID - at the IdP for Step A. + at the IdP for IdP exchange. type: string idpClientSecret: description: IDPClientSecret is the client secret - at the IdP for Step A. + at the IdP for IdP exchange. type: string idpClientSecretEnv: description: IDPClientSecretEnv is the env var containing @@ -5543,7 +5543,7 @@ spec: type: string idpTokenUrl: description: IDPTokenURL is the IdP token endpoint - for Step A (RFC 8693 exchange). + for IdP exchange (RFC 8693 exchange). type: string insecureTargetTokenUrl: description: |- @@ -5553,7 +5553,7 @@ spec: type: boolean scopes: description: Scopes are the requested scopes for - Steps A and B. + IdP exchange and target grant. items: type: string type: array @@ -5566,7 +5566,7 @@ spec: subjectTokenType: description: |- SubjectTokenType is the token-type URN of the upstream subject token - used in Step A. Defaults to TokenTypeIDToken when empty. Currently only + used in IdP exchange. Defaults to TokenTypeIDToken when empty. Currently only urn:ietf:params:oauth:token-type:id_token is accepted; the field exists to allow future expansion to SAML upstreams without an API break. type: string @@ -5576,11 +5576,11 @@ spec: type: string targetClientId: description: TargetClientID is the OAuth client - ID at the target AS for Step B. + ID at the target AS for target grant. type: string targetClientSecret: description: TargetClientSecret is the client secret - at the target AS for Step B. + at the target AS for target grant. type: string targetClientSecretEnv: description: TargetClientSecretEnv is the env var @@ -5589,13 +5589,13 @@ spec: targetResource: description: |- TargetResource is the RFC 8707 resource indicator sent as the `resource` - parameter in Step A's RFC 8693 token exchange (draft §4.3, OPTIONAL). It + parameter in IdP exchange's RFC 8693 token exchange (draft §4.3, OPTIONAL). It identifies the target resource server — not the access-token audience, which is governed by TargetAudience. For MCP backends, set to the MCP server URL. type: string targetTokenUrl: description: TargetTokenURL is the target AS token - endpoint for Step B (JWT Bearer grant). + endpoint for target grant (JWT Bearer grant). type: string required: - idpTokenUrl @@ -5835,11 +5835,11 @@ spec: properties: idpClientId: description: IDPClientID is the OAuth client ID at - the IdP for Step A. + the IdP for IdP exchange. type: string idpClientSecret: description: IDPClientSecret is the client secret - at the IdP for Step A. + at the IdP for IdP exchange. type: string idpClientSecretEnv: description: IDPClientSecretEnv is the env var containing @@ -5847,7 +5847,7 @@ spec: type: string idpTokenUrl: description: IDPTokenURL is the IdP token endpoint - for Step A (RFC 8693 exchange). + for IdP exchange (RFC 8693 exchange). type: string insecureTargetTokenUrl: description: |- @@ -5856,8 +5856,8 @@ spec: development/testing endpoints — never in production. type: boolean scopes: - description: Scopes are the requested scopes for Steps - A and B. + description: Scopes are the requested scopes for IdP + exchange and target grant. items: type: string type: array @@ -5870,7 +5870,7 @@ spec: subjectTokenType: description: |- SubjectTokenType is the token-type URN of the upstream subject token - used in Step A. Defaults to TokenTypeIDToken when empty. Currently only + used in IdP exchange. Defaults to TokenTypeIDToken when empty. Currently only urn:ietf:params:oauth:token-type:id_token is accepted; the field exists to allow future expansion to SAML upstreams without an API break. type: string @@ -5880,11 +5880,11 @@ spec: type: string targetClientId: description: TargetClientID is the OAuth client ID - at the target AS for Step B. + at the target AS for target grant. type: string targetClientSecret: description: TargetClientSecret is the client secret - at the target AS for Step B. + at the target AS for target grant. type: string targetClientSecretEnv: description: TargetClientSecretEnv is the env var @@ -5893,13 +5893,13 @@ spec: targetResource: description: |- TargetResource is the RFC 8707 resource indicator sent as the `resource` - parameter in Step A's RFC 8693 token exchange (draft §4.3, OPTIONAL). It + parameter in IdP exchange's RFC 8693 token exchange (draft §4.3, OPTIONAL). It identifies the target resource server — not the access-token audience, which is governed by TargetAudience. For MCP backends, set to the MCP server URL. type: string targetTokenUrl: description: TargetTokenURL is the target AS token - endpoint for Step B (JWT Bearer grant). + endpoint for target grant (JWT Bearer grant). type: string required: - idpTokenUrl diff --git a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml index 5034268f91..b73e66445b 100644 --- a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml +++ b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml @@ -1458,7 +1458,7 @@ spec: targetResource: description: |- TargetResource is the RFC 8707 resource indicator sent as the `resource` - parameter in the IdP exchange (RFC 8693, draft §4.3, OPTIONAL). It + parameter in IdP exchange (RFC 8693, draft §4.3, OPTIONAL). It identifies the target resource server — not the access-token audience, which is governed by TargetAudience. For MCP backends, set to the MCP server URL. Some authorization servers (e.g. Okta's early ID-JAG implementation) require @@ -3060,7 +3060,7 @@ spec: targetResource: description: |- TargetResource is the RFC 8707 resource indicator sent as the `resource` - parameter in the IdP exchange (RFC 8693, draft §4.3, OPTIONAL). It + parameter in IdP exchange (RFC 8693, draft §4.3, OPTIONAL). It identifies the target resource server — not the access-token audience, which is governed by TargetAudience. For MCP backends, set to the MCP server URL. Some authorization servers (e.g. Okta's early ID-JAG implementation) require diff --git a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml index bf51183620..849803c47e 100644 --- a/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml +++ b/deploy/charts/operator-crds/templates/toolhive.stacklok.dev_virtualmcpservers.yaml @@ -2128,11 +2128,11 @@ spec: properties: idpClientId: description: IDPClientID is the OAuth client ID - at the IdP for Step A. + at the IdP for IdP exchange. type: string idpClientSecret: description: IDPClientSecret is the client secret - at the IdP for Step A. + at the IdP for IdP exchange. type: string idpClientSecretEnv: description: IDPClientSecretEnv is the env var containing @@ -2140,7 +2140,7 @@ spec: type: string idpTokenUrl: description: IDPTokenURL is the IdP token endpoint - for Step A (RFC 8693 exchange). + for IdP exchange (RFC 8693 exchange). type: string insecureTargetTokenUrl: description: |- @@ -2150,7 +2150,7 @@ spec: type: boolean scopes: description: Scopes are the requested scopes for - Steps A and B. + IdP exchange and target grant. items: type: string type: array @@ -2163,7 +2163,7 @@ spec: subjectTokenType: description: |- SubjectTokenType is the token-type URN of the upstream subject token - used in Step A. Defaults to TokenTypeIDToken when empty. Currently only + used in IdP exchange. Defaults to TokenTypeIDToken when empty. Currently only urn:ietf:params:oauth:token-type:id_token is accepted; the field exists to allow future expansion to SAML upstreams without an API break. type: string @@ -2173,11 +2173,11 @@ spec: type: string targetClientId: description: TargetClientID is the OAuth client - ID at the target AS for Step B. + ID at the target AS for target grant. type: string targetClientSecret: description: TargetClientSecret is the client secret - at the target AS for Step B. + at the target AS for target grant. type: string targetClientSecretEnv: description: TargetClientSecretEnv is the env var @@ -2186,13 +2186,13 @@ spec: targetResource: description: |- TargetResource is the RFC 8707 resource indicator sent as the `resource` - parameter in Step A's RFC 8693 token exchange (draft §4.3, OPTIONAL). It + parameter in IdP exchange's RFC 8693 token exchange (draft §4.3, OPTIONAL). It identifies the target resource server — not the access-token audience, which is governed by TargetAudience. For MCP backends, set to the MCP server URL. type: string targetTokenUrl: description: TargetTokenURL is the target AS token - endpoint for Step B (JWT Bearer grant). + endpoint for target grant (JWT Bearer grant). type: string required: - idpTokenUrl @@ -2432,11 +2432,11 @@ spec: properties: idpClientId: description: IDPClientID is the OAuth client ID at - the IdP for Step A. + the IdP for IdP exchange. type: string idpClientSecret: description: IDPClientSecret is the client secret - at the IdP for Step A. + at the IdP for IdP exchange. type: string idpClientSecretEnv: description: IDPClientSecretEnv is the env var containing @@ -2444,7 +2444,7 @@ spec: type: string idpTokenUrl: description: IDPTokenURL is the IdP token endpoint - for Step A (RFC 8693 exchange). + for IdP exchange (RFC 8693 exchange). type: string insecureTargetTokenUrl: description: |- @@ -2453,8 +2453,8 @@ spec: development/testing endpoints — never in production. type: boolean scopes: - description: Scopes are the requested scopes for Steps - A and B. + description: Scopes are the requested scopes for IdP + exchange and target grant. items: type: string type: array @@ -2467,7 +2467,7 @@ spec: subjectTokenType: description: |- SubjectTokenType is the token-type URN of the upstream subject token - used in Step A. Defaults to TokenTypeIDToken when empty. Currently only + used in IdP exchange. Defaults to TokenTypeIDToken when empty. Currently only urn:ietf:params:oauth:token-type:id_token is accepted; the field exists to allow future expansion to SAML upstreams without an API break. type: string @@ -2477,11 +2477,11 @@ spec: type: string targetClientId: description: TargetClientID is the OAuth client ID - at the target AS for Step B. + at the target AS for target grant. type: string targetClientSecret: description: TargetClientSecret is the client secret - at the target AS for Step B. + at the target AS for target grant. type: string targetClientSecretEnv: description: TargetClientSecretEnv is the env var @@ -2490,13 +2490,13 @@ spec: targetResource: description: |- TargetResource is the RFC 8707 resource indicator sent as the `resource` - parameter in Step A's RFC 8693 token exchange (draft §4.3, OPTIONAL). It + parameter in IdP exchange's RFC 8693 token exchange (draft §4.3, OPTIONAL). It identifies the target resource server — not the access-token audience, which is governed by TargetAudience. For MCP backends, set to the MCP server URL. type: string targetTokenUrl: description: TargetTokenURL is the target AS token - endpoint for Step B (JWT Bearer grant). + endpoint for target grant (JWT Bearer grant). type: string required: - idpTokenUrl @@ -5534,11 +5534,11 @@ spec: properties: idpClientId: description: IDPClientID is the OAuth client ID - at the IdP for Step A. + at the IdP for IdP exchange. type: string idpClientSecret: description: IDPClientSecret is the client secret - at the IdP for Step A. + at the IdP for IdP exchange. type: string idpClientSecretEnv: description: IDPClientSecretEnv is the env var containing @@ -5546,7 +5546,7 @@ spec: type: string idpTokenUrl: description: IDPTokenURL is the IdP token endpoint - for Step A (RFC 8693 exchange). + for IdP exchange (RFC 8693 exchange). type: string insecureTargetTokenUrl: description: |- @@ -5556,7 +5556,7 @@ spec: type: boolean scopes: description: Scopes are the requested scopes for - Steps A and B. + IdP exchange and target grant. items: type: string type: array @@ -5569,7 +5569,7 @@ spec: subjectTokenType: description: |- SubjectTokenType is the token-type URN of the upstream subject token - used in Step A. Defaults to TokenTypeIDToken when empty. Currently only + used in IdP exchange. Defaults to TokenTypeIDToken when empty. Currently only urn:ietf:params:oauth:token-type:id_token is accepted; the field exists to allow future expansion to SAML upstreams without an API break. type: string @@ -5579,11 +5579,11 @@ spec: type: string targetClientId: description: TargetClientID is the OAuth client - ID at the target AS for Step B. + ID at the target AS for target grant. type: string targetClientSecret: description: TargetClientSecret is the client secret - at the target AS for Step B. + at the target AS for target grant. type: string targetClientSecretEnv: description: TargetClientSecretEnv is the env var @@ -5592,13 +5592,13 @@ spec: targetResource: description: |- TargetResource is the RFC 8707 resource indicator sent as the `resource` - parameter in Step A's RFC 8693 token exchange (draft §4.3, OPTIONAL). It + parameter in IdP exchange's RFC 8693 token exchange (draft §4.3, OPTIONAL). It identifies the target resource server — not the access-token audience, which is governed by TargetAudience. For MCP backends, set to the MCP server URL. type: string targetTokenUrl: description: TargetTokenURL is the target AS token - endpoint for Step B (JWT Bearer grant). + endpoint for target grant (JWT Bearer grant). type: string required: - idpTokenUrl @@ -5838,11 +5838,11 @@ spec: properties: idpClientId: description: IDPClientID is the OAuth client ID at - the IdP for Step A. + the IdP for IdP exchange. type: string idpClientSecret: description: IDPClientSecret is the client secret - at the IdP for Step A. + at the IdP for IdP exchange. type: string idpClientSecretEnv: description: IDPClientSecretEnv is the env var containing @@ -5850,7 +5850,7 @@ spec: type: string idpTokenUrl: description: IDPTokenURL is the IdP token endpoint - for Step A (RFC 8693 exchange). + for IdP exchange (RFC 8693 exchange). type: string insecureTargetTokenUrl: description: |- @@ -5859,8 +5859,8 @@ spec: development/testing endpoints — never in production. type: boolean scopes: - description: Scopes are the requested scopes for Steps - A and B. + description: Scopes are the requested scopes for IdP + exchange and target grant. items: type: string type: array @@ -5873,7 +5873,7 @@ spec: subjectTokenType: description: |- SubjectTokenType is the token-type URN of the upstream subject token - used in Step A. Defaults to TokenTypeIDToken when empty. Currently only + used in IdP exchange. Defaults to TokenTypeIDToken when empty. Currently only urn:ietf:params:oauth:token-type:id_token is accepted; the field exists to allow future expansion to SAML upstreams without an API break. type: string @@ -5883,11 +5883,11 @@ spec: type: string targetClientId: description: TargetClientID is the OAuth client ID - at the target AS for Step B. + at the target AS for target grant. type: string targetClientSecret: description: TargetClientSecret is the client secret - at the target AS for Step B. + at the target AS for target grant. type: string targetClientSecretEnv: description: TargetClientSecretEnv is the env var @@ -5896,13 +5896,13 @@ spec: targetResource: description: |- TargetResource is the RFC 8707 resource indicator sent as the `resource` - parameter in Step A's RFC 8693 token exchange (draft §4.3, OPTIONAL). It + parameter in IdP exchange's RFC 8693 token exchange (draft §4.3, OPTIONAL). It identifies the target resource server — not the access-token audience, which is governed by TargetAudience. For MCP backends, set to the MCP server URL. type: string targetTokenUrl: description: TargetTokenURL is the target AS token - endpoint for Step B (JWT Bearer grant). + endpoint for target grant (JWT Bearer grant). type: string required: - idpTokenUrl diff --git a/docs/operator/crd-api.md b/docs/operator/crd-api.md index d783eadb4a..ebd11aa58e 100644 --- a/docs/operator/crd-api.md +++ b/docs/operator/crd-api.md @@ -222,8 +222,8 @@ _Appears in:_ XAAConfig configures the XAA (Cross-Application Access) auth strategy. XAA implements draft-ietf-oauth-identity-assertion-authz-grant (ID-JAG) as a two-step flow: - - Step A (RFC 8693): Exchange the user's ID token at their IdP for an ID-JAG JWT - - Step B (RFC 7523): Exchange the ID-JAG at the target app's AS for an access token + - IdP exchange (RFC 8693): Exchange the user's ID token at their IdP for an ID-JAG JWT + - Target grant (RFC 7523): Exchange the ID-JAG at the target app's AS for an access token @@ -232,20 +232,20 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `idpTokenUrl` _string_ | IDPTokenURL is the IdP token endpoint for Step A (RFC 8693 exchange). | | | -| `idpClientId` _string_ | IDPClientID is the OAuth client ID at the IdP for Step A. | | | -| `idpClientSecret` _string_ | IDPClientSecret is the client secret at the IdP for Step A. | | | +| `idpTokenUrl` _string_ | IDPTokenURL is the IdP token endpoint for IdP exchange (RFC 8693 exchange). | | | +| `idpClientId` _string_ | IDPClientID is the OAuth client ID at the IdP for IdP exchange. | | | +| `idpClientSecret` _string_ | IDPClientSecret is the client secret at the IdP for IdP exchange. | | | | `idpClientSecretEnv` _string_ | IDPClientSecretEnv is the env var containing the IdP client secret. | | | -| `targetTokenUrl` _string_ | TargetTokenURL is the target AS token endpoint for Step B (JWT Bearer grant). | | | +| `targetTokenUrl` _string_ | TargetTokenURL is the target AS token endpoint for target grant (JWT Bearer grant). | | | | `insecureTargetTokenUrl` _boolean_ | InsecureTargetTokenURL allows plain HTTP for TargetTokenURL.
WARNING: this is insecure and must only be set for in-cluster or
development/testing endpoints — never in production. | | | -| `targetClientId` _string_ | TargetClientID is the OAuth client ID at the target AS for Step B. | | | -| `targetClientSecret` _string_ | TargetClientSecret is the client secret at the target AS for Step B. | | | +| `targetClientId` _string_ | TargetClientID is the OAuth client ID at the target AS for target grant. | | | +| `targetClientSecret` _string_ | TargetClientSecret is the client secret at the target AS for target grant. | | | | `targetClientSecretEnv` _string_ | TargetClientSecretEnv is the env var containing the target AS client secret. | | | | `targetAudience` _string_ | TargetAudience is the resource AS URL for the ID-JAG audience claim (required). | | | -| `targetResource` _string_ | TargetResource is the RFC 8707 resource indicator sent as the `resource`
parameter in Step A's RFC 8693 token exchange (draft §4.3, OPTIONAL). It
identifies the target resource server — not the access-token audience, which
is governed by TargetAudience. For MCP backends, set to the MCP server URL. | | | -| `scopes` _string array_ | Scopes are the requested scopes for Steps A and B. | | | +| `targetResource` _string_ | TargetResource is the RFC 8707 resource indicator sent as the `resource`
parameter in IdP exchange's RFC 8693 token exchange (draft §4.3, OPTIONAL). It
identifies the target resource server — not the access-token audience, which
is governed by TargetAudience. For MCP backends, set to the MCP server URL. | | | +| `scopes` _string array_ | Scopes are the requested scopes for IdP exchange and target grant. | | | | `subjectProviderName` _string_ | SubjectProviderName selects which upstream provider's ID token to use.
Auto-populated when embedded AS is active. | | | -| `subjectTokenType` _string_ | SubjectTokenType is the token-type URN of the upstream subject token
used in Step A. Defaults to TokenTypeIDToken when empty. Currently only
urn:ietf:params:oauth:token-type:id_token is accepted; the field exists
to allow future expansion to SAML upstreams without an API break. | | | +| `subjectTokenType` _string_ | SubjectTokenType is the token-type URN of the upstream subject token
used in IdP exchange. Defaults to TokenTypeIDToken when empty. Currently only
urn:ietf:params:oauth:token-type:id_token is accepted; the field exists
to allow future expansion to SAML upstreams without an API break. | | | @@ -1555,7 +1555,7 @@ _Appears in:_ | `awsSts` | ExternalAuthTypeAWSSts is the type for AWS STS authentication
| | `upstreamInject` | ExternalAuthTypeUpstreamInject is the type for upstream token injection
This injects an upstream IdP access token as the Authorization: Bearer header
| | `obo` | ExternalAuthTypeOBO is the type for on-behalf-of (OBO) flows.
This type requires a build with an OBO handler registered via
controllerutil.RegisterOBOHandler; an upstream-only build surfaces
status.conditions[Valid] = False with Reason: EnterpriseRequired
when an obo-typed MCPExternalAuthConfig is applied.
| -| `xaa` | ExternalAuthTypeXAA is the type for XAA (Cross-Application Access) auth.
XAA performs a two-step token exchange to obtain access tokens for target services:
- Step A (RFC 8693): Exchange the user's ID token at their IdP for an ID-JAG JWT
- Step B (RFC 7523): Exchange the ID-JAG at the target app's AS for an access token
| +| `xaa` | ExternalAuthTypeXAA is the type for XAA (Cross-Application Access) auth.
XAA performs a two-step token exchange to obtain access tokens for target services:
- IdP exchange (RFC 8693): Exchange the user's ID token at their IdP for an ID-JAG JWT
- Target grant (RFC 7523): Exchange the ID-JAG at the target app's AS for an access token
| #### api.v1beta1.HeaderForwardConfig @@ -4072,8 +4072,8 @@ _Appears in:_ XAASpec holds configuration for the XAA (Cross-Application Access) auth strategy. XAA implements draft-ietf-oauth-identity-assertion-authz-grant (ID-JAG) — a two-step token exchange to obtain access tokens for target services: - - Step A (RFC 8693): Exchange the user's ID token at their IdP for an ID-JAG JWT - - Step B (RFC 7523): Exchange the ID-JAG at the target app's AS for an access token + - IdP exchange (RFC 8693): Exchange the user's ID token at their IdP for an ID-JAG JWT + - Target grant (RFC 7523): Exchange the ID-JAG at the target app's AS for an access token @@ -4082,17 +4082,17 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `idpTokenUrl` _string_ | IDPTokenURL is the IdP token endpoint for Step A (RFC 8693 exchange).
Must be a valid HTTPS URL. | | Pattern: `^https://.*$`
Required: \{\}
| -| `idpClientId` _string_ | IDPClientID is the OAuth client ID at the IdP for Step A. | | Optional: \{\}
| +| `idpTokenUrl` _string_ | IDPTokenURL is the IdP token endpoint for IdP exchange (RFC 8693).
Must be a valid HTTPS URL. | | Pattern: `^https://.*$`
Required: \{\}
| +| `idpClientId` _string_ | IDPClientID is the OAuth client ID at the IdP for IdP exchange. | | Optional: \{\}
| | `idpClientSecretRef` _[api.v1beta1.SecretKeyRef](#apiv1beta1secretkeyref)_ | IDPClientSecretRef references a Kubernetes Secret containing the IdP client secret. | | Optional: \{\}
| -| `targetTokenUrl` _string_ | TargetTokenURL is the target AS token endpoint for Step B (JWT Bearer grant). | | Required: \{\}
| +| `targetTokenUrl` _string_ | TargetTokenURL is the target AS token endpoint for target grant (RFC 7523). | | Required: \{\}
| | `insecureTargetTokenUrl` _boolean_ | InsecureTargetTokenURL allows plain HTTP for TargetTokenURL.
WARNING: this is insecure and must only be set for in-cluster or
development/testing endpoints — never in production. | | Optional: \{\}
| -| `targetClientId` _string_ | TargetClientID is the OAuth client ID at the target AS for Step B.
ID-JAG draft §9.1 RECOMMENDS confidential clients for Step B; most
conformant target authorization servers will reject an unauthenticated
JWT-bearer grant per the §4.4.1 client_id continuity requirement. | | Optional: \{\}
| +| `targetClientId` _string_ | TargetClientID is the OAuth client ID at the target AS for target grant.
ID-JAG draft §9.1 RECOMMENDS confidential clients for target grant; most
conformant target authorization servers will reject an unauthenticated
JWT-bearer grant per the §4.4.1 client_id continuity requirement. | | Optional: \{\}
| | `targetClientSecretRef` _[api.v1beta1.SecretKeyRef](#apiv1beta1secretkeyref)_ | TargetClientSecretRef references a Kubernetes Secret for the target AS client secret. | | Optional: \{\}
| | `targetAudience` _string_ | TargetAudience is the resource AS URL for the ID-JAG audience claim. | | Required: \{\}
| -| `targetResource` _string_ | TargetResource is the RFC 8707 resource indicator sent as the `resource`
parameter in Step A's RFC 8693 token exchange (draft §4.3, OPTIONAL). It
identifies the target resource server — not the access-token audience, which
is governed by TargetAudience. For MCP backends, set to the MCP server URL.
Some authorization servers (e.g. Okta's early ID-JAG implementation) require
this parameter in practice despite the draft marking it optional — set it
when your IdP needs it. | | Optional: \{\}
| -| `scopes` _string array_ | Scopes are the requested scopes for the XAA exchange (Steps A and B). | | Optional: \{\}
| +| `targetResource` _string_ | TargetResource is the RFC 8707 resource indicator sent as the `resource`
parameter in IdP exchange (RFC 8693, draft §4.3, OPTIONAL). It
identifies the target resource server — not the access-token audience, which
is governed by TargetAudience. For MCP backends, set to the MCP server URL.
Some authorization servers (e.g. Okta's early ID-JAG implementation) require
this parameter in practice despite the draft marking it optional — set it
when your IdP needs it. | | Optional: \{\}
| +| `scopes` _string array_ | Scopes are the requested scopes for the XAA exchange (IdP exchange and target grant). | | Optional: \{\}
| | `subjectProviderName` _string_ | SubjectProviderName selects which upstream provider's ID token to use.
When left empty and an embedded authorization server is configured,
the controller automatically populates this field with the first configured
upstream provider name. | | Optional: \{\}
| -| `subjectTokenType` _string_ | SubjectTokenType is the token-type URN of the upstream subject token
used in Step A. Defaults to "urn:ietf:params:oauth:token-type:id_token"
when empty. | | Enum: [urn:ietf:params:oauth:token-type:id_token]
Optional: \{\}
| +| `subjectTokenType` _string_ | SubjectTokenType is the token-type URN of the upstream subject token
used in IdP exchange. Defaults to "urn:ietf:params:oauth:token-type:id_token"
when empty. | | Enum: [urn:ietf:params:oauth:token-type:id_token]
Optional: \{\}
| diff --git a/pkg/oauthproto/jwtbearer/grant.go b/pkg/oauthproto/jwtbearer/grant.go index 3b1f605f04..bf9f6b7c62 100644 --- a/pkg/oauthproto/jwtbearer/grant.go +++ b/pkg/oauthproto/jwtbearer/grant.go @@ -37,7 +37,7 @@ type Config struct { // Scopes are the requested scopes for the access token. Scopes []string - // AssertionProvider returns the JWT assertion (e.g., the ID-JAG from Step A). + // AssertionProvider returns the JWT assertion (e.g., the ID-JAG from XAA's IdP exchange). // Called on each Token() invocation; must not be nil. The returned JWT must // satisfy RFC 7523 Section 3 (iss/sub/aud/exp). For an ID-JAG the aud is the // Resource AS issuer identifier (per draft-ietf-oauth-identity-assertion-authz-grant), diff --git a/pkg/oauthproto/tokenexchange/exchange.go b/pkg/oauthproto/tokenexchange/exchange.go index 0308efeec4..55d90f6a81 100644 --- a/pkg/oauthproto/tokenexchange/exchange.go +++ b/pkg/oauthproto/tokenexchange/exchange.go @@ -241,7 +241,7 @@ func (ts *tokenSource) Token() (*oauth2.Token, error) { } // Surface the RFC 8693 issued_token_type so callers can verify the server - // returned the type they asked for (e.g., XAA Step A requires an ID-JAG). + // returned the type they asked for (e.g., XAA's IdP exchange requires an ID-JAG). token := resp.Token.WithExtra(map[string]any{ "issued_token_type": resp.IssuedTokenType, }) diff --git a/pkg/vmcp/auth/strategies/xaa.go b/pkg/vmcp/auth/strategies/xaa.go index 016b8fae15..093afbf34d 100644 --- a/pkg/vmcp/auth/strategies/xaa.go +++ b/pkg/vmcp/auth/strategies/xaa.go @@ -29,16 +29,16 @@ import ( // XAAStrategy implements XAA (Cross-Application Access) as a two-step token // exchange, following draft-ietf-oauth-identity-assertion-authz-grant (ID-JAG): // -// - Step A (RFC 8693): Exchange the user's ID token at the IdP for an ID-JAG JWT. -// - Step B (RFC 7523): Exchange the ID-JAG at the target AS for an access token. +// - IdP exchange (RFC 8693): Exchange the user's ID token at the IdP for an ID-JAG JWT. +// - Target grant (RFC 7523): Exchange the ID-JAG at the target AS for an access token. // // Both steps run on every Authenticate call. The upper vMCP TokenCache layer is // responsible for reusing the resulting access token across requests; this // strategy holds no local cache. // -// The subject ID token is not validated locally before Step A. The IdP enforces -// its own exp check; if the token is expired, Step A returns an OAuth -// invalid_grant error which propagates to the caller. Callers that need +// The subject ID token is not validated locally before IdP exchange. The IdP +// enforces its own exp check; if the token is expired, IdP exchange returns an +// OAuth invalid_grant error which propagates to the caller. Callers that need // fine-grained re-auth signals can inspect it with: // // var re *oauth2.RetrieveError @@ -67,8 +67,8 @@ func (*XAAStrategy) Name() string { // 2. For health check requests: uses a client credentials grant at TargetTokenURL // if target client credentials are configured; otherwise skips authentication // 3. For regular requests: retrieves the user's ID token from the identity's -// UpstreamIDTokens map, performs Step A (token exchange for ID-JAG) and -// Step B (JWT Bearer grant for access token), then injects the access token +// UpstreamIDTokens map, performs IdP exchange (token exchange for ID-JAG) and +// target grant (JWT Bearer grant for access token), then injects the access token // // Parameters: // - ctx: Request context containing the authenticated identity (or health check marker) @@ -79,7 +79,7 @@ func (*XAAStrategy) Name() string { // - Strategy configuration is invalid or incomplete // - No identity is found in the context (regular requests only) // - The upstream ID token for the configured provider is not found -// - Step A (token exchange) or Step B (JWT Bearer grant) fails +// - IdP exchange (token exchange) or target grant (JWT Bearer grant) fails func (s *XAAStrategy) Authenticate( ctx context.Context, req *http.Request, strategy *authtypes.BackendAuthStrategy, ) error { @@ -122,23 +122,23 @@ func (s *XAAStrategy) Authenticate( slog.Debug("xaa: found ID token for provider", "provider", config.subjectProviderName) - // Step A: Exchange the user's ID token for an ID-JAG at the IdP. - assertion, err := s.performStepA(ctx, idToken, config) + // IdP exchange: Exchange the user's ID token for an ID-JAG at the IdP. + assertion, err := s.performIDPExchange(ctx, idToken, config) if err != nil { - slog.Debug("xaa: Step A failed", "error", err) - return fmt.Errorf("step A (ID-JAG exchange) failed: %w", err) + slog.Debug("xaa: IdP exchange failed", "error", err) + return fmt.Errorf("IdP exchange failed: %w", err) } - slog.Debug("xaa: Step A succeeded, got ID-JAG") + slog.Debug("xaa: IdP exchange succeeded, got ID-JAG") - // Step B: Exchange the ID-JAG for an access token at the target AS. - accessToken, err := s.performStepB(ctx, assertion, config) + // Target grant: Exchange the ID-JAG for an access token at the target AS. + accessToken, err := s.performTargetGrant(ctx, assertion, config) if err != nil { - slog.Debug("xaa: Step B failed", "error", err) - return fmt.Errorf("step B (JWT Bearer grant) failed: %w", err) + slog.Debug("xaa: target grant failed", "error", err) + return fmt.Errorf("target grant failed: %w", err) } - slog.Debug("xaa: Step B succeeded, setting Bearer token") + slog.Debug("xaa: target grant succeeded, setting Bearer token") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken)) return nil @@ -173,7 +173,7 @@ func (s *XAAStrategy) Validate(strategy *authtypes.BackendAuthStrategy) error { ) } if config.targetClientID == "" { - slog.Warn("xaa: Step B will run without client authentication; " + + slog.Warn("xaa: target grant will run without client authentication; " + "most authorization servers reject unauthenticated JWT-bearer grants " + "(ID-JAG draft §9.1 recommends confidential clients)") } @@ -223,10 +223,10 @@ func (*XAAStrategy) authenticateWithClientCredentials( return nil } -// performStepA runs the RFC 8693 token exchange at the IdP to obtain an ID-JAG. -// It verifies the response's issued_token_type matches the ID-JAG URN per -// draft-ietf-oauth-identity-assertion-authz-grant §4.3.3. -func (*XAAStrategy) performStepA( +// performIDPExchange runs the RFC 8693 token exchange at the IdP to obtain an +// ID-JAG. It verifies the response's issued_token_type matches the ID-JAG URN +// per draft-ietf-oauth-identity-assertion-authz-grant §4.3.3. +func (*XAAStrategy) performIDPExchange( ctx context.Context, idToken string, config *xaaParsedConfig, ) (string, error) { exchangeCfg := &tokenexchange.ExchangeConfig{ @@ -245,34 +245,34 @@ func (*XAAStrategy) performStepA( token, err := exchangeCfg.TokenSource(ctx).Token() if err != nil { - return "", fmt.Errorf("step A token exchange: %w", err) + return "", fmt.Errorf("IdP exchange: %w", err) } // Per draft-ietf-oauth-identity-assertion-authz-grant §4.3.3, the IdP MUST // return issued_token_type = urn:ietf:params:oauth:token-type:id-jag. Fail - // loudly if it returned anything else so Step B cannot accidentally be fed - // a non-assertion token. + // loudly if it returned anything else so target grant cannot accidentally + // be fed a non-assertion token. got, _ := token.Extra("issued_token_type").(string) if got != oauthproto.TokenTypeIDJAG { - return "", fmt.Errorf("step A: IdP returned issued_token_type=%q, want %q", got, oauthproto.TokenTypeIDJAG) + return "", fmt.Errorf("IdP exchange: IdP returned issued_token_type=%q, want %q", got, oauthproto.TokenTypeIDJAG) } // Per draft-ietf-oauth-identity-assertion-authz-grant §5.2, the RFC 8693 // response MUST carry token_type=N_A for an assertion that is not usable // as a bearer token at the IdP. if !strings.EqualFold(token.TokenType, "N_A") { - return "", fmt.Errorf("step A: IdP returned token_type=%q, want N_A (draft §5.2)", token.TokenType) + return "", fmt.Errorf("IdP exchange: IdP returned token_type=%q, want N_A (draft §5.2)", token.TokenType) } // The AccessToken field holds the ID-JAG per RFC 8693 response format. assertion := token.AccessToken if assertion == "" { - return "", fmt.Errorf("step A: IdP returned an empty ID-JAG assertion") + return "", fmt.Errorf("IdP exchange: IdP returned an empty ID-JAG assertion") } // Defence-in-depth: inspect the JWT typ header. The draft §3.1 MUST for // typ: oauth-id-jag+jwt is on the issuer (IdP); ToolHive is the - // holder/presenter — the target AS validates the ID-JAG in Step B. A + // holder/presenter — the target AS validates the ID-JAG in target grant. A // mismatch is deliberately logged rather than fatal; the fatal contract // enforced here is issued_token_type (RFC 8693 §2.2.1, checked above). const idJAGJWTType = "oauth-id-jag+jwt" @@ -291,9 +291,9 @@ func (*XAAStrategy) performStepA( return assertion, nil } -// performStepB runs the RFC 7523 JWT Bearer grant at the target AS using the -// ID-JAG assertion returned by Step A. -func (*XAAStrategy) performStepB( +// performTargetGrant runs the RFC 7523 JWT Bearer grant at the target AS using +// the ID-JAG assertion returned by IdP exchange. +func (*XAAStrategy) performTargetGrant( ctx context.Context, assertion string, config *xaaParsedConfig, ) (string, error) { bearerCfg := &jwtbearer.Config{ @@ -309,7 +309,7 @@ func (*XAAStrategy) performStepB( token, err := bearerCfg.TokenSource(ctx).Token() if err != nil { - return "", fmt.Errorf("step B jwt-bearer grant: %w", err) + return "", fmt.Errorf("target grant: %w", err) } return token.AccessToken, nil diff --git a/pkg/vmcp/auth/strategies/xaa_test.go b/pkg/vmcp/auth/strategies/xaa_test.go index fb25ac7fb7..9a4793cb8f 100644 --- a/pkg/vmcp/auth/strategies/xaa_test.go +++ b/pkg/vmcp/auth/strategies/xaa_test.go @@ -60,8 +60,8 @@ func createXAAStrategy(opts ...func(*authtypes.XAAConfig)) *authtypes.BackendAut } } -// createXAAIdPServer creates a mock IdP server that validates Step A (RFC 8693) -// token exchange requests and returns an ID-JAG response. +// createXAAIdPServer creates a mock IdP server that validates IdP exchange +// (RFC 8693) token exchange requests and returns an ID-JAG response. func createXAAIdPServer(t *testing.T, expectedIDToken, idJAGToReturn string) *httptest.Server { t.Helper() return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -91,8 +91,8 @@ func createXAAIdPServer(t *testing.T, expectedIDToken, idJAGToReturn string) *ht })) } -// createXAATargetServer creates a mock target AS server that validates Step B -// (RFC 7523 JWT Bearer grant) requests and returns an access token. +// createXAATargetServer creates a mock target AS server that validates target +// grant (RFC 7523 JWT Bearer grant) requests and returns an access token. func createXAATargetServer(t *testing.T, expectedAssertion, accessTokenToReturn string) *httptest.Server { t.Helper() return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -592,7 +592,7 @@ func TestXAAStrategy_Authenticate(t *testing.T) { }, }, { - name: "Step A fails with IdP error", + name: "IdP exchange fails with IdP error", setupCtx: func() context.Context { return createContextWithUpstreamIDTokens( map[string]string{testProviderGitHub: "bad-id-token"}) @@ -608,7 +608,7 @@ func TestXAAStrategy_Authenticate(t *testing.T) { assert.NoError(t, err) })) targetServer := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { - t.Error("target AS should not be called when Step A fails") + t.Error("target AS should not be called when IdP exchange fails") })) return idpServer, targetServer }, @@ -622,10 +622,10 @@ func TestXAAStrategy_Authenticate(t *testing.T) { }) }, expectError: true, - errorContains: []string{"step A (ID-JAG exchange) failed"}, + errorContains: []string{"IdP exchange failed"}, }, { - name: "Step B fails with target AS error", + name: "target grant fails with target AS error", setupCtx: func() context.Context { return createContextWithUpstreamIDTokens( map[string]string{testProviderGitHub: "valid-id-token"}) @@ -653,10 +653,10 @@ func TestXAAStrategy_Authenticate(t *testing.T) { }) }, expectError: true, - errorContains: []string{"step B (JWT Bearer grant) failed"}, + errorContains: []string{"target grant failed"}, }, { - name: "Step A rejects wrong issued_token_type", + name: "IdP exchange rejects wrong issued_token_type", setupCtx: func() context.Context { return createContextWithUpstreamIDTokens( map[string]string{testProviderGitHub: "user-id-token-jwt"}) @@ -674,7 +674,7 @@ func TestXAAStrategy_Authenticate(t *testing.T) { assert.NoError(t, err) })) targetServer := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { - t.Error("target AS must not be called when Step A returns the wrong issued_token_type") + t.Error("target AS must not be called when IdP exchange returns the wrong issued_token_type") })) return idpServer, targetServer }, @@ -688,10 +688,10 @@ func TestXAAStrategy_Authenticate(t *testing.T) { }) }, expectError: true, - errorContains: []string{"step A", "issued_token_type", oauthproto.TokenTypeIDJAG}, + errorContains: []string{"IdP exchange", "issued_token_type", oauthproto.TokenTypeIDJAG}, }, { - name: "Step A rejects wrong token_type", + name: "IdP exchange rejects wrong token_type", setupCtx: func() context.Context { return createContextWithUpstreamIDTokens( map[string]string{testProviderGitHub: "user-id-token-jwt"}) @@ -709,7 +709,7 @@ func TestXAAStrategy_Authenticate(t *testing.T) { assert.NoError(t, err) })) targetServer := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { - t.Error("target AS must not be called when Step A returns the wrong token_type") + t.Error("target AS must not be called when IdP exchange returns the wrong token_type") })) return idpServer, targetServer }, @@ -726,7 +726,7 @@ func TestXAAStrategy_Authenticate(t *testing.T) { errorContains: []string{"N_A"}, }, { - name: "Step A rejects empty access_token", + name: "IdP exchange rejects empty access_token", setupCtx: func() context.Context { return createContextWithUpstreamIDTokens( map[string]string{testProviderGitHub: "user-id-token-jwt"}) @@ -744,7 +744,7 @@ func TestXAAStrategy_Authenticate(t *testing.T) { assert.NoError(t, err) })) targetServer := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { - t.Error("target AS must not be called when Step A returns an empty assertion") + t.Error("target AS must not be called when IdP exchange returns an empty assertion") })) return idpServer, targetServer }, @@ -758,10 +758,10 @@ func TestXAAStrategy_Authenticate(t *testing.T) { }) }, expectError: true, - errorContains: []string{"step A", "access_token"}, + errorContains: []string{"IdP exchange", "access_token"}, }, { - name: "Step A typ header mismatch is not fatal", + name: "IdP exchange typ header mismatch is not fatal", setupCtx: func() context.Context { return createContextWithUpstreamIDTokens( map[string]string{testProviderGitHub: "user-id-token-jwt"}) @@ -798,7 +798,7 @@ func TestXAAStrategy_Authenticate(t *testing.T) { }, }, { - name: "Step A typ header matches oauth-id-jag+jwt", + name: "IdP exchange typ header matches oauth-id-jag+jwt", setupCtx: func() context.Context { return createContextWithUpstreamIDTokens( map[string]string{testProviderGitHub: "user-id-token-jwt"}) @@ -878,7 +878,7 @@ func TestXAAStrategy_Authenticate(t *testing.T) { // buildJWTWithTypHeader signs a minimal JWT with the given typ header value using // a throwaway RSA key. The token is not intended to be verified — it exists only -// to exercise the header-parsing path in performStepA. +// to exercise the header-parsing path in performIDPExchange. func buildJWTWithTypHeader(t *testing.T, typ string) string { t.Helper() diff --git a/pkg/vmcp/auth/types/types.go b/pkg/vmcp/auth/types/types.go index bbf5982f23..6c0d028b41 100644 --- a/pkg/vmcp/auth/types/types.go +++ b/pkg/vmcp/auth/types/types.go @@ -267,26 +267,26 @@ type AwsStsConfig struct { // XAAConfig configures the XAA (Cross-Application Access) auth strategy. // XAA implements draft-ietf-oauth-identity-assertion-authz-grant (ID-JAG) as a // two-step flow: -// - Step A (RFC 8693): Exchange the user's ID token at their IdP for an ID-JAG JWT -// - Step B (RFC 7523): Exchange the ID-JAG at the target app's AS for an access token +// - IdP exchange (RFC 8693): Exchange the user's ID token at their IdP for an ID-JAG JWT +// - Target grant (RFC 7523): Exchange the ID-JAG at the target app's AS for an access token // // +kubebuilder:object:generate=true // +gendoc type XAAConfig struct { - // IDPTokenURL is the IdP token endpoint for Step A (RFC 8693 exchange). + // IDPTokenURL is the IdP token endpoint for IdP exchange (RFC 8693 exchange). IDPTokenURL string `json:"idpTokenUrl" yaml:"idpTokenUrl"` - // IDPClientID is the OAuth client ID at the IdP for Step A. + // IDPClientID is the OAuth client ID at the IdP for IdP exchange. IDPClientID string `json:"idpClientId,omitempty" yaml:"idpClientId,omitempty"` - // IDPClientSecret is the client secret at the IdP for Step A. + // IDPClientSecret is the client secret at the IdP for IdP exchange. //nolint:gosec // G101: field legitimately holds sensitive data IDPClientSecret string `json:"idpClientSecret,omitempty" yaml:"idpClientSecret,omitempty"` // IDPClientSecretEnv is the env var containing the IdP client secret. IDPClientSecretEnv string `json:"idpClientSecretEnv,omitempty" yaml:"idpClientSecretEnv,omitempty"` - // TargetTokenURL is the target AS token endpoint for Step B (JWT Bearer grant). + // TargetTokenURL is the target AS token endpoint for target grant (JWT Bearer grant). TargetTokenURL string `json:"targetTokenUrl" yaml:"targetTokenUrl"` // InsecureTargetTokenURL allows plain HTTP for TargetTokenURL. @@ -294,10 +294,10 @@ type XAAConfig struct { // development/testing endpoints — never in production. InsecureTargetTokenURL bool `json:"insecureTargetTokenUrl,omitempty" yaml:"insecureTargetTokenUrl,omitempty"` - // TargetClientID is the OAuth client ID at the target AS for Step B. + // TargetClientID is the OAuth client ID at the target AS for target grant. TargetClientID string `json:"targetClientId,omitempty" yaml:"targetClientId,omitempty"` - // TargetClientSecret is the client secret at the target AS for Step B. + // TargetClientSecret is the client secret at the target AS for target grant. //nolint:gosec // G101: field legitimately holds sensitive data TargetClientSecret string `json:"targetClientSecret,omitempty" yaml:"targetClientSecret,omitempty"` @@ -308,12 +308,12 @@ type XAAConfig struct { TargetAudience string `json:"targetAudience" yaml:"targetAudience"` // TargetResource is the RFC 8707 resource indicator sent as the `resource` - // parameter in Step A's RFC 8693 token exchange (draft §4.3, OPTIONAL). It + // parameter in IdP exchange's RFC 8693 token exchange (draft §4.3, OPTIONAL). It // identifies the target resource server — not the access-token audience, which // is governed by TargetAudience. For MCP backends, set to the MCP server URL. TargetResource string `json:"targetResource,omitempty" yaml:"targetResource,omitempty"` - // Scopes are the requested scopes for Steps A and B. + // Scopes are the requested scopes for IdP exchange and target grant. // +listType=atomic Scopes []string `json:"scopes,omitempty" yaml:"scopes,omitempty"` @@ -322,7 +322,7 @@ type XAAConfig struct { SubjectProviderName string `json:"subjectProviderName,omitempty" yaml:"subjectProviderName,omitempty"` // SubjectTokenType is the token-type URN of the upstream subject token - // used in Step A. Defaults to TokenTypeIDToken when empty. Currently only + // used in IdP exchange. Defaults to TokenTypeIDToken when empty. Currently only // urn:ietf:params:oauth:token-type:id_token is accepted; the field exists // to allow future expansion to SAML upstreams without an API break. SubjectTokenType string `json:"subjectTokenType,omitempty" yaml:"subjectTokenType,omitempty"`