Enterprise managed authorization (#770)

`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>
This commit is contained in:
Pranav RK 2026-03-30 19:59:30 +05:30 • committed by GitHub
commit 2d43865b97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -11,6 +11,7 @@ import (
"crypto/rand"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"slices"
@ -20,18 +21,6 @@ import (
"golang.org/x/oauth2"
)
// ClientSecretAuthConfig is used to configure client authentication using client_secret.
// Authentication method will be selected based on the authorization server's supported methods,
// according to the following preference order:
// 1. client_secret_post
// 2. client_secret_basic
type ClientSecretAuthConfig struct {
// ClientID is the client ID to be used for client authentication.
ClientID string
// ClientSecret is the client secret to be used for client authentication.
ClientSecret string
}
// ClientIDMetadataDocumentConfig is used to configure the Client ID Metadata Document
// based client registration per
// https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#client-id-metadata-documents.
@ -42,14 +31,6 @@ type ClientIDMetadataDocumentConfig struct {
URL string
}
// PreregisteredClientConfig is used to configure a pre-registered client per
// https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#preregistration.
// Currently only "client_secret_basic" and "client_secret_post" authentication methods are supported.
type PreregisteredClientConfig struct {
// ClientSecretAuthConfig is the client_secret based configuration to be used for client authentication.
ClientSecretAuthConfig *ClientSecretAuthConfig
}
// DynamicClientRegistrationConfig is used to configure dynamic client registration per
// https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#dynamic-client-registration.
type DynamicClientRegistrationConfig struct {
@ -67,12 +48,18 @@ type AuthorizationResult struct {
State string
}
// AuthorizationArgs is the input to [AuthorizationCodeHandlerConfig].AuthorizationCodeFetcher.
// AuthorizationArgs is the input to [AuthorizationCodeFetcher].
type AuthorizationArgs struct {
// Authorization URL to be opened in a browser for the user to start the authorization process.
URL string
}
// AuthorizationCodeFetcher is called to initiate the OAuth authorization flow.
// It is responsible for directing the user to the authorization URL (e.g., opening
// in a browser) and returning the authorization code and state once the Authorization
// Server redirects back to the configured RedirectURL.
type AuthorizationCodeFetcher func(ctx context.Context, args *AuthorizationArgs) (*AuthorizationResult, error)
// AuthorizationCodeHandlerConfig is the configuration for [AuthorizationCodeHandler].
type AuthorizationCodeHandlerConfig struct {
// Client registration configuration.
@ -82,7 +69,7 @@ type AuthorizationCodeHandlerConfig struct {
// 3. Dynamic Client Registration
// At least one method must be configured.
ClientIDMetadataDocumentConfig *ClientIDMetadataDocumentConfig
PreregisteredClientConfig *PreregisteredClientConfig
PreregisteredClient *oauthex.ClientCredentials
DynamicClientRegistrationConfig *DynamicClientRegistrationConfig
// RedirectURL is a required URL to redirect to after authorization.
@ -97,10 +84,8 @@ type AuthorizationCodeHandlerConfig struct {
RedirectURL string
// AuthorizationCodeFetcher is a required function called to initiate the authorization flow.
// It is responsible for opening the URL in a browser for the user to start the authorization process.
// It should return the authorization code and state once the Authorization Server
// redirects back to the RedirectURL.
AuthorizationCodeFetcher func(ctx context.Context, args *AuthorizationArgs) (*AuthorizationResult, error)
// See [AuthorizationCodeFetcher] for details.
AuthorizationCodeFetcher AuthorizationCodeFetcher
// Client is an optional HTTP client to use for HTTP requests.
// It is used for the following requests:
@ -127,8 +112,6 @@ type AuthorizationCodeHandler struct {
var _ OAuthHandler = (*AuthorizationCodeHandler)(nil)
func (h *AuthorizationCodeHandler) isOAuthHandler() {}
func (h *AuthorizationCodeHandler) TokenSource(ctx context.Context) (oauth2.TokenSource, error) {
return h.tokenSource, nil
}
@ -141,7 +124,7 @@ func NewAuthorizationCodeHandler(config *AuthorizationCodeHandlerConfig) (*Autho
return nil, errors.New("config must be provided")
}
if config.ClientIDMetadataDocumentConfig == nil &&
config.PreregisteredClientConfig == nil &&
config.PreregisteredClient == nil &&
config.DynamicClientRegistrationConfig == nil {
return nil, errors.New("at least one client registration configuration must be provided")
}
@ -151,13 +134,9 @@ func NewAuthorizationCodeHandler(config *AuthorizationCodeHandlerConfig) (*Autho
if config.ClientIDMetadataDocumentConfig != nil && !isNonRootHTTPSURL(config.ClientIDMetadataDocumentConfig.URL) {
return nil, fmt.Errorf("client ID metadata document URL must be a non-root HTTPS URL")
}
preCfg := config.PreregisteredClientConfig
if preCfg != nil {
if preCfg.ClientSecretAuthConfig == nil {
return nil, errors.New("ClientSecretAuthConfig is required for pre-registered client")
}
if preCfg.ClientSecretAuthConfig.ClientID == "" || preCfg.ClientSecretAuthConfig.ClientSecret == "" {
return nil, fmt.Errorf("pre-registered client ID or secret is empty")
if config.PreregisteredClient != nil {
if err := config.PreregisteredClient.Validate(); err != nil {
return nil, fmt.Errorf("invalid PreregisteredClient configuration: %w", err)
}
}
dCfg := config.DynamicClientRegistrationConfig
@ -198,6 +177,7 @@ func isNonRootHTTPSURL(u string) bool {
// On success, [AuthorizationCodeHandler.TokenSource] will return a token source with the fetched token.
func (h *AuthorizationCodeHandler) Authorize(ctx context.Context, req *http.Request, resp *http.Response) error {
defer resp.Body.Close()
defer io.Copy(io.Discard, resp.Body)
wwwChallenges, err := oauthex.ParseWWWAuthenticate(resp.Header[http.CanonicalHeaderKey("WWW-Authenticate")])
if err != nil {
@ -218,9 +198,20 @@ func (h *AuthorizationCodeHandler) Authorize(ctx context.Context, req *http.Requ
return err
}
asm, err := h.getAuthServerMetadata(ctx, prm)
asm, err := GetAuthServerMetadata(ctx, prm.AuthorizationServers[0], h.config.Client)
if err != nil {
return err
return fmt.Errorf("failed to get authorization server metadata: %w", err)
}
if asm == nil {
// Fallback to 2025-03-26 spec: predefined endpoints.
// https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#fallbacks-for-servers-without-metadata-discovery
authServerURL := prm.AuthorizationServers[0]
asm = &oauthex.AuthServerMeta{
Issuer: authServerURL,
AuthorizationEndpoint: authServerURL + "/authorize",
TokenEndpoint: authServerURL + "/token",
RegistrationEndpoint: authServerURL + "/register",
}
}
resolvedClientConfig, err := h.handleRegistration(ctx, asm)
@ -365,70 +356,6 @@ func protectedResourceMetadataURLs(metadataURL, resourceURL string) []prmURL {
return urls
}
// getAuthServerMetadata returns the authorization server metadata.
// The provided Protected Resource Metadata must not be nil and must contain
// at least one authorization server.
// It returns an error if the metadata request fails with non-4xx HTTP status code
// or the fetched metadata fails security checks.
// If no metadata was found, it returns a minimal set of endpoints
// as a fallback to 2025-03-26 spec.
func (h *AuthorizationCodeHandler) getAuthServerMetadata(ctx context.Context, prm *oauthex.ProtectedResourceMetadata) (*oauthex.AuthServerMeta, error) {
authServerURL := prm.AuthorizationServers[0]
for _, u := range authorizationServerMetadataURLs(authServerURL) {
asm, err := oauthex.GetAuthServerMeta(ctx, u, authServerURL, h.config.Client)
if err != nil {
return nil, fmt.Errorf("failed to get authorization server metadata: %w", err)
}
if asm != nil {
return asm, nil
}
}
// Fallback to 2025-03-26 spec: predefined endpoints.
// https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#fallbacks-for-servers-without-metadata-discovery
asm := &oauthex.AuthServerMeta{
Issuer: authServerURL,
AuthorizationEndpoint: authServerURL + "/authorize",
TokenEndpoint: authServerURL + "/token",
RegistrationEndpoint: authServerURL + "/register",
}
return asm, nil
}
// authorizationServerMetadataURLs returns a list of URLs to try when looking for
// authorization server metadata as mandated by the MCP specification:
// https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#authorization-server-metadata-discovery.
func authorizationServerMetadataURLs(issuerURL string) []string {
var urls []string
baseURL, err := url.Parse(issuerURL)
if err != nil {
return nil
}
if baseURL.Path == "" {
// "OAuth 2.0 Authorization Server Metadata".
baseURL.Path = "/.well-known/oauth-authorization-server"
urls = append(urls, baseURL.String())
// "OpenID Connect Discovery 1.0".
baseURL.Path = "/.well-known/openid-configuration"
urls = append(urls, baseURL.String())
return urls
}
originalPath := baseURL.Path
// "OAuth 2.0 Authorization Server Metadata with path insertion".
baseURL.Path = "/.well-known/oauth-authorization-server/" + strings.TrimLeft(originalPath, "/")
urls = append(urls, baseURL.String())
// "OpenID Connect Discovery 1.0 with path insertion".
baseURL.Path = "/.well-known/openid-configuration/" + strings.TrimLeft(originalPath, "/")
urls = append(urls, baseURL.String())
// "OpenID Connect Discovery 1.0 with path appending".
baseURL.Path = "/" + strings.Trim(originalPath, "/") + "/.well-known/openid-configuration"
urls = append(urls, baseURL.String())
return urls
}
type registrationType int
const (
@ -491,13 +418,17 @@ func (h *AuthorizationCodeHandler) handleRegistration(ctx context.Context, asm *
}, nil
}
// 2. Attempt to use pre-registered client configuration.
pCfg := h.config.PreregisteredClientConfig
if pCfg != nil {
preCfg := h.config.PreregisteredClient
if preCfg != nil {
authStyle := selectTokenAuthMethod(asm.TokenEndpointAuthMethodsSupported)
clientSecret := ""
if preCfg.ClientSecretAuth != nil {
clientSecret = preCfg.ClientSecretAuth.ClientSecret
}
return &resolvedClientConfig{
registrationType: registrationTypePreregistered,
clientID: pCfg.ClientSecretAuthConfig.ClientID,
clientSecret: pCfg.ClientSecretAuthConfig.ClientSecret,
clientID: preCfg.ClientID,
clientSecret: clientSecret,
authStyle: authStyle,
}, nil
}