Skip to content
18 changes: 17 additions & 1 deletion internal/testing/fake_auth_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"net"
"net/http"
"time"

Expand Down Expand Up @@ -44,7 +45,22 @@ func NewFakeAuthMux() *http.ServeMux {
}

func (s *state) handleMetadata(w http.ResponseWriter, r *http.Request) {
issuer := "https://localhost:" + r.URL.Port()
// Derive the port from the request Host; r.URL.Port() is empty on server side.
hostPort := r.Host
port := ""

if hp := hostPort; hp != "" {
if _, p, err := net.SplitHostPort(hp); err == nil {
port = p
}
}

if port == "" {
// Fallback attempt; may still be empty depending on server/request.
port = r.URL.Port()
}

issuer := "https://localhost:" + port
metadata := map[string]any{
"issuer": issuer,
"authorization_endpoint": issuer + "/authorize",
Expand Down
16 changes: 16 additions & 0 deletions oauthex/auth_meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,19 @@ func GetAuthServerMeta(ctx context.Context, issuerURL string, c *http.Client) (*
}
return nil, fmt.Errorf("failed to get auth server metadata from %q: %w", issuerURL, errors.Join(errs...))
}

// RequirePKCE checks that the authorization server for issuerURL supports PKCE,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should have an exported function for this. Why can't we do it inside GetAuthServerMeta?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revised

// by verifying that CodeChallengeMethodsSupported is non-empty.
// It returns an error if metadata cannot be fetched or PKCE is not advertised.
func RequirePKCE(ctx context.Context, issuerURL string, c *http.Client) error {
asm, err := GetAuthServerMeta(ctx, issuerURL, c)
if err != nil {
return err
}

if len(asm.CodeChallengeMethodsSupported) == 0 {
return fmt.Errorf("authorization server at %s does not implement PKCE", issuerURL)
}

return nil
}
37 changes: 37 additions & 0 deletions oauthex/auth_meta_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@
package oauthex

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"testing"

itesting "github.com/modelcontextprotocol/go-sdk/internal/testing"
)

func TestAuthMetaParse(t *testing.T) {
Expand All @@ -28,3 +34,34 @@ func TestAuthMetaParse(t *testing.T) {
t.Errorf("got %q, want %q", g, w)
}
}

func TestRequirePKCE(t *testing.T) {
ctx := context.Background()

// Start a fake OAuth 2.1 auth server that advertises PKCE (S256).
orig := itesting.NewFakeAuthMux()
wrapper := http.NewServeMux()
wrapper.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
orig.ServeHTTP(w, r)
})
ts := httptest.NewTLSServer(wrapper)
defer ts.Close()

// Validate that the server supports PKCE per MCP auth requirements.
// The fake server sets issuer to https://localhost:<port>, so compute that issuer.
u, _ := url.Parse(ts.URL)
issuer := "https://localhost:" + u.Port()

// The fake server presents a cert for example.com; set ServerName accordingly.
httpClient := ts.Client()
if tr, ok := httpClient.Transport.(*http.Transport); ok {
clone := tr.Clone()
clone.TLSClientConfig.ServerName = "example.com"
httpClient.Transport = clone
}

if err := RequirePKCE(ctx, issuer, httpClient); err != nil {
t.Fatal(err)
}

}