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:
@@ -36,6 +36,8 @@ type Event struct {
|
||||
EventType string
|
||||
EventTimestamp time.Time
|
||||
SubjectIssuer string
|
||||
ApplicationID string
|
||||
SubjectType string
|
||||
Subject string
|
||||
TenantID string
|
||||
InitiatingEntity string
|
||||
@@ -50,6 +52,8 @@ type VerifierConfig struct {
|
||||
Audience string
|
||||
SubjectIssuer string
|
||||
TenantID string
|
||||
ApplicationID string
|
||||
TenantMode string
|
||||
StreamID string
|
||||
ClockSkew time.Duration
|
||||
HTTPClient *http.Client
|
||||
@@ -93,8 +97,11 @@ func (e *protocolError) Error() string { return e.Code }
|
||||
func NewVerifier(config VerifierConfig) (*Verifier, error) {
|
||||
config.TransmitterIssuer = strings.TrimRight(strings.TrimSpace(config.TransmitterIssuer), "/")
|
||||
config.SubjectIssuer = strings.TrimRight(strings.TrimSpace(config.SubjectIssuer), "/")
|
||||
if !absoluteHTTPURL(config.TransmitterIssuer) || !absoluteHTTPURL(config.SubjectIssuer) || config.Audience == "" || config.TenantID == "" {
|
||||
return nil, errors.New("SSF transmitter, audience, subject issuer, and tenant are required")
|
||||
multiTenant := config.TenantMode == "multi_tenant"
|
||||
validBinding := multiTenant && uuid.Validate(config.ApplicationID) == nil ||
|
||||
!multiTenant && uuid.Validate(config.TenantID) == nil
|
||||
if !absoluteHTTPURL(config.TransmitterIssuer) || !absoluteHTTPURL(config.SubjectIssuer) || config.Audience == "" || !validBinding {
|
||||
return nil, errors.New("SSF transmitter, audience, subject issuer, and identity binding are required")
|
||||
}
|
||||
if _, err := uuid.Parse(config.StreamID); err != nil {
|
||||
return nil, errors.New("SSF stream id must be a UUID")
|
||||
@@ -179,6 +186,7 @@ func (v *Verifier) Verify(ctx context.Context, raw string) (Event, error) {
|
||||
return Event{}, &protocolError{Code: "invalid_request"}
|
||||
}
|
||||
event.EventType, event.EventTimestamp, event.InitiatingEntity = SessionRevokedEventType, unixClaim(value["event_timestamp"]), stringValue(value["initiating_entity"])
|
||||
event.ApplicationID, event.SubjectType = stringValue(value["application_id"]), stringValue(value["subject_type"])
|
||||
if event.EventTimestamp.IsZero() || event.EventTimestamp.After(v.now().Add(v.config.ClockSkew)) ||
|
||||
!validInitiator(event.InitiatingEntity) || !v.readSessionSubject(claims["sub_id"], &event) {
|
||||
return Event{}, &protocolError{Code: "invalid_request"}
|
||||
@@ -259,11 +267,19 @@ func (v *Verifier) readSessionSubject(raw any, event *Event) bool {
|
||||
return false
|
||||
}
|
||||
event.SubjectIssuer, event.Subject, event.TenantID = strings.TrimRight(stringValue(user["iss"]), "/"), stringValue(user["sub"]), stringValue(tenant["id"])
|
||||
if event.SubjectIssuer != v.config.SubjectIssuer || event.TenantID != v.config.TenantID {
|
||||
if event.SubjectIssuer != v.config.SubjectIssuer || uuid.Validate(event.TenantID) != nil {
|
||||
return false
|
||||
}
|
||||
_, err := uuid.Parse(event.Subject)
|
||||
return err == nil
|
||||
if v.config.TenantMode == "multi_tenant" {
|
||||
if event.ApplicationID != v.config.ApplicationID ||
|
||||
event.SubjectType != "principal" && event.SubjectType != "tenant" ||
|
||||
event.SubjectType == "tenant" && event.Subject != event.TenantID {
|
||||
return false
|
||||
}
|
||||
} else if event.ApplicationID != "" || event.SubjectType != "" || event.TenantID != v.config.TenantID {
|
||||
return false
|
||||
}
|
||||
return uuid.Validate(event.Subject) == nil
|
||||
}
|
||||
|
||||
func (v *Verifier) readStreamSubject(raw any, event *Event) bool {
|
||||
|
||||
Reference in New Issue
Block a user