diff --git a/controller/passkey.go b/controller/passkey.go index a2cc5369..d37fb9f7 100644 --- a/controller/passkey.go +++ b/controller/passkey.go @@ -470,6 +470,15 @@ func PasskeyVerifyFinish(c *gin.Context) { return } + session := sessions.Default(c) + // Mark passkey as ready; /api/verify will convert this into the final secure verification session. + session.Set(PasskeyReadySessionKey, time.Now().Unix()) + session.Delete(SecureVerificationSessionKey) + if err := session.Save(); err != nil { + common.ApiError(c, fmt.Errorf("保存验证状态失败: %v", err)) + return + } + c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Passkey 验证成功", diff --git a/controller/secure_verification.go b/controller/secure_verification.go index ad1a615e..b229a66b 100644 --- a/controller/secure_verification.go +++ b/controller/secure_verification.go @@ -7,18 +7,19 @@ import ( "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/model" - passkeysvc "github.com/QuantumNous/new-api/service/passkey" - "github.com/QuantumNous/new-api/setting/system_setting" - "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" ) const ( - // SecureVerificationSessionKey 安全验证的 session key + // SecureVerificationSessionKey means the user has fully passed secure verification. SecureVerificationSessionKey = "secure_verified_at" + // PasskeyReadySessionKey means WebAuthn finished and /api/verify can finalize step-up verification. + PasskeyReadySessionKey = "secure_passkey_ready_at" // SecureVerificationTimeout 验证有效期(秒) SecureVerificationTimeout = 300 // 5分钟 + // PasskeyReadyTimeout passkey ready 标记有效期(秒) + PasskeyReadyTimeout = 60 ) type UniversalVerifyRequest struct { @@ -76,6 +77,7 @@ func UniversalVerify(c *gin.Context) { // 根据验证方式进行验证 var verified bool var verifyMethod string + var err error switch req.Method { case "2fa": @@ -95,10 +97,16 @@ func UniversalVerify(c *gin.Context) { common.ApiError(c, fmt.Errorf("用户未启用Passkey")) return } - // Passkey 验证需要先调用 PasskeyVerifyBegin 和 PasskeyVerifyFinish - // 这里只是验证 Passkey 验证流程是否已经完成 - // 实际上,前端应该先调用这两个接口,然后再调用本接口 - verified = true // Passkey 验证逻辑已在 PasskeyVerifyFinish 中完成 + // Passkey branch only trusts the short-lived marker written by PasskeyVerifyFinish. + verified, err = consumePasskeyReady(c) + if err != nil { + common.ApiError(c, fmt.Errorf("Passkey 验证状态异常: %v", err)) + return + } + if !verified { + common.ApiError(c, fmt.Errorf("请先完成 Passkey 验证")) + return + } verifyMethod = "Passkey" default: @@ -112,10 +120,8 @@ func UniversalVerify(c *gin.Context) { } // 验证成功,在 session 中记录时间戳 - session := sessions.Default(c) - now := time.Now().Unix() - session.Set(SecureVerificationSessionKey, now) - if err := session.Save(); err != nil { + now, err := setSecureVerificationSession(c) + if err != nil { common.ApiError(c, fmt.Errorf("保存验证状态失败: %v", err)) return } @@ -133,94 +139,37 @@ func UniversalVerify(c *gin.Context) { }) } -// PasskeyVerifyAndSetSession Passkey 验证完成后设置 session -// 这是一个辅助函数,供 PasskeyVerifyFinish 调用 -func PasskeyVerifyAndSetSession(c *gin.Context) { +func setSecureVerificationSession(c *gin.Context) (int64, error) { session := sessions.Default(c) + session.Delete(PasskeyReadySessionKey) now := time.Now().Unix() session.Set(SecureVerificationSessionKey, now) - _ = session.Save() + if err := session.Save(); err != nil { + return 0, err + } + return now, nil } -// PasskeyVerifyForSecure 用于安全验证的 Passkey 验证流程 -// 整合了 begin 和 finish 流程 -func PasskeyVerifyForSecure(c *gin.Context) { - if !system_setting.GetPasskeySettings().Enabled { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "管理员未启用 Passkey 登录", - }) - return +func consumePasskeyReady(c *gin.Context) (bool, error) { + session := sessions.Default(c) + readyAtRaw := session.Get(PasskeyReadySessionKey) + if readyAtRaw == nil { + return false, nil } - userId := c.GetInt("id") - if userId == 0 { - c.JSON(http.StatusUnauthorized, gin.H{ - "success": false, - "message": "未登录", - }) - return + readyAt, ok := readyAtRaw.(int64) + if !ok { + session.Delete(PasskeyReadySessionKey) + _ = session.Save() + return false, fmt.Errorf("无效的 Passkey 验证状态") } - - user := &model.User{Id: userId} - if err := user.FillUserById(); err != nil { - common.ApiError(c, fmt.Errorf("获取用户信息失败: %v", err)) - return + session.Delete(PasskeyReadySessionKey) + if err := session.Save(); err != nil { + return false, err } - - if user.Status != common.UserStatusEnabled { - common.ApiError(c, fmt.Errorf("该用户已被禁用")) - return + // Expired ready markers cannot be reused. + if time.Now().Unix()-readyAt >= PasskeyReadyTimeout { + return false, nil } - - credential, err := model.GetPasskeyByUserID(userId) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": "该用户尚未绑定 Passkey", - }) - return - } - - wa, err := passkeysvc.BuildWebAuthn(c.Request) - if err != nil { - common.ApiError(c, err) - return - } - - waUser := passkeysvc.NewWebAuthnUser(user, credential) - sessionData, err := passkeysvc.PopSessionData(c, passkeysvc.VerifySessionKey) - if err != nil { - common.ApiError(c, err) - return - } - - _, err = wa.FinishLogin(waUser, *sessionData, c.Request) - if err != nil { - common.ApiError(c, err) - return - } - - // 更新凭证的最后使用时间 - now := time.Now() - credential.LastUsedAt = &now - if err := model.UpsertPasskeyCredential(credential); err != nil { - common.ApiError(c, err) - return - } - - // 验证成功,设置 session - PasskeyVerifyAndSetSession(c) - - // 记录日志 - model.RecordLog(userId, model.LogTypeSystem, "Passkey 安全验证成功") - - c.JSON(http.StatusOK, gin.H{ - "success": true, - "message": "Passkey 验证成功", - "data": gin.H{ - "verified": true, - "expires_at": time.Now().Unix() + SecureVerificationTimeout, - }, - }) + return true, nil }