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:
@@ -255,6 +255,42 @@ func TestServiceDeletesSessionEvenWhenCiphertextCannotBeDecryptedDuringLogout(t
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceBindsMultiTenantSessionToIssuerApplicationTenantAndClient(t *testing.T) {
|
||||
now := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC)
|
||||
tenantA := "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
|
||||
tenantB := "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"
|
||||
applicationID := "cccccccc-cccc-4ccc-8ccc-cccccccccccc"
|
||||
verified := &auth.User{
|
||||
ID: "shared-subject", Source: "oidc", Issuer: "https://auth.example.test",
|
||||
ApplicationID: applicationID, TenantID: tenantA, OIDCClientID: "gateway-browser",
|
||||
TokenExpiresAt: now.Add(5 * time.Minute),
|
||||
}
|
||||
verifier := fakeVerifier{users: map[string]*auth.User{"access": verified}}
|
||||
repository := newFakeRepository("shared-subject")
|
||||
service := newTestService(t, repository, verifier, &fakePublicClient{})
|
||||
service.now = func() time.Time { return now }
|
||||
local := &auth.User{
|
||||
ID: "shared-subject", GatewayUserID: "11111111-1111-4111-8111-111111111111",
|
||||
GatewayTenantID: "22222222-2222-4222-8222-222222222222",
|
||||
OIDCUserBindingID: "44444444-4444-4444-8444-444444444444",
|
||||
Issuer: "https://auth.example.test", ApplicationID: applicationID,
|
||||
TenantID: tenantA, OIDCClientID: "gateway-browser",
|
||||
}
|
||||
raw, err := service.Create(context.Background(), TokenBundle{
|
||||
AccessToken: "access", RefreshToken: "refresh",
|
||||
}, local)
|
||||
if err != nil {
|
||||
t.Fatalf("Create() error = %v", err)
|
||||
}
|
||||
verified.TenantID = tenantB
|
||||
if _, err := service.Resolve(context.Background(), raw); !errors.Is(err, ErrSessionInvalid) {
|
||||
t.Fatalf("Resolve() error = %v, want ErrSessionInvalid", err)
|
||||
}
|
||||
if !repository.isDeleted() {
|
||||
t.Fatal("identity-binding mismatch did not destroy the session")
|
||||
}
|
||||
}
|
||||
|
||||
func testLocalUser() *auth.User {
|
||||
return &auth.User{
|
||||
ID: "subject-1", GatewayUserID: "11111111-1111-4111-8111-111111111111",
|
||||
@@ -323,6 +359,8 @@ func (f *fakeRepository) CreateOIDCSession(_ context.Context, input store.Create
|
||||
f.record = store.OIDCSession{
|
||||
ID: "33333333-3333-4333-8333-333333333333", SessionTokenHash: append([]byte(nil), input.SessionTokenHash...),
|
||||
GatewayUserID: input.GatewayUserID, GatewayTenantID: input.GatewayTenantID, ExternalUserID: f.external,
|
||||
OIDCUserBindingID: input.OIDCUserBindingID, OIDCClientID: input.OIDCClientID,
|
||||
Issuer: input.Issuer, ApplicationID: input.ApplicationID, TenantID: input.TenantID,
|
||||
UserStatus: "active", TokenCiphertext: append([]byte(nil), input.TokenCiphertext...), AccessTokenExpiresAt: input.AccessTokenExpiresAt,
|
||||
LastSeenAt: input.LastSeenAt, IdleExpiresAt: input.IdleExpiresAt, AbsoluteExpiresAt: input.AbsoluteExpiresAt, RefreshVersion: 1,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user