registry/cmd/publisher/auth/github-oidc_internal_test.go
Radoslav Dimitrov 3f89fc2b1f
auth: bind GitHub OIDC token exchange to a per-deployment audience (#1229)
## What this does

The publisher CLI now derives the OIDC audience to request from the
`--registry` URL instead of using a fixed constant, and the server
validates the token's `aud` claim against a new per-deployment env var
(`MCP_REGISTRY_GITHUB_OIDC_AUDIENCE`).

Production sets this env var to
`https://registry.modelcontextprotocol.io` (and per-environment URL
elsewhere), so the deployment only accepts tokens that were requested
for it.

## Compatibility

This is a breaking change for older `mcp-publisher` binaries that still
request the legacy hardcoded audience. After this rolls to a deployment,
users on old `mcp-publisher` will see a clear `invalid audience:
expected …, got […]` error and need to upgrade. Users following the
documented `releases/latest` install pattern get the new binary
automatically.

## Test plan

- [ ] CI green
- [ ] Run a synthetic publish from a real GitHub Actions workflow
against staging after deploy
- [ ] Confirm an older `mcp-publisher` against the same staging deploy
returns the expected `invalid audience` error

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 20:27:26 -04:00

48 lines
1.4 KiB
Go

package auth
import "testing"
func TestAudienceFromRegistryURL(t *testing.T) {
tests := []struct {
input string
want string
wantErr bool
}{
// Canonical forms
{"https://registry.modelcontextprotocol.io", "https://registry.modelcontextprotocol.io", false},
{"https://staging.registry.modelcontextprotocol.io", "https://staging.registry.modelcontextprotocol.io", false},
// Trailing slash and path are stripped
{"https://registry.modelcontextprotocol.io/", "https://registry.modelcontextprotocol.io", false},
{"https://registry.modelcontextprotocol.io/api/v0", "https://registry.modelcontextprotocol.io", false},
// Host is lowercased
{"https://Registry.Example.COM", "https://registry.example.com", false},
// Whitespace is tolerated
{" https://registry.example ", "https://registry.example", false},
// Invalid inputs
{"", "", true},
{"registry.example", "", true}, // missing scheme
{"https://", "", true}, // missing host
{"://nothing", "", true}, // missing scheme
}
for _, tc := range tests {
t.Run(tc.input, func(t *testing.T) {
got, err := audienceFromRegistryURL(tc.input)
if tc.wantErr {
if err == nil {
t.Fatalf("expected error for %q, got %q", tc.input, got)
}
return
}
if err != nil {
t.Fatalf("unexpected error for %q: %v", tc.input, err)
}
if got != tc.want {
t.Errorf("audienceFromRegistryURL(%q) = %q, want %q", tc.input, got, tc.want)
}
})
}
}