|
@@ -7,18 +7,19 @@ import (
|
|
|
|
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/common"
|
|
|
"github.com/QuantumNous/new-api/model"
|
|
"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-contrib/sessions"
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gin-gonic/gin"
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
const (
|
|
|
- // SecureVerificationSessionKey 安全验证的 session key
|
|
|
|
|
|
|
+ // SecureVerificationSessionKey means the user has fully passed secure verification.
|
|
|
SecureVerificationSessionKey = "secure_verified_at"
|
|
SecureVerificationSessionKey = "secure_verified_at"
|
|
|
|
|
+ // PasskeyReadySessionKey means WebAuthn finished and /api/verify can finalize step-up verification.
|
|
|
|
|
+ PasskeyReadySessionKey = "secure_passkey_ready_at"
|
|
|
// SecureVerificationTimeout 验证有效期(秒)
|
|
// SecureVerificationTimeout 验证有效期(秒)
|
|
|
SecureVerificationTimeout = 300 // 5分钟
|
|
SecureVerificationTimeout = 300 // 5分钟
|
|
|
|
|
+ // PasskeyReadyTimeout passkey ready 标记有效期(秒)
|
|
|
|
|
+ PasskeyReadyTimeout = 60
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
type UniversalVerifyRequest struct {
|
|
type UniversalVerifyRequest struct {
|
|
@@ -76,6 +77,7 @@ func UniversalVerify(c *gin.Context) {
|
|
|
// 根据验证方式进行验证
|
|
// 根据验证方式进行验证
|
|
|
var verified bool
|
|
var verified bool
|
|
|
var verifyMethod string
|
|
var verifyMethod string
|
|
|
|
|
+ var err error
|
|
|
|
|
|
|
|
switch req.Method {
|
|
switch req.Method {
|
|
|
case "2fa":
|
|
case "2fa":
|
|
@@ -95,10 +97,16 @@ func UniversalVerify(c *gin.Context) {
|
|
|
common.ApiError(c, fmt.Errorf("用户未启用Passkey"))
|
|
common.ApiError(c, fmt.Errorf("用户未启用Passkey"))
|
|
|
return
|
|
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"
|
|
verifyMethod = "Passkey"
|
|
|
|
|
|
|
|
default:
|
|
default:
|
|
@@ -112,10 +120,8 @@ func UniversalVerify(c *gin.Context) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 验证成功,在 session 中记录时间戳
|
|
// 验证成功,在 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))
|
|
common.ApiError(c, fmt.Errorf("保存验证状态失败: %v", err))
|
|
|
return
|
|
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 := sessions.Default(c)
|
|
|
|
|
+ session.Delete(PasskeyReadySessionKey)
|
|
|
now := time.Now().Unix()
|
|
now := time.Now().Unix()
|
|
|
session.Set(SecureVerificationSessionKey, now)
|
|
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
|
|
|
}
|
|
}
|