67 lines
3.2 KiB
Go
67 lines
3.2 KiB
Go
|
|
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'.")
|
||
|
|
}
|
||
|
|
}
|