package oidc_test import ( "net/http" "net/http/httptest" "net/url" "testing" "keycape/internal/domain" "keycape/internal/server/oidc" ) // KEY-WP-0030. A client registration may declare the tenant its users' tokens // carry (humanTenant). That is safe for exactly one reason: registrations here // are static and deployment-owned. A self-service client able to name its users' // tenant would be a straightforward escalation — register a client, declare the // landlord zone, and any user who logs in through it is labelled with it. // // informed-decision asked for that caveat to be a condition of the capability // rather than a paragraph, so that a future change to registration policy has to // confront it. This test is that condition: it fails if the exclusion is ever // lifted while the capability is present, and its failure message says what to do // about it rather than merely reporting the endpoint. // // The two halves are asserted together on purpose. Whichever is removed first, // the test points at the other. func TestRegistrationBoundTenantRequiresStaticRegistration(t *testing.T) { // Half one: the capability exists. If this stops holding, the precondition // below is no longer load-bearing and this test can go with it. sessions := oidc.NewSessionStore() h, _ := newTokenHandler(t, sessions, &mockUserRepo{users: map[string]*domain.User{"alice": aliceUser()}}) h.ClientConfig["test-client"].Tenant = "tenant:platform" verifier := "test-verifier" code := seededSession(sessions, verifier) w := httptest.NewRecorder() h.ServeHTTP(w, codeExchange(t, url.Values{ "grant_type": {"authorization_code"}, "client_id": {"test-client"}, "code": {code}, "code_verifier": {verifier}, "redirect_uri": {seededRedirectURI}, })) if w.Code != http.StatusOK { t.Fatalf("client-declared tenant no longer issues (status %d): if the capability was "+ "removed deliberately, remove this test too — the dynamic-registration "+ "precondition below only exists to protect it", w.Code) } if got := parseJWTPayload(t, decodeTokenResponse(t, w.Body.String())["access_token"].(string))["tenant"]; got != "tenant:platform" { t.Fatalf("client-declared tenant resolved to %v, want tenant:platform", got) } // Half two: the precondition holds. Dynamic client registration must stay // absent while any client can declare a tenant. doc := discoveryDoc(t, oidc.DiscoveryConfig{ Issuer: "https://auth.netkingdom.local", AuthorizationEndpoint: "https://auth.netkingdom.local/authorize", TokenEndpoint: "https://auth.netkingdom.local/token", JWKSUri: "https://auth.netkingdom.local/jwks", }) if _, advertised := doc["registration_endpoint"]; advertised { t.Fatal("dynamic client registration is advertised while a client registration " + "may declare its users' tenant. That combination lets anyone who can register " + "a client relabel the users who log in through it into a zone of their choosing. " + "Resolve it deliberately: either drop the registration-bound tenant, or gate it " + "so only statically configured registrations may declare one. See " + "docs/tenant-claim-contract.md, 'How a human token's tenant is resolved'.") } }