mirror of
https://gh-proxy.org/https://github.com/klinsmaya/new-api.git
synced 2026-11-09 18:07:04 +08:00
Preserve legacy credentials across primary RP ID changes and bind successful assertions to their verified RP ID. Share domain selection across sign-in, login verification and sensitive operations without replaying ceremonies. Save domain settings atomically, preview known and unknown credential impact, and require fresh confirmation before removing domains still in use. Recheck trusted RP IDs when persisting credentials and record safe operation audits. Use read-only configuration snapshots, accept trusted HTTPS intranet hosts, check registration origins early, and preserve historical capitalization. Update settings guidance and all supported translations. Validation: SQLite 3.50.4, MySQL 8.0.46 and PostgreSQL 16.15, including fresh and released-schema upgrade checks; Go regression and race checks; 79 frontend tests; TypeScript, lint, frontend build and root Go build. Security references: OWASP ASVS 5.0.0 V6.3.4 and V6.3.8, Authentication and Session Management Cheat Sheets, and the WebAuthn RP ID/origin rules.
162 lines
5.5 KiB
Go
162 lines
5.5 KiB
Go
package controller
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/model"
|
|
"github.com/QuantumNous/new-api/service"
|
|
passkeysvc "github.com/QuantumNous/new-api/service/passkey"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/go-webauthn/webauthn/protocol"
|
|
webauthnlib "github.com/go-webauthn/webauthn/webauthn"
|
|
)
|
|
|
|
func VerifyLogin(c *gin.Context) {
|
|
var request struct {
|
|
FlowToken string `json:"flow_token"`
|
|
Method string `json:"method"`
|
|
Code string `json:"code"`
|
|
}
|
|
if common.DecodeJson(c.Request.Body, &request) != nil || request.FlowToken == "" || request.Code == "" {
|
|
common.ApiErrorMsg(c, "参数错误")
|
|
return
|
|
}
|
|
if request.Method == "" {
|
|
request.Method = service.VerificationMethodTwoFA
|
|
}
|
|
if request.Method != service.VerificationMethodTwoFA {
|
|
writeSecurityOperationError(c, service.ErrProofMethod)
|
|
return
|
|
}
|
|
bundle, err := service.VerifyLoginCode(request.FlowToken, request.Code, c.ClientIP(), c.Request.UserAgent())
|
|
if err != nil {
|
|
writeSecurityOperationError(c, err)
|
|
return
|
|
}
|
|
completeVerifiedLoginResponse(c, bundle, service.VerificationMethodTwoFA)
|
|
}
|
|
|
|
func LoginPasskeyBegin(c *gin.Context) {
|
|
var request struct {
|
|
FlowToken string `json:"flow_token"`
|
|
RPID string `json:"rp_id"`
|
|
}
|
|
if common.DecodeJson(c.Request.Body, &request) != nil || request.FlowToken == "" {
|
|
common.ApiErrorMsg(c, "参数错误")
|
|
return
|
|
}
|
|
verification, err := service.RequireLoginVerification(request.FlowToken, service.VerificationMethodPasskey)
|
|
if err != nil {
|
|
writeSecurityOperationError(c, err)
|
|
return
|
|
}
|
|
credential, err := model.GetPasskeyByUserID(verification.State.UserID)
|
|
if err != nil {
|
|
writeSecurityOperationError(c, err)
|
|
return
|
|
}
|
|
credentialRPID := ""
|
|
if credential.RPID != nil {
|
|
credentialRPID = *credential.RPID
|
|
}
|
|
wa, rpIDs, err := passkeysvc.BuildLoginWebAuthn(c.Request, request.RPID, credentialRPID)
|
|
if err != nil {
|
|
writeSecurityOperationError(c, err)
|
|
return
|
|
}
|
|
user := &model.User{Id: verification.State.UserID}
|
|
options, sessionData, err := wa.BeginLogin(passkeysvc.NewWebAuthnUser(user, credential), webauthnlib.WithUserVerification(protocol.VerificationRequired))
|
|
if err != nil {
|
|
writeSecurityOperationError(c, err)
|
|
return
|
|
}
|
|
token, expiresAt, err := passkeysvc.CreateSessionDataFlow(model.AuthFlowPurposeLoginPasskey, passkeysvc.FlowSecurity{
|
|
AuthSessionIdentity: model.AuthSessionIdentity{UserID: user.Id, UserAuthVersion: verification.State.AuthVersion},
|
|
LoginFlowID: verification.Flow.Id, LoginExpiresAt: verification.Flow.ExpiresAt.Unix(),
|
|
}, sessionData)
|
|
if err != nil {
|
|
writeSecurityOperationError(c, err)
|
|
return
|
|
}
|
|
common.ApiSuccess(c, gin.H{"flow_token": token, "expires_at": expiresAt, "options": options, "rp_ids": rpIDs})
|
|
}
|
|
|
|
func LoginPasskeyFinish(c *gin.Context) {
|
|
var request struct {
|
|
FlowToken string `json:"flow_token"`
|
|
PasskeyFlowToken string `json:"passkey_flow_token"`
|
|
Credential json.RawMessage `json:"credential"`
|
|
}
|
|
if common.DecodeJson(c.Request.Body, &request) != nil || request.FlowToken == "" || request.PasskeyFlowToken == "" || len(request.Credential) == 0 {
|
|
common.ApiErrorMsg(c, "参数错误")
|
|
return
|
|
}
|
|
verification, err := service.RequireLoginVerification(request.FlowToken, service.VerificationMethodPasskey)
|
|
if err != nil {
|
|
writeSecurityOperationError(c, err)
|
|
return
|
|
}
|
|
parsed, err := protocol.ParseCredentialRequestResponseBytes(request.Credential)
|
|
if err != nil {
|
|
writeSecurityOperationError(c, err)
|
|
return
|
|
}
|
|
identity := model.AuthSessionIdentity{UserID: verification.State.UserID, UserAuthVersion: verification.State.AuthVersion}
|
|
sessionData, security, err := passkeysvc.PopSessionDataFlow(request.PasskeyFlowToken, model.AuthFlowPurposeLoginPasskey, identity)
|
|
if err != nil {
|
|
writeSecurityOperationError(c, err)
|
|
return
|
|
}
|
|
c.Set("passkey_rp_id", sessionData.RelyingPartyID)
|
|
if security.LoginFlowID != verification.Flow.Id || sessionData.UserVerification != protocol.VerificationRequired {
|
|
writeSecurityOperationError(c, model.ErrAuthFlowInvalid)
|
|
return
|
|
}
|
|
credential, err := model.GetPasskeyByUserID(identity.UserID)
|
|
if err != nil {
|
|
writeSecurityOperationError(c, err)
|
|
return
|
|
}
|
|
wa, err := passkeysvc.BuildWebAuthnForRPID(c.Request, sessionData.RelyingPartyID)
|
|
if err != nil {
|
|
writeSecurityOperationError(c, err)
|
|
return
|
|
}
|
|
if credential.RPID != nil && *credential.RPID != "" && *credential.RPID != sessionData.RelyingPartyID {
|
|
writeSecurityOperationError(c, service.ErrVerificationFailed)
|
|
return
|
|
}
|
|
validated, err := wa.ValidateLogin(passkeysvc.NewWebAuthnUser(&model.User{Id: identity.UserID}, credential), *sessionData, parsed)
|
|
if err != nil {
|
|
writeSecurityOperationError(c, err)
|
|
return
|
|
}
|
|
if err := model.UpdatePasskeyAssertionState(identity.UserID, validated, time.Now(), sessionData.RelyingPartyID); err != nil {
|
|
writeSecurityOperationError(c, err)
|
|
return
|
|
}
|
|
bundle, err := service.CompleteLoginVerification(request.FlowToken, verification, service.VerificationMethodPasskey, c.ClientIP(), c.Request.UserAgent())
|
|
if err != nil {
|
|
writeSecurityOperationError(c, err)
|
|
return
|
|
}
|
|
completeVerifiedLoginResponse(c, bundle, service.VerificationMethodPasskey)
|
|
}
|
|
|
|
func completeVerifiedLoginResponse(c *gin.Context, bundle *service.AuthBundle, method string) {
|
|
identity, err := service.ParseAccessToken(bundle.AccessToken)
|
|
if err != nil {
|
|
writeAuthSessionError(c, err)
|
|
return
|
|
}
|
|
user, err := model.GetSelfUserById(identity.UserID)
|
|
if err != nil {
|
|
writeAuthSessionError(c, err)
|
|
return
|
|
}
|
|
c.Set("login_verification_method", method)
|
|
writeLoginResponse(c, user, bundle)
|
|
}
|