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:
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user