mirror of https://github.com/Xhofe/alist
feat: 2fa/otp support
parent
b51e664543
commit
a6ed4afdae
2
go.mod
2
go.mod
|
@ -13,6 +13,7 @@ require (
|
||||||
github.com/json-iterator/go v1.1.12
|
github.com/json-iterator/go v1.1.12
|
||||||
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
|
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/pquerna/otp v1.3.0
|
||||||
github.com/sirupsen/logrus v1.8.1
|
github.com/sirupsen/logrus v1.8.1
|
||||||
github.com/winfsp/cgofuse v1.5.0
|
github.com/winfsp/cgofuse v1.5.0
|
||||||
gorm.io/driver/mysql v1.3.4
|
gorm.io/driver/mysql v1.3.4
|
||||||
|
@ -22,6 +23,7 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.0 // indirect
|
github.com/go-playground/locales v0.14.0 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -3,6 +3,8 @@ github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030I
|
||||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||||
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a h1:RenIAa2q4H8UcS/cqmwdT1WCWIAH5aumP8m8RpbqVsE=
|
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a h1:RenIAa2q4H8UcS/cqmwdT1WCWIAH5aumP8m8RpbqVsE=
|
||||||
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a/go.mod h1:sSBbaOg90XwWKtpT56kVujF0bIeVITnPlssLclogS04=
|
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a/go.mod h1:sSBbaOg90XwWKtpT56kVujF0bIeVITnPlssLclogS04=
|
||||||
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
|
||||||
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
github.com/caarlos0/env/v6 v6.9.3 h1:Tyg69hoVXDnpO5Qvpsu8EoquarbPyQb+YwExWHP8wWU=
|
github.com/caarlos0/env/v6 v6.9.3 h1:Tyg69hoVXDnpO5Qvpsu8EoquarbPyQb+YwExWHP8wWU=
|
||||||
github.com/caarlos0/env/v6 v6.9.3/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc=
|
github.com/caarlos0/env/v6 v6.9.3/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc=
|
||||||
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||||
|
@ -160,6 +162,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs=
|
||||||
|
github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||||
|
|
|
@ -29,6 +29,7 @@ type User struct {
|
||||||
// 8: webdav read
|
// 8: webdav read
|
||||||
// 9: webdav write
|
// 9: webdav write
|
||||||
Permission int32 `json:"permission"`
|
Permission int32 `json:"permission"`
|
||||||
|
OtpSecret string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u User) IsGuest() bool {
|
func (u User) IsGuest() bool {
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package handles
|
package handles
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"image/png"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Xhofe/go-cache"
|
"github.com/Xhofe/go-cache"
|
||||||
|
@ -8,6 +11,7 @@ import (
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
"github.com/alist-org/alist/v3/server/common"
|
"github.com/alist-org/alist/v3/server/common"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var loginCache = cache.NewMemCache[int]()
|
var loginCache = cache.NewMemCache[int]()
|
||||||
|
@ -19,6 +23,7 @@ var (
|
||||||
type LoginReq struct {
|
type LoginReq struct {
|
||||||
Username string `json:"username" binding:"required"`
|
Username string `json:"username" binding:"required"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
OTPCode string `json:"otp_code"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Login(c *gin.Context) {
|
func Login(c *gin.Context) {
|
||||||
|
@ -26,7 +31,7 @@ func Login(c *gin.Context) {
|
||||||
ip := c.ClientIP()
|
ip := c.ClientIP()
|
||||||
count, ok := loginCache.Get(ip)
|
count, ok := loginCache.Get(ip)
|
||||||
if ok && count >= defaultTimes {
|
if ok && count >= defaultTimes {
|
||||||
common.ErrorStrResp(c, "Too many unsuccessful sign-in attempts have been made using an incorrect username or password, Try again later.", 403)
|
common.ErrorStrResp(c, "Too many unsuccessful sign-in attempts have been made using an incorrect username or password, Try again later.", 429)
|
||||||
loginCache.Expire(ip, defaultDuration)
|
loginCache.Expire(ip, defaultDuration)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -48,6 +53,14 @@ func Login(c *gin.Context) {
|
||||||
loginCache.Set(ip, count+1)
|
loginCache.Set(ip, count+1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// check 2FA
|
||||||
|
if user.OtpSecret != "" {
|
||||||
|
if !totp.Validate(req.OTPCode, user.OtpSecret) {
|
||||||
|
common.ErrorStrResp(c, "Invalid 2FA code", 402)
|
||||||
|
loginCache.Set(ip, count+1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
// generate token
|
// generate token
|
||||||
token, err := common.GenerateToken(user.Username)
|
token, err := common.GenerateToken(user.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -84,3 +97,60 @@ func UpdateCurrent(c *gin.Context) {
|
||||||
common.SuccessResp(c)
|
common.SuccessResp(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Generate2FA(c *gin.Context) {
|
||||||
|
user := c.MustGet("user").(*model.User)
|
||||||
|
if user.IsGuest() {
|
||||||
|
common.ErrorStrResp(c, "Guest user can not generate 2FA code", 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
key, err := totp.Generate(totp.GenerateOpts{
|
||||||
|
Issuer: "Alist",
|
||||||
|
AccountName: user.Username,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResp(c, err, 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
img, err := key.Image(400, 400)
|
||||||
|
if err != nil {
|
||||||
|
common.ErrorResp(c, err, 500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// to base64
|
||||||
|
var buf bytes.Buffer
|
||||||
|
png.Encode(&buf, img)
|
||||||
|
base64 := base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||||
|
common.SuccessResp(c, gin.H{
|
||||||
|
"qr": "data:image/png;base64," + base64,
|
||||||
|
"secret": key.Secret(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Verify2FAReq struct {
|
||||||
|
Code string `json:"code" binding:"required"`
|
||||||
|
Secret string `json:"secret" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Verify2FA(c *gin.Context) {
|
||||||
|
var req Verify2FAReq
|
||||||
|
if err := c.ShouldBind(&req); err != nil {
|
||||||
|
common.ErrorResp(c, err, 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := c.MustGet("user").(*model.User)
|
||||||
|
if user.IsGuest() {
|
||||||
|
common.ErrorStrResp(c, "Guest user can not generate 2FA code", 403)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !totp.Validate(req.Code, req.Secret) {
|
||||||
|
common.ErrorStrResp(c, "Invalid 2FA code", 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.OtpSecret = req.Secret
|
||||||
|
if err := db.UpdateUser(user); err != nil {
|
||||||
|
common.ErrorResp(c, err, 500)
|
||||||
|
} else {
|
||||||
|
common.SuccessResp(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -61,6 +61,9 @@ func UpdateUser(c *gin.Context) {
|
||||||
common.ErrorStrResp(c, "role can not be changed", 400)
|
common.ErrorStrResp(c, "role can not be changed", 400)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if req.Password == "" {
|
||||||
|
req.Password = user.Password
|
||||||
|
}
|
||||||
if err := db.UpdateUser(&req); err != nil {
|
if err := db.UpdateUser(&req); err != nil {
|
||||||
common.ErrorResp(c, err, 500)
|
common.ErrorResp(c, err, 500)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -25,6 +25,8 @@ func Init(r *gin.Engine) {
|
||||||
api.POST("/auth/login", handles.Login)
|
api.POST("/auth/login", handles.Login)
|
||||||
auth.GET("/me", handles.CurrentUser)
|
auth.GET("/me", handles.CurrentUser)
|
||||||
auth.POST("/me/update", handles.UpdateCurrent)
|
auth.POST("/me/update", handles.UpdateCurrent)
|
||||||
|
auth.POST("/auth/2fa/generate", handles.Generate2FA)
|
||||||
|
auth.POST("/auth/2fa/verify", handles.Verify2FA)
|
||||||
|
|
||||||
// no need auth
|
// no need auth
|
||||||
public := api.Group("/public")
|
public := api.Group("/public")
|
||||||
|
|
Loading…
Reference in New Issue