Files
new-api-custom/service/login_verification.go
T
Codex bb7968b612
Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 9m20s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 55m7s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
feat: publish v1.0.0-rc.36-custom.1
2026-09-11 05:07:48 +08:00

150 lines
5.1 KiB
Go

package service
import (
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/model"
)
const LoginVerificationTTL = 5 * time.Minute
type LoginChallenge struct {
RequireVerification bool `json:"require_verification"`
FlowToken string `json:"flow_token"`
ExpiresAt int64 `json:"expires_at"`
Methods []VerificationMethodOption `json:"methods"`
}
type loginFlowPayload struct {
AuthVersion int64 `json:"auth_version"`
LoginMethod string `json:"login_method"`
}
// LoginVerification is server-owned state read from a primary-authenticated flow.
// It is never constructed from a user ID or an authentication claim in a request.
type LoginVerification struct {
Flow *model.AuthFlow
State *model.UserVerificationState
payload loginFlowPayload
}
func StartLoginVerification(user *model.User, loginMethod string) (*LoginChallenge, error) {
if user == nil || user.Id <= 0 || user.AuthVersion <= 0 || loginMethod == "" {
return nil, model.ErrAuthFlowInvalid
}
state, err := model.GetUserVerificationState(user.Id)
if err != nil {
return nil, err
}
if state.Status != common.UserStatusEnabled || state.AuthVersion != user.AuthVersion {
return nil, model.ErrUserSessionInactive
}
methods, err := securityVerificationPolicy(VerificationScopeLogin, *state)
if err != nil || len(methods) == 0 {
return nil, err
}
available := false
for _, method := range methods {
available = available || method.Available
}
if !available {
return nil, ErrVerificationUnavailable
}
payload, err := common.Marshal(loginFlowPayload{AuthVersion: state.AuthVersion, LoginMethod: loginMethod})
if err != nil {
return nil, err
}
expiresAt := time.Now().Add(LoginVerificationTTL)
token, _, err := model.CreateAuthFlow(model.AuthFlowCreate{
Purpose: model.AuthFlowPurposeLoginVerification, UserId: user.Id,
Payload: string(payload), ExpiresAt: expiresAt,
})
if err != nil {
return nil, err
}
return &LoginChallenge{RequireVerification: true, FlowToken: token, ExpiresAt: expiresAt.Unix(), Methods: methods}, nil
}
func RequireLoginVerification(token, method string) (*LoginVerification, error) {
flow, err := model.GetAuthFlow(token, model.AuthFlowMatch{Purpose: model.AuthFlowPurposeLoginVerification})
if err != nil {
return nil, err
}
var payload loginFlowPayload
if err := common.UnmarshalJsonStr(flow.Payload, &payload); err != nil || payload.AuthVersion <= 0 || payload.LoginMethod == "" {
return nil, model.ErrAuthFlowInvalid
}
state, err := model.GetUserVerificationState(flow.UserId)
if err != nil {
return nil, err
}
if state.Status != common.UserStatusEnabled || state.AuthVersion != payload.AuthVersion {
return nil, model.ErrUserSessionInactive
}
if err := requireLoginVerificationMethod(state, method); err != nil {
return nil, err
}
return &LoginVerification{Flow: flow, State: state, payload: payload}, nil
}
func requireLoginVerificationMethod(state *model.UserVerificationState, method string) error {
methods, err := securityVerificationPolicy(VerificationScopeLogin, *state)
if err != nil {
return err
}
for _, option := range methods {
if option.Method != method {
continue
}
if !option.Available {
return ErrVerificationUnavailable
}
return nil
}
return ErrProofMethod
}
func VerifyLoginCode(token, code, ip, userAgent string) (*AuthBundle, error) {
verification, err := RequireLoginVerification(token, VerificationMethodTwoFA)
if err != nil {
return nil, err
}
twoFA, err := model.GetTwoFAByUserId(verification.State.UserID)
if err != nil {
return nil, err
}
if err := VerifyTwoFactorCode(twoFA, code); err != nil {
return nil, err
}
return CompleteLoginVerification(token, verification, VerificationMethodTwoFA, ip, userAgent)
}
// CompleteLoginVerification must only run after a concrete factor ceremony.
// Recheck the bound version and method while consuming the flow and creating the
// session atomically; a different request cannot reuse this authorization.
func CompleteLoginVerification(token string, verification *LoginVerification, method, ip, userAgent string) (*AuthBundle, error) {
if verification == nil || verification.Flow == nil || verification.State == nil {
return nil, model.ErrAuthFlowInvalid
}
session, refreshSecret, err := newLoginSession(verification.State.UserID, verification.payload.AuthVersion, verification.payload.LoginMethod, ip, userAgent)
if err != nil {
return nil, err
}
if err := model.CreateUserSessionFromLoginFlow(token, session, func(flow *model.AuthFlow, state *model.UserVerificationState) error {
var payload loginFlowPayload
if flow.Id != verification.Flow.Id || common.UnmarshalJsonStr(flow.Payload, &payload) != nil || payload != verification.payload {
return model.ErrAuthFlowInvalid
}
return requireLoginVerificationMethod(state, method)
}); err != nil {
return nil, err
}
bundle, err := issueAuthBundle(session, session.SID+"."+refreshSecret, true)
if err != nil {
_, _ = model.RevokeUserSession(session.UserID, session.SID, "token_issue_failed")
return nil, err
}
return bundle, nil
}