Skip to content

extauth: add ClientCredentialsHandler for OAuth client credentials grant#895

Open
ravyg wants to merge 1 commit intomodelcontextprotocol:mainfrom
ravyg:feat/627-oauth-client-credentials
Open

extauth: add ClientCredentialsHandler for OAuth client credentials grant#895
ravyg wants to merge 1 commit intomodelcontextprotocol:mainfrom
ravyg:feat/627-oauth-client-credentials

Conversation

@ravyg
Copy link
Copy Markdown
Contributor

@ravyg ravyg commented Apr 14, 2026

Summary

  • Add ClientCredentialsHandler implementing auth.OAuthHandler using the OAuth 2.0 Client Credentials grant (RFC 6749 Section 4.4) for service-to-service authentication with pre-registered credentials.
  • Bypasses both dynamic client registration and the authorization code flow — the handler takes a client ID + secret directly and exchanges them at the token endpoint.
  • Supports two modes: direct TokenEndpoint URL, or metadata discovery via AuthServerURL (RFC 8414).
  • Add client_credentials grant type support to the fake authorization server in internal/oauthtest for testing.

Context

Per @jba's comment on #627:

Our default implementation for that framework should support client ID and client secret as options, to handle that variant. [...] If those are set, our implementation would bypass dynamic client reservation. That is just a couple of lines of code, I believe.

The client-side OAuth scaffolding (#785) that this was blocked on is now complete, as @brkane noted.

Implements the client credentials variant of SEP-1046. The JWT Assertions variant (RFC 7523) is left for a follow-up as its API surface needs more design discussion.

Test plan

  • TestNewClientCredentialsHandler_Validation — validates all config error cases (nil config, missing fields, mutual exclusivity)
  • TestClientCredentialsHandler_Authorize/direct_token_endpoint — end-to-end with fake auth server
  • TestClientCredentialsHandler_Authorize/metadata_discovery — discovers token endpoint via RFC 8414 metadata
  • TestClientCredentialsHandler_Authorize/bad_credentials — verifies failure with wrong secret
  • go test ./... -count=1 passes
  • go vet ./... clean

Refs #627

@ravyg ravyg force-pushed the feat/627-oauth-client-credentials branch from 013c785 to 0977382 Compare April 14, 2026 04:59
@ravyg ravyg marked this pull request as ready for review April 14, 2026 05:12
Comment thread auth/extauth/client_credentials.go Outdated
Comment thread auth/extauth/client_credentials.go Outdated
Comment thread auth/extauth/client_credentials.go Outdated
Comment thread auth/extauth/client_credentials.go
Comment thread auth/extauth/client_credentials.go Outdated
Comment thread auth/extauth/client_credentials_test.go
Comment thread auth/extauth/client_credentials_test.go Outdated
Comment thread auth/extauth/client_credentials_test.go Outdated
Comment thread auth/extauth/client_credentials_test.go Outdated
Comment thread internal/oauthtest/fake_authorization_server.go Outdated
@ravyg ravyg force-pushed the feat/627-oauth-client-credentials branch from 8814ca6 to 6383b63 Compare April 15, 2026 01:01
Comment thread auth/extauth/client_credentials.go Outdated
// scopesFromChallenges extracts scope values from WWW-Authenticate challenges.
func scopesFromChallenges(cs []oauthex.Challenge) []string {
for _, c := range cs {
if s := c.Params["scope"]; s != "" {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the difference in implementation from the AuthorizationCodeHandler? I believe we should split scopes into a slice instead of interpreting the space separated list as a single string.

// getProtectedResourceMetadata discovers Protected Resource Metadata (RFC 9728)
// from the request URL. It tries the resource_metadata URL from WWW-Authenticate
// challenges first, then falls back to well-known discovery.
func getProtectedResourceMetadata(ctx context.Context, wwwChallenges []oauthex.Challenge, mcpServerURL string, httpClient *http.Client) (*oauthex.ProtectedResourceMetadata, error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a particular reason we're changing the logic compared to the AuthorizationCodeHandler? Why not just copy the logic, do we want different behavior?

Comment thread auth/extauth/client_credentials.go Outdated
}
u.Path = ""
rootURL := u.String()
prm, err := oauthex.GetProtectedResourceMetadata(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep the call on a single line.

Comment thread auth/extauth/client_credentials_test.go Outdated
func TestNewClientCredentialsHandler_Validation(t *testing.T) {
tests := []struct {
name string
modify func(*ClientCredentialsHandlerConfig) *ClientCredentialsHandlerConfig
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't need to be a function, just the config. You can change the function signature to take 0 arguments, create valid config inside the function and call the function immediately in the test case definition. This way the config creation is local and easy to reason about.

Comment thread auth/extauth/client_credentials_test.go Outdated

// Set up a fake MCP server that returns PRM pointing to the auth server.
mcpMux := http.NewServeMux()
mcpMux.HandleFunc("/.well-known/oauth-protected-resource", func(w http.ResponseWriter, r *http.Request) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use auth.ProtectedResourceMetadataHandler like in the authorization_code_test.go.

@ravyg
Copy link
Copy Markdown
Contributor Author

ravyg commented Apr 24, 2026

taking a look

@ravyg ravyg force-pushed the feat/627-oauth-client-credentials branch from 6383b63 to 4d96e06 Compare April 24, 2026 15:18
@ravyg
Copy link
Copy Markdown
Contributor Author

ravyg commented Apr 24, 2026

found one issue, I am fixing it.

Add an implementation of auth.OAuthHandler that uses the OAuth 2.0
Client Credentials grant (RFC 6749 Section 4.4) for service-to-service
authentication with pre-registered credentials. This bypasses both
dynamic client registration and the authorization code flow.

The handler supports two modes:
- Direct token endpoint URL
- Metadata discovery via AuthServerURL (RFC 8414)

Also adds client_credentials grant type support to the fake
authorization server in internal/oauthtest for testing.

Refs modelcontextprotocol#627
@ravyg ravyg force-pushed the feat/627-oauth-client-credentials branch from 4d96e06 to 2b46590 Compare April 24, 2026 17:24
@ravyg ravyg requested a review from maciej-kisiel April 24, 2026 22:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants