`auth`, `oauthex`: implement Enterprise Managed Authorization (SEP-990) This PR implements Enterprise Managed Authorization (SEP-990) for the Go MCP SDK, enabling MCP Clients and Servers to leverage enterprise Identity Providers for seamless authorization without requiring users to authenticate separately to each MCP Server. Overview Enterprise Managed Authorization follows the Identity Assertion Authorization Grant specification (draft-ietf-oauth-identity-assertion-authz-grant), implementing a three-step flow: 1. **Single Sign-On (SSO)**: User authenticates to the MCP Client via enterprise IdP (Okta, Auth0, Azure AD, etc.) 2. **Token Exchange (RFC 8693)**: Client exchanges ID Token for Identity Assertion JWT Authorization Grant (ID-JAG) at the IdP 3. **JWT Bearer Grant (RFC 7523)**: Client exchanges ID-JAG for Access Token at the MCP Server This enables: - **For end users**: Single sign-on across MCP Clients and Servers—no manual connection/authorization per server - **For enterprise admins**: Centralized visibility and control over which MCP Servers can be used within the organization - **For MCP clients**: Automatic token acquisition without user interaction for each server Closes: [#628](https://github.com/modelcontextprotocol/go-sdk/issues/628) --------- Co-authored-by: Maciej Kisiel <mkisiel@google.com>
139 lines
4.1 KiB
Go
139 lines
4.1 KiB
Go
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
|
|
// Use of this source code is governed by an MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// The conformance client implements features required for MCP conformance testing.
|
|
// It mirrors the functionality of the TypeScript conformance client at
|
|
// https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/conformance/everything-client.ts
|
|
|
|
//go:build mcp_go_client_oauth
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"github.com/modelcontextprotocol/go-sdk/auth"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
"github.com/modelcontextprotocol/go-sdk/oauthex"
|
|
)
|
|
|
|
func init() {
|
|
authScenarios := []string{
|
|
"auth/2025-03-26-oauth-metadata-backcompat",
|
|
"auth/2025-03-26-oauth-endpoint-fallback",
|
|
"auth/basic-cimd",
|
|
"auth/metadata-default",
|
|
"auth/metadata-var1",
|
|
"auth/metadata-var2",
|
|
"auth/metadata-var3",
|
|
"auth/pre-registration",
|
|
"auth/resource-mismatch",
|
|
"auth/scope-from-www-authenticate",
|
|
"auth/scope-from-scopes-supported",
|
|
"auth/scope-omitted-when-undefined",
|
|
"auth/scope-step-up",
|
|
"auth/scope-retry-limit",
|
|
"auth/token-endpoint-auth-basic",
|
|
"auth/token-endpoint-auth-post",
|
|
"auth/token-endpoint-auth-none",
|
|
}
|
|
for _, scenario := range authScenarios {
|
|
registerScenario(scenario, runAuthClient)
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Auth scenarios
|
|
// ============================================================================
|
|
|
|
func fetchAuthorizationCodeAndState(ctx context.Context, args *auth.AuthorizationArgs) (*auth.AuthorizationResult, error) {
|
|
client := &http.Client{
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
},
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, "GET", args.URL, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// In conformance tests the authorization server immediately redirects
|
|
// to the callback URL with the authorization code and state.
|
|
locURL, err := url.Parse(resp.Header.Get("Location"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse location: %v", err)
|
|
}
|
|
|
|
return &auth.AuthorizationResult{
|
|
Code: locURL.Query().Get("code"),
|
|
State: locURL.Query().Get("state"),
|
|
}, nil
|
|
}
|
|
|
|
func runAuthClient(ctx context.Context, serverURL string, configCtx map[string]any) error {
|
|
authConfig := &auth.AuthorizationCodeHandlerConfig{
|
|
RedirectURL: "http://localhost:3000/callback",
|
|
AuthorizationCodeFetcher: fetchAuthorizationCodeAndState,
|
|
// Try client ID metadata document based registration.
|
|
ClientIDMetadataDocumentConfig: &auth.ClientIDMetadataDocumentConfig{
|
|
URL: "https://conformance-test.local/client-metadata.json",
|
|
},
|
|
// Try dynamic client registration.
|
|
DynamicClientRegistrationConfig: &auth.DynamicClientRegistrationConfig{
|
|
Metadata: &oauthex.ClientRegistrationMetadata{
|
|
RedirectURIs: []string{"http://localhost:3000/callback"},
|
|
},
|
|
},
|
|
}
|
|
// Try pre-registered client information if provided in the context.
|
|
if clientID, ok := configCtx["client_id"].(string); ok {
|
|
if clientSecret, ok := configCtx["client_secret"].(string); ok {
|
|
authConfig.PreregisteredClient = &oauthex.ClientCredentials{
|
|
ClientID: clientID,
|
|
ClientSecretAuth: &oauthex.ClientSecretAuth{
|
|
ClientSecret: clientSecret,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
authHandler, err := auth.NewAuthorizationCodeHandler(authConfig)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create auth handler: %w", err)
|
|
}
|
|
|
|
session, err := connectToServer(ctx, serverURL, withOAuthHandler(authHandler))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer session.Close()
|
|
|
|
if _, err := session.ListTools(ctx, nil); err != nil {
|
|
return fmt.Errorf("session.ListTools(): %v", err)
|
|
}
|
|
|
|
if _, err := session.CallTool(ctx, &mcp.CallToolParams{
|
|
Name: "test-tool",
|
|
Arguments: map[string]any{},
|
|
}); err != nil {
|
|
return fmt.Errorf("session.CallTool('test-tool'): %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func withOAuthHandler(handler auth.OAuthHandler) connectOption {
|
|
return func(c *connectConfig) {
|
|
c.oauthHandler = handler
|
|
}
|
|
}
|