279 lines
8 KiB
Go
279 lines
8 KiB
Go
// Copyright 2026 The Go MCP SDK Authors. All rights reserved.
|
|
// Use of this source code is governed by the license
|
|
// that can be found in the LICENSE file.
|
|
|
|
package extauth
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/modelcontextprotocol/go-sdk/internal/oauthtest"
|
|
"github.com/modelcontextprotocol/go-sdk/oauthex"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
func validEnterpriseHandlerConfig() *EnterpriseHandlerConfig {
|
|
return &EnterpriseHandlerConfig{
|
|
IdPIssuerURL: "https://idp.example.com",
|
|
IdPCredentials: &oauthex.ClientCredentials{ClientID: "idp_client_id"},
|
|
MCPAuthServerURL: "https://mcp-auth.example.com",
|
|
MCPResourceURI: "https://mcp.example.com",
|
|
MCPCredentials: &oauthex.ClientCredentials{ClientID: "mcp_client_id"},
|
|
IDTokenFetcher: func(ctx context.Context) (*oauth2.Token, error) {
|
|
token := &oauth2.Token{AccessToken: "mock", TokenType: "Bearer"}
|
|
return token.WithExtra(map[string]any{"id_token": "mock_id_token"}), nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestNewEnterpriseHandler_Validation(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config *EnterpriseHandlerConfig
|
|
wantError string
|
|
}{
|
|
{
|
|
name: "nil config",
|
|
config: nil,
|
|
wantError: "config must be provided",
|
|
},
|
|
{
|
|
name: "missing IdPIssuerURL",
|
|
config: func() *EnterpriseHandlerConfig {
|
|
c := validEnterpriseHandlerConfig()
|
|
c.IdPIssuerURL = ""
|
|
return c
|
|
}(),
|
|
wantError: "IdPIssuerURL is required",
|
|
},
|
|
{
|
|
name: "nil IdPCredentials",
|
|
config: func() *EnterpriseHandlerConfig {
|
|
c := validEnterpriseHandlerConfig()
|
|
c.IdPCredentials = nil
|
|
return c
|
|
}(),
|
|
wantError: "IdPCredentials is required",
|
|
},
|
|
{
|
|
name: "invalid IdPCredentials - empty ClientID",
|
|
config: func() *EnterpriseHandlerConfig {
|
|
c := validEnterpriseHandlerConfig()
|
|
c.IdPCredentials = &oauthex.ClientCredentials{ClientID: ""}
|
|
return c
|
|
}(),
|
|
wantError: "invalid IdPCredentials",
|
|
},
|
|
{
|
|
name: "invalid IdPCredentials - empty ClientSecret in ClientSecretAuth",
|
|
config: func() *EnterpriseHandlerConfig {
|
|
c := validEnterpriseHandlerConfig()
|
|
c.IdPCredentials = &oauthex.ClientCredentials{
|
|
ClientID: "idp_client_id",
|
|
ClientSecretAuth: &oauthex.ClientSecretAuth{ClientSecret: ""},
|
|
}
|
|
return c
|
|
}(),
|
|
wantError: "invalid IdPCredentials",
|
|
},
|
|
{
|
|
name: "missing MCPAuthServerURL",
|
|
config: func() *EnterpriseHandlerConfig {
|
|
c := validEnterpriseHandlerConfig()
|
|
c.MCPAuthServerURL = ""
|
|
return c
|
|
}(),
|
|
wantError: "MCPAuthServerURL is required",
|
|
},
|
|
{
|
|
name: "missing MCPResourceURI",
|
|
config: func() *EnterpriseHandlerConfig {
|
|
c := validEnterpriseHandlerConfig()
|
|
c.MCPResourceURI = ""
|
|
return c
|
|
}(),
|
|
wantError: "MCPResourceURI is required",
|
|
},
|
|
{
|
|
name: "nil MCPCredentials",
|
|
config: func() *EnterpriseHandlerConfig {
|
|
c := validEnterpriseHandlerConfig()
|
|
c.MCPCredentials = nil
|
|
return c
|
|
}(),
|
|
wantError: "MCPCredentials is required",
|
|
},
|
|
{
|
|
name: "invalid MCPCredentials - empty ClientID",
|
|
config: func() *EnterpriseHandlerConfig {
|
|
c := validEnterpriseHandlerConfig()
|
|
c.MCPCredentials = &oauthex.ClientCredentials{ClientID: ""}
|
|
return c
|
|
}(),
|
|
wantError: "invalid MCPCredentials",
|
|
},
|
|
{
|
|
name: "missing IDTokenFetcher",
|
|
config: func() *EnterpriseHandlerConfig {
|
|
c := validEnterpriseHandlerConfig()
|
|
c.IDTokenFetcher = nil
|
|
return c
|
|
}(),
|
|
wantError: "IDTokenFetcher is required",
|
|
},
|
|
{
|
|
name: "valid public clients",
|
|
config: validEnterpriseHandlerConfig(),
|
|
wantError: "",
|
|
},
|
|
{
|
|
name: "valid confidential clients",
|
|
config: func() *EnterpriseHandlerConfig {
|
|
c := validEnterpriseHandlerConfig()
|
|
c.IdPCredentials.ClientSecretAuth = &oauthex.ClientSecretAuth{ClientSecret: "idp_secret"}
|
|
c.MCPCredentials.ClientSecretAuth = &oauthex.ClientSecretAuth{ClientSecret: "mcp_secret"}
|
|
return c
|
|
}(),
|
|
wantError: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
handler, err := NewEnterpriseHandler(tt.config)
|
|
if tt.wantError != "" {
|
|
if err == nil {
|
|
t.Fatalf("expected error containing %q, got nil", tt.wantError)
|
|
}
|
|
if !strings.Contains(err.Error(), tt.wantError) {
|
|
t.Fatalf("expected error containing %q, got %v", tt.wantError, err)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if handler == nil {
|
|
t.Fatal("expected handler to be non-nil")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEnterpriseHandler_Authorize_E2E(t *testing.T) {
|
|
const idJAGToken = "id-jag-token-from-idp"
|
|
|
|
idpServer := oauthtest.NewFakeIdPServer(oauthtest.IdPConfig{
|
|
PreregisteredClients: map[string]oauthtest.ClientInfo{
|
|
"idp_client_id": {Secret: "idp_secret"},
|
|
},
|
|
TokenExchangeConfig: &oauthtest.TokenExchangeConfig{
|
|
IDJAGToken: idJAGToken,
|
|
},
|
|
})
|
|
idpServer.Start(t)
|
|
|
|
mcpAuthServer := oauthtest.NewFakeAuthorizationServer(oauthtest.Config{
|
|
RegistrationConfig: &oauthtest.RegistrationConfig{
|
|
PreregisteredClients: map[string]oauthtest.ClientInfo{
|
|
"mcp_client_id": {Secret: "mcp_secret"},
|
|
},
|
|
},
|
|
JWTBearerConfig: &oauthtest.JWTBearerConfig{
|
|
ValidAssertions: []string{idJAGToken},
|
|
},
|
|
})
|
|
mcpAuthServer.Start(t)
|
|
|
|
config := validEnterpriseHandlerConfig()
|
|
config.IdPIssuerURL = idpServer.URL()
|
|
config.IdPCredentials.ClientSecretAuth = &oauthex.ClientSecretAuth{ClientSecret: "idp_secret"}
|
|
config.MCPAuthServerURL = mcpAuthServer.URL()
|
|
config.MCPCredentials.ClientSecretAuth = &oauthex.ClientSecretAuth{ClientSecret: "mcp_secret"}
|
|
config.MCPScopes = []string{"read", "write"}
|
|
config.IDTokenFetcher = func(ctx context.Context) (*oauth2.Token, error) {
|
|
token := &oauth2.Token{AccessToken: "mock_access_token", TokenType: "Bearer"}
|
|
return token.WithExtra(map[string]any{"id_token": "mock_id_token_from_user_login"}), nil
|
|
}
|
|
|
|
handler, err := NewEnterpriseHandler(config)
|
|
if err != nil {
|
|
t.Fatalf("NewEnterpriseHandler failed: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "https://mcp.example.com/api", nil)
|
|
resp := &http.Response{
|
|
StatusCode: http.StatusUnauthorized,
|
|
Header: make(http.Header),
|
|
Body: http.NoBody,
|
|
Request: req,
|
|
}
|
|
|
|
if err := handler.Authorize(context.Background(), req, resp); err != nil {
|
|
t.Fatalf("Authorize failed: %v", err)
|
|
}
|
|
|
|
tokenSource, err := handler.TokenSource(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("TokenSource failed: %v", err)
|
|
}
|
|
if tokenSource == nil {
|
|
t.Fatal("expected token source to be set after authorization")
|
|
}
|
|
|
|
token, err := tokenSource.Token()
|
|
if err != nil {
|
|
t.Fatalf("Token() failed: %v", err)
|
|
}
|
|
if token.AccessToken != "test_access_token" {
|
|
t.Errorf("AccessToken = %q, want %q", token.AccessToken, "test_access_token")
|
|
}
|
|
}
|
|
|
|
func TestEnterpriseHandler_Authorize_IDTokenFetcherError(t *testing.T) {
|
|
config := validEnterpriseHandlerConfig()
|
|
config.IDTokenFetcher = func(ctx context.Context) (*oauth2.Token, error) {
|
|
return nil, fmt.Errorf("user cancelled login")
|
|
}
|
|
|
|
handler, err := NewEnterpriseHandler(config)
|
|
if err != nil {
|
|
t.Fatalf("NewEnterpriseHandler failed: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "https://mcp.example.com/api", nil)
|
|
resp := &http.Response{
|
|
StatusCode: http.StatusUnauthorized,
|
|
Header: make(http.Header),
|
|
Body: http.NoBody,
|
|
Request: req,
|
|
}
|
|
|
|
err = handler.Authorize(context.Background(), req, resp)
|
|
if err == nil {
|
|
t.Fatal("expected error from Authorize, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "failed to obtain ID token") {
|
|
t.Errorf("error = %v, want error containing %q", err, "failed to obtain ID token")
|
|
}
|
|
}
|
|
|
|
func TestEnterpriseHandler_TokenSource_BeforeAuthorization(t *testing.T) {
|
|
handler, err := NewEnterpriseHandler(validEnterpriseHandlerConfig())
|
|
if err != nil {
|
|
t.Fatalf("NewEnterpriseHandler failed: %v", err)
|
|
}
|
|
|
|
tokenSource, err := handler.TokenSource(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("TokenSource failed: %v", err)
|
|
}
|
|
if tokenSource != nil {
|
|
t.Errorf("expected nil token source before authorization, got %v", tokenSource)
|
|
}
|
|
}
|