feat(identity): 接入认证中心多租户登录

支持 Manifest V2 动态 tid 验证、Tenant Context 同步和租户内 JIT 投影,并保留 Manifest V1 与旧 Session 兼容。\n\n增加 tenantHint、租户切换、普通注册关闭及 application/principal/tenant 两级 SSF 撤销;迁移、定向安全测试和本地双租户跨仓 E2E 已通过。\n\nrelease_required=true;未执行 Release、Staging 或真实链路。
This commit is contained in:
2026-07-28 17:28:35 +08:00
parent 0b02e62c72
commit 5c679ff13f
45 changed files with 2986 additions and 139 deletions
@@ -109,6 +109,54 @@ func TestReceiverAcceptsValidSessionRevokedAndDuplicate(t *testing.T) {
}
}
func TestVerifierAcceptsApplicationScopedPrincipalAndTenantEvents(t *testing.T) {
privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
applicationID, tenantID, subjectID := uuid.NewString(), uuid.NewString(), uuid.NewString()
verifier := &Verifier{config: VerifierConfig{
TransmitterIssuer: "https://auth.example/ssf", Audience: "urn:easyai:ssf:receiver:" + applicationID,
SubjectIssuer: "https://auth.example/issuer/shared", ApplicationID: applicationID,
TenantMode: "multi_tenant", StreamID: uuid.NewString(), ClockSkew: time.Minute,
}, keys: map[string]*ecdsa.PublicKey{"current": &privateKey.PublicKey}, expiresAt: time.Now().Add(time.Hour), client: http.DefaultClient}
now := time.Now().UTC().Truncate(time.Second)
claims := func(subjectType, subject, eventApplicationID string) jwt.MapClaims {
return jwt.MapClaims{
"iss": verifier.config.TransmitterIssuer, "aud": verifier.config.Audience, "iat": now.Unix(),
"jti": uuid.NewString(), "txn": uuid.NewString(),
"sub_id": map[string]any{
"format": "complex",
"user": map[string]any{"format": "iss_sub", "iss": verifier.config.SubjectIssuer, "sub": subject},
"tenant": map[string]any{"format": "opaque", "id": tenantID},
},
"events": map[string]any{SessionRevokedEventType: map[string]any{
"event_timestamp": now.Unix(), "initiating_entity": "admin",
"application_id": eventApplicationID, "subject_type": subjectType,
}},
}
}
for _, test := range []struct {
name, subjectType, subject string
}{
{name: "principal", subjectType: "principal", subject: subjectID},
{name: "tenant", subjectType: "tenant", subject: tenantID},
} {
t.Run(test.name, func(t *testing.T) {
event, err := verifier.Verify(context.Background(), signSET(t, privateKey, claims(test.subjectType, test.subject, applicationID)))
if err != nil || event.ApplicationID != applicationID || event.SubjectType != test.subjectType ||
event.Subject != test.subject || event.TenantID != tenantID {
t.Fatalf("event=%#v error=%v", event, err)
}
})
}
for _, invalid := range []jwt.MapClaims{
claims("principal", subjectID, uuid.NewString()),
claims("tenant", subjectID, applicationID),
} {
if _, err := verifier.Verify(context.Background(), signSET(t, privateKey, invalid)); err == nil {
t.Fatal("invalid application-scoped event was accepted")
}
}
}
func TestReceiverRejectsAuthenticationMediaTypeAndForbiddenClaims(t *testing.T) {
privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
verifier := &Verifier{config: VerifierConfig{