Преглед изворни кода

Merge pull request #3257 from seefs001/fix/passkey-verify

enhance channel key viewing
Calcium-Ion пре 1 месец
родитељ
комит
95738594b4
2 измењених фајлова са 50 додато и 92 уклоњено
  1. 9 0
      controller/passkey.go
  2. 41 92
      controller/secure_verification.go

+ 9 - 0
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 验证成功",

+ 41 - 92
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()
-}
-
-// 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
-	}
-
-	userId := c.GetInt("id")
-	if userId == 0 {
-		c.JSON(http.StatusUnauthorized, gin.H{
-			"success": false,
-			"message": "未登录",
-		})
-		return
-	}
-
-	user := &model.User{Id: userId}
-	if err := user.FillUserById(); err != nil {
-		common.ApiError(c, fmt.Errorf("获取用户信息失败: %v", err))
-		return
-	}
-
-	if user.Status != common.UserStatusEnabled {
-		common.ApiError(c, fmt.Errorf("该用户已被禁用"))
-		return
-	}
-
-	credential, err := model.GetPasskeyByUserID(userId)
-	if err != nil {
-		c.JSON(http.StatusOK, gin.H{
-			"success": false,
-			"message": "该用户尚未绑定 Passkey",
-		})
-		return
+	if err := session.Save(); err != nil {
+		return 0, err
 	}
+	return now, nil
+}
 
-	wa, err := passkeysvc.BuildWebAuthn(c.Request)
-	if err != nil {
-		common.ApiError(c, err)
-		return
+func consumePasskeyReady(c *gin.Context) (bool, error) {
+	session := sessions.Default(c)
+	readyAtRaw := session.Get(PasskeyReadySessionKey)
+	if readyAtRaw == nil {
+		return false, nil
 	}
 
-	waUser := passkeysvc.NewWebAuthnUser(user, credential)
-	sessionData, err := passkeysvc.PopSessionData(c, passkeysvc.VerifySessionKey)
-	if err != nil {
-		common.ApiError(c, err)
-		return
+	readyAt, ok := readyAtRaw.(int64)
+	if !ok {
+		session.Delete(PasskeyReadySessionKey)
+		_ = session.Save()
+		return false, fmt.Errorf("无效的 Passkey 验证状态")
 	}
-
-	_, err = wa.FinishLogin(waUser, *sessionData, c.Request)
-	if err != nil {
-		common.ApiError(c, err)
-		return
+	session.Delete(PasskeyReadySessionKey)
+	if err := session.Save(); err != nil {
+		return false, err
 	}
-
-	// 更新凭证的最后使用时间
-	now := time.Now()
-	credential.LastUsedAt = &now
-	if err := model.UpsertPasskeyCredential(credential); err != nil {
-		common.ApiError(c, err)
-		return
+	// Expired ready markers cannot be reused.
+	if time.Now().Unix()-readyAt >= PasskeyReadyTimeout {
+		return false, nil
 	}
-
-	// 验证成功,设置 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
 }