Skip to content

Commit fd618d5

Browse files
aron-muonclaude
andcommitted
Regenerate CRD manifests and docs for tokenResponseMapping
Run `task operator-manifests` and `crd-ref-docs` to update the CRD schema with the new tokenResponseMapping field on OAuth2UpstreamConfig and regenerate the API reference docs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3fd0431 commit fd618d5

6 files changed

Lines changed: 313 additions & 338 deletions

File tree

deploy/charts/operator-crds/files/crds/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,43 @@ spec:
441441
token endpoint.
442442
pattern: ^https?://.*$
443443
type: string
444+
tokenResponseMapping:
445+
description: |-
446+
TokenResponseMapping configures custom field extraction from non-standard token responses.
447+
Some OAuth providers (e.g., GovSlack) nest token fields under non-standard paths
448+
instead of returning them at the top level. When set, ToolHive performs the token
449+
exchange HTTP call directly and extracts fields using the configured dot-notation paths.
450+
If nil, standard OAuth 2.0 token response parsing is used.
451+
properties:
452+
accessTokenPath:
453+
description: |-
454+
AccessTokenPath is the dot-notation path to the access token in the response.
455+
Example: "authed_user.access_token"
456+
minLength: 1
457+
type: string
458+
expiresInPath:
459+
description: |-
460+
ExpiresInPath is the dot-notation path to the expires_in value (in seconds).
461+
If not specified, defaults to "expires_in".
462+
type: string
463+
refreshTokenPath:
464+
description: |-
465+
RefreshTokenPath is the dot-notation path to the refresh token in the response.
466+
If not specified, defaults to "refresh_token".
467+
type: string
468+
scopePath:
469+
description: |-
470+
ScopePath is the dot-notation path to the scope string in the response.
471+
If not specified, defaults to "scope".
472+
type: string
473+
tokenTypePath:
474+
description: |-
475+
TokenTypePath is the dot-notation path to the token type in the response.
476+
If not specified, defaults to "token_type".
477+
type: string
478+
required:
479+
- accessTokenPath
480+
type: object
444481
userInfo:
445482
description: |-
446483
UserInfo contains configuration for fetching user information from the upstream provider.

deploy/charts/operator-crds/templates/toolhive.stacklok.dev_mcpexternalauthconfigs.yaml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,43 @@ spec:
444444
token endpoint.
445445
pattern: ^https?://.*$
446446
type: string
447+
tokenResponseMapping:
448+
description: |-
449+
TokenResponseMapping configures custom field extraction from non-standard token responses.
450+
Some OAuth providers (e.g., GovSlack) nest token fields under non-standard paths
451+
instead of returning them at the top level. When set, ToolHive performs the token
452+
exchange HTTP call directly and extracts fields using the configured dot-notation paths.
453+
If nil, standard OAuth 2.0 token response parsing is used.
454+
properties:
455+
accessTokenPath:
456+
description: |-
457+
AccessTokenPath is the dot-notation path to the access token in the response.
458+
Example: "authed_user.access_token"
459+
minLength: 1
460+
type: string
461+
expiresInPath:
462+
description: |-
463+
ExpiresInPath is the dot-notation path to the expires_in value (in seconds).
464+
If not specified, defaults to "expires_in".
465+
type: string
466+
refreshTokenPath:
467+
description: |-
468+
RefreshTokenPath is the dot-notation path to the refresh token in the response.
469+
If not specified, defaults to "refresh_token".
470+
type: string
471+
scopePath:
472+
description: |-
473+
ScopePath is the dot-notation path to the scope string in the response.
474+
If not specified, defaults to "scope".
475+
type: string
476+
tokenTypePath:
477+
description: |-
478+
TokenTypePath is the dot-notation path to the token type in the response.
479+
If not specified, defaults to "token_type".
480+
type: string
481+
required:
482+
- accessTokenPath
483+
type: object
447484
userInfo:
448485
description: |-
449486
UserInfo contains configuration for fetching user information from the upstream provider.

docs/operator/crd-api.md

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

pkg/authserver/upstream/oauth2.go

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -420,16 +420,13 @@ func (p *BaseOAuth2Provider) exchangeCodeForTokens(ctx context.Context, code, co
420420
slog.Info("exchanging authorization code for tokens",
421421
"token_endpoint", p.config.TokenEndpoint,
422422
"has_pkce_verifier", codeVerifier != "",
423-
"custom_mapping", p.config.TokenResponseMapping != nil,
424423
)
425424

426-
// If custom token response mapping is configured, bypass oauth2 library
427-
if p.config.TokenResponseMapping != nil {
428-
return p.customExchangeCodeForTokens(ctx, code, codeVerifier)
429-
}
430-
431-
// Inject our custom HTTP client into the context for oauth2 to use
432-
ctx = context.WithValue(ctx, oauth2.HTTPClient, p.httpClient)
425+
// Wrap HTTP client with token response rewriter if mapping is configured.
426+
// This normalizes non-standard responses (e.g., GovSlack's nested fields)
427+
// before the oauth2 library parses them, keeping the standard exchange flow.
428+
httpClient := wrapHTTPClientWithMapping(p.httpClient, p.config.TokenResponseMapping, p.config.TokenEndpoint)
429+
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
433430

434431
// Build exchange options
435432
var opts []oauth2.AuthCodeOption
@@ -465,13 +462,9 @@ func (p *BaseOAuth2Provider) RefreshTokens(ctx context.Context, refreshToken, _
465462
"token_endpoint", p.config.TokenEndpoint,
466463
)
467464

468-
// If custom token response mapping is configured, bypass oauth2 library
469-
if p.config.TokenResponseMapping != nil {
470-
return p.customRefreshTokens(ctx, refreshToken)
471-
}
472-
473-
// Inject our custom HTTP client into the context for oauth2 to use
474-
ctx = context.WithValue(ctx, oauth2.HTTPClient, p.httpClient)
465+
// Wrap HTTP client with token response rewriter if mapping is configured.
466+
httpClient := wrapHTTPClientWithMapping(p.httpClient, p.config.TokenResponseMapping, p.config.TokenEndpoint)
467+
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
475468

476469
// Create an expired token with the refresh token to trigger refresh
477470
expiredToken := &oauth2.Token{

0 commit comments

Comments
 (0)