`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>
623 lines
19 KiB
Go
623 lines
19 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.
|
|
|
|
//go:build mcp_go_client_oauth
|
|
|
|
package auth
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/http/httputil"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/modelcontextprotocol/go-sdk/internal/oauthtest"
|
|
"github.com/modelcontextprotocol/go-sdk/oauthex"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
func TestAuthorize(t *testing.T) {
|
|
authServer := oauthtest.NewFakeAuthorizationServer(oauthtest.Config{
|
|
RegistrationConfig: &oauthtest.RegistrationConfig{
|
|
PreregisteredClients: map[string]oauthtest.ClientInfo{
|
|
"test_client_id": {
|
|
Secret: "test_client_secret",
|
|
RedirectURIs: []string{"http://localhost:12345/callback"},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
authServer.Start(t)
|
|
|
|
resourceMux := http.NewServeMux()
|
|
resourceServer := httptest.NewServer(resourceMux)
|
|
t.Cleanup(resourceServer.Close)
|
|
resourceURL := resourceServer.URL + "/resource"
|
|
|
|
resourceMux.Handle("/.well-known/oauth-protected-resource/resource", ProtectedResourceMetadataHandler(&oauthex.ProtectedResourceMetadata{
|
|
Resource: resourceURL,
|
|
AuthorizationServers: []string{authServer.URL()},
|
|
}))
|
|
|
|
handler, err := NewAuthorizationCodeHandler(&AuthorizationCodeHandlerConfig{
|
|
RedirectURL: "http://localhost:12345/callback",
|
|
PreregisteredClient: &oauthex.ClientCredentials{
|
|
ClientID: "test_client_id",
|
|
ClientSecretAuth: &oauthex.ClientSecretAuth{
|
|
ClientSecret: "test_client_secret",
|
|
},
|
|
},
|
|
AuthorizationCodeFetcher: func(ctx context.Context, args *AuthorizationArgs) (*AuthorizationResult, error) {
|
|
// The fake authorization server will redirect to an URL with code and state.
|
|
client := &http.Client{
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
},
|
|
}
|
|
resp, err := client.Get(args.URL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to visit auth URL: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
dump, err := httputil.DumpResponse(resp, true)
|
|
if err != nil {
|
|
t.Fatalf("failed to dump response: %v", err)
|
|
}
|
|
t.Log(string(dump))
|
|
|
|
location, err := resp.Location()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get location header: %v", err)
|
|
}
|
|
return &AuthorizationResult{
|
|
Code: location.Query().Get("code"),
|
|
State: location.Query().Get("state"),
|
|
}, nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewAuthorizationCodeHandler failed: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, resourceURL, nil)
|
|
resp := &http.Response{
|
|
StatusCode: http.StatusUnauthorized,
|
|
Header: make(http.Header),
|
|
Body: http.NoBody,
|
|
Request: req,
|
|
}
|
|
resp.Header.Set(
|
|
"WWW-Authenticate",
|
|
"Bearer resource_metadata="+resourceServer.URL+"/.well-known/oauth-protected-resource/resource",
|
|
)
|
|
|
|
if err := handler.Authorize(context.Background(), req, resp); err != nil {
|
|
t.Fatalf("Authorize failed: %v", err)
|
|
}
|
|
|
|
tokenSource, err := handler.TokenSource(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("Failed to get token source: %v", err)
|
|
}
|
|
token, err := tokenSource.Token()
|
|
if err != nil {
|
|
t.Fatalf("Failed to get token: %v", err)
|
|
}
|
|
if token.AccessToken != "test_access_token" {
|
|
t.Errorf("Expected access token 'test_access_token', got '%s'", token.AccessToken)
|
|
}
|
|
}
|
|
|
|
func TestAuthorize_ForbiddenUnhandledError(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "http://example.com/resource", nil)
|
|
resp := &http.Response{
|
|
StatusCode: http.StatusForbidden,
|
|
Header: make(http.Header),
|
|
Body: http.NoBody,
|
|
Request: req,
|
|
}
|
|
resp.Header.Set(
|
|
"WWW-Authenticate",
|
|
"Bearer error=invalid_token",
|
|
)
|
|
handler, err := NewAuthorizationCodeHandler(validConfig())
|
|
if err != nil {
|
|
t.Fatalf("NewAuthorizationCodeHandler failed: %v", err)
|
|
}
|
|
err = handler.Authorize(t.Context(), req, resp)
|
|
if err != nil {
|
|
t.Fatalf("Authorize() failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNewAuthorizationCodeHandler_Success(t *testing.T) {
|
|
simpleHandler := func(ctx context.Context, args *AuthorizationArgs) (*AuthorizationResult, error) {
|
|
return nil, nil
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
config *AuthorizationCodeHandlerConfig
|
|
}{
|
|
{
|
|
name: "ClientIDMetadataDocumentConfig",
|
|
config: &AuthorizationCodeHandlerConfig{
|
|
ClientIDMetadataDocumentConfig: &ClientIDMetadataDocumentConfig{URL: "https://example.com/client"},
|
|
RedirectURL: "https://example.com/callback",
|
|
AuthorizationCodeFetcher: simpleHandler,
|
|
},
|
|
},
|
|
{
|
|
name: "PreregisteredClientConfig",
|
|
config: &AuthorizationCodeHandlerConfig{
|
|
PreregisteredClient: &oauthex.ClientCredentials{
|
|
ClientID: "test_client_id",
|
|
ClientSecretAuth: &oauthex.ClientSecretAuth{
|
|
ClientSecret: "test_client_secret",
|
|
},
|
|
},
|
|
RedirectURL: "https://example.com/callback",
|
|
AuthorizationCodeFetcher: simpleHandler,
|
|
},
|
|
},
|
|
{
|
|
name: "DynamicClientRegistrationConfig_NoRedirectURL",
|
|
config: &AuthorizationCodeHandlerConfig{
|
|
DynamicClientRegistrationConfig: &DynamicClientRegistrationConfig{
|
|
Metadata: &oauthex.ClientRegistrationMetadata{
|
|
RedirectURIs: []string{
|
|
"https://example.com/callback",
|
|
},
|
|
},
|
|
},
|
|
AuthorizationCodeFetcher: simpleHandler,
|
|
},
|
|
},
|
|
{
|
|
name: "DynamicClientRegistrationConfig_WithRedirectURL",
|
|
config: &AuthorizationCodeHandlerConfig{
|
|
DynamicClientRegistrationConfig: &DynamicClientRegistrationConfig{
|
|
Metadata: &oauthex.ClientRegistrationMetadata{
|
|
RedirectURIs: []string{
|
|
"https://example.com/callback",
|
|
},
|
|
},
|
|
},
|
|
RedirectURL: "https://example.com/callback",
|
|
AuthorizationCodeFetcher: simpleHandler,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if _, err := NewAuthorizationCodeHandler(tt.config); err != nil {
|
|
t.Fatalf("NewAuthorizationCodeHandler failed: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewAuthorizationCodeHandler_Error(t *testing.T) {
|
|
// Ensure the base config is valid.
|
|
if _, err := NewAuthorizationCodeHandler(validConfig()); err != nil {
|
|
t.Fatalf("NewAuthorizationCodeHandler failed: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
config func() *AuthorizationCodeHandlerConfig
|
|
}{
|
|
{
|
|
name: "NilConfig",
|
|
config: func() *AuthorizationCodeHandlerConfig {
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
name: "NoRegistrationConfig",
|
|
config: func() *AuthorizationCodeHandlerConfig {
|
|
cfg := validConfig()
|
|
cfg.ClientIDMetadataDocumentConfig = nil
|
|
cfg.PreregisteredClient = nil
|
|
cfg.DynamicClientRegistrationConfig = nil
|
|
return cfg
|
|
},
|
|
},
|
|
{
|
|
name: "MissingRedirectURL",
|
|
config: func() *AuthorizationCodeHandlerConfig {
|
|
cfg := validConfig()
|
|
cfg.RedirectURL = ""
|
|
return cfg
|
|
},
|
|
},
|
|
{
|
|
name: "MissingAuthorizationCodeFetcher",
|
|
config: func() *AuthorizationCodeHandlerConfig {
|
|
cfg := validConfig()
|
|
cfg.AuthorizationCodeFetcher = nil
|
|
return cfg
|
|
},
|
|
},
|
|
{
|
|
name: "InvalidMetadataURL",
|
|
config: func() *AuthorizationCodeHandlerConfig {
|
|
cfg := validConfig()
|
|
cfg.ClientIDMetadataDocumentConfig.URL = "https://example.com"
|
|
return cfg
|
|
},
|
|
},
|
|
{
|
|
name: "InvalidPreregistered_MissingSecretConfig",
|
|
config: func() *AuthorizationCodeHandlerConfig {
|
|
cfg := validConfig()
|
|
cfg.PreregisteredClient = &oauthex.ClientCredentials{}
|
|
return cfg
|
|
},
|
|
},
|
|
{
|
|
name: "InvalidPreregistered_EmptyID",
|
|
config: func() *AuthorizationCodeHandlerConfig {
|
|
cfg := validConfig()
|
|
cfg.PreregisteredClient = &oauthex.ClientCredentials{
|
|
ClientID: "",
|
|
ClientSecretAuth: &oauthex.ClientSecretAuth{
|
|
ClientSecret: "secret",
|
|
},
|
|
}
|
|
return cfg
|
|
},
|
|
},
|
|
{
|
|
name: "InvalidPreregistered_EmptySecret",
|
|
config: func() *AuthorizationCodeHandlerConfig {
|
|
cfg := validConfig()
|
|
cfg.PreregisteredClient = &oauthex.ClientCredentials{
|
|
ClientID: "test_client_id",
|
|
ClientSecretAuth: &oauthex.ClientSecretAuth{
|
|
ClientSecret: "",
|
|
},
|
|
}
|
|
return cfg
|
|
},
|
|
},
|
|
{
|
|
name: "InvalidDynamic_MissingMetadata",
|
|
config: func() *AuthorizationCodeHandlerConfig {
|
|
cfg := validConfig()
|
|
cfg.DynamicClientRegistrationConfig = &DynamicClientRegistrationConfig{}
|
|
return cfg
|
|
},
|
|
},
|
|
{
|
|
name: "InvalidDynamic_InconsistentRedirectURI",
|
|
config: func() *AuthorizationCodeHandlerConfig {
|
|
cfg := validConfig()
|
|
cfg.DynamicClientRegistrationConfig = &DynamicClientRegistrationConfig{
|
|
Metadata: &oauthex.ClientRegistrationMetadata{
|
|
RedirectURIs: []string{"https://example.com/callback1"},
|
|
},
|
|
}
|
|
cfg.RedirectURL = "https://example.com/callback2"
|
|
return cfg
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := NewAuthorizationCodeHandler(tt.config())
|
|
if err == nil {
|
|
t.Errorf("NewAuthorizationCodeHandler() = nil, want error")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetProtectedResourceMetadata_Success(t *testing.T) {
|
|
handler, err := NewAuthorizationCodeHandler(validConfig())
|
|
if err != nil {
|
|
t.Fatalf("NewAuthorizationCodeHandler() error = %v", err)
|
|
}
|
|
|
|
pathForChallenge := "/protected-resource"
|
|
|
|
tests := []struct {
|
|
name string
|
|
challengesProvided bool
|
|
// Path of the PRM endpoint.
|
|
prmPath string
|
|
// Path of the MCP server that is accessed.
|
|
mcpServerPath string
|
|
// Path for the Resource expected in the returned PRM.
|
|
resourcePath string
|
|
}{
|
|
{
|
|
name: "FromChallenges",
|
|
challengesProvided: true,
|
|
prmPath: pathForChallenge,
|
|
mcpServerPath: "/resource",
|
|
resourcePath: "/resource",
|
|
},
|
|
{
|
|
name: "FallbackToEndpoint",
|
|
challengesProvided: false,
|
|
prmPath: "/.well-known/oauth-protected-resource/resource",
|
|
mcpServerPath: "/resource",
|
|
resourcePath: "/resource",
|
|
},
|
|
{
|
|
name: "FallbackToRoot",
|
|
challengesProvided: false,
|
|
prmPath: "/.well-known/oauth-protected-resource",
|
|
mcpServerPath: "/resource",
|
|
resourcePath: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
server := httptest.NewServer(mux)
|
|
t.Cleanup(server.Close)
|
|
metadata := &oauthex.ProtectedResourceMetadata{
|
|
Resource: server.URL + tt.resourcePath,
|
|
AuthorizationServers: []string{"https://oauth.example.com"},
|
|
ScopesSupported: []string{"read", "write"},
|
|
}
|
|
mux.Handle(tt.prmPath, ProtectedResourceMetadataHandler(metadata))
|
|
var challenges []oauthex.Challenge
|
|
if tt.challengesProvided {
|
|
challenges = []oauthex.Challenge{
|
|
{
|
|
Scheme: "Bearer",
|
|
Params: map[string]string{
|
|
"resource_metadata": server.URL + pathForChallenge,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
got, err := handler.getProtectedResourceMetadata(t.Context(), challenges, server.URL+tt.mcpServerPath)
|
|
if err != nil {
|
|
t.Fatalf("getProtectedResourceMetadata() error = %v", err)
|
|
}
|
|
if got == nil {
|
|
t.Fatal("getProtectedResourceMetadata() got nil, want metadata")
|
|
}
|
|
if diff := cmp.Diff(metadata, got); diff != "" {
|
|
t.Errorf("getProtectedResourceMetadata() metadata mismatch (-want +got):\n%s", diff)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetProtectedResourceMetadata_Backcompat(t *testing.T) {
|
|
handler, err := NewAuthorizationCodeHandler(validConfig())
|
|
if err != nil {
|
|
t.Fatalf("NewAuthorizationCodeHandler() error = %v", err)
|
|
}
|
|
var challenges []oauthex.Challenge
|
|
got, err := handler.getProtectedResourceMetadata(t.Context(), challenges, "http://localhost:1234/resource")
|
|
if err != nil {
|
|
t.Fatalf("getProtectedResourceMetadata() error = %v", err)
|
|
}
|
|
wantPRM := &oauthex.ProtectedResourceMetadata{
|
|
Resource: "http://localhost:1234/resource",
|
|
AuthorizationServers: []string{"http://localhost:1234"},
|
|
}
|
|
if diff := cmp.Diff(wantPRM, got); diff != "" {
|
|
t.Errorf("getProtectedResourceMetadata() metadata mismatch (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
func TestGetProtectedResourceMetadata_Error(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
server := httptest.NewServer(mux)
|
|
t.Cleanup(server.Close)
|
|
metadata := &oauthex.ProtectedResourceMetadata{
|
|
Resource: server.URL + "/resource",
|
|
AuthorizationServers: nil, // Empty list is invalid
|
|
ScopesSupported: []string{"read", "write"},
|
|
}
|
|
mux.Handle("/.well-known/oauth-protected-resource/resource", ProtectedResourceMetadataHandler(metadata))
|
|
handler, err := NewAuthorizationCodeHandler(validConfig())
|
|
if err != nil {
|
|
t.Fatalf("NewAuthorizationCodeHandler() error = %v", err)
|
|
}
|
|
var challenges []oauthex.Challenge
|
|
got, err := handler.getProtectedResourceMetadata(t.Context(), challenges, server.URL+"/resource")
|
|
if err == nil || !strings.Contains(err.Error(), "authorization servers") {
|
|
t.Errorf("getProtectedResourceMetadata() = %v, want error containing \"authorization servers\"", err)
|
|
}
|
|
if got != nil {
|
|
t.Errorf("getProtectedResourceMetadata() = %+v, want nil", got)
|
|
}
|
|
}
|
|
|
|
func TestSelectTokenAuthMethod(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
supported []string
|
|
want oauth2.AuthStyle
|
|
}{
|
|
{
|
|
name: "PostPreferredOverBasic",
|
|
supported: []string{"client_secret_basic", "client_secret_post"},
|
|
want: oauth2.AuthStyleInParams,
|
|
},
|
|
{
|
|
name: "BasicChosenIfPostNotSupported",
|
|
supported: []string{"private_key_jwt", "client_secret_basic"},
|
|
want: oauth2.AuthStyleInHeader,
|
|
},
|
|
{
|
|
name: "NoneSupported",
|
|
supported: []string{"private_key_jwt"},
|
|
want: oauth2.AuthStyleAutoDetect,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := selectTokenAuthMethod(tt.supported)
|
|
if got != tt.want {
|
|
t.Errorf("selectTokenAuthMethod() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandleRegistration(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
serverConfig *oauthtest.RegistrationConfig
|
|
handlerConfig *AuthorizationCodeHandlerConfig
|
|
asm *oauthex.AuthServerMeta
|
|
want *resolvedClientConfig
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "ClientIDMetadataDocument",
|
|
serverConfig: &oauthtest.RegistrationConfig{
|
|
ClientIDMetadataDocumentSupported: true,
|
|
},
|
|
handlerConfig: &AuthorizationCodeHandlerConfig{
|
|
ClientIDMetadataDocumentConfig: &ClientIDMetadataDocumentConfig{URL: "https://client.example.com/metadata.json"},
|
|
},
|
|
want: &resolvedClientConfig{
|
|
registrationType: registrationTypeClientIDMetadataDocument,
|
|
clientID: "https://client.example.com/metadata.json",
|
|
},
|
|
},
|
|
{
|
|
name: "Preregistered",
|
|
serverConfig: &oauthtest.RegistrationConfig{
|
|
PreregisteredClients: map[string]oauthtest.ClientInfo{
|
|
"pre_client_id": {
|
|
Secret: "pre_client_secret",
|
|
},
|
|
},
|
|
},
|
|
handlerConfig: &AuthorizationCodeHandlerConfig{
|
|
PreregisteredClient: &oauthex.ClientCredentials{
|
|
ClientID: "pre_client_id",
|
|
ClientSecretAuth: &oauthex.ClientSecretAuth{
|
|
ClientSecret: "pre_client_secret",
|
|
},
|
|
},
|
|
},
|
|
want: &resolvedClientConfig{
|
|
registrationType: registrationTypePreregistered,
|
|
clientID: "pre_client_id",
|
|
clientSecret: "pre_client_secret",
|
|
authStyle: oauth2.AuthStyleInParams,
|
|
},
|
|
},
|
|
{
|
|
name: "NoneSupported",
|
|
handlerConfig: &AuthorizationCodeHandlerConfig{
|
|
ClientIDMetadataDocumentConfig: &ClientIDMetadataDocumentConfig{URL: "https://client.example.com/metadata.json"},
|
|
},
|
|
wantError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
s := oauthtest.NewFakeAuthorizationServer(oauthtest.Config{RegistrationConfig: tt.serverConfig})
|
|
s.Start(t)
|
|
tt.handlerConfig.AuthorizationCodeFetcher = func(ctx context.Context, args *AuthorizationArgs) (*AuthorizationResult, error) {
|
|
return nil, nil
|
|
}
|
|
tt.handlerConfig.RedirectURL = "https://example.com/callback"
|
|
handler, err := NewAuthorizationCodeHandler(tt.handlerConfig)
|
|
if err != nil {
|
|
t.Fatalf("NewAuthorizationCodeHandler() error = %v, want nil", err)
|
|
}
|
|
asm, err := GetAuthServerMetadata(t.Context(), s.URL(), http.DefaultClient)
|
|
if err != nil {
|
|
t.Fatalf("GetAuthServerMetadata() unexpected error = %v", err)
|
|
}
|
|
got, err := handler.handleRegistration(t.Context(), asm)
|
|
if err != nil {
|
|
if !tt.wantError {
|
|
t.Fatalf("handleRegistration() unexpected error = %v", err)
|
|
}
|
|
return
|
|
}
|
|
if got.registrationType != tt.want.registrationType {
|
|
t.Errorf("handleRegistration() registrationType = %v, want %v", got.registrationType, tt.want.registrationType)
|
|
}
|
|
if got.clientID != tt.want.clientID {
|
|
t.Errorf("handleRegistration() clientID = %q, want %q", got.clientID, tt.want.clientID)
|
|
}
|
|
if got.clientSecret != tt.want.clientSecret {
|
|
t.Errorf("handleRegistration() clientSecret = %q, want %q", got.clientSecret, tt.want.clientSecret)
|
|
}
|
|
if got.authStyle != tt.want.authStyle {
|
|
t.Errorf("handleRegistration() authStyle = %v, want %v", got.authStyle, tt.want.authStyle)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDynamicRegistration(t *testing.T) {
|
|
s := oauthtest.NewFakeAuthorizationServer(oauthtest.Config{
|
|
RegistrationConfig: &oauthtest.RegistrationConfig{
|
|
DynamicClientRegistrationEnabled: true,
|
|
},
|
|
})
|
|
s.Start(t)
|
|
handler, err := NewAuthorizationCodeHandler(&AuthorizationCodeHandlerConfig{
|
|
DynamicClientRegistrationConfig: &DynamicClientRegistrationConfig{
|
|
Metadata: &oauthex.ClientRegistrationMetadata{
|
|
RedirectURIs: []string{"https://example.com/callback"},
|
|
},
|
|
},
|
|
RedirectURL: "https://example.com/callback",
|
|
AuthorizationCodeFetcher: func(ctx context.Context, args *AuthorizationArgs) (*AuthorizationResult, error) {
|
|
return nil, nil
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewAuthorizationCodeHandler() error = %v", err)
|
|
}
|
|
asm, err := GetAuthServerMetadata(t.Context(), s.URL(), http.DefaultClient)
|
|
if err != nil {
|
|
t.Fatalf("GetAuthServerMetadata() unexpected error = %v", err)
|
|
}
|
|
got, err := handler.handleRegistration(t.Context(), asm)
|
|
if err != nil {
|
|
t.Fatalf("handleRegistration() error = %v, want nil", err)
|
|
}
|
|
if got.registrationType != registrationTypeDynamic {
|
|
t.Errorf("handleRegistration() registrationType = %v, want %v", got.registrationType, registrationTypeDynamic)
|
|
}
|
|
if got.clientID == "" {
|
|
t.Errorf("handleRegistration() clientID = %q, want non-empty", got.clientID)
|
|
}
|
|
if got.clientSecret == "" {
|
|
t.Errorf("handleRegistration() clientSecret = %q, want non-empty", got.clientSecret)
|
|
}
|
|
if got.authStyle != oauth2.AuthStyleInHeader {
|
|
t.Errorf("handleRegistration() authStyle = %v, want %v", got.authStyle, oauth2.AuthStyleInHeader)
|
|
}
|
|
}
|
|
|
|
// validConfig for test to create an AuthorizationCodeHandler using its constructor.
|
|
// Values that are relevant to the test should be set explicitly.
|
|
func validConfig() *AuthorizationCodeHandlerConfig {
|
|
return &AuthorizationCodeHandlerConfig{
|
|
ClientIDMetadataDocumentConfig: &ClientIDMetadataDocumentConfig{URL: "https://example.com/client"},
|
|
RedirectURL: "https://example.com/callback",
|
|
AuthorizationCodeFetcher: func(ctx context.Context, args *AuthorizationArgs) (*AuthorizationResult, error) {
|
|
return nil, nil
|
|
},
|
|
}
|
|
}
|