// Copy into a disposable KeyCape src/internal/authclient checkout. This uses // its native token endpoint/JWKS verifier, with a synthetic directory and seeded // authorization sessions. It does not prove upstream login or Kubernetes access. package authclient import ( "context" "crypto/sha256" "encoding/base64" "net/url" "os" "testing" "time" "keycape/internal/config" "keycape/internal/domain" "keycape/internal/server/oidc" ) type playwrightUsers struct{ users } func (playwrightUsers) LookupUser(context.Context, string) (*domain.User, error) { return &domain.User{ID: "user:playwright-test", Username: "test", Enabled: true, Tenant: "tenant:test"}, nil } func TestNetKingdomPlaywrightGeneratedRegistration(t *testing.T) { path := os.Getenv("PLAYWRIGHT_SYNTHETIC_CONFIG") if path == "" { t.Fatal("synthetic generated config required") } cfg, err := config.Load(path) if err != nil { t.Fatal("generated config did not load") } if len(config.ValidateConfig(cfg)) != 0 { t.Fatal("native config validation failed") } records, err := cfg.Registrations() if err != nil { t.Fatal("native registration conversion failed") } c, d, h := provider(t) h.Users = playwrightUsers{} h.ClientConfig = map[string]*domain.Client{} for i := range records { record := &records[i] if record.ClientType == "confidential" { record.ClientSecret = "synthetic-playwright-fixture" } h.ClientConfig[record.ClientID] = record } ctx := context.Background() agentID := "playwright-agent-test-agent" form := url.Values{"grant_type": {"client_credentials"}, "scope": {"playwright:connect"}} token, err := c.Exchange(ctx, d, form, agentID, "synthetic-playwright-fixture", "playwright-kubernetes", "") if err != nil { t.Fatal("generated service exchange failed") } claims, err := c.Verify(ctx, d, token.AccessToken, "playwright-kubernetes", "") if err != nil || claims["principal_type"] != "service" || claims["sub"] != "service:test-agent:platform" || claims["tenant"] != "tenant:test" { t.Fatal("service claim contract mismatch") } roles, ok := claims["roles"].([]any) if !ok || len(roles) != 1 || roles[0] != "coding-agent" { t.Fatal("agent role mismatch") } if token.ExpiresIn != 900 { t.Fatal("agent lifetime mismatch") } if _, err = c.Verify(ctx, d, token.AccessToken, "openbao", ""); err == nil { t.Fatal("wrong audience accepted") } if _, err = c.Verify(ctx, d, token.AccessToken+"tampered", "playwright-kubernetes", ""); err == nil { t.Fatal("tampered token accepted") } if _, err = c.Exchange(ctx, d, form, agentID, "wrong", "playwright-kubernetes", ""); err == nil { t.Fatal("wrong secret accepted") } form.Set("scope", "openbao:login") if _, err = c.Exchange(ctx, d, form, agentID, "synthetic-playwright-fixture", "playwright-kubernetes", ""); err == nil { t.Fatal("excess scope accepted") } verifier := "synthetic-playwright-verifier-with-enough-length-12345" digest := sha256.Sum256([]byte(verifier)) callback := "http://127.0.0.1:18764/callback" exchange := func(sentVerifier, sentCallback string) (Tokens, error) { code := h.Sessions.Create(&oidc.PKCESession{ClientID: "playwright-cli", Username: "test", Nonce: "synthetic-nonce", Scopes: []string{"openid", "profile", "groups", "playwright:connect"}, PKCEChallenge: base64.RawURLEncoding.EncodeToString(digest[:]), RedirectURI: callback, ExpiresAt: time.Now().Add(time.Minute)}) fields := url.Values{"grant_type": {"authorization_code"}, "client_id": {"playwright-cli"}, "code": {code}, "redirect_uri": {sentCallback}, "code_verifier": {sentVerifier}, "scope": {"openid playwright:connect"}} return c.Exchange(ctx, d, fields, "playwright-cli", "", "playwright-kubernetes", "synthetic-nonce") } human, err := exchange(verifier, callback) if err != nil { t.Fatal("public PKCE token exchange failed") } claims, err = c.Verify(ctx, d, human.AccessToken, "playwright-kubernetes", "") if err != nil || claims["principal_type"] != "human" || claims["sub"] != "user:playwright-test" || claims["tenant"] != "tenant:test" { t.Fatal("human claim contract mismatch") } if _, err = c.Verify(ctx, d, human.IDToken, "playwright-kubernetes", ""); err == nil { t.Fatal("ID token accepted as resource token") } if _, err = exchange("wrong-verifier", callback); err == nil { t.Fatal("wrong PKCE verifier accepted") } if _, err = exchange(verifier, "http://127.0.0.1:18765/callback"); err == nil { t.Fatal("wrong callback accepted") } delete(h.ClientConfig, agentID) form.Set("scope", "playwright:connect") if _, err = c.Exchange(ctx, d, form, agentID, "synthetic-playwright-fixture", "playwright-kubernetes", ""); err == nil { t.Fatal("removed registration still issues") } if _, err = c.Verify(ctx, d, token.AccessToken, "playwright-kubernetes", ""); err != nil { t.Fatal("issued JWT lifetime limitation changed") } }