Skip to content

Set AuthStyle to InParams for public PKCE OAuth clients#4150

Open
gkatz2 wants to merge 2 commits intostacklok:mainfrom
gkatz2:fix/oauth-authstyle-inparams
Open

Set AuthStyle to InParams for public PKCE OAuth clients#4150
gkatz2 wants to merge 2 commits intostacklok:mainfrom
gkatz2:fix/oauth-authstyle-inparams

Conversation

@gkatz2
Copy link
Contributor

@gkatz2 gkatz2 commented Mar 15, 2026

Summary

ToolHive's OAuth PKCE token exchange fails against strict OAuth 2.1 servers (e.g., Datadog's mcp.datadoghq.com) because oauth2.Endpoint structs default to AuthStyleAutoDetect, which probes with HTTP Basic Auth first and burns the single-use authorization code. See the linked issue for the full mechanism.

  • Set AuthStyle: oauth2.AuthStyleInParams in the three locations where oauth2.Endpoint is constructed without it
  • Add a regression test with a strict mock server that rejects Basic Auth for public clients

Fixes #4149

Type of change

  • Bug fix
  • New feature
  • Refactoring (no behavior change)
  • Dependency update
  • Documentation
  • Other (describe):

Test plan

  • Unit tests (task test)
  • Linting (task lint-fix)
  • Manual testing: Verified OAuth PKCE flow completes successfully against Datadog's MCP server (mcp.datadoghq.com)

Changes

File Change
pkg/auth/oauth/flow.go Set AuthStyleInParams on endpoint in NewFlow
pkg/auth/remote/handler.go Set AuthStyleInParams on endpoint in tryRestoreFromCachedTokens
pkg/registry/auth/oauth_token_source.go Set AuthStyleInParams on endpoint in buildOAuth2Config
pkg/auth/oauth/flow_test.go Add TestAuthStyleInParams_StrictPublicClientServer regression test

Does this introduce a user-facing change?

Yes. Remote MCP servers with strict OAuth 2.1 implementations that previously failed with invalid_grant during PKCE token exchange will now connect successfully. No configuration changes needed.

Special notes for reviewers

Why AuthStyleInParams is safe: AuthStyleAutoDetect tries HTTP Basic Auth first. For public PKCE clients (the common case — ToolHive's DCR registers with token_endpoint_auth_method=none), this sends an Authorization: Basic base64(client_id:) header with an empty password. Spec-compliant servers reject this and consume the single-use authorization code, making the retry fail with invalid_grant. AuthStyleInParams sends client_id in the POST body instead, avoiding the problem entirely.

For the less common case where users provide their own client credentials via --remote-auth-client-id/--remote-auth-client-secret, AuthStyleInParams sends the secret in the POST body (client_secret_post style). This is accepted by the vast majority of OAuth servers. The only servers that would break are those that exclusively require client_secret_basic and reject POST body credentials — an uncommon configuration, and a much smaller risk than the auth-code-burning bug this fixes.

Generated with Claude Code

When oauth2.Endpoint.AuthStyle is unset (zero value), Go's oauth2
library uses AuthStyleAutoDetect, which tries HTTP Basic Auth first.
For public PKCE clients (token_endpoint_auth_method=none), this sends
an Authorization header with an empty password. Spec-compliant servers
reject this and consume the single-use authorization code, causing the
retry with client_id in POST body to fail with invalid_grant.

Set AuthStyleInParams explicitly in all three locations where
oauth2.Endpoint is constructed without AuthStyle:
- pkg/auth/oauth/flow.go (authorization code exchange)
- pkg/auth/remote/handler.go (token refresh from cached tokens)
- pkg/registry/auth/oauth_token_source.go (registry auth)

Add regression test with a strict mock server that rejects Basic Auth
for public clients. Without the fix: 2 requests (auto-detect probing).
With the fix: exactly 1 request.

Fixes stacklok#4149

Signed-off-by: Greg Katz <gkatz@indeed.com>
@github-actions github-actions bot added the size/S Small PR: 100-299 lines changed label Mar 15, 2026
Signed-off-by: Greg Katz <gkatz@indeed.com>
@github-actions github-actions bot added size/S Small PR: 100-299 lines changed and removed size/S Small PR: 100-299 lines changed labels Mar 15, 2026
@codecov
Copy link

codecov bot commented Mar 15, 2026

Codecov Report

❌ Patch coverage is 66.66667% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.79%. Comparing base (ba38a12) to head (0b50b34).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
pkg/auth/remote/handler.go 0.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4150      +/-   ##
==========================================
- Coverage   68.88%   68.79%   -0.09%     
==========================================
  Files         461      464       +3     
  Lines       46562    46732     +170     
==========================================
+ Hits        32075    32151      +76     
- Misses      11987    12019      +32     
- Partials     2500     2562      +62     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/S Small PR: 100-299 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OAuth PKCE token exchange fails with invalid_grant against strict OAuth 2.1 servers

1 participant