From 3e10fb33ffde02e63883a68e34cffea32e5fac76 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Wed, 29 Apr 2026 14:27:16 +0100 Subject: [PATCH 1/3] Translate identityFromToken from CRD to runtime config Add the operator-side and runtime-side translation that carries IdentityFromToken from MCPExternalAuthConfig (v1beta1 CRD) through the authserver run-config and into upstream.OAuth2Config so the provider added in the previous commit actually receives the field. Two translation points mirror the existing TokenResponseMapping plumbing: * cmd/thv-operator/pkg/controllerutil/authserver.go CRD v1beta1.IdentityFromTokenConfig -> authserver.IdentityFromTokenRunConfig (operator path) * pkg/authserver/runner/embeddedauthserver.go authserver.IdentityFromTokenRunConfig -> upstream.IdentityFromTokenConfig (runtime path) A new IdentityFromTokenRunConfig type lives next to TokenResponseMappingRunConfig in pkg/authserver/config.go. Tests cover the round-trip on both sides and the nil-config case (no spurious allocation). --- .../pkg/controllerutil/authserver.go | 8 ++ .../pkg/controllerutil/authserver_test.go | 115 ++++++++++++++++++ pkg/authserver/config.go | 28 +++++ pkg/authserver/config_test.go | 26 ++++ pkg/authserver/runner/embeddedauthserver.go | 8 ++ .../runner/embeddedauthserver_test.go | 67 ++++++++++ 6 files changed, 252 insertions(+) diff --git a/cmd/thv-operator/pkg/controllerutil/authserver.go b/cmd/thv-operator/pkg/controllerutil/authserver.go index e735e691e3..346c4d87ad 100644 --- a/cmd/thv-operator/pkg/controllerutil/authserver.go +++ b/cmd/thv-operator/pkg/controllerutil/authserver.go @@ -834,6 +834,14 @@ func buildOAuth2UpstreamRunConfig( ExpiresInPath: m.ExpiresInPath, } } + if cfg.IdentityFromToken != nil { + ift := cfg.IdentityFromToken + runConfig.IdentityFromToken = &authserver.IdentityFromTokenRunConfig{ + SubjectPath: ift.SubjectPath, + NamePath: ift.NamePath, + EmailPath: ift.EmailPath, + } + } if cfg.DCRConfig != nil { runConfig.DCRConfig = buildDCRUpstreamRunConfig(cfg.DCRConfig, initialAccessTokenEnvVar) } diff --git a/cmd/thv-operator/pkg/controllerutil/authserver_test.go b/cmd/thv-operator/pkg/controllerutil/authserver_test.go index c8cda64f38..fcb0bd6379 100644 --- a/cmd/thv-operator/pkg/controllerutil/authserver_test.go +++ b/cmd/thv-operator/pkg/controllerutil/authserver_test.go @@ -1423,6 +1423,121 @@ func TestBuildAuthServerRunConfig(t *testing.T) { "DCRConfig should remain nil when only ClientID is set") }, }, + { + name: "OAuth2 upstream with identityFromToken all fields set", + resourceURL: defaultResourceURL, + authConfig: &mcpv1beta1.EmbeddedAuthServerConfig{ + Issuer: "https://auth.example.com", + SigningKeySecretRefs: []mcpv1beta1.SecretKeyRef{ + {Name: "signing-key", Key: "private.pem"}, + }, + HMACSecretRefs: []mcpv1beta1.SecretKeyRef{ + {Name: "hmac-secret", Key: "hmac"}, + }, + UpstreamProviders: []mcpv1beta1.UpstreamProviderConfig{ + { + Name: "snowflake", + Type: mcpv1beta1.UpstreamProviderTypeOAuth2, + OAuth2Config: &mcpv1beta1.OAuth2UpstreamConfig{ + AuthorizationEndpoint: "https://account.snowflakecomputing.com/oauth/authorize", + TokenEndpoint: "https://account.snowflakecomputing.com/oauth/token-request", + ClientID: "sf-client-id", + IdentityFromToken: &mcpv1beta1.IdentityFromTokenConfig{ + SubjectPath: "username", + NamePath: "display_name", + EmailPath: "email", + }, + }, + }, + }, + }, + allowedAudiences: defaultAudiences, + scopesSupported: defaultScopes, + checkFunc: func(t *testing.T, config *authserver.RunConfig) { + t.Helper() + require.Len(t, config.Upstreams, 1) + upstream := config.Upstreams[0] + require.NotNil(t, upstream.OAuth2Config) + require.NotNil(t, upstream.OAuth2Config.IdentityFromToken) + assert.Equal(t, "username", upstream.OAuth2Config.IdentityFromToken.SubjectPath) + assert.Equal(t, "display_name", upstream.OAuth2Config.IdentityFromToken.NamePath) + assert.Equal(t, "email", upstream.OAuth2Config.IdentityFromToken.EmailPath) + }, + }, + { + name: "OAuth2 upstream with identityFromToken only subjectPath set", + resourceURL: defaultResourceURL, + authConfig: &mcpv1beta1.EmbeddedAuthServerConfig{ + Issuer: "https://auth.example.com", + SigningKeySecretRefs: []mcpv1beta1.SecretKeyRef{ + {Name: "signing-key", Key: "private.pem"}, + }, + HMACSecretRefs: []mcpv1beta1.SecretKeyRef{ + {Name: "hmac-secret", Key: "hmac"}, + }, + UpstreamProviders: []mcpv1beta1.UpstreamProviderConfig{ + { + Name: "slack", + Type: mcpv1beta1.UpstreamProviderTypeOAuth2, + OAuth2Config: &mcpv1beta1.OAuth2UpstreamConfig{ + AuthorizationEndpoint: "https://slack.com/oauth/v2/authorize", + TokenEndpoint: "https://slack.com/api/oauth.v2.access", + ClientID: "slack-client-id", + IdentityFromToken: &mcpv1beta1.IdentityFromTokenConfig{ + SubjectPath: "authed_user.id", + }, + }, + }, + }, + }, + allowedAudiences: defaultAudiences, + scopesSupported: defaultScopes, + checkFunc: func(t *testing.T, config *authserver.RunConfig) { + t.Helper() + require.Len(t, config.Upstreams, 1) + upstream := config.Upstreams[0] + require.NotNil(t, upstream.OAuth2Config) + require.NotNil(t, upstream.OAuth2Config.IdentityFromToken) + assert.Equal(t, "authed_user.id", upstream.OAuth2Config.IdentityFromToken.SubjectPath) + assert.Empty(t, upstream.OAuth2Config.IdentityFromToken.NamePath) + assert.Empty(t, upstream.OAuth2Config.IdentityFromToken.EmailPath) + }, + }, + { + name: "OAuth2 upstream with no identityFromToken produces nil", + resourceURL: defaultResourceURL, + authConfig: &mcpv1beta1.EmbeddedAuthServerConfig{ + Issuer: "https://auth.example.com", + SigningKeySecretRefs: []mcpv1beta1.SecretKeyRef{ + {Name: "signing-key", Key: "private.pem"}, + }, + HMACSecretRefs: []mcpv1beta1.SecretKeyRef{ + {Name: "hmac-secret", Key: "hmac"}, + }, + UpstreamProviders: []mcpv1beta1.UpstreamProviderConfig{ + { + Name: "github-no-ift", + Type: mcpv1beta1.UpstreamProviderTypeOAuth2, + OAuth2Config: &mcpv1beta1.OAuth2UpstreamConfig{ + AuthorizationEndpoint: "https://github.com/login/oauth/authorize", + TokenEndpoint: "https://github.com/login/oauth/access_token", + UserInfo: &mcpv1beta1.UserInfoConfig{EndpointURL: "https://api.github.com/user"}, + ClientID: "client-id", + }, + }, + }, + }, + allowedAudiences: defaultAudiences, + scopesSupported: defaultScopes, + checkFunc: func(t *testing.T, config *authserver.RunConfig) { + t.Helper() + require.Len(t, config.Upstreams, 1) + upstream := config.Upstreams[0] + require.NotNil(t, upstream.OAuth2Config) + assert.Nil(t, upstream.OAuth2Config.IdentityFromToken, + "IdentityFromToken must be nil when not configured") + }, + }, } for _, tt := range tests { diff --git a/pkg/authserver/config.go b/pkg/authserver/config.go index 5a57047b7e..b6df5ac7a1 100644 --- a/pkg/authserver/config.go +++ b/pkg/authserver/config.go @@ -280,6 +280,14 @@ type OAuth2UpstreamRunConfig struct { //nolint:lll // field tags require full JSON+YAML names TokenResponseMapping *TokenResponseMappingRunConfig `json:"token_response_mapping,omitempty" yaml:"token_response_mapping,omitempty"` + // IdentityFromToken extracts user identity (subject, name, email) directly from the + // OAuth2 token-endpoint response body using gjson dot-notation paths. When set, the + // embedded auth server skips the userinfo HTTP call entirely. Mirrors the CRD type + // (cmd/thv-operator/api/v1beta1.IdentityFromTokenConfig) — the authoritative + // trust-model and uniqueness documentation lives there. + //nolint:lll // field tags require full JSON+YAML names + IdentityFromToken *IdentityFromTokenRunConfig `json:"identity_from_token,omitempty" yaml:"identity_from_token,omitempty"` + // AdditionalAuthorizationParams are extra query parameters to include in // authorization requests. Useful for provider-specific parameters like // Google's access_type=offline. @@ -383,6 +391,22 @@ type TokenResponseMappingRunConfig struct { ExpiresInPath string `json:"expires_in_path,omitempty" yaml:"expires_in_path,omitempty"` } +// IdentityFromTokenRunConfig configures extracting user identity claims directly from +// the token-endpoint response body. Mirrors the CRD type +// (cmd/thv-operator/api/v1beta1.IdentityFromTokenConfig) — the authoritative +// trust-model and uniqueness documentation lives there. +type IdentityFromTokenRunConfig struct { + // SubjectPath is the dot-notation path to the subject (user ID) field. + // Required when IdentityFromToken is set. + SubjectPath string `json:"subject_path" yaml:"subject_path"` + + // NamePath is the dot-notation path to the display name field. + NamePath string `json:"name_path,omitempty" yaml:"name_path,omitempty"` + + // EmailPath is the dot-notation path to the email address field. + EmailPath string `json:"email_path,omitempty" yaml:"email_path,omitempty"` +} + // UserInfoRunConfig contains UserInfo endpoint configuration. // This supports both standard OIDC UserInfo endpoints and custom provider-specific endpoints. type UserInfoRunConfig struct { @@ -618,6 +642,10 @@ func (c *OAuth2UpstreamRunConfig) Validate() error { } } + if c.IdentityFromToken != nil && c.IdentityFromToken.SubjectPath == "" { + return fmt.Errorf("oauth2 upstream: identity_from_token.subject_path must not be empty when identity_from_token is configured") + } + return nil } diff --git a/pkg/authserver/config_test.go b/pkg/authserver/config_test.go index e07345be8e..54837e0367 100644 --- a/pkg/authserver/config_test.go +++ b/pkg/authserver/config_test.go @@ -347,6 +347,32 @@ func TestOAuth2UpstreamRunConfigValidate(t *testing.T) { }, }, }, + + // IdentityFromToken subject_path requirement. + { + name: "IdentityFromToken with empty SubjectPath rejects", + config: OAuth2UpstreamRunConfig{ + ClientID: "c", + IdentityFromToken: &IdentityFromTokenRunConfig{}, + }, + wantErr: true, + errMsg: "identity_from_token.subject_path must not be empty", + }, + { + name: "IdentityFromToken with non-empty SubjectPath is valid", + config: OAuth2UpstreamRunConfig{ + ClientID: "c", + IdentityFromToken: &IdentityFromTokenRunConfig{ + SubjectPath: "username", + }, + }, + }, + { + name: "nil IdentityFromToken is valid", + config: OAuth2UpstreamRunConfig{ + ClientID: "c", + }, + }, } for _, tt := range tests { diff --git a/pkg/authserver/runner/embeddedauthserver.go b/pkg/authserver/runner/embeddedauthserver.go index b8eb04f99d..e735d1c3bb 100644 --- a/pkg/authserver/runner/embeddedauthserver.go +++ b/pkg/authserver/runner/embeddedauthserver.go @@ -569,6 +569,14 @@ func buildPureOAuth2Config(rc *authserver.UpstreamRunConfig) (*upstream.OAuth2Co } } + if oauth2.IdentityFromToken != nil { + cfg.IdentityFromToken = &upstream.IdentityFromTokenConfig{ + SubjectPath: oauth2.IdentityFromToken.SubjectPath, + NamePath: oauth2.IdentityFromToken.NamePath, + EmailPath: oauth2.IdentityFromToken.EmailPath, + } + } + return cfg, nil } diff --git a/pkg/authserver/runner/embeddedauthserver_test.go b/pkg/authserver/runner/embeddedauthserver_test.go index 14259b955e..f9d9030d43 100644 --- a/pkg/authserver/runner/embeddedauthserver_test.go +++ b/pkg/authserver/runner/embeddedauthserver_test.go @@ -623,6 +623,73 @@ func TestBuildPureOAuth2ConfigWithEnvVar(t *testing.T) { }) } +func TestBuildPureOAuth2ConfigIdentityFromToken(t *testing.T) { + t.Parallel() + + baseRC := func() *authserver.UpstreamRunConfig { + return &authserver.UpstreamRunConfig{ + Type: authserver.UpstreamProviderTypeOAuth2, + OAuth2Config: &authserver.OAuth2UpstreamRunConfig{ + AuthorizationEndpoint: "https://example.com/authorize", + TokenEndpoint: "https://example.com/token", + ClientID: "my-client-id", + RedirectURI: "https://my-app.com/callback", + }, + } + } + + t.Run("nil IdentityFromToken produces nil in runtime config", func(t *testing.T) { + t.Parallel() + + rc := baseRC() + // IdentityFromToken is not set + + cfg, err := buildPureOAuth2Config(rc) + require.NoError(t, err) + require.NotNil(t, cfg) + + assert.Nil(t, cfg.IdentityFromToken, "IdentityFromToken must be nil when not configured") + }) + + t.Run("all three paths round-trip correctly", func(t *testing.T) { + t.Parallel() + + rc := baseRC() + rc.OAuth2Config.IdentityFromToken = &authserver.IdentityFromTokenRunConfig{ + SubjectPath: "username", + NamePath: "display_name", + EmailPath: "email", + } + + cfg, err := buildPureOAuth2Config(rc) + require.NoError(t, err) + require.NotNil(t, cfg) + + require.NotNil(t, cfg.IdentityFromToken) + assert.Equal(t, "username", cfg.IdentityFromToken.SubjectPath) + assert.Equal(t, "display_name", cfg.IdentityFromToken.NamePath) + assert.Equal(t, "email", cfg.IdentityFromToken.EmailPath) + }) + + t.Run("only SubjectPath set, name and email empty", func(t *testing.T) { + t.Parallel() + + rc := baseRC() + rc.OAuth2Config.IdentityFromToken = &authserver.IdentityFromTokenRunConfig{ + SubjectPath: "authed_user.id", + } + + cfg, err := buildPureOAuth2Config(rc) + require.NoError(t, err) + require.NotNil(t, cfg) + + require.NotNil(t, cfg.IdentityFromToken) + assert.Equal(t, "authed_user.id", cfg.IdentityFromToken.SubjectPath) + assert.Empty(t, cfg.IdentityFromToken.NamePath) + assert.Empty(t, cfg.IdentityFromToken.EmailPath) + }) +} + func TestNewHMACSecrets(t *testing.T) { t.Parallel() From 9d6ea5a7a4d4c174da752fd7883a124c87f1fea6 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Thu, 7 May 2026 12:34:25 +0100 Subject: [PATCH 2/3] Register gjson modifiers in embedded auth-server bootstrap IdentityFromToken paths that use the `@upstreamjwt` modifier (e.g. `access_token|@upstreamjwt|sub` for upstreams that embed identity inside a JWS access token) require RegisterModifiers() to run before any extraction. Until now the call lived only in test TestMain functions, so production paths silently no-op'd and operators saw a generic "path not found" error. Add the call near the top of NewEmbeddedAuthServer. Protect the gjson.AddModifier write with a sync.Once so concurrent constructors (parallel test cases, thundering-herd restarts) do not race on the gjson global modifier map. --- pkg/authserver/runner/embeddedauthserver.go | 4 ++++ pkg/authserver/upstream/identity_from_token.go | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/pkg/authserver/runner/embeddedauthserver.go b/pkg/authserver/runner/embeddedauthserver.go index e735d1c3bb..19dedb1b74 100644 --- a/pkg/authserver/runner/embeddedauthserver.go +++ b/pkg/authserver/runner/embeddedauthserver.go @@ -62,6 +62,10 @@ func NewEmbeddedAuthServer(ctx context.Context, cfg *authserver.RunConfig) (*Emb return nil, fmt.Errorf("config is required") } + // Register gjson modifiers used by IdentityFromToken configs (e.g. @upstreamjwt). + // Without this, modifier-bearing paths silently fail to resolve. + upstream.RegisterModifiers() + // Fail loudly on operator-supplied misconfiguration (e.g. a baseline // scope absent from scopes_supported) BEFORE touching storage or any // other side-effecting work, so a bad config never reaches the network diff --git a/pkg/authserver/upstream/identity_from_token.go b/pkg/authserver/upstream/identity_from_token.go index e827d19815..9a3c4bf434 100644 --- a/pkg/authserver/upstream/identity_from_token.go +++ b/pkg/authserver/upstream/identity_from_token.go @@ -9,6 +9,7 @@ import ( "fmt" "log/slog" "strings" + "sync" "github.com/tidwall/gjson" ) @@ -29,7 +30,7 @@ type partialIdentity struct { // "associated_user.id") into the raw JSON body returned by the token // endpoint. Path semantics, trust-model warnings, and uniqueness notes are // documented on the corresponding CRD type -// (cmd/thv-operator/api/v1alpha1.IdentityFromTokenConfig). +// (cmd/thv-operator/api/v1beta1.IdentityFromTokenConfig). type IdentityFromTokenConfig struct { // SubjectPath is the gjson path to the unique user identifier (required). SubjectPath string @@ -43,16 +44,25 @@ type IdentityFromTokenConfig struct { EmailPath string } +// registerOnce ensures gjson modifiers are registered exactly once, even +// when RegisterModifiers is called concurrently from multiple goroutines +// (e.g. parallel auth-server constructors in tests or during thundering-herd +// restarts). gjson.AddModifier writes to a global map without internal +// synchronization, so concurrent calls race without this guard. +var registerOnce sync.Once + // RegisterModifiers registers the gjson custom modifiers used by this // package's path-based identity extractors. Call once during application // or test wire-up before invoking any extractor that consumes a -// modifier-bearing path. Repeated calls are safe — gjson.AddModifier -// overwrites the existing entry. +// modifier-bearing path. Repeated calls are safe — the registration is +// protected by a sync.Once and executes at most once per process. // // Modifiers registered: // - @upstreamjwt: see upstreamJWTModifier. func RegisterModifiers() { - gjson.AddModifier("upstreamjwt", upstreamJWTModifier) + registerOnce.Do(func() { + gjson.AddModifier("upstreamjwt", upstreamJWTModifier) + }) } // extractIdentityFromTokenResponse extracts user identity fields from a raw From 1c1749c981badfaf59ea2ca2839d574128873657 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Thu, 14 May 2026 22:26:08 +0100 Subject: [PATCH 3/3] Regenerate swagger docs Co-Authored-By: Claude Sonnet 4.6 --- docs/server/docs.go | 21 +++++++++++++++++++++ docs/server/swagger.json | 21 +++++++++++++++++++++ docs/server/swagger.yaml | 22 ++++++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/docs/server/docs.go b/docs/server/docs.go index 3172764981..8ac24cddf0 100644 --- a/docs/server/docs.go +++ b/docs/server/docs.go @@ -375,6 +375,24 @@ const docTemplate = `{ }, "type": "object" }, + "github_com_stacklok_toolhive_pkg_authserver.IdentityFromTokenRunConfig": { + "description": "IdentityFromToken extracts user identity (subject, name, email) directly from the\nOAuth2 token-endpoint response body using gjson dot-notation paths. When set, the\nembedded auth server skips the userinfo HTTP call entirely. Mirrors the CRD type\n(cmd/thv-operator/api/v1beta1.IdentityFromTokenConfig) — the authoritative\ntrust-model and uniqueness documentation lives there.", + "properties": { + "email_path": { + "description": "EmailPath is the dot-notation path to the email address field.", + "type": "string" + }, + "name_path": { + "description": "NamePath is the dot-notation path to the display name field.", + "type": "string" + }, + "subject_path": { + "description": "SubjectPath is the dot-notation path to the subject (user ID) field.\nRequired when IdentityFromToken is set.", + "type": "string" + } + }, + "type": "object" + }, "github_com_stacklok_toolhive_pkg_authserver.OAuth2UpstreamRunConfig": { "description": "OAuth2Config contains OAuth 2.0-specific configuration.\nRequired when Type is \"oauth2\", must be nil when Type is \"oidc\".", "properties": { @@ -404,6 +422,9 @@ const docTemplate = `{ "dcr_config": { "$ref": "#/components/schemas/github_com_stacklok_toolhive_pkg_authserver.DCRUpstreamConfig" }, + "identity_from_token": { + "$ref": "#/components/schemas/github_com_stacklok_toolhive_pkg_authserver.IdentityFromTokenRunConfig" + }, "redirect_uri": { "description": "RedirectURI is the callback URL where the upstream IDP will redirect after authentication.\nWhen not specified, defaults to ` + "`" + `{issuer}/oauth/callback` + "`" + `.", "type": "string" diff --git a/docs/server/swagger.json b/docs/server/swagger.json index 4e2e679b7c..79cbf9fa19 100644 --- a/docs/server/swagger.json +++ b/docs/server/swagger.json @@ -368,6 +368,24 @@ }, "type": "object" }, + "github_com_stacklok_toolhive_pkg_authserver.IdentityFromTokenRunConfig": { + "description": "IdentityFromToken extracts user identity (subject, name, email) directly from the\nOAuth2 token-endpoint response body using gjson dot-notation paths. When set, the\nembedded auth server skips the userinfo HTTP call entirely. Mirrors the CRD type\n(cmd/thv-operator/api/v1beta1.IdentityFromTokenConfig) — the authoritative\ntrust-model and uniqueness documentation lives there.", + "properties": { + "email_path": { + "description": "EmailPath is the dot-notation path to the email address field.", + "type": "string" + }, + "name_path": { + "description": "NamePath is the dot-notation path to the display name field.", + "type": "string" + }, + "subject_path": { + "description": "SubjectPath is the dot-notation path to the subject (user ID) field.\nRequired when IdentityFromToken is set.", + "type": "string" + } + }, + "type": "object" + }, "github_com_stacklok_toolhive_pkg_authserver.OAuth2UpstreamRunConfig": { "description": "OAuth2Config contains OAuth 2.0-specific configuration.\nRequired when Type is \"oauth2\", must be nil when Type is \"oidc\".", "properties": { @@ -397,6 +415,9 @@ "dcr_config": { "$ref": "#/components/schemas/github_com_stacklok_toolhive_pkg_authserver.DCRUpstreamConfig" }, + "identity_from_token": { + "$ref": "#/components/schemas/github_com_stacklok_toolhive_pkg_authserver.IdentityFromTokenRunConfig" + }, "redirect_uri": { "description": "RedirectURI is the callback URL where the upstream IDP will redirect after authentication.\nWhen not specified, defaults to `{issuer}/oauth/callback`.", "type": "string" diff --git a/docs/server/swagger.yaml b/docs/server/swagger.yaml index 274bc898c3..420b2979d8 100644 --- a/docs/server/swagger.yaml +++ b/docs/server/swagger.yaml @@ -410,6 +410,26 @@ components: server trusts. type: string type: object + github_com_stacklok_toolhive_pkg_authserver.IdentityFromTokenRunConfig: + description: |- + IdentityFromToken extracts user identity (subject, name, email) directly from the + OAuth2 token-endpoint response body using gjson dot-notation paths. When set, the + embedded auth server skips the userinfo HTTP call entirely. Mirrors the CRD type + (cmd/thv-operator/api/v1beta1.IdentityFromTokenConfig) — the authoritative + trust-model and uniqueness documentation lives there. + properties: + email_path: + description: EmailPath is the dot-notation path to the email address field. + type: string + name_path: + description: NamePath is the dot-notation path to the display name field. + type: string + subject_path: + description: |- + SubjectPath is the dot-notation path to the subject (user ID) field. + Required when IdentityFromToken is set. + type: string + type: object github_com_stacklok_toolhive_pkg_authserver.OAuth2UpstreamRunConfig: description: |- OAuth2Config contains OAuth 2.0-specific configuration. @@ -445,6 +465,8 @@ components: type: string dcr_config: $ref: '#/components/schemas/github_com_stacklok_toolhive_pkg_authserver.DCRUpstreamConfig' + identity_from_token: + $ref: '#/components/schemas/github_com_stacklok_toolhive_pkg_authserver.IdentityFromTokenRunConfig' redirect_uri: description: |- RedirectURI is the callback URL where the upstream IDP will redirect after authentication.