enhance channel key viewing
This commit is contained in:
@@ -470,6 +470,15 @@ func PasskeyVerifyFinish(c *gin.Context) {
|
|||||||
return
|
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{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "Passkey 验证成功",
|
"message": "Passkey 验证成功",
|
||||||
|
|||||||
@@ -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 branch only trusts the short-lived marker written by PasskeyVerifyFinish.
|
||||||
// 这里只是验证 Passkey 验证流程是否已经完成
|
verified, err = consumePasskeyReady(c)
|
||||||
// 实际上,前端应该先调用这两个接口,然后再调用本接口
|
if err != nil {
|
||||||
verified = true // Passkey 验证逻辑已在 PasskeyVerifyFinish 中完成
|
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, err := setSecureVerificationSession(c)
|
||||||
now := time.Now().Unix()
|
if err != nil {
|
||||||
session.Set(SecureVerificationSessionKey, now)
|
|
||||||
if err := session.Save(); 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
|
func setSecureVerificationSession(c *gin.Context) (int64, error) {
|
||||||
// 这是一个辅助函数,供 PasskeyVerifyFinish 调用
|
|
||||||
func PasskeyVerifyAndSetSession(c *gin.Context) {
|
|
||||||
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()
|
if err := session.Save(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return now, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PasskeyVerifyForSecure 用于安全验证的 Passkey 验证流程
|
func consumePasskeyReady(c *gin.Context) (bool, error) {
|
||||||
// 整合了 begin 和 finish 流程
|
session := sessions.Default(c)
|
||||||
func PasskeyVerifyForSecure(c *gin.Context) {
|
readyAtRaw := session.Get(PasskeyReadySessionKey)
|
||||||
if !system_setting.GetPasskeySettings().Enabled {
|
if readyAtRaw == nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
return false, nil
|
||||||
"success": false,
|
|
||||||
"message": "管理员未启用 Passkey 登录",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
userId := c.GetInt("id")
|
readyAt, ok := readyAtRaw.(int64)
|
||||||
if userId == 0 {
|
if !ok {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{
|
session.Delete(PasskeyReadySessionKey)
|
||||||
"success": false,
|
_ = session.Save()
|
||||||
"message": "未登录",
|
return false, fmt.Errorf("无效的 Passkey 验证状态")
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
session.Delete(PasskeyReadySessionKey)
|
||||||
user := &model.User{Id: userId}
|
if err := session.Save(); err != nil {
|
||||||
if err := user.FillUserById(); err != nil {
|
return false, err
|
||||||
common.ApiError(c, fmt.Errorf("获取用户信息失败: %v", err))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
// Expired ready markers cannot be reused.
|
||||||
if user.Status != common.UserStatusEnabled {
|
if time.Now().Unix()-readyAt >= PasskeyReadyTimeout {
|
||||||
common.ApiError(c, fmt.Errorf("该用户已被禁用"))
|
return false, nil
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
return true, 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,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user