Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions cmd/thv-operator/api/v1alpha1/mcpexternalauthconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,40 @@ type OAuth2UpstreamConfig struct {
// Scopes are the OAuth scopes to request from the upstream IDP.
// +optional
Scopes []string `json:"scopes,omitempty"`

// TokenResponseMapping configures custom field extraction from non-standard token responses.
// Some OAuth providers (e.g., GovSlack) nest token fields under non-standard paths
// instead of returning them at the top level. When set, ToolHive performs the token
// exchange HTTP call directly and extracts fields using the configured dot-notation paths.
// If nil, standard OAuth 2.0 token response parsing is used.
// +optional
TokenResponseMapping *TokenResponseMapping `json:"tokenResponseMapping,omitempty"`
Comment thread
aron-muon marked this conversation as resolved.
}

// TokenResponseMapping maps non-standard token response fields to standard OAuth 2.0 fields
// using dot-notation JSON paths. This supports upstream providers like GovSlack that nest
// the access token under paths like "authed_user.access_token".
type TokenResponseMapping struct {
// AccessTokenPath is the dot-notation path to the access token in the response.
// Example: "authed_user.access_token"
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinLength=1
AccessTokenPath string `json:"accessTokenPath"`

// ScopePath is the dot-notation path to the scope string in the response.
// If not specified, defaults to "scope".
// +optional
ScopePath string `json:"scopePath,omitempty"`

// RefreshTokenPath is the dot-notation path to the refresh token in the response.
// If not specified, defaults to "refresh_token".
// +optional
RefreshTokenPath string `json:"refreshTokenPath,omitempty"`

// ExpiresInPath is the dot-notation path to the expires_in value (in seconds).
// If not specified, defaults to "expires_in".
// +optional
ExpiresInPath string `json:"expiresInPath,omitempty"`
}

// UserInfoConfig contains configuration for fetching user information from an upstream provider.
Expand Down
20 changes: 20 additions & 0 deletions cmd/thv-operator/api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions cmd/thv-operator/pkg/controllerutil/authserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,15 @@ func buildUpstreamRunConfig(
if provider.OAuth2Config.UserInfo != nil {
config.OAuth2Config.UserInfo = buildUserInfoRunConfig(provider.OAuth2Config.UserInfo)
}
if provider.OAuth2Config.TokenResponseMapping != nil {
m := provider.OAuth2Config.TokenResponseMapping
config.OAuth2Config.TokenResponseMapping = &authserver.TokenResponseMappingRunConfig{
AccessTokenPath: m.AccessTokenPath,
ScopePath: m.ScopePath,
RefreshTokenPath: m.RefreshTokenPath,
ExpiresInPath: m.ExpiresInPath,
}
}
}
}

Expand Down
7 changes: 7 additions & 0 deletions cmd/thv-proxyrunner/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ import (
)

func main() {
// Bind TOOLHIVE_DEBUG env var early, before logger initialization.
// This must happen before viper.GetBool("debug") so the env var
// is available when configuring the log level.
if err := viper.BindEnv("debug", "TOOLHIVE_DEBUG"); err != nil {
slog.Error("failed to bind TOOLHIVE_DEBUG env var", "error", err)
Comment thread
aron-muon marked this conversation as resolved.
}

// Initialize the logger
var opts []logging.Option
if viper.GetBool("debug") {
Expand Down
7 changes: 7 additions & 0 deletions cmd/thv/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ import (
)

func main() {
// Bind TOOLHIVE_DEBUG env var early, before logger initialization.
// This must happen before viper.GetBool("debug") so the env var
// is available when configuring the log level.
if err := viper.BindEnv("debug", "TOOLHIVE_DEBUG"); err != nil {
slog.Error("failed to bind TOOLHIVE_DEBUG env var", "error", err)
}

// Initialize the logger
var opts []logging.Option
if viper.GetBool("debug") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,38 @@ spec:
token endpoint.
pattern: ^https?://.*$
type: string
tokenResponseMapping:
description: |-
TokenResponseMapping configures custom field extraction from non-standard token responses.
Some OAuth providers (e.g., GovSlack) nest token fields under non-standard paths
instead of returning them at the top level. When set, ToolHive performs the token
exchange HTTP call directly and extracts fields using the configured dot-notation paths.
If nil, standard OAuth 2.0 token response parsing is used.
properties:
accessTokenPath:
description: |-
AccessTokenPath is the dot-notation path to the access token in the response.
Example: "authed_user.access_token"
minLength: 1
type: string
expiresInPath:
description: |-
ExpiresInPath is the dot-notation path to the expires_in value (in seconds).
If not specified, defaults to "expires_in".
type: string
refreshTokenPath:
description: |-
RefreshTokenPath is the dot-notation path to the refresh token in the response.
If not specified, defaults to "refresh_token".
type: string
scopePath:
description: |-
ScopePath is the dot-notation path to the scope string in the response.
If not specified, defaults to "scope".
type: string
required:
- accessTokenPath
type: object
userInfo:
description: |-
UserInfo contains configuration for fetching user information from the upstream provider.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,38 @@ spec:
token endpoint.
pattern: ^https?://.*$
type: string
tokenResponseMapping:
description: |-
TokenResponseMapping configures custom field extraction from non-standard token responses.
Some OAuth providers (e.g., GovSlack) nest token fields under non-standard paths
instead of returning them at the top level. When set, ToolHive performs the token
exchange HTTP call directly and extracts fields using the configured dot-notation paths.
If nil, standard OAuth 2.0 token response parsing is used.
properties:
accessTokenPath:
description: |-
AccessTokenPath is the dot-notation path to the access token in the response.
Example: "authed_user.access_token"
minLength: 1
type: string
expiresInPath:
description: |-
ExpiresInPath is the dot-notation path to the expires_in value (in seconds).
If not specified, defaults to "expires_in".
type: string
refreshTokenPath:
description: |-
RefreshTokenPath is the dot-notation path to the refresh token in the response.
If not specified, defaults to "refresh_token".
type: string
scopePath:
description: |-
ScopePath is the dot-notation path to the scope string in the response.
If not specified, defaults to "scope".
type: string
required:
- accessTokenPath
type: object
userInfo:
description: |-
UserInfo contains configuration for fetching user information from the upstream provider.
Expand Down
22 changes: 22 additions & 0 deletions docs/operator/crd-api.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions docs/server/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions docs/server/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions docs/server/swagger.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions pkg/authserver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,28 @@ type OAuth2UpstreamRunConfig struct {

// UserInfo contains configuration for fetching user information (required for OAuth2).
UserInfo *UserInfoRunConfig `json:"userinfo" yaml:"userinfo"`

// TokenResponseMapping configures custom field extraction from non-standard token responses.
// When set, the token exchange bypasses golang.org/x/oauth2 and extracts fields using
// the configured dot-notation paths.
//nolint:lll // field tags require full JSON+YAML names
TokenResponseMapping *TokenResponseMappingRunConfig `json:"token_response_mapping,omitempty" yaml:"token_response_mapping,omitempty"`
}

// TokenResponseMappingRunConfig maps non-standard token response fields to standard fields.
// Paths support dot-notation for nested JSON fields (e.g., "authed_user.access_token").
type TokenResponseMappingRunConfig struct {
// AccessTokenPath is the dot-notation path to the access token (required).
AccessTokenPath string `json:"access_token_path" yaml:"access_token_path"`

// ScopePath is the dot-notation path to the scope. Defaults to "scope".
ScopePath string `json:"scope_path,omitempty" yaml:"scope_path,omitempty"`

// RefreshTokenPath is the dot-notation path to the refresh token. Defaults to "refresh_token".
RefreshTokenPath string `json:"refresh_token_path,omitempty" yaml:"refresh_token_path,omitempty"`

// ExpiresInPath is the dot-notation path to the expires_in value. Defaults to "expires_in".
ExpiresInPath string `json:"expires_in_path,omitempty" yaml:"expires_in_path,omitempty"`
}

// UserInfoRunConfig contains UserInfo endpoint configuration.
Expand Down
Loading
Loading