auth: allow passing custom http.Client to AuthorizationCodeHandler (#840)

This commit is contained in:
Maciej Kisiel 2026-03-13 10:06:58 +01:00 • committed by GitHub
commit d836ceb87d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 173 additions and 51 deletions

View file

@ -101,6 +101,19 @@ type AuthorizationCodeHandlerConfig struct {
// It should return the authorization code and state once the Authorization Server // It should return the authorization code and state once the Authorization Server
// redirects back to the RedirectURL. // redirects back to the RedirectURL.
AuthorizationCodeFetcher func(ctx context.Context, args *AuthorizationArgs) (*AuthorizationResult, error) AuthorizationCodeFetcher func(ctx context.Context, args *AuthorizationArgs) (*AuthorizationResult, error)
// 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 // AuthorizationCodeHandler is an implementation of [OAuthHandler] that uses
@ -166,6 +179,9 @@ func NewAuthorizationCodeHandler(config *AuthorizationCodeHandlerConfig) (*Autho
// it should have been set by now. Otherwise, it is required. // it should have been set by now. Otherwise, it is required.
return nil, errors.New("RedirectURL is required") return nil, errors.New("RedirectURL is required")
} }
if config.Client == nil {
config.Client = http.DefaultClient
}
return &AuthorizationCodeHandler{config: config}, nil return &AuthorizationCodeHandler{config: config}, nil
} }
@ -280,7 +296,7 @@ func (h *AuthorizationCodeHandler) getProtectedResourceMetadata(ctx context.Cont
// Use MCP server URL as the resource URI per // Use MCP server URL as the resource URI per
// https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#canonical-server-uri. // https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#canonical-server-uri.
for _, url := range protectedResourceMetadataURLs(resourceMetadataURLFromChallenges(wwwChallenges), mcpServerURL) { for _, url := range protectedResourceMetadataURLs(resourceMetadataURLFromChallenges(wwwChallenges), mcpServerURL) {
prm, err := oauthex.GetProtectedResourceMetadata(ctx, url.URL, url.Resource, http.DefaultClient) prm, err := oauthex.GetProtectedResourceMetadata(ctx, url.URL, url.Resource, h.config.Client)
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
continue continue
@ -359,7 +375,7 @@ func protectedResourceMetadataURLs(metadataURL, resourceURL string) []prmURL {
func (h *AuthorizationCodeHandler) getAuthServerMetadata(ctx context.Context, prm *oauthex.ProtectedResourceMetadata) (*oauthex.AuthServerMeta, error) { func (h *AuthorizationCodeHandler) getAuthServerMetadata(ctx context.Context, prm *oauthex.ProtectedResourceMetadata) (*oauthex.AuthServerMeta, error) {
authServerURL := prm.AuthorizationServers[0] authServerURL := prm.AuthorizationServers[0]
for _, u := range authorizationServerMetadataURLs(authServerURL) { for _, u := range authorizationServerMetadataURLs(authServerURL) {
asm, err := oauthex.GetAuthServerMeta(ctx, u, authServerURL, http.DefaultClient) asm, err := oauthex.GetAuthServerMeta(ctx, u, authServerURL, h.config.Client)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get authorization server metadata: %w", err) return nil, fmt.Errorf("failed to get authorization server metadata: %w", err)
} }
@ -488,7 +504,7 @@ func (h *AuthorizationCodeHandler) handleRegistration(ctx context.Context, asm *
// 3. Attempt to use dynamic client registration. // 3. Attempt to use dynamic client registration.
dcrCfg := h.config.DynamicClientRegistrationConfig dcrCfg := h.config.DynamicClientRegistrationConfig
if dcrCfg != nil && asm.RegistrationEndpoint != "" { if dcrCfg != nil && asm.RegistrationEndpoint != "" {
regResp, err := oauthex.RegisterClient(ctx, asm.RegistrationEndpoint, dcrCfg.Metadata, http.DefaultClient) regResp, err := oauthex.RegisterClient(ctx, asm.RegistrationEndpoint, dcrCfg.Metadata, h.config.Client)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to register client: %w", err) return nil, fmt.Errorf("failed to register client: %w", err)
} }
@ -542,10 +558,11 @@ func (h *AuthorizationCodeHandler) exchangeAuthorizationCode(ctx context.Context
oauth2.VerifierOption(authResult.usedCodeVerifier), oauth2.VerifierOption(authResult.usedCodeVerifier),
oauth2.SetAuthURLParam("resource", resourceURL), oauth2.SetAuthURLParam("resource", resourceURL),
} }
token, err := cfg.Exchange(ctx, authResult.Code, opts...) clientCtx := context.WithValue(ctx, oauth2.HTTPClient, h.config.Client)
token, err := cfg.Exchange(clientCtx, authResult.Code, opts...)
if err != nil { if err != nil {
return fmt.Errorf("token exchange failed: %w", err) return fmt.Errorf("token exchange failed: %w", err)
} }
h.tokenSource = cfg.TokenSource(ctx, token) h.tokenSource = cfg.TokenSource(clientCtx, token)
return nil return nil
} }

View file

@ -125,8 +125,11 @@ func TestAuthorize_ForbiddenUnhandledError(t *testing.T) {
"WWW-Authenticate", "WWW-Authenticate",
"Bearer error=invalid_token", "Bearer error=invalid_token",
) )
handler := &AuthorizationCodeHandler{} // No config needed for this test. handler, err := NewAuthorizationCodeHandler(validConfig())
err := handler.Authorize(t.Context(), req, resp) if err != nil {
t.Fatalf("NewAuthorizationCodeHandler failed: %v", err)
}
err = handler.Authorize(t.Context(), req, resp)
if err != nil { if err != nil {
t.Fatalf("Authorize() failed: %v", err) t.Fatalf("Authorize() failed: %v", err)
} }
@ -200,16 +203,7 @@ func TestNewAuthorizationCodeHandler_Success(t *testing.T) {
} }
func TestNewAuthorizationCodeHandler_Error(t *testing.T) { func TestNewAuthorizationCodeHandler_Error(t *testing.T) {
validConfig := func() *AuthorizationCodeHandlerConfig { // Ensure the base config is valid.
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
},
}
}
// Ensure the base config is valid
if _, err := NewAuthorizationCodeHandler(validConfig()); err != nil { if _, err := NewAuthorizationCodeHandler(validConfig()); err != nil {
t.Fatalf("NewAuthorizationCodeHandler failed: %v", err) t.Fatalf("NewAuthorizationCodeHandler failed: %v", err)
} }
@ -324,7 +318,11 @@ func TestNewAuthorizationCodeHandler_Error(t *testing.T) {
} }
func TestGetProtectedResourceMetadata_Success(t *testing.T) { func TestGetProtectedResourceMetadata_Success(t *testing.T) {
handler := &AuthorizationCodeHandler{} // No config needed for this method handler, err := NewAuthorizationCodeHandler(validConfig())
if err != nil {
t.Fatalf("NewAuthorizationCodeHandler() error = %v", err)
}
pathForChallenge := "/protected-resource" pathForChallenge := "/protected-resource"
tests := []struct { tests := []struct {
@ -398,8 +396,11 @@ func TestGetProtectedResourceMetadata_Success(t *testing.T) {
} }
func TestGetProtectedResourceMetadata_Backcompat(t *testing.T) { func TestGetProtectedResourceMetadata_Backcompat(t *testing.T) {
handler, err := NewAuthorizationCodeHandler(validConfig())
if err != nil {
t.Fatalf("NewAuthorizationCodeHandler() error = %v", err)
}
var challenges []oauthex.Challenge var challenges []oauthex.Challenge
handler := &AuthorizationCodeHandler{} // No config needed for this method
got, err := handler.getProtectedResourceMetadata(t.Context(), challenges, "http://localhost:1234/resource") got, err := handler.getProtectedResourceMetadata(t.Context(), challenges, "http://localhost:1234/resource")
if err != nil { if err != nil {
t.Fatalf("getProtectedResourceMetadata() error = %v", err) t.Fatalf("getProtectedResourceMetadata() error = %v", err)
@ -423,8 +424,11 @@ func TestGetProtectedResourceMetadata_Error(t *testing.T) {
ScopesSupported: []string{"read", "write"}, ScopesSupported: []string{"read", "write"},
} }
mux.Handle("/.well-known/oauth-protected-resource/resource", ProtectedResourceMetadataHandler(metadata)) 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 var challenges []oauthex.Challenge
handler := &AuthorizationCodeHandler{} // No config needed for this method
got, err := handler.getProtectedResourceMetadata(t.Context(), challenges, server.URL+"/resource") got, err := handler.getProtectedResourceMetadata(t.Context(), challenges, server.URL+"/resource")
if err == nil || !strings.Contains(err.Error(), "authorization servers") { if err == nil || !strings.Contains(err.Error(), "authorization servers") {
t.Errorf("getProtectedResourceMetadata() = %v, want error containing \"authorization servers\"", err) t.Errorf("getProtectedResourceMetadata() = %v, want error containing \"authorization servers\"", err)
@ -435,7 +439,10 @@ func TestGetProtectedResourceMetadata_Error(t *testing.T) {
} }
func TestGetAuthServerMetadata(t *testing.T) { func TestGetAuthServerMetadata(t *testing.T) {
handler := &AuthorizationCodeHandler{} // No config needed for this method handler, err := NewAuthorizationCodeHandler(validConfig())
if err != nil {
t.Fatalf("NewAuthorizationCodeHandler() error = %v", err)
}
tests := []struct { tests := []struct {
name string name string
@ -563,11 +570,11 @@ func TestHandleRegistration(t *testing.T) {
ClientIDMetadataDocumentSupported: true, ClientIDMetadataDocumentSupported: true,
}, },
handlerConfig: &AuthorizationCodeHandlerConfig{ handlerConfig: &AuthorizationCodeHandlerConfig{
ClientIDMetadataDocumentConfig: &ClientIDMetadataDocumentConfig{URL: "https://client.example.com"}, ClientIDMetadataDocumentConfig: &ClientIDMetadataDocumentConfig{URL: "https://client.example.com/metadata.json"},
}, },
want: &resolvedClientConfig{ want: &resolvedClientConfig{
registrationType: registrationTypeClientIDMetadataDocument, registrationType: registrationTypeClientIDMetadataDocument,
clientID: "https://client.example.com", clientID: "https://client.example.com/metadata.json",
}, },
}, },
{ {
@ -597,7 +604,7 @@ func TestHandleRegistration(t *testing.T) {
{ {
name: "NoneSupported", name: "NoneSupported",
handlerConfig: &AuthorizationCodeHandlerConfig{ handlerConfig: &AuthorizationCodeHandlerConfig{
ClientIDMetadataDocumentConfig: &ClientIDMetadataDocumentConfig{URL: "https://client.example.com"}, ClientIDMetadataDocumentConfig: &ClientIDMetadataDocumentConfig{URL: "https://client.example.com/metadata.json"},
}, },
wantError: true, wantError: true,
}, },
@ -607,7 +614,14 @@ func TestHandleRegistration(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
s := oauthtest.NewFakeAuthorizationServer(oauthtest.Config{RegistrationConfig: tt.serverConfig}) s := oauthtest.NewFakeAuthorizationServer(oauthtest.Config{RegistrationConfig: tt.serverConfig})
s.Start(t) s.Start(t)
handler := &AuthorizationCodeHandler{config: tt.handlerConfig} 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 := handler.getAuthServerMetadata(t.Context(), &oauthex.ProtectedResourceMetadata{ asm, err := handler.getAuthServerMetadata(t.Context(), &oauthex.ProtectedResourceMetadata{
AuthorizationServers: []string{s.URL()}, AuthorizationServers: []string{s.URL()},
}) })
@ -644,11 +658,20 @@ func TestDynamicRegistration(t *testing.T) {
}, },
}) })
s.Start(t) s.Start(t)
handler := &AuthorizationCodeHandler{config: &AuthorizationCodeHandlerConfig{ handler, err := NewAuthorizationCodeHandler(&AuthorizationCodeHandlerConfig{
DynamicClientRegistrationConfig: &DynamicClientRegistrationConfig{ DynamicClientRegistrationConfig: &DynamicClientRegistrationConfig{
Metadata: &oauthex.ClientRegistrationMetadata{}, 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 := handler.getAuthServerMetadata(t.Context(), &oauthex.ProtectedResourceMetadata{ asm, err := handler.getAuthServerMetadata(t.Context(), &oauthex.ProtectedResourceMetadata{
AuthorizationServers: []string{s.URL()}, AuthorizationServers: []string{s.URL()},
}) })
@ -672,3 +695,15 @@ func TestDynamicRegistration(t *testing.T) {
t.Errorf("handleRegistration() authStyle = %v, want %v", 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
},
}
}

View file

@ -13,6 +13,7 @@
1. [Security](#security) 1. [Security](#security)
1. [Confused Deputy](#confused-deputy) 1. [Confused Deputy](#confused-deputy)
1. [Token Passthrough](#token-passthrough) 1. [Token Passthrough](#token-passthrough)
1. [Server-Side Request Forgery (SSRF)](#server-side-request-forgery-(ssrf))
1. [Session Hijacking](#session-hijacking) 1. [Session Hijacking](#session-hijacking)
1. [Utilities](#utilities) 1. [Utilities](#utilities)
1. [Cancellation](#cancellation) 1. [Cancellation](#cancellation)
@ -363,7 +364,7 @@ the MCP spec's [Security Best Practices](https://modelcontextprotocol.io/specifi
### Confused Deputy ### Confused Deputy
The [mitigation](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#mitigation), The [mitigation](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices#mitigation),
obtaining user consent for dynamically registered clients, is mostly the obtaining user consent for dynamically registered clients, is mostly the
responsibility of the MCP Proxy server implementation. The SDK client does responsibility of the MCP Proxy server implementation. The SDK client does
generate cryptographically secure random `state` values for each authorization generate cryptographically secure random `state` values for each authorization
@ -372,15 +373,36 @@ Mismatched state values will result in an error.
### Token Passthrough ### Token Passthrough
The [mitigation](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#mitigation-2), accepting only tokens that were issued for the server, depends on the structure The [mitigation](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices#mitigation-2), accepting only tokens that were issued for the server, depends on the structure
of tokens and is the responsibility of the of tokens and is the responsibility of the
[`TokenVerifier`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/auth#TokenVerifier) [`TokenVerifier`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/auth#TokenVerifier)
provided to provided to
[`RequireBearerToken`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/auth#RequireBearerToken). [`RequireBearerToken`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/auth#RequireBearerToken).
### Server-Side Request Forgery (SSRF)
The [mitigations](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices#mitigation-3) are as follows:
- _Enforce HTTPS_. The OAuth helpers provided by the SDK reject the `http://` URLs
except loopback addresses (`localhost`, `127.0.0.1`, `::1`).
- _Block Private IP Ranges_. The OAuth helpers provided by the SDK allow passing
a custom `http.Client`. Developers are advised to customize the client it with
appropriate network protections, including IP range blocking. The SDK does not provide
this capability out of the box.
- _Validate Redirect Targets_. Similarly to previous point, customized `http.Client`
can be used to validate network hops. The SDK does not provide this capability out
of the box.
- _Use Egress Proxies_. This is out of scope for the SDK and can be configured separately.
- _DNS Resolution Considerations_. The SDK has DNS rebinding protection on the server side which is enabled by default. For the client side, consider providing
a custom `http.Client` that would implement DNS pinning.
### Session Hijacking ### Session Hijacking
The [mitigations](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#mitigation-3) are as follows: The [mitigations](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices#mitigation-4) are as follows:
- _Verify all inbound requests_. The [`RequireBearerToken`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/auth#RequireBearerToken) - _Verify all inbound requests_. The [`RequireBearerToken`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/auth#RequireBearerToken)
middleware function will verify all HTTP requests that it receives. It is the middleware function will verify all HTTP requests that it receives. It is the

View file

@ -289,7 +289,7 @@ the MCP spec's [Security Best Practices](https://modelcontextprotocol.io/specifi
### Confused Deputy ### Confused Deputy
The [mitigation](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#mitigation), The [mitigation](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices#mitigation),
obtaining user consent for dynamically registered clients, is mostly the obtaining user consent for dynamically registered clients, is mostly the
responsibility of the MCP Proxy server implementation. The SDK client does responsibility of the MCP Proxy server implementation. The SDK client does
generate cryptographically secure random `state` values for each authorization generate cryptographically secure random `state` values for each authorization
@ -298,15 +298,36 @@ Mismatched state values will result in an error.
### Token Passthrough ### Token Passthrough
The [mitigation](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#mitigation-2), accepting only tokens that were issued for the server, depends on the structure The [mitigation](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices#mitigation-2), accepting only tokens that were issued for the server, depends on the structure
of tokens and is the responsibility of the of tokens and is the responsibility of the
[`TokenVerifier`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/auth#TokenVerifier) [`TokenVerifier`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/auth#TokenVerifier)
provided to provided to
[`RequireBearerToken`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/auth#RequireBearerToken). [`RequireBearerToken`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/auth#RequireBearerToken).
### Server-Side Request Forgery (SSRF)
The [mitigations](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices#mitigation-3) are as follows:
- _Enforce HTTPS_. The OAuth helpers provided by the SDK reject the `http://` URLs
except loopback addresses (`localhost`, `127.0.0.1`, `::1`).
- _Block Private IP Ranges_. The OAuth helpers provided by the SDK allow passing
a custom `http.Client`. Developers are advised to customize the client it with
appropriate network protections, including IP range blocking. The SDK does not provide
this capability out of the box.
- _Validate Redirect Targets_. Similarly to previous point, customized `http.Client`
can be used to validate network hops. The SDK does not provide this capability out
of the box.
- _Use Egress Proxies_. This is out of scope for the SDK and can be configured separately.
- _DNS Resolution Considerations_. The SDK has DNS rebinding protection on the server side which is enabled by default. For the client side, consider providing
a custom `http.Client` that would implement DNS pinning.
### Session Hijacking ### Session Hijacking
The [mitigations](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#mitigation-3) are as follows: The [mitigations](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices#mitigation-4) are as follows:
- _Verify all inbound requests_. The [`RequireBearerToken`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/auth#RequireBearerToken) - _Verify all inbound requests_. The [`RequireBearerToken`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/auth#RequireBearerToken)
middleware function will verify all HTTP requests that it receives. It is the middleware function will verify all HTTP requests that it receives. It is the

View file

@ -14,9 +14,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"github.com/modelcontextprotocol/go-sdk/internal/util"
) )
// AuthServerMeta represents the metadata for an OAuth 2.0 authorization server, // AuthServerMeta represents the metadata for an OAuth 2.0 authorization server,
@ -136,13 +133,9 @@ type AuthServerMeta struct {
// //
// [RFC 8414]: https://tools.ietf.org/html/rfc8414 // [RFC 8414]: https://tools.ietf.org/html/rfc8414
func GetAuthServerMeta(ctx context.Context, metadataURL, issuer string, c *http.Client) (*AuthServerMeta, error) { func GetAuthServerMeta(ctx context.Context, metadataURL, issuer string, c *http.Client) (*AuthServerMeta, error) {
u, err := url.Parse(metadataURL)
if err != nil {
return nil, err
}
// Only allow HTTP for local addresses (testing or development purposes). // Only allow HTTP for local addresses (testing or development purposes).
if !util.IsLoopback(u.Host) && u.Scheme != "https" { if err := checkHTTPSOrLoopback(metadataURL); err != nil {
return nil, fmt.Errorf("metadataURL %q does not use HTTPS", metadataURL) return nil, fmt.Errorf("metadataURL: %v", err)
} }
asm, err := getJSON[AuthServerMeta](ctx, c, metadataURL, 1<<20) asm, err := getJSON[AuthServerMeta](ctx, c, metadataURL, 1<<20)
if err != nil { if err != nil {
@ -173,6 +166,8 @@ func GetAuthServerMeta(ctx context.Context, metadataURL, issuer string, c *http.
// validateAuthServerMetaURLs validates all URL fields in AuthServerMeta // validateAuthServerMetaURLs validates all URL fields in AuthServerMeta
// to ensure they don't use dangerous schemes that could enable XSS attacks. // to ensure they don't use dangerous schemes that could enable XSS attacks.
// It also validates that URLs likely to be called by the client use
// HTTPS or are loopback addresses.
func validateAuthServerMetaURLs(asm *AuthServerMeta) error { func validateAuthServerMetaURLs(asm *AuthServerMeta) error {
urls := []struct { urls := []struct {
name string name string
@ -194,5 +189,22 @@ func validateAuthServerMetaURLs(asm *AuthServerMeta) error {
return fmt.Errorf("%s: %w", u.name, err) return fmt.Errorf("%s: %w", u.name, err)
} }
} }
urls = []struct {
name string
value string
}{
{"authorization_endpoint", asm.AuthorizationEndpoint},
{"token_endpoint", asm.TokenEndpoint},
{"registration_endpoint", asm.RegistrationEndpoint},
{"introspection_endpoint", asm.IntrospectionEndpoint},
}
for _, u := range urls {
if err := checkHTTPSOrLoopback(u.value); err != nil {
return fmt.Errorf("%s: %w", u.name, err)
}
}
return nil return nil
} }

View file

@ -17,6 +17,8 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"github.com/modelcontextprotocol/go-sdk/internal/util"
) )
type httpStatusError struct { type httpStatusError struct {
@ -78,3 +80,17 @@ func checkURLScheme(u string) error {
} }
return nil return nil
} }
func checkHTTPSOrLoopback(addr string) error {
if addr == "" {
return nil
}
u, err := url.Parse(addr)
if err != nil {
return err
}
if !util.IsLoopback(u.Host) && u.Scheme != "https" {
return fmt.Errorf("URL %q does not use HTTPS or is not a loopback address", addr)
}
return nil
}

View file

@ -98,13 +98,9 @@ func resourceMetadataURL(cs []Challenge) string {
// - The authorization_servers field of the resulting metadata is checked for dangerous URL schemes. // - The authorization_servers field of the resulting metadata is checked for dangerous URL schemes.
func GetProtectedResourceMetadata(ctx context.Context, metadataURL, resourceURL string, c *http.Client) (_ *ProtectedResourceMetadata, err error) { func GetProtectedResourceMetadata(ctx context.Context, metadataURL, resourceURL string, c *http.Client) (_ *ProtectedResourceMetadata, err error) {
defer util.Wrapf(&err, "GetProtectedResourceMetadata(%q)", metadataURL) defer util.Wrapf(&err, "GetProtectedResourceMetadata(%q)", metadataURL)
u, err := url.Parse(metadataURL)
if err != nil {
return nil, err
}
// Only allow HTTP for local addresses (testing or development purposes). // Only allow HTTP for local addresses (testing or development purposes).
if !util.IsLoopback(u.Host) && u.Scheme != "https" { if err := checkHTTPSOrLoopback(metadataURL); err != nil {
return nil, fmt.Errorf("metadataURL %q does not use HTTPS", metadataURL) return nil, fmt.Errorf("metadataURL: %v", err)
} }
prm, err := getJSON[ProtectedResourceMetadata](ctx, c, metadataURL, 1<<20) prm, err := getJSON[ProtectedResourceMetadata](ctx, c, metadataURL, 1<<20)
if err != nil { if err != nil {
@ -115,9 +111,12 @@ func GetProtectedResourceMetadata(ctx context.Context, metadataURL, resourceURL
return nil, fmt.Errorf("got metadata resource %q, want %q", prm.Resource, resourceURL) return nil, fmt.Errorf("got metadata resource %q, want %q", prm.Resource, resourceURL)
} }
// Validate the authorization server URLs to prevent XSS attacks (see #526). // Validate the authorization server URLs to prevent XSS attacks (see #526).
for _, u := range prm.AuthorizationServers { for i, u := range prm.AuthorizationServers {
if err := checkURLScheme(u); err != nil { if err := checkURLScheme(u); err != nil {
return nil, err return nil, fmt.Errorf("authorization_servers[%d]: %v", i, err)
}
if err := checkHTTPSOrLoopback(u); err != nil {
return nil, fmt.Errorf("authorization_servers[%d]: %v", i, err)
} }
} }
return prm, nil return prm, nil