统一认证使用 HttpOnly SameSite=Strict 会话 Cookie。配对时若 Public Base 与 Web Base 不属于同一 schemeful site,浏览器回调会丢失事务 Cookie。新增基于 Public Suffix List 的同站校验,并在运行时重建时重复门禁,防止旧持久化数据绕过。 验证:go test ./... -count=1(apps/api)
179 lines
6.5 KiB
Go
179 lines
6.5 KiB
Go
package identity
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestRevisionStateTransitions(t *testing.T) {
|
|
tests := []struct {
|
|
from, to RevisionState
|
|
allowed bool
|
|
}{
|
|
{RevisionDraft, RevisionValidated, true},
|
|
{RevisionDraft, RevisionFailed, true},
|
|
{RevisionValidated, RevisionActive, true},
|
|
{RevisionValidated, RevisionFailed, true},
|
|
{RevisionActive, RevisionSuperseded, true},
|
|
{RevisionSuperseded, RevisionValidated, true},
|
|
{RevisionSuperseded, RevisionActive, false},
|
|
{RevisionDraft, RevisionActive, false},
|
|
{RevisionFailed, RevisionActive, false},
|
|
{RevisionActive, RevisionValidated, false},
|
|
}
|
|
for _, test := range tests {
|
|
if got := CanTransition(test.from, test.to); got != test.allowed {
|
|
t.Errorf("CanTransition(%q, %q)=%v, want %v", test.from, test.to, got, test.allowed)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRevisionNeverSerializesSecretValues(t *testing.T) {
|
|
type secretFields interface {
|
|
MachineSecret() string
|
|
}
|
|
var _ = any((*Revision)(nil))
|
|
if _, exposesSecret := any((*Revision)(nil)).(secretFields); exposesSecret {
|
|
t.Fatal("Revision unexpectedly exposes a machine secret value")
|
|
}
|
|
revision := Revision{MachineCredentialRef: "identity-machine-example", SessionEncryptionKeyRef: "identity-session-example"}
|
|
if revision.MachineCredentialRef == "" || revision.SessionEncryptionKeyRef == "" {
|
|
t.Fatal("Revision must retain SecretStore references")
|
|
}
|
|
}
|
|
|
|
func TestValidatePairingInputDerivesExactGatewayURIs(t *testing.T) {
|
|
input := PairingInput{
|
|
AuthCenterURL: "https://auth.example.com", PublicBaseURL: "https://api.example.com",
|
|
WebBaseURL: "https://gateway.example.com", LocalTenantKey: "default",
|
|
}
|
|
metadata, err := input.ConsumerMetadata(true, "production")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if metadata.RedirectURIs[0] != "https://api.example.com/api/v1/auth/oidc/callback" ||
|
|
metadata.LogoutURIs[0] != "https://gateway.example.com/" ||
|
|
metadata.ReceiverEndpoint != "https://api.example.com/api/v1/security-events/ssf" {
|
|
t.Fatalf("unexpected derived metadata: %#v", metadata)
|
|
}
|
|
}
|
|
|
|
func TestValidatePairingInputRejectsRemoteHTTPAndURLCredentials(t *testing.T) {
|
|
for _, input := range []PairingInput{
|
|
{AuthCenterURL: "http://auth.example.com", PublicBaseURL: "https://api.example.com", WebBaseURL: "https://gateway.example.com", LocalTenantKey: "default"},
|
|
{AuthCenterURL: "https://user:password@auth.example.com", PublicBaseURL: "https://api.example.com", WebBaseURL: "https://gateway.example.com", LocalTenantKey: "default"},
|
|
} {
|
|
if _, err := input.ConsumerMetadata(false, "production"); err == nil {
|
|
t.Fatalf("unsafe pairing input accepted: %#v", input)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidatePairingInputRejectsCrossSiteBrowserEndpoints(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
appEnv string
|
|
input PairingInput
|
|
}{
|
|
{
|
|
name: "different registrable domains",
|
|
appEnv: "production",
|
|
input: PairingInput{
|
|
AuthCenterURL: "https://auth.example.com", PublicBaseURL: "https://api.example.com",
|
|
WebBaseURL: "https://gateway.example.net", LocalTenantKey: "default",
|
|
},
|
|
},
|
|
{
|
|
name: "different loopback sites",
|
|
appEnv: "test",
|
|
input: PairingInput{
|
|
AuthCenterURL: "http://127.0.0.1:18000", PublicBaseURL: "http://127.0.0.1:18088",
|
|
WebBaseURL: "http://localhost:15178", LocalTenantKey: "default",
|
|
},
|
|
},
|
|
{
|
|
name: "different schemes",
|
|
appEnv: "test",
|
|
input: PairingInput{
|
|
AuthCenterURL: "http://127.0.0.1:18000", PublicBaseURL: "http://localhost:18088",
|
|
WebBaseURL: "https://localhost:15178", LocalTenantKey: "default",
|
|
},
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if _, err := test.input.ConsumerMetadata(false, test.appEnv); err == nil {
|
|
t.Fatalf("cross-site browser endpoints were accepted: %#v", test.input)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateRevisionURLsRejectsCrossSiteBrowserEndpoints(t *testing.T) {
|
|
revision := Revision{
|
|
AuthCenterURL: "https://auth.example.com",
|
|
Issuer: "https://auth.example.com/issuer/shared",
|
|
PublicBaseURL: "https://api.example.com",
|
|
WebBaseURL: "https://gateway.example.net",
|
|
}
|
|
if err := ValidateRevisionURLs(revision, "production"); err == nil {
|
|
t.Fatal("cross-site persisted browser endpoints were accepted")
|
|
}
|
|
}
|
|
|
|
func TestNewDraftAppliesSessionDefaultsWithoutPersistingOnboardingCode(t *testing.T) {
|
|
draft, err := NewDraft(PairingInput{
|
|
AuthCenterURL: "https://auth.example.com", OnboardingCode: "onb1.must-never-be-persisted",
|
|
PublicBaseURL: "https://api.example.com", WebBaseURL: "https://gateway.example.com",
|
|
LocalTenantKey: "default", LegacyJWTEnabled: true,
|
|
}, "production")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if draft.State != RevisionDraft || draft.SessionIdleSeconds != 1800 || draft.SessionAbsoluteSeconds != 28800 || draft.SessionRefreshSeconds != 60 {
|
|
t.Fatalf("unexpected draft defaults: %#v", draft)
|
|
}
|
|
payload, _ := json.Marshal(draft)
|
|
if strings.Contains(string(payload), "must-never-be-persisted") || strings.Contains(string(payload), "onboardingCode") {
|
|
t.Fatalf("draft exposed onboarding code: %s", payload)
|
|
}
|
|
}
|
|
|
|
func TestNewDraftAllowsLoopbackHTTPOnlyInLocalEnvironments(t *testing.T) {
|
|
localInput := PairingInput{
|
|
AuthCenterURL: "http://localhost:18000", PublicBaseURL: "http://127.0.0.1:18089",
|
|
WebBaseURL: "http://127.0.0.1:5178", LocalTenantKey: "default",
|
|
}
|
|
secureInput := PairingInput{
|
|
AuthCenterURL: "https://auth.example.com", PublicBaseURL: "https://api.example.com",
|
|
WebBaseURL: "https://gateway.example.com", LocalTenantKey: "default",
|
|
}
|
|
for _, test := range []struct {
|
|
name string
|
|
mutate func(*PairingInput)
|
|
}{
|
|
{name: "auth center", mutate: func(input *PairingInput) { input.AuthCenterURL = localInput.AuthCenterURL }},
|
|
{name: "public base", mutate: func(input *PairingInput) { input.PublicBaseURL = localInput.PublicBaseURL }},
|
|
{name: "web base", mutate: func(input *PairingInput) { input.WebBaseURL = localInput.WebBaseURL }},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
input := secureInput
|
|
test.mutate(&input)
|
|
for _, appEnv := range []string{"", "production", "staging"} {
|
|
if _, err := NewDraft(input, appEnv); err == nil {
|
|
t.Fatalf("%s accepted loopback HTTP %s URL", appEnv, test.name)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
for _, appEnv := range []string{"local", "development", "dev", "test"} {
|
|
draft, err := NewDraft(localInput, appEnv)
|
|
if err != nil {
|
|
t.Fatalf("%s rejected loopback HTTP pairing URLs: %v", appEnv, err)
|
|
}
|
|
if draft.AuthCenterURL != localInput.AuthCenterURL || draft.PublicBaseURL != localInput.PublicBaseURL || draft.WebBaseURL != localInput.WebBaseURL {
|
|
t.Fatalf("%s changed normalized loopback URLs: %#v", appEnv, draft)
|
|
}
|
|
}
|
|
}
|