## Summary - Add an exported \`oauthex.MatchesResource(claims []string, resource string) bool\` helper that compares an \`aud\` claim slice against an expected resource URL with the canonical-form (trailing slash) tolerance required by RFC 9728 and RFC 8707. - Tests cover the common shape combinations (canonical/trimmed in either direction, surrounding whitespace, multiple claims) plus negative cases ensuring scheme + path mismatches still fail. ## Motivation RFC 9728 §3.3 canonicalises the protected-resource identifier with a trailing slash, but RFC 8707 resource indicators frequently omit it. Upstream IdPs vary in which form they emit in the \`aud\` claim (Google trims, Auth0 retains, some clients round-trip whatever the IdP sent). Strict byte equality therefore fails routinely on legitimate setups. Resource servers writing this comparison by hand all converge on the same trim-trailing-slash + whitespace logic; exporting a canonical helper saves every consumer from re-deriving it. ## Test plan - [x] \`go test ./oauthex/... -run MatchesResource\` passes. - [x] Positive cases: exact match, trailing slash on either side, surrounding whitespace, multi-claim slice with one match. - [x] Negative cases: empty slice, no match, path mismatch, scheme mismatch (HTTPS vs HTTP must still fail). --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Guglielmo Colombo <guglielmoc@google.com>
101 lines
2.8 KiB
Go
101 lines
2.8 KiB
Go
// 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.
|
|
|
|
package oauthex
|
|
|
|
import "testing"
|
|
|
|
func TestMatchesResource(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
claims []string
|
|
resource string
|
|
want bool
|
|
}{
|
|
{
|
|
name: "exact match",
|
|
claims: []string{"https://mcp.example.com/"},
|
|
resource: "https://mcp.example.com/",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "claim has trailing slash, resource does not",
|
|
claims: []string{"https://mcp.example.com/"},
|
|
resource: "https://mcp.example.com",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "claim missing trailing slash, resource has one",
|
|
claims: []string{"https://mcp.example.com"},
|
|
resource: "https://mcp.example.com/",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "multiple claims, one matches",
|
|
claims: []string{"https://other.example.com/", "https://mcp.example.com"},
|
|
resource: "https://mcp.example.com/",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "no claims",
|
|
claims: nil,
|
|
resource: "https://mcp.example.com/",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "claim does not match resource",
|
|
claims: []string{"https://attacker.example.com/"},
|
|
resource: "https://mcp.example.com/",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "path mismatch is not tolerated",
|
|
claims: []string{"https://mcp.example.com/v2"},
|
|
resource: "https://mcp.example.com",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "scheme mismatch is not tolerated",
|
|
claims: []string{"http://mcp.example.com/"},
|
|
resource: "https://mcp.example.com/",
|
|
want: false,
|
|
},
|
|
// The following document the intentional boundaries: §6.2.3 also
|
|
// permits scheme/host case folding and default-port elision, but
|
|
// MatchesResource deliberately does NOT, so two distinct
|
|
// registered resources cannot collide via these normalizations.
|
|
{
|
|
name: "host case difference is not tolerated",
|
|
claims: []string{"https://MCP.example.com/"},
|
|
resource: "https://mcp.example.com/",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "default-port elision is not tolerated",
|
|
claims: []string{"https://mcp.example.com:443/"},
|
|
resource: "https://mcp.example.com/",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "query string difference is not tolerated",
|
|
claims: []string{"https://mcp.example.com/?x=y"},
|
|
resource: "https://mcp.example.com/",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "surrounding whitespace is not tolerated (malformed claim)",
|
|
claims: []string{" https://mcp.example.com/ "},
|
|
resource: "https://mcp.example.com/",
|
|
want: false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := MatchesResource(tt.claims, tt.resource)
|
|
if got != tt.want {
|
|
t.Errorf("MatchesResource(%v, %q) = %v, want %v", tt.claims, tt.resource, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|