fix: add involves edge from task to agent:nomos at creation
Plus sync vendor directory for Docker build compatibility.
This commit is contained in:
180
vendor/github.com/modelcontextprotocol/go-sdk/auth/auth.go
generated
vendored
Normal file
180
vendor/github.com/modelcontextprotocol/go-sdk/auth/auth.go
generated
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
// 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.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/oauthex"
|
||||
)
|
||||
|
||||
// TokenInfo holds information from a bearer token.
|
||||
type TokenInfo struct {
|
||||
Scopes []string
|
||||
Expiration time.Time
|
||||
// UserID is an optional identifier for the authenticated user.
|
||||
// If set by a TokenVerifier, it can be used by transports to prevent
|
||||
// session hijacking by ensuring that all requests for a given session
|
||||
// come from the same user.
|
||||
UserID string
|
||||
Extra map[string]any
|
||||
}
|
||||
|
||||
// The error that a TokenVerifier should return if the token cannot be verified.
|
||||
var ErrInvalidToken = errors.New("invalid token")
|
||||
|
||||
// The error that a TokenVerifier should return for OAuth-specific protocol errors.
|
||||
var ErrOAuth = errors.New("oauth error")
|
||||
|
||||
// A TokenVerifier checks the validity of a bearer token, and extracts information
|
||||
// from it. If verification fails, it should return an error that unwraps to ErrInvalidToken.
|
||||
// The HTTP request is provided in case verifying the token involves checking it.
|
||||
type TokenVerifier func(ctx context.Context, token string, req *http.Request) (*TokenInfo, error)
|
||||
|
||||
// RequireBearerTokenOptions are options for [RequireBearerToken].
|
||||
type RequireBearerTokenOptions struct {
|
||||
// The URL for the resource server metadata OAuth flow, to be returned as part
|
||||
// of the WWW-Authenticate header.
|
||||
ResourceMetadataURL string
|
||||
// The required scopes.
|
||||
Scopes []string
|
||||
}
|
||||
|
||||
type tokenInfoKey struct{}
|
||||
|
||||
// TokenInfoFromContext returns the [TokenInfo] stored in ctx, or nil if none.
|
||||
func TokenInfoFromContext(ctx context.Context) *TokenInfo {
|
||||
ti := ctx.Value(tokenInfoKey{})
|
||||
if ti == nil {
|
||||
return nil
|
||||
}
|
||||
return ti.(*TokenInfo)
|
||||
}
|
||||
|
||||
// RequireBearerToken returns a piece of middleware that verifies a bearer token using the verifier.
|
||||
// If verification succeeds, the [TokenInfo] is added to the request's context and the request proceeds.
|
||||
// If verification fails, the request fails with a 401 Unauthenticated, and the WWW-Authenticate header
|
||||
// is populated to enable [protected resource metadata].
|
||||
//
|
||||
// [protected resource metadata]: https://datatracker.ietf.org/doc/rfc9728
|
||||
func RequireBearerToken(verifier TokenVerifier, opts *RequireBearerTokenOptions) func(http.Handler) http.Handler {
|
||||
// Based on typescript-sdk/src/server/auth/middleware/bearerAuth.ts.
|
||||
|
||||
return func(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tokenInfo, errmsg, code := verify(r, verifier, opts)
|
||||
if code != 0 {
|
||||
if code == http.StatusUnauthorized || code == http.StatusForbidden {
|
||||
if opts != nil {
|
||||
var params []string
|
||||
if opts.ResourceMetadataURL != "" {
|
||||
params = append(params, fmt.Sprintf("resource_metadata=%q", opts.ResourceMetadataURL))
|
||||
}
|
||||
if len(opts.Scopes) > 0 {
|
||||
params = append(params, fmt.Sprintf("scope=%q", strings.Join(opts.Scopes, " ")))
|
||||
}
|
||||
if len(params) > 0 {
|
||||
w.Header().Add("WWW-Authenticate", "Bearer "+strings.Join(params, ", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
http.Error(w, errmsg, code)
|
||||
return
|
||||
}
|
||||
r = r.WithContext(context.WithValue(r.Context(), tokenInfoKey{}, tokenInfo))
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func verify(req *http.Request, verifier TokenVerifier, opts *RequireBearerTokenOptions) (_ *TokenInfo, errmsg string, code int) {
|
||||
// Extract bearer token.
|
||||
authHeader := req.Header.Get("Authorization")
|
||||
fields := strings.Fields(authHeader)
|
||||
if len(fields) != 2 || strings.ToLower(fields[0]) != "bearer" {
|
||||
return nil, "no bearer token", http.StatusUnauthorized
|
||||
}
|
||||
|
||||
// Verify the token and get information from it.
|
||||
tokenInfo, err := verifier(req.Context(), fields[1], req)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrInvalidToken) {
|
||||
return nil, err.Error(), http.StatusUnauthorized
|
||||
}
|
||||
if errors.Is(err, ErrOAuth) {
|
||||
return nil, err.Error(), http.StatusBadRequest
|
||||
}
|
||||
return nil, err.Error(), http.StatusInternalServerError
|
||||
}
|
||||
if tokenInfo == nil {
|
||||
return nil, "token validation failed", http.StatusInternalServerError
|
||||
}
|
||||
|
||||
// Check scopes. All must be present.
|
||||
if opts != nil {
|
||||
// Note: quadratic, but N is small.
|
||||
for _, s := range opts.Scopes {
|
||||
if !slices.Contains(tokenInfo.Scopes, s) {
|
||||
return nil, "insufficient scope", http.StatusForbidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check expiration.
|
||||
if tokenInfo.Expiration.IsZero() {
|
||||
return nil, "token missing expiration", http.StatusUnauthorized
|
||||
}
|
||||
if tokenInfo.Expiration.Before(time.Now()) {
|
||||
return nil, "token expired", http.StatusUnauthorized
|
||||
}
|
||||
return tokenInfo, "", 0
|
||||
}
|
||||
|
||||
// ProtectedResourceMetadataHandler returns an http.Handler that serves OAuth 2.0
|
||||
// protected resource metadata (RFC 9728) with CORS support.
|
||||
//
|
||||
// This handler allows cross-origin requests from any origin (Access-Control-Allow-Origin: *)
|
||||
// because OAuth metadata is public information intended for client discovery (RFC 9728 §3.1).
|
||||
// The metadata contains only non-sensitive configuration data about authorization servers
|
||||
// and supported scopes.
|
||||
//
|
||||
// No validation of metadata fields is performed; ensure metadata accuracy at configuration time.
|
||||
//
|
||||
// For more sophisticated CORS policies or to restrict origins, wrap this handler with a
|
||||
// CORS middleware like github.com/rs/cors or github.com/jub0bs/cors.
|
||||
func ProtectedResourceMetadataHandler(metadata *oauthex.ProtectedResourceMetadata) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Set CORS headers for cross-origin client discovery.
|
||||
// OAuth metadata is public information, so allowing any origin is safe.
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
// Handle CORS preflight requests
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
// Only GET allowed for metadata retrieval
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(metadata); err != nil {
|
||||
http.Error(w, "Failed to encode metadata", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
535
vendor/github.com/modelcontextprotocol/go-sdk/auth/authorization_code.go
generated
vendored
Normal file
535
vendor/github.com/modelcontextprotocol/go-sdk/auth/authorization_code.go
generated
vendored
Normal file
@@ -0,0 +1,535 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/internal/util"
|
||||
"github.com/modelcontextprotocol/go-sdk/oauthex"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// 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.
|
||||
// See https://client.dev/ for more information.
|
||||
type ClientIDMetadataDocumentConfig struct {
|
||||
// URL is the client identifier URL as per
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document-00#section-3.
|
||||
URL string
|
||||
}
|
||||
|
||||
// DynamicClientRegistrationConfig is used to configure dynamic client registration per
|
||||
// https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#dynamic-client-registration.
|
||||
type DynamicClientRegistrationConfig struct {
|
||||
// Metadata to be used in dynamic client registration request as per
|
||||
// https://datatracker.ietf.org/doc/html/rfc7591#section-2.
|
||||
//
|
||||
// If Metadata.ApplicationType is empty, it will be inferred from
|
||||
// Metadata.RedirectURIs. When set, it will be validated against the inferred type
|
||||
// and an error will be returned if they conflict.
|
||||
Metadata *oauthex.ClientRegistrationMetadata
|
||||
}
|
||||
|
||||
// AuthorizationResult is the result of an authorization flow.
|
||||
// It is returned by [AuthorizationCodeHandler].AuthorizationCodeFetcher implementations.
|
||||
type AuthorizationResult struct {
|
||||
// Code is the authorization code obtained from the authorization server.
|
||||
Code string
|
||||
// State string returned by the authorization server.
|
||||
State string
|
||||
}
|
||||
|
||||
// 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.
|
||||
// It is attempted in the following order:
|
||||
// 1. Client ID Metadata Document
|
||||
// 2. Preregistration
|
||||
// 3. Dynamic Client Registration
|
||||
// At least one method must be configured.
|
||||
ClientIDMetadataDocumentConfig *ClientIDMetadataDocumentConfig
|
||||
PreregisteredClient *oauthex.ClientCredentials
|
||||
DynamicClientRegistrationConfig *DynamicClientRegistrationConfig
|
||||
|
||||
// RedirectURL is a required URL to redirect to after authorization.
|
||||
// The caller is responsible for handling the redirect out of band.
|
||||
//
|
||||
// If Dynamic Client Registration is used:
|
||||
// - this field is permitted to be empty, in which case it will be set
|
||||
// to the first redirect URI from
|
||||
// DynamicClientRegistrationConfig.Metadata.RedirectURIs.
|
||||
// - if the field is not empty, it must be one of the redirect URIs in
|
||||
// DynamicClientRegistrationConfig.Metadata.RedirectURIs.
|
||||
RedirectURL string
|
||||
|
||||
// AuthorizationCodeFetcher is a required function called to initiate the authorization flow.
|
||||
// See [AuthorizationCodeFetcher] for details.
|
||||
AuthorizationCodeFetcher AuthorizationCodeFetcher
|
||||
|
||||
// Client is an optional HTTP client to use for HTTP requests.
|
||||
// It is used for the following requests:
|
||||
// - Fetching Protected Resource Metadata
|
||||
// - Fetching Authorization Server Metadata
|
||||
// - Registering a client dynamically
|
||||
// - Exchanging an authorization code for an access token
|
||||
// - Refreshing an access token
|
||||
// Custom clients can include additional security configurations,
|
||||
// such as SSRF protections, see
|
||||
// https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices#server-side-request-forgery-ssrf
|
||||
// If not provided, http.DefaultClient will be used.
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
// AuthorizationCodeHandler is an implementation of [OAuthHandler] that uses
|
||||
// the authorization code flow to obtain access tokens.
|
||||
type AuthorizationCodeHandler struct {
|
||||
config *AuthorizationCodeHandlerConfig
|
||||
|
||||
// tokenSource is the token source to use for authorization.
|
||||
tokenSource oauth2.TokenSource
|
||||
}
|
||||
|
||||
var _ OAuthHandler = (*AuthorizationCodeHandler)(nil)
|
||||
|
||||
func (h *AuthorizationCodeHandler) TokenSource(ctx context.Context) (oauth2.TokenSource, error) {
|
||||
return h.tokenSource, nil
|
||||
}
|
||||
|
||||
// NewAuthorizationCodeHandler creates a new AuthorizationCodeHandler.
|
||||
// It performs validation of the configuration and returns an error if it is invalid.
|
||||
// The passed config is consumed by the handler and should not be modified after.
|
||||
func NewAuthorizationCodeHandler(config *AuthorizationCodeHandlerConfig) (*AuthorizationCodeHandler, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config must be provided")
|
||||
}
|
||||
if config.ClientIDMetadataDocumentConfig == nil &&
|
||||
config.PreregisteredClient == nil &&
|
||||
config.DynamicClientRegistrationConfig == nil {
|
||||
return nil, errors.New("at least one client registration configuration must be provided")
|
||||
}
|
||||
if config.AuthorizationCodeFetcher == nil {
|
||||
return nil, errors.New("AuthorizationCodeFetcher is required")
|
||||
}
|
||||
if config.ClientIDMetadataDocumentConfig != nil && !isNonRootHTTPSURL(config.ClientIDMetadataDocumentConfig.URL) {
|
||||
return nil, fmt.Errorf("client ID metadata document URL must be a non-root HTTPS URL")
|
||||
}
|
||||
if config.PreregisteredClient != nil {
|
||||
if err := config.PreregisteredClient.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid PreregisteredClient configuration: %w", err)
|
||||
}
|
||||
}
|
||||
dCfg := config.DynamicClientRegistrationConfig
|
||||
if dCfg != nil {
|
||||
if dCfg.Metadata == nil {
|
||||
return nil, errors.New("dynamic client registration requires non-nil Metadata")
|
||||
}
|
||||
if len(dCfg.Metadata.RedirectURIs) == 0 {
|
||||
return nil, errors.New("Metadata.RedirectURIs is required for dynamic client registration")
|
||||
}
|
||||
if config.RedirectURL == "" {
|
||||
config.RedirectURL = dCfg.Metadata.RedirectURIs[0]
|
||||
} else if !slices.Contains(dCfg.Metadata.RedirectURIs, config.RedirectURL) {
|
||||
return nil, fmt.Errorf("RedirectURL %q is not in the list of allowed redirect URIs for dynamic client registration", config.RedirectURL)
|
||||
}
|
||||
applicationType := inferApplicationType(dCfg.Metadata.RedirectURIs)
|
||||
if dCfg.Metadata.ApplicationType == "" {
|
||||
dCfg.Metadata.ApplicationType = applicationType
|
||||
} else if dCfg.Metadata.ApplicationType != applicationType {
|
||||
return nil, fmt.Errorf("application type %q conflicts with the application type inferred from redirect URIs", dCfg.Metadata.ApplicationType)
|
||||
}
|
||||
}
|
||||
if config.RedirectURL == "" {
|
||||
// If the RedirectURL was supposed to be set by the dynamic client registration,
|
||||
// it should have been set by now. Otherwise, it is required.
|
||||
return nil, errors.New("RedirectURL is required")
|
||||
}
|
||||
if config.Client == nil {
|
||||
config.Client = http.DefaultClient
|
||||
}
|
||||
return &AuthorizationCodeHandler{config: config}, nil
|
||||
}
|
||||
|
||||
func isNonRootHTTPSURL(u string) bool {
|
||||
pu, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return pu.Scheme == "https" && pu.Path != ""
|
||||
}
|
||||
|
||||
// inferApplicationType returns an application type based on the redirect URIs.
|
||||
func inferApplicationType(redirectURIs []string) string {
|
||||
hasNative := false
|
||||
hasWeb := false
|
||||
for _, uri := range redirectURIs {
|
||||
u, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
switch u.Scheme {
|
||||
case "http", "https":
|
||||
if util.IsLoopback(u.Hostname()) {
|
||||
hasNative = true
|
||||
} else {
|
||||
hasWeb = true
|
||||
}
|
||||
default:
|
||||
hasNative = true
|
||||
}
|
||||
}
|
||||
|
||||
if hasNative && hasWeb {
|
||||
return ""
|
||||
}
|
||||
if hasNative {
|
||||
return "native"
|
||||
}
|
||||
return "web"
|
||||
}
|
||||
|
||||
// Authorize performs the authorization flow.
|
||||
// It is designed to perform the whole Authorization Code Grant flow.
|
||||
// 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 {
|
||||
return fmt.Errorf("failed to parse WWW-Authenticate header: %v", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusForbidden && errorFromChallenges(wwwChallenges) != "insufficient_scope" {
|
||||
// We only want to perform step-up authorization for insufficient_scope errors.
|
||||
// Returning nil, so that the call is retried immediately and the response
|
||||
// is handled appropriately by the connection.
|
||||
// Step-up authorization is defined at
|
||||
// https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#step-up-authorization-flow
|
||||
return nil
|
||||
}
|
||||
|
||||
prm, err := h.getProtectedResourceMetadata(ctx, wwwChallenges, req.URL.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
asm, err := GetAuthServerMetadata(ctx, prm.AuthorizationServers[0], h.config.Client)
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scps := scopesFromChallenges(wwwChallenges)
|
||||
if len(scps) == 0 && len(prm.ScopesSupported) > 0 {
|
||||
scps = prm.ScopesSupported
|
||||
}
|
||||
|
||||
cfg := &oauth2.Config{
|
||||
ClientID: resolvedClientConfig.clientID,
|
||||
ClientSecret: resolvedClientConfig.clientSecret,
|
||||
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: asm.AuthorizationEndpoint,
|
||||
TokenURL: asm.TokenEndpoint,
|
||||
AuthStyle: resolvedClientConfig.authStyle,
|
||||
},
|
||||
RedirectURL: h.config.RedirectURL,
|
||||
Scopes: scps,
|
||||
}
|
||||
|
||||
authRes, err := h.getAuthorizationCode(ctx, cfg, prm.Resource)
|
||||
if err != nil {
|
||||
// Purposefully leaving the error unwrappable so it can be handled by the caller.
|
||||
return err
|
||||
}
|
||||
|
||||
return h.exchangeAuthorizationCode(ctx, cfg, authRes, prm.Resource)
|
||||
}
|
||||
|
||||
// resourceMetadataURLFromChallenges returns a resource metadata URL from the given "WWW-Authenticate" header challenges,
|
||||
// or the empty string if there is none.
|
||||
func resourceMetadataURLFromChallenges(cs []oauthex.Challenge) string {
|
||||
for _, c := range cs {
|
||||
if u := c.Params["resource_metadata"]; u != "" {
|
||||
return u
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// scopesFromChallenges returns the scopes from the given "WWW-Authenticate" header challenges.
|
||||
// It only looks at challenges with the "Bearer" scheme.
|
||||
func scopesFromChallenges(cs []oauthex.Challenge) []string {
|
||||
for _, c := range cs {
|
||||
if c.Scheme == "bearer" && c.Params["scope"] != "" {
|
||||
return strings.Fields(c.Params["scope"])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// errorFromChallenges returns the error from the given "WWW-Authenticate" header challenges.
|
||||
// It only looks at challenges with the "Bearer" scheme.
|
||||
func errorFromChallenges(cs []oauthex.Challenge) string {
|
||||
for _, c := range cs {
|
||||
if c.Scheme == "bearer" && c.Params["error"] != "" {
|
||||
return c.Params["error"]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getProtectedResourceMetadata returns the protected resource metadata.
|
||||
// If no metadata was found or the fetched metadata fails security checks,
|
||||
// it returns an error.
|
||||
func (h *AuthorizationCodeHandler) getProtectedResourceMetadata(ctx context.Context, wwwChallenges []oauthex.Challenge, mcpServerURL string) (*oauthex.ProtectedResourceMetadata, error) {
|
||||
// Use MCP server URL as the resource URI per
|
||||
// https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#canonical-server-uri.
|
||||
for _, url := range protectedResourceMetadataURLs(resourceMetadataURLFromChallenges(wwwChallenges), mcpServerURL) {
|
||||
prm, err := oauthex.GetProtectedResourceMetadata(ctx, url.URL, url.Resource, h.config.Client)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if prm == nil {
|
||||
continue
|
||||
}
|
||||
if len(prm.AuthorizationServers) == 0 {
|
||||
// If we found PRM, we enforce the 2025-11-25 spec and not search further.
|
||||
return nil, fmt.Errorf("protected resource metadata has no authorization servers specified")
|
||||
}
|
||||
return prm, nil
|
||||
}
|
||||
// Fallback to 2025-03-26 spec MCP server root is the Authorization Server:
|
||||
// https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#server-metadata-discovery
|
||||
u, err := url.Parse(mcpServerURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse MCP server URL: %v", err)
|
||||
}
|
||||
u.Path = ""
|
||||
prm := &oauthex.ProtectedResourceMetadata{
|
||||
AuthorizationServers: []string{u.String()},
|
||||
Resource: mcpServerURL,
|
||||
}
|
||||
return prm, nil
|
||||
}
|
||||
|
||||
type prmURL struct {
|
||||
// URL represents a URL where Protected Resource Metadata may be retrieved.
|
||||
URL string
|
||||
// Resource represents the corresponding resource URL for [URL].
|
||||
// It is required to perform validation described in RFC 9728, section 3.3.
|
||||
Resource string
|
||||
}
|
||||
|
||||
// protectedResourceMetadataURLs returns a list of URLs to try when looking for
|
||||
// protected resource metadata as mandated by the MCP specification:
|
||||
// https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#protected-resource-metadata-discovery-requirements
|
||||
func protectedResourceMetadataURLs(metadataURL, resourceURL string) []prmURL {
|
||||
var urls []prmURL
|
||||
if metadataURL != "" {
|
||||
urls = append(urls, prmURL{
|
||||
URL: metadataURL,
|
||||
Resource: resourceURL,
|
||||
})
|
||||
}
|
||||
ru, err := url.Parse(resourceURL)
|
||||
if err != nil {
|
||||
return urls
|
||||
}
|
||||
mu := *ru
|
||||
// "At the path of the server's MCP endpoint".
|
||||
mu.Path = "/.well-known/oauth-protected-resource/" + strings.TrimLeft(ru.Path, "/")
|
||||
urls = append(urls, prmURL{
|
||||
URL: mu.String(),
|
||||
Resource: resourceURL,
|
||||
})
|
||||
// "At the root".
|
||||
mu.Path = "/.well-known/oauth-protected-resource"
|
||||
ru.Path = ""
|
||||
urls = append(urls, prmURL{
|
||||
URL: mu.String(),
|
||||
Resource: ru.String(),
|
||||
})
|
||||
return urls
|
||||
}
|
||||
|
||||
type registrationType int
|
||||
|
||||
const (
|
||||
registrationTypeClientIDMetadataDocument registrationType = iota
|
||||
registrationTypePreregistered
|
||||
registrationTypeDynamic
|
||||
)
|
||||
|
||||
type resolvedClientConfig struct {
|
||||
registrationType registrationType
|
||||
clientID string
|
||||
clientSecret string
|
||||
authStyle oauth2.AuthStyle
|
||||
}
|
||||
|
||||
func selectTokenAuthMethod(supported []string) oauth2.AuthStyle {
|
||||
prefOrder := []string{
|
||||
// Preferred in OAuth 2.1 draft: https://www.ietf.org/archive/id/draft-ietf-oauth-v2-1-14.html#name-client-secret.
|
||||
"client_secret_post",
|
||||
"client_secret_basic",
|
||||
}
|
||||
for _, method := range prefOrder {
|
||||
if slices.Contains(supported, method) {
|
||||
return authMethodToStyle(method)
|
||||
}
|
||||
}
|
||||
return oauth2.AuthStyleAutoDetect
|
||||
}
|
||||
|
||||
func authMethodToStyle(method string) oauth2.AuthStyle {
|
||||
switch method {
|
||||
case "client_secret_post":
|
||||
return oauth2.AuthStyleInParams
|
||||
case "client_secret_basic":
|
||||
return oauth2.AuthStyleInHeader
|
||||
case "none":
|
||||
// "none" is equivalent to "client_secret_post" but without sending client secret.
|
||||
return oauth2.AuthStyleInParams
|
||||
default:
|
||||
// "client_secret_basic" is the default per https://datatracker.ietf.org/doc/html/rfc7591#section-2.
|
||||
return oauth2.AuthStyleInHeader
|
||||
}
|
||||
}
|
||||
|
||||
// handleRegistration handles client registration.
|
||||
// The provided authorization server metadata must be non-nil.
|
||||
// Support for different registration methods is defined as follows:
|
||||
// - Client ID Metadata Document: metadata must have
|
||||
// `ClientIDMetadataDocumentSupported` set to true.
|
||||
// - Pre-registered client: assumed to be supported.
|
||||
// - Dynamic client registration: metadata must have
|
||||
// `RegistrationEndpoint` set to a non-empty value.
|
||||
func (h *AuthorizationCodeHandler) handleRegistration(ctx context.Context, asm *oauthex.AuthServerMeta) (*resolvedClientConfig, error) {
|
||||
// 1. Attempt to use Client ID Metadata Document (SEP-991).
|
||||
cimdCfg := h.config.ClientIDMetadataDocumentConfig
|
||||
if cimdCfg != nil && asm.ClientIDMetadataDocumentSupported {
|
||||
return &resolvedClientConfig{
|
||||
registrationType: registrationTypeClientIDMetadataDocument,
|
||||
clientID: cimdCfg.URL,
|
||||
}, nil
|
||||
}
|
||||
// 2. Attempt to use pre-registered client configuration.
|
||||
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: preCfg.ClientID,
|
||||
clientSecret: clientSecret,
|
||||
authStyle: authStyle,
|
||||
}, nil
|
||||
}
|
||||
// 3. Attempt to use dynamic client registration.
|
||||
dcrCfg := h.config.DynamicClientRegistrationConfig
|
||||
if dcrCfg != nil && asm.RegistrationEndpoint != "" {
|
||||
regResp, err := oauthex.RegisterClient(ctx, asm.RegistrationEndpoint, dcrCfg.Metadata, h.config.Client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to register client: %w", err)
|
||||
}
|
||||
cfg := &resolvedClientConfig{
|
||||
registrationType: registrationTypeDynamic,
|
||||
clientID: regResp.ClientID,
|
||||
clientSecret: regResp.ClientSecret,
|
||||
authStyle: authMethodToStyle(regResp.TokenEndpointAuthMethod),
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no configured client registration methods are supported by the authorization server")
|
||||
}
|
||||
|
||||
type authResult struct {
|
||||
*AuthorizationResult
|
||||
// usedCodeVerifier is the PKCE code verifier used to obtain the authorization code.
|
||||
// It is preserved for the token exchange step.
|
||||
usedCodeVerifier string
|
||||
}
|
||||
|
||||
// getAuthorizationCode uses the [AuthorizationCodeHandler.AuthorizationCodeFetcher]
|
||||
// to obtain an authorization code.
|
||||
func (h *AuthorizationCodeHandler) getAuthorizationCode(ctx context.Context, cfg *oauth2.Config, resourceURL string) (*authResult, error) {
|
||||
codeVerifier := oauth2.GenerateVerifier()
|
||||
state := rand.Text()
|
||||
|
||||
authURL := cfg.AuthCodeURL(state,
|
||||
oauth2.S256ChallengeOption(codeVerifier),
|
||||
oauth2.SetAuthURLParam("resource", resourceURL),
|
||||
)
|
||||
|
||||
authRes, err := h.config.AuthorizationCodeFetcher(ctx, &AuthorizationArgs{URL: authURL})
|
||||
if err != nil {
|
||||
// Purposefully leaving the error unwrappable so it can be handled by the caller.
|
||||
return nil, err
|
||||
}
|
||||
if authRes.State != state {
|
||||
return nil, fmt.Errorf("state mismatch")
|
||||
}
|
||||
return &authResult{
|
||||
AuthorizationResult: authRes,
|
||||
usedCodeVerifier: codeVerifier,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// exchangeAuthorizationCode exchanges the authorization code for a token
|
||||
// and stores it in a token source.
|
||||
func (h *AuthorizationCodeHandler) exchangeAuthorizationCode(ctx context.Context, cfg *oauth2.Config, authResult *authResult, resourceURL string) error {
|
||||
opts := []oauth2.AuthCodeOption{
|
||||
oauth2.VerifierOption(authResult.usedCodeVerifier),
|
||||
oauth2.SetAuthURLParam("resource", resourceURL),
|
||||
}
|
||||
clientCtx := context.WithValue(ctx, oauth2.HTTPClient, h.config.Client)
|
||||
token, err := cfg.Exchange(clientCtx, authResult.Code, opts...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("token exchange failed: %w", err)
|
||||
}
|
||||
h.tokenSource = cfg.TokenSource(clientCtx, token)
|
||||
return nil
|
||||
}
|
||||
40
vendor/github.com/modelcontextprotocol/go-sdk/auth/client.go
generated
vendored
Normal file
40
vendor/github.com/modelcontextprotocol/go-sdk/auth/client.go
generated
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
// 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.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// OAuthHandler is an interface for handling OAuth flows.
|
||||
//
|
||||
// If a transport wishes to support OAuth 2 authorization, it should support
|
||||
// being configured with an OAuthHandler. It should call the handler's
|
||||
// TokenSource method whenever it sends an HTTP request to set the
|
||||
// Authorization header. If a request fails with a 401 or 403, it should call
|
||||
// Authorize, and if that returns nil, it should retry the request. It should
|
||||
// not call Authorize after the second failure. See
|
||||
// [github.com/modelcontextprotocol/go-sdk/mcp.StreamableClientTransport]
|
||||
// for an example.
|
||||
type OAuthHandler interface {
|
||||
// TokenSource returns a token source to be used for outgoing requests.
|
||||
// Returned token source might be nil. In that case, the transport will not
|
||||
// add any authorization headers to the request.
|
||||
TokenSource(context.Context) (oauth2.TokenSource, error)
|
||||
|
||||
// Authorize is called when an HTTP request results in an error that may
|
||||
// be addressed by the authorization flow (currently 401 Unauthorized and 403 Forbidden).
|
||||
// It is responsible for performing the OAuth flow to obtain an access token.
|
||||
// The arguments are the request that failed and the response that was received for it.
|
||||
// The headers of the request are available, but the body will have already been consumed
|
||||
// when Authorize is called.
|
||||
// If the returned error is nil, TokenSource is expected to return a non-nil token source.
|
||||
// After a successful call to Authorize, the HTTP request will be retried by the transport.
|
||||
// The function is responsible for closing the response body.
|
||||
Authorize(context.Context, *http.Request, *http.Response) error
|
||||
}
|
||||
69
vendor/github.com/modelcontextprotocol/go-sdk/auth/shared.go
generated
vendored
Normal file
69
vendor/github.com/modelcontextprotocol/go-sdk/auth/shared.go
generated
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
// 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 contains shared utilities for OAuth handlers.
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/oauthex"
|
||||
)
|
||||
|
||||
// GetAuthServerMetadata 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.
|
||||
//
|
||||
// Returns (nil, nil) when no metadata endpoints respond (404s), allowing callers to implement
|
||||
// fallback logic. Returns an error for any non-client error (network failures, invalid JSON, etc.).
|
||||
func GetAuthServerMetadata(ctx context.Context, issuerURL string, httpClient *http.Client) (*oauthex.AuthServerMeta, error) {
|
||||
for _, metadataURL := range authorizationServerMetadataURLs(issuerURL) {
|
||||
asm, err := oauthex.GetAuthServerMeta(ctx, metadataURL, issuerURL, httpClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if asm != nil {
|
||||
return asm, nil
|
||||
}
|
||||
}
|
||||
return nil, 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
|
||||
}
|
||||
Reference in New Issue
Block a user