package wristband import ( "encoding/json" "net/http" "net/http/httptest" "slices" "testing" ) // TestMCPScopes (D-21, the framework side of the backend scope): the MCP // flavour's authorize allow-list is exactly Options.ScopesSupported. The // default server refuses backend; a server that lists it accepts "read // backend" and advertises it in its metadata; on either, openid and a scope // nobody lists stay refused. func TestMCPScopes(t *testing.T) { _, challenge := s256Pair(t) authorize := func(t *testing.T, s *Server, scope string) map[string]string { t.Helper() rec := httptest.NewRecorder() s.Authorize(rec, authorizeRequest(map[string]string{ "client_id": "cli-scopes", "redirect_uri": authorizeTestRedirect, "response_type": "code", "code_challenge": challenge, "code_challenge_method": "S256", "scope": scope, "state": "s", })) if rec.Code != http.StatusFound { t.Fatalf("%q: status = %d, want 302", scope, rec.Code) } return queryOf(t, rec.Header().Get("Location")) } plain, plainBackend := newAuthorizeTestServer() insertAuthorizeTestClient(plainBackend, "cli-scopes", []string{authorizeTestRedirect}, nil) listing := DefaultOptions() listing.Issuer = "https://issuer.example.test" listing.Resource = "https://resource.example.test/mcp" listing.ScopesSupported = append(listing.ScopesSupported, "backend") withBackend := NewServer(listing) backend := newMemoryBackend() withBackend.SetBackend(backend) insertAuthorizeTestClient(backend, "cli-scopes", []string{authorizeTestRedirect}, nil) t.Run("default server refuses backend", func(t *testing.T) { if q := authorize(t, plain, "read backend"); q["error"] != "invalid_scope" { t.Fatalf("default server = %v", q) } if q := authorize(t, plain, "read write"); q["error"] != "" { t.Fatalf("default server refused read write: %v", q) } }) t.Run("listing server accepts and advertises backend", func(t *testing.T) { if q := authorize(t, withBackend, "read backend"); q["error"] != "" { t.Fatalf("read backend = %v", q) } rec := httptest.NewRecorder() withBackend.Metadata(rec, httptest.NewRequest(http.MethodGet, "/.well-known/oauth-authorization-server", nil)) var doc struct { Scopes []string `json:"scopes_supported"` } if err := json.Unmarshal(rec.Body.Bytes(), &doc); err != nil || !slices.Contains(doc.Scopes, "backend") { t.Fatalf("scopes_supported = %v (%v)", doc.Scopes, err) } rec = httptest.NewRecorder() plain.Metadata(rec, httptest.NewRequest(http.MethodGet, "/.well-known/oauth-authorization-server", nil)) if err := json.Unmarshal(rec.Body.Bytes(), &doc); err != nil || slices.Contains(doc.Scopes, "backend") { t.Fatalf("default scopes_supported = %v (%v)", doc.Scopes, err) } }) t.Run("openid and unlisted scopes stay refused", func(t *testing.T) { for _, s := range []*Server{plain, withBackend} { for _, scope := range []string{"openid backend", "read openid", "read admin"} { if q := authorize(t, s, scope); q["error"] != "invalid_scope" { t.Fatalf("%q = %v", scope, q) } } } }) }