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
+23 -5
View File
@@ -22,6 +22,7 @@ var (
)
type RevisionPolicy struct {
TenantMode string `json:"tenantMode"`
LocalTenantKey string `json:"localTenantKey"`
RolePrefix string `json:"rolePrefix"`
JITEnabled bool `json:"jitEnabled"`
@@ -32,7 +33,8 @@ type RevisionPolicy struct {
}
func (policy RevisionPolicy) Validate() error {
if strings.TrimSpace(policy.LocalTenantKey) == "" || strings.TrimSpace(policy.RolePrefix) == "" {
if strings.TrimSpace(policy.RolePrefix) == "" ||
policy.TenantMode != "multi_tenant" && strings.TrimSpace(policy.LocalTenantKey) == "" {
return errors.New("local tenant mapping and role prefix are required")
}
if policy.SessionIdleSeconds <= 0 || policy.SessionAbsoluteSeconds <= policy.SessionIdleSeconds ||
@@ -56,6 +58,7 @@ type Revision struct {
ID string `json:"id"`
State RevisionState `json:"state"`
SchemaVersion int `json:"schemaVersion"`
TenantMode string `json:"tenantMode,omitempty"`
AuthCenterURL string `json:"authCenterUrl"`
Issuer string `json:"issuer,omitempty"`
TenantID string `json:"tenantId,omitempty"`
@@ -76,6 +79,9 @@ type Revision struct {
SecurityEventIssuer string `json:"securityEventIssuer,omitempty"`
SecurityEventConfigURL string `json:"securityEventConfigurationUrl,omitempty"`
SecurityEventAudience string `json:"securityEventAudience,omitempty"`
TenantContextEndpoint string `json:"tenantContextEndpoint,omitempty"`
TenantContextAudience string `json:"tenantContextAudience,omitempty"`
TenantContextScope string `json:"tenantContextScope,omitempty"`
MachineCredentialRef string `json:"-"`
SessionEncryptionKeyRef string `json:"-"`
SessionIdleSeconds int `json:"sessionIdleSeconds"`
@@ -126,7 +132,7 @@ func NewDraft(input PairingInput, appEnv string) (Revision, error) {
publicBase, _ := exactBaseURL(input.PublicBaseURL, appEnv)
webBase, _ := exactBaseURL(input.WebBaseURL, appEnv)
return Revision{
ID: uuid.NewString(), State: RevisionDraft, SchemaVersion: 1,
ID: uuid.NewString(), State: RevisionDraft, SchemaVersion: 1, TenantMode: "single_tenant",
AuthCenterURL: authCenter, RolePrefix: "gateway.", LocalTenantKey: strings.TrimSpace(input.LocalTenantKey),
PublicBaseURL: publicBase, WebBaseURL: webBase, JITEnabled: true, LegacyJWTEnabled: input.LegacyJWTEnabled,
Scopes: []string{}, Capabilities: []string{}, SessionIdleSeconds: 1800, SessionAbsoluteSeconds: 28800,
@@ -151,8 +157,13 @@ func ApplyManifest(revision Revision, input ManifestApplication) (Revision, erro
if capabilities["oidc_login"] && strings.TrimSpace(input.SessionEncryptionKeyRef) == "" {
return Revision{}, errors.New("session encryption key reference is required")
}
if input.Manifest.SchemaVersion == 1 && strings.TrimSpace(revision.LocalTenantKey) == "" {
return Revision{}, ErrLocalTenantInvalid
}
revision.SchemaVersion = input.Manifest.SchemaVersion
revision.Issuer = strings.TrimRight(input.Manifest.Issuer, "/")
revision.TenantID = input.Manifest.TenantID
revision.TenantMode = input.Manifest.TenantMode
revision.ApplicationID = input.Manifest.ApplicationID
revision.Audience = input.Manifest.Audience
revision.Scopes = append([]string(nil), input.Manifest.Scopes...)
@@ -165,6 +176,16 @@ func ApplyManifest(revision Revision, input ManifestApplication) (Revision, erro
}
revision.TokenIntrospection = capabilities["token_introspection"]
revision.SessionRevocation = capabilities["session_revocation"]
if input.Manifest.SchemaVersion == 2 {
revision.LocalTenantKey = ""
revision.TenantContextEndpoint = input.Manifest.TenantContext.Endpoint
revision.TenantContextAudience = input.Manifest.TenantContext.Audience
revision.TenantContextScope = input.Manifest.TenantContext.Scope
} else {
revision.TenantContextEndpoint = ""
revision.TenantContextAudience = ""
revision.TenantContextScope = ""
}
if input.Manifest.SecurityEvents != nil {
revision.SecurityEventIssuer = input.Manifest.SecurityEvents.TransmitterIssuer
revision.SecurityEventConfigURL = input.Manifest.SecurityEvents.ConfigurationEndpoint
@@ -209,9 +230,6 @@ func (input PairingInput) ConsumerMetadata(sessionRevocation bool, appEnv string
if !sameSiteBaseURLs(publicBase, webBase) {
return ConsumerMetadata{}, errors.New("public base URL and web base URL must be same-site")
}
if strings.TrimSpace(input.LocalTenantKey) == "" {
return ConsumerMetadata{}, errors.New("local tenant mapping is required")
}
metadata := ConsumerMetadata{
PublicBaseURL: publicBase, WebBaseURL: webBase,
RedirectURIs: []string{publicBase + "/api/v1/auth/oidc/callback"},