extauth: add ClientCredentialsHandler for OAuth client credentials grant (#895)
## 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](https://github.com/modelcontextprotocol/go-sdk/issues/627#issuecomment-3640982691): > 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](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1046). The JWT Assertions variant (RFC 7523) is left for a follow-up as its API surface needs more design discussion. ## Test plan - [x] `TestNewClientCredentialsHandler_Validation` — validates all config error cases (nil config, missing fields, mutual exclusivity) - [x] `TestClientCredentialsHandler_Authorize/direct_token_endpoint` — end-to-end with fake auth server - [x] `TestClientCredentialsHandler_Authorize/metadata_discovery` — discovers token endpoint via RFC 8414 metadata - [x] `TestClientCredentialsHandler_Authorize/bad_credentials` — verifies failure with wrong secret - [x] `go test ./... -count=1` passes - [x] `go vet ./...` clean Refs #627 Co-authored-by: Maciej Kisiel <mkisiel@google.com>
This commit is contained in:
parent
2643b22a4e
commit
2e21834ad3
1 changed files with 572 additions and 0 deletions
|
|
@ -57,6 +57,15 @@ type JWTBearerConfig struct {
|
|||
ValidAssertions []string
|
||||
}
|
||||
|
||||
// ClientCredentialsConfig configures support for the client_credentials
|
||||
// grant type (RFC 6749 Section 4.4) on a [FakeAuthorizationServer].
|
||||
type ClientCredentialsConfig struct {
|
||||
// Enabled controls whether the /token endpoint accepts
|
||||
// grant_type=client_credentials and returns an access token
|
||||
// if client authentication succeeds.
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
// Config holds configuration for FakeAuthorizationServer.
|
||||
type Config struct {
|
||||
// The optional path component of the issuer URL.
|
||||
|
|
@ -70,6 +79,9 @@ type Config struct {
|
|||
// JWTBearerConfig enables RFC 7523 JWT Bearer grant at the /token endpoint.
|
||||
// If non-nil, the server accepts grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer.
|
||||
JWTBearerConfig *JWTBearerConfig
|
||||
// ClientCredentialsConfig enables RFC 6749 Section 4.4 client credentials
|
||||
// grant at the /token endpoint.
|
||||
ClientCredentialsConfig *ClientCredentialsConfig
|
||||
}
|
||||
|
||||
// FakeAuthorizationServer is a fake OAuth 2.0 Authorization Server for testing.
|
||||
|
|
@ -271,6 +283,8 @@ func (s *FakeAuthorizationServer) handleToken(w http.ResponseWriter, r *http.Req
|
|||
s.handleAuthorizationCodeGrant(w, r)
|
||||
case "urn:ietf:params:oauth:grant-type:jwt-bearer":
|
||||
s.handleJWTBearerGrant(w, r)
|
||||
case "client_credentials":
|
||||
s.handleClientCredentialsGrant(w, r)
|
||||
default:
|
||||
http.Error(w, fmt.Sprintf("unsupported grant_type: %s", grantType), http.StatusBadRequest)
|
||||
}
|
||||
|
|
@ -332,6 +346,20 @@ func (s *FakeAuthorizationServer) handleJWTBearerGrant(w http.ResponseWriter, r
|
|||
})
|
||||
}
|
||||
|
||||
func (s *FakeAuthorizationServer) handleClientCredentialsGrant(w http.ResponseWriter, r *http.Request) {
|
||||
if s.config.ClientCredentialsConfig == nil || !s.config.ClientCredentialsConfig.Enabled {
|
||||
http.Error(w, "client_credentials grant not supported", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Client was already authenticated in handleToken.
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"access_token": "test_access_token",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *FakeAuthorizationServer) authenticateClient(r *http.Request) error {
|
||||
clientID, clientSecret, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
|
|
|
|||
Loading…
Reference in a new issue