Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a1346b-72a4-7d40-b8bd-8b8ca346cfa2
120 lines
4.8 KiB
Go
120 lines
4.8 KiB
Go
// 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")
|
|
}
|
|
}
|