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:
@@ -20,6 +20,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const maxOIDCResponseBytes = 1 << 20
|
||||
@@ -30,6 +31,9 @@ type OIDCConfig struct {
|
||||
Issuer string
|
||||
Audience string
|
||||
TenantID string
|
||||
TenantMode string
|
||||
ApplicationID string
|
||||
ClientID string
|
||||
RolePrefix string
|
||||
RequiredScopes []string
|
||||
JWKSCacheTTL time.Duration
|
||||
@@ -44,10 +48,11 @@ type OIDCConfig struct {
|
||||
}
|
||||
|
||||
type OIDCSecurityEventIdentity struct {
|
||||
Issuer string
|
||||
TenantID string
|
||||
Subject string
|
||||
IssuedAt time.Time
|
||||
Issuer string
|
||||
ApplicationID string
|
||||
TenantID string
|
||||
Subject string
|
||||
IssuedAt time.Time
|
||||
}
|
||||
|
||||
type OIDCSecurityEventEvaluation struct {
|
||||
@@ -91,8 +96,16 @@ func NewOIDCVerifier(config OIDCConfig) (*OIDCVerifier, error) {
|
||||
config.Issuer = strings.TrimRight(strings.TrimSpace(config.Issuer), "/")
|
||||
config.Audience = strings.TrimSpace(config.Audience)
|
||||
config.TenantID = strings.TrimSpace(config.TenantID)
|
||||
config.TenantMode = strings.TrimSpace(config.TenantMode)
|
||||
config.ApplicationID = strings.TrimSpace(config.ApplicationID)
|
||||
config.ClientID = strings.TrimSpace(config.ClientID)
|
||||
config.RolePrefix = strings.TrimSpace(config.RolePrefix)
|
||||
if err := validatePublicURL(config.Issuer, config.AppEnv); err != nil || config.Audience == "" || config.TenantID == "" || config.RolePrefix == "" {
|
||||
if config.TenantMode == "" {
|
||||
config.TenantMode = "single_tenant"
|
||||
}
|
||||
validTenantMode := config.TenantMode == "single_tenant" && config.TenantID != "" ||
|
||||
config.TenantMode == "multi_tenant" && config.TenantID == "" && config.ApplicationID != ""
|
||||
if err := validatePublicURL(config.Issuer, config.AppEnv); err != nil || config.Audience == "" || !validTenantMode || config.RolePrefix == "" {
|
||||
return nil, errors.New("issuer, audience, tenant and role prefix are required")
|
||||
}
|
||||
if config.IntrospectionEnabled && config.IntrospectionCredentialProvider == nil &&
|
||||
@@ -166,7 +179,14 @@ func (v *OIDCVerifier) Verify(ctx context.Context, raw string) (*User, error) {
|
||||
return nil, oidcUnauthorized(registeredClaimsValidationCategory(err), "signature or registered claims are invalid", err)
|
||||
}
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || stringClaim(claims, "sub") == "" || stringClaim(claims, "tid") != v.config.TenantID {
|
||||
tenantID := stringClaim(claims, "tid")
|
||||
validTenant := tenantID == v.config.TenantID
|
||||
if v.config.TenantMode == "multi_tenant" {
|
||||
validTenant = uuid.Validate(tenantID) == nil
|
||||
}
|
||||
clientID := stringClaim(claims, "client_id")
|
||||
if !ok || stringClaim(claims, "sub") == "" || !validTenant ||
|
||||
v.config.ClientID != "" && clientID != v.config.ClientID {
|
||||
return nil, oidcUnauthorized("STABLE_IDENTITY_CLAIMS_INVALID", "stable identity claims are invalid", nil)
|
||||
}
|
||||
expiresAt, ok := numericDateClaim(claims["exp"])
|
||||
@@ -190,7 +210,8 @@ func (v *OIDCVerifier) Verify(ctx context.Context, raw string) (*User, error) {
|
||||
}
|
||||
if v.config.SecurityEventEvaluator != nil {
|
||||
evaluation, evaluateErr := v.config.SecurityEventEvaluator(ctx, OIDCSecurityEventIdentity{
|
||||
Issuer: v.config.Issuer, TenantID: v.config.TenantID, Subject: stringClaim(claims, "sub"), IssuedAt: issuedAt,
|
||||
Issuer: v.config.Issuer, ApplicationID: v.config.ApplicationID,
|
||||
TenantID: tenantID, Subject: stringClaim(claims, "sub"), IssuedAt: issuedAt,
|
||||
})
|
||||
if evaluateErr != nil {
|
||||
return nil, NewRequestAuthError(http.StatusServiceUnavailable, "OIDC_SECURITY_EVENT_STATE_UNAVAILABLE", "认证撤销状态暂时不可用")
|
||||
@@ -227,7 +248,8 @@ func (v *OIDCVerifier) Verify(ctx context.Context, raw string) (*User, error) {
|
||||
}
|
||||
return &User{
|
||||
ID: stringClaim(claims, "sub"), Username: username, Roles: roles,
|
||||
TenantID: v.config.TenantID, Source: "oidc", TokenExpiresAt: expiresAt, TokenIssuedAt: issuedAt, Issuer: v.config.Issuer,
|
||||
TenantID: tenantID, Source: "oidc", TokenExpiresAt: expiresAt, TokenIssuedAt: issuedAt, Issuer: v.config.Issuer,
|
||||
ApplicationID: v.config.ApplicationID, OIDCClientID: clientID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user