Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions auth/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,10 @@ type OAuthHandler interface {
// The function is responsible for closing the response body.
Authorize(context.Context, *http.Request, *http.Response) error
}

// OAuthHandlerBase is an embeddable type that satisfies the private method
// requirement of [OAuthHandler]. Extension packages should embed this type
// in their handler structs to implement OAuthHandler.
type OAuthHandlerBase struct{}

func (OAuthHandlerBase) isOAuthHandler() {}
196 changes: 196 additions & 0 deletions auth/enterprise_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// Copyright 2026 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.

// This file implements the client-side Enterprise Managed Authorization flow
// for MCP as specified in SEP-990.

//go:build mcp_go_client_oauth

package auth

import (
"context"
"fmt"
"net/http"

"github.com/modelcontextprotocol/go-sdk/oauthex"
"golang.org/x/oauth2"
)

// EnterpriseAuthConfig contains configuration for Enterprise Managed Authorization
// (SEP-990). This configures both the IdP (for token exchange) and the MCP Server
// (for JWT Bearer grant).
type EnterpriseAuthConfig struct {
// IdP configuration (where the user authenticates)
IdPIssuerURL string // e.g., "https://acme.okta.com"
IdPClientID string // MCP Client's ID at the IdP
IdPClientSecret string // MCP Client's secret at the IdP

// MCP Server configuration (the resource being accessed)
MCPAuthServerURL string // MCP Server's auth server issuer URL
MCPResourceURI string // MCP Server's resource identifier
MCPClientID string // MCP Client's ID at the MCP Server
MCPClientSecret string // MCP Client's secret at the MCP Server
MCPScopes []string // Requested scopes at the MCP Server

// Optional HTTP client for customization
HTTPClient *http.Client
}

// EnterpriseAuthFlow performs the complete Enterprise Managed Authorization flow:
// 1. Token Exchange: ID Token → ID-JAG at IdP
// 2. JWT Bearer: ID-JAG → Access Token at MCP Server
//
// This function takes an ID Token that was obtained via SSO (e.g., OIDC login)
// and exchanges it for an access token that can be used to call the MCP Server.
//
// There are two ways to obtain an ID Token for use with this function:
//
// Option 1: Use the OIDC login helper functions (full flow with SSO):
//
// // Step 1: Initiate OIDC login
// oidcConfig := &OIDCLoginConfig{
// IssuerURL: "https://acme.okta.com",
// ClientID: "client-id",
// RedirectURL: "http://localhost:8080/callback",
// Scopes: []string{"openid", "profile", "email"},
// }
// authReq, err := InitiateOIDCLogin(ctx, oidcConfig)
// if err != nil {
// log.Fatal(err)
// }
//
// // Step 2: Direct user to authReq.AuthURL for authentication
// fmt.Printf("Visit: %s\n", authReq.AuthURL)
//
// // Step 3: After redirect, complete login with authorization code
// tokens, err := CompleteOIDCLogin(ctx, oidcConfig, authCode, authReq.CodeVerifier)
// if err != nil {
// log.Fatal(err)
// }
//
// // Step 4: Use ID token for enterprise auth
// enterpriseConfig := &EnterpriseAuthConfig{
// IdPIssuerURL: "https://acme.okta.com",
// IdPClientID: "client-id-at-idp",
// IdPClientSecret: "secret-at-idp",
// MCPAuthServerURL: "https://auth.mcpserver.example",
// MCPResourceURI: "https://mcp.mcpserver.example",
// MCPClientID: "client-id-at-mcp",
// MCPClientSecret: "secret-at-mcp",
// MCPScopes: []string{"read", "write"},
// }
// accessToken, err := EnterpriseAuthFlow(ctx, enterpriseConfig, tokens.IDToken)
// if err != nil {
// log.Fatal(err)
// }
//
// Option 2: Bring your own ID Token (if you already have one):
//
// config := &EnterpriseAuthConfig{
// IdPIssuerURL: "https://acme.okta.com",
// IdPClientID: "client-id-at-idp",
// IdPClientSecret: "secret-at-idp",
// MCPAuthServerURL: "https://auth.mcpserver.example",
// MCPResourceURI: "https://mcp.mcpserver.example",
// MCPClientID: "client-id-at-mcp",
// MCPClientSecret: "secret-at-mcp",
// MCPScopes: []string{"read", "write"},
// }
//
// // If you already obtained an ID token through your own means
// accessToken, err := EnterpriseAuthFlow(ctx, config, myIDToken)
// if err != nil {
// log.Fatal(err)
// }
//
// // Use accessToken to call MCP Server APIs
func EnterpriseAuthFlow(
ctx context.Context,
config *EnterpriseAuthConfig,
idToken string,
) (*oauth2.Token, error) {
if config == nil {
return nil, fmt.Errorf("config is required")
}
if idToken == "" {
return nil, fmt.Errorf("idToken is required")
}
// Validate configuration
if config.IdPIssuerURL == "" {
return nil, fmt.Errorf("IdPIssuerURL is required")
}
if config.MCPAuthServerURL == "" {
return nil, fmt.Errorf("MCPAuthServerURL is required")
}
if config.MCPResourceURI == "" {
return nil, fmt.Errorf("MCPResourceURI is required")
}
httpClient := config.HTTPClient
if httpClient == nil {
httpClient = http.DefaultClient
}

// Step 1: Discover IdP token endpoint via OIDC discovery
idpMeta, err := GetAuthServerMetadatForIssuer(ctx, config.IdPIssuerURL, httpClient)
if err != nil {
return nil, fmt.Errorf("failed to discover IdP metadata: %w", err)
}

// Step 2: Token Exchange (ID Token → ID-JAG)
tokenExchangeReq := &oauthex.TokenExchangeRequest{
RequestedTokenType: oauthex.TokenTypeIDJAG,
Audience: config.MCPAuthServerURL,
Resource: config.MCPResourceURI,
Scope: config.MCPScopes,
SubjectToken: idToken,
SubjectTokenType: oauthex.TokenTypeIDToken,
}

tokenExchangeResp, err := oauthex.ExchangeToken(
ctx,
idpMeta.TokenEndpoint,
tokenExchangeReq,
config.IdPClientID,
config.IdPClientSecret,
httpClient,
)
if err != nil {
return nil, fmt.Errorf("token exchange failed: %w", err)
}

// Step 3: JWT Bearer Grant (ID-JAG → Access Token)
mcpMeta, err := GetAuthServerMetadatForIssuer(ctx, config.MCPAuthServerURL, httpClient)
if err != nil {
return nil, fmt.Errorf("failed to discover MCP auth server metadata: %w", err)
}

accessToken, err := oauthex.ExchangeJWTBearer(
ctx,
mcpMeta.TokenEndpoint,
tokenExchangeResp.AccessToken,
config.MCPClientID,
config.MCPClientSecret,
httpClient,
)
if err != nil {
return nil, fmt.Errorf("JWT bearer grant failed: %w", err)
}
return accessToken, nil
}

// GetAuthServerMetadatForIssuer fetches authorization server metadata for the given issuer URL.
// It tries standard well-known endpoints (OAuth 2.0 and OIDC) and returns the first successful result.
func GetAuthServerMetadatForIssuer(ctx context.Context, IssuerURL string, httpClient *httpClient) (*oauthex.AuthServerMeta, error) {
for _, metadataURL := range authorizationServerMetadataURLs(issuerURL) {
asm, err := oauthex.GetAuthServerMeta(ctx, metadataURL, issuerURL, httpClient)
if err != nil {
return nil, fmt.Errorf("failed to get authorization server metadata: %w", err)
}
if asm != nil {
return asm, nil
}
}
return nil, fmt.Errorf("no authorization server metadata found for %s", issuerURL)
}
Loading