fix(oidc): 修复登录会话落库兼容问题
部分已配对环境在 0090 首次执行后缺少 oidc_client_id,导致统一认证完成投影后无法写入 Gateway 服务端会话。新增幂等前向迁移补齐该列,并为 Token 处理及会话创建增加不泄露凭据的稳定失败分类和关联诊断。\n\n验证:OIDC Session 全量单测、HTTP 回调定向测试、迁移升级集成测试、隔离 PostgreSQL 跨仓库 OIDC E2E 和本地 Chrome 真实登录均通过。
This commit is contained in:
@@ -23,6 +23,38 @@ var (
|
||||
ErrSecurityStateUnavailable = errors.New("OIDC security event state is unavailable")
|
||||
)
|
||||
|
||||
const (
|
||||
sessionCreationPrerequisitesInvalid = "SESSION_PREREQUISITES_INVALID"
|
||||
sessionCreationAccessTokenInvalid = "SESSION_ACCESS_TOKEN_INVALID"
|
||||
sessionCreationIdentityMismatch = "SESSION_IDENTITY_MISMATCH"
|
||||
sessionCreationTokenExpired = "SESSION_TOKEN_EXPIRED"
|
||||
sessionCreationTokenGeneration = "SESSION_TOKEN_GENERATION_FAILED"
|
||||
sessionCreationTokenEncryption = "SESSION_TOKEN_ENCRYPTION_FAILED"
|
||||
sessionCreationRepositoryFailed = "SESSION_REPOSITORY_FAILED"
|
||||
)
|
||||
|
||||
type sessionCreationError struct {
|
||||
category string
|
||||
root error
|
||||
}
|
||||
|
||||
func (e *sessionCreationError) Error() string { return e.category }
|
||||
func (e *sessionCreationError) Unwrap() error { return e.root }
|
||||
|
||||
func newSessionCreationError(category string, root error) error {
|
||||
return &sessionCreationError{category: category, root: root}
|
||||
}
|
||||
|
||||
// SessionCreationCategory returns a stable, non-sensitive failure category.
|
||||
// It intentionally does not expose verifier, encryption, or repository errors.
|
||||
func SessionCreationCategory(err error) string {
|
||||
var creationError *sessionCreationError
|
||||
if errors.As(err, &creationError) {
|
||||
return creationError.category
|
||||
}
|
||||
return "SESSION_CREATE_FAILED"
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
CreateOIDCSession(context.Context, store.CreateOIDCSessionInput) (store.OIDCSession, error)
|
||||
FindOIDCSessionByHash(context.Context, []byte) (store.OIDCSession, error)
|
||||
@@ -79,30 +111,33 @@ func NewService(repository Repository, cipher *Cipher, verifier TokenVerifier, c
|
||||
|
||||
func (s *Service) Create(ctx context.Context, bundle TokenBundle, localUser *auth.User) (string, error) {
|
||||
if localUser == nil || localUser.GatewayUserID == "" || localUser.GatewayTenantID == "" || bundle.RefreshToken == "" {
|
||||
return "", ErrSessionInvalid
|
||||
return "", newSessionCreationError(sessionCreationPrerequisitesInvalid, ErrSessionInvalid)
|
||||
}
|
||||
verified, err := s.verifier.Verify(ctx, bundle.AccessToken)
|
||||
if err != nil || verified == nil || verified.Source != "oidc" || verified.ID == "" || verified.ID != localUser.ID {
|
||||
return "", ErrSessionInvalid
|
||||
if err != nil || verified == nil || verified.Source != "oidc" || verified.ID == "" {
|
||||
return "", newSessionCreationError(sessionCreationAccessTokenInvalid, ErrSessionInvalid)
|
||||
}
|
||||
if verified.ID != localUser.ID {
|
||||
return "", newSessionCreationError(sessionCreationIdentityMismatch, ErrSessionInvalid)
|
||||
}
|
||||
if localUser.OIDCUserBindingID != "" &&
|
||||
(localUser.Issuer == "" || localUser.ApplicationID == "" || localUser.TenantID == "" || localUser.OIDCClientID == "" ||
|
||||
verified.Issuer != localUser.Issuer || verified.ApplicationID != localUser.ApplicationID ||
|
||||
verified.TenantID != localUser.TenantID || verified.OIDCClientID != localUser.OIDCClientID) {
|
||||
return "", ErrSessionInvalid
|
||||
return "", newSessionCreationError(sessionCreationIdentityMismatch, ErrSessionInvalid)
|
||||
}
|
||||
now := s.now()
|
||||
if !verified.TokenExpiresAt.After(now) {
|
||||
return "", ErrSessionExpired
|
||||
return "", newSessionCreationError(sessionCreationTokenExpired, ErrSessionExpired)
|
||||
}
|
||||
raw, hash, err := newSessionToken()
|
||||
if err != nil {
|
||||
return "", ErrSessionStoreUnavailable
|
||||
return "", newSessionCreationError(sessionCreationTokenGeneration, ErrSessionStoreUnavailable)
|
||||
}
|
||||
aadSessionID := hex.EncodeToString(hash)
|
||||
ciphertext, err := s.cipher.EncryptBundle(bundle, aadSessionID, localUser.GatewayUserID)
|
||||
if err != nil {
|
||||
return "", ErrSessionStoreUnavailable
|
||||
return "", newSessionCreationError(sessionCreationTokenEncryption, ErrSessionStoreUnavailable)
|
||||
}
|
||||
_, err = s.repository.CreateOIDCSession(ctx, store.CreateOIDCSessionInput{
|
||||
SessionTokenHash: hash, GatewayUserID: localUser.GatewayUserID, GatewayTenantID: localUser.GatewayTenantID,
|
||||
@@ -112,7 +147,7 @@ func (s *Service) Create(ctx context.Context, bundle TokenBundle, localUser *aut
|
||||
LastSeenAt: now, IdleExpiresAt: now.Add(s.config.IdleTTL), AbsoluteExpiresAt: now.Add(s.config.AbsoluteTTL),
|
||||
})
|
||||
if err != nil {
|
||||
return "", ErrSessionStoreUnavailable
|
||||
return "", newSessionCreationError(sessionCreationRepositoryFailed, ErrSessionStoreUnavailable)
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user