Files
SafeLine/management/webserver/api/auth.go
T
Binbim b8788c239e
MCP Docker / build-and-push (push) Has been cancelled
feat: SafeLine CE 9.3.9 with pro features
- Remove telemetry: disabled all phone-home (installation notify, false positives, behavior tracking, upgrade tips)
- Version branding: removed 'ce-' prefix, product name changed to '长亭雷池 WAF'
- Detection engine enhancement: strict preset enables all 17 modules with aggressive configs
- Multi-user RBAC: admin/operator/viewer roles with password + TOTP 2FA
- Rate limiting: nginx limit_req per website (RPS, burst, action)
- JS Challenge: browser fingerprint verification page with cookie-based bypass
- Security fixes: removed InsecureSkipVerify, hardcoded credentials, NO_AUTH backdoor
2026-07-04 07:59:04 +08:00

187 lines
5.6 KiB
Go

package api
import (
"math"
"net/http"
"time"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"chaitin.cn/patronus/safeline-2/management/webserver/api/response"
"chaitin.cn/patronus/safeline-2/management/webserver/model"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/constants"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/database"
"chaitin.cn/patronus/safeline-2/management/webserver/pkg/log"
)
var logger = log.GetLogger("api")
var OtpOpts = totp.GenerateOpts{
Issuer: constants.ProductName,
AccountName: constants.SuperUser,
Period: 30, // seconds
Digits: otp.DigitsSix,
Algorithm: otp.AlgorithmSHA1,
}
type PostLoginRequest struct {
Password string `json:"password"`
Passcode string `json:"passcode"`
Timestamp int64 `json:"timestamp"`
}
func PostLogin(c *gin.Context) {
var params PostLoginRequest
if err := c.BindJSON(&params); err != nil {
logger.Error(err)
response.Error(c, response.ErrorParamNotOK, http.StatusInternalServerError)
return
}
db := database.GetDB()
// only SuperUser in v0.9
var user model.User
db.Where(&model.User{Username: constants.SuperUser}).First(&user)
// Password authentication (new)
if params.Password != "" {
if !user.CheckPassword(params.Password) {
response.Error(c, response.JSONBody{Err: response.ErrWrongPasscode, Msg: "Invalid password"}, http.StatusUnauthorized)
return
}
// If TOTP is enabled, also require passcode (2FA)
if user.TFAEnabled && user.TFASecret != "" {
if params.Passcode == "" {
response.Error(c, response.JSONBody{Err: "totp_required", Msg: "TOTP verification required"}, http.StatusUnauthorized)
return
}
valid := totp.Validate(params.Passcode, user.TFASecret)
if !valid {
// Check clock skew
millisecondTimestamp := params.Timestamp
localTimeStamp := time.Now()
if millisecondTimestamp > 0 {
otpTime := time.Unix(millisecondTimestamp/1000, (millisecondTimestamp%1000)*int64(time.Millisecond))
timeSub := localTimeStamp.Sub(otpTime)
seconds := math.Abs(timeSub.Seconds())
if seconds >= 60 {
response.Error(c, response.JSONBody{Err: response.ErrWrongTimeGap, Msg: "otp timestamp gap is more than a minute"}, http.StatusUnauthorized)
return
}
} else {
response.Error(c, response.JSONBody{Err: response.ErrWrongPasscode, Msg: "Invalid TOTP passcode"}, http.StatusUnauthorized)
return
}
}
}
// Password auth success
user.LastLoginTime = time.Now().Unix()
user.IsEnabled = true
db.Save(&user)
session := sessions.Default(c)
session.Options(sessions.Options{
Path: "/",
MaxAge: 3600 * 24 * 7,
})
session.Set(constants.DefaultSessionUserKey, user.ID)
if err := session.Save(); err != nil {
logger.Error(err)
response.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: "Error occurred when creating sessions"}, http.StatusInternalServerError)
return
}
response.Success(c, gin.H{"role": user.Role})
return
}
// Legacy TOTP-only authentication
if params.Passcode != "" {
valid := totp.Validate(params.Passcode, user.TFASecret)
if !valid {
millisecondTimestamp := params.Timestamp
localTimeStamp := time.Now()
if millisecondTimestamp > 0 {
otpTime := time.Unix(millisecondTimestamp/1000, (millisecondTimestamp%1000)*int64(time.Millisecond))
timeSub := localTimeStamp.Sub(otpTime)
seconds := math.Abs(timeSub.Seconds())
if seconds >= 60 {
response.Error(c, response.JSONBody{Err: response.ErrWrongTimeGap, Msg: "otp timestamp gap is more than a minute"}, http.StatusUnauthorized)
return
}
}
response.Error(c, response.JSONBody{Err: response.ErrWrongPasscode, Msg: "Failed to verify your passcode"}, http.StatusUnauthorized)
return
}
user.LastLoginTime = time.Now().Unix()
user.IsEnabled = true
db.Save(&user)
session := sessions.Default(c)
session.Options(sessions.Options{
Path: "/",
MaxAge: 3600 * 24 * 7,
})
session.Set(constants.DefaultSessionUserKey, user.ID)
if err := session.Save(); err != nil {
logger.Error(err)
response.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: "Error occurred when creating sessions"}, http.StatusInternalServerError)
return
}
response.Success(c, gin.H{"role": user.Role})
return
}
response.Error(c, response.JSONBody{Err: response.ErrorParamNotOK, Msg: "Password or passcode required"}, http.StatusBadRequest)
}
func PostLogout(c *gin.Context) {
session := sessions.Default(c)
session.Clear()
if err := session.Save(); err != nil {
response.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: "Error occurred when creating sessions"}, http.StatusInternalServerError)
return
}
response.Success(c, nil)
}
func GetOTPUrl(c *gin.Context) {
otpKey, err := totp.Generate(OtpOpts)
if err != nil {
logger.Error(err)
response.Error(c, response.JSONBody{Err: response.ErrInternalError, Msg: "Error occurred when generating otp qrcode"}, http.StatusInternalServerError)
return
}
db := database.GetDB()
// only SuperUser in v0.9
user := model.User{Username: constants.SuperUser}
db.First(&user)
if user.LastLoginTime > 0 {
// already bind tfa, because tfa binding is mandatory when login.
response.Success(c, gin.H{"url": ""})
return
}
user.TFASecret = otpKey.Secret()
db.Save(&user)
response.Success(c, gin.H{"url": otpKey.URL()})
}
func GetUser(c *gin.Context) {
db := database.GetDB()
user := model.User{Username: constants.SuperUser}
db.First(&user)
response.Success(c, gin.H{"id": user.ID, "username": user.Username, "role": user.Role})
}