add models / services / handlers of user / token / 2fa, add web server command

This commit is contained in:
MaysWind
2020-10-17 20:01:24 +08:00
parent c9ae001aef
commit 60c31e8894
18 changed files with 1649 additions and 2 deletions

View File

@@ -29,7 +29,8 @@ func updateDatabaseStructure(c *cli.Context) error {
log.BootInfof("[database.updateDatabaseStructure] starting maintaining")
_ = datastore.Container.UserStore.SyncStructs(new(models.User))
_ = datastore.Container.UserStore.SyncStructs(new(models.User), new(models.TwoFactor), new(models.TwoFactorRecoveryCode))
_ = datastore.Container.TokenStore.SyncStructs(new(models.TokenRecord))
log.BootInfof("[database.updateDatabaseStructure] maintained successfully")

176
cmd/webserver.go Normal file
View File

@@ -0,0 +1,176 @@
package cmd
import (
"fmt"
"path/filepath"
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
"github.com/urfave/cli"
"github.com/mayswind/lab/pkg/api"
"github.com/mayswind/lab/pkg/core"
"github.com/mayswind/lab/pkg/errs"
"github.com/mayswind/lab/pkg/log"
"github.com/mayswind/lab/pkg/middlewares"
"github.com/mayswind/lab/pkg/requestid"
"github.com/mayswind/lab/pkg/settings"
"github.com/mayswind/lab/pkg/utils"
"github.com/mayswind/lab/pkg/validators"
)
var WebServer = cli.Command{
Name: "server",
Usage: "lab web server operation",
Subcommands: []cli.Command{
{
Name: "run",
Usage: "Run lab web server",
Action: startWebServer,
},
},
}
func startWebServer(c *cli.Context) error {
config, err := initializeSystem(c)
if err != nil {
return err
}
log.BootInfof("[server.startWebServer] static root path is %s", config.StaticRootPath)
err = requestid.InitializeRequestIdGenerator(config)
if err != nil {
log.BootErrorf("[server.startWebServer] initializes requestid generator failed, because %s", err.Error())
return err
}
serverInfo := fmt.Sprintf("current server id is %d, current instance id is %d", requestid.Container.Current.GetCurrentServerUniqId(), requestid.Container.Current.GetCurrentInstanceUniqId())
uuidServerInfo := ""
if config.UuidGeneratorType == settings.UUID_GENERATOR_TYPE_INTERNAL {
uuidServerInfo = fmt.Sprintf(", current uuid server id is %d", config.UuidServerId)
}
log.BootInfof("[server.startWebServer] %s%s", serverInfo, uuidServerInfo)
if config.Mode == settings.MODE_PRODUCTION {
gin.SetMode(gin.ReleaseMode)
}
router := gin.New()
router.Use(bindMiddleware(middlewares.Recovery))
if config.EnableGZip {
router.Use(gzip.Gzip(gzip.DefaultCompression))
}
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
_ = v.RegisterValidation("notBlank", validators.NotBlank)
_ = v.RegisterValidation("validUsername", validators.ValidUsername)
_ = v.RegisterValidation("validEmail", validators.ValidEmail)
}
router.NoRoute(bindApi(api.Default.ApiNotFound))
router.NoMethod(bindApi(api.Default.MethodNotAllowed))
router.StaticFile("/", filepath.Join(config.StaticRootPath, "index.html"))
router.StaticFile("login", filepath.Join(config.StaticRootPath, "login.html"))
if config.EnableUserRegister {
router.StaticFile("register", filepath.Join(config.StaticRootPath, "register.html"))
}
router.StaticFile("robots.txt", filepath.Join(config.StaticRootPath, "robots.txt"))
router.Static("/js", filepath.Join(config.StaticRootPath, "js"))
router.Static("/css", filepath.Join(config.StaticRootPath, "css"))
router.Static("/img", filepath.Join(config.StaticRootPath, "img"))
router.Static("/lang", filepath.Join(config.StaticRootPath, "lang"))
apiRoute := router.Group("/api")
apiRoute.Use(bindMiddleware(middlewares.RequestId(config)))
apiRoute.Use(bindMiddleware(middlewares.RequestLog))
{
apiRoute.POST("/authorize.json", bindApi(api.Authorizations.AuthorizeHandler))
if config.EnableTwoFactor {
twoFactorRoute := apiRoute.Group("/2fa")
twoFactorRoute.Use(bindMiddleware(middlewares.JWTTwoFactorAuthorization))
{
twoFactorRoute.POST("/authorize.json", bindApi(api.Authorizations.TwoFactorAuthorizeHandler))
twoFactorRoute.POST("/recovery.json", bindApi(api.Authorizations.TwoFactorAuthorizeByRecoveryCodeHandler))
}
}
if config.EnableUserRegister {
apiRoute.POST("/register.json", bindApi(api.Users.UserRegisterHandler))
}
apiV1Route := apiRoute.Group("/v1")
apiV1Route.Use(bindMiddleware(middlewares.JWTAuthorization))
{
// Tokens
apiV1Route.GET("/tokens/list.json", bindApi(api.Tokens.TokenListHandler))
apiV1Route.POST("/tokens/revoke.json", bindApi(api.Tokens.TokenRevokeHandler))
apiV1Route.POST("/tokens/refresh.json", bindApi(api.Tokens.TokenRefreshHandler))
// Users
apiV1Route.GET("/users/profile/get.json", bindApi(api.Users.UserProfileHandler))
apiV1Route.POST("/users/profile/update.json", bindApi(api.Users.UserUpdateProfileHandler))
// Two Factor Authorization
if config.EnableTwoFactor {
apiV1Route.GET("/users/2fa/status.json", bindApi(api.TwoFactorAuthorizations.TwoFactorStatusHandler))
apiV1Route.POST("/users/2fa/enable/request.json", bindApi(api.TwoFactorAuthorizations.TwoFactorEnableRequestHandler))
apiV1Route.POST("/users/2fa/enable/confirm.json", bindApi(api.TwoFactorAuthorizations.TwoFactorEnableConfirmHandler))
apiV1Route.POST("/users/2fa/disable.json", bindApi(api.TwoFactorAuthorizations.TwoFactorDisableHandler))
apiV1Route.POST("/users/2fa/recovery/regenerate.json", bindApi(api.TwoFactorAuthorizations.TwoFactorRecoveryCodeRegenerateHandler))
}
}
}
listenAddr := fmt.Sprintf("%s:%d", config.HttpAddr, config.HttpPort)
if config.Protocol == settings.SCHEME_SOCKET {
log.BootInfof("[server.startWebServer] will run at socks:%s", config.UnixSocketPath)
err = router.RunUnix(config.UnixSocketPath)
} else if config.Protocol == settings.SCHEME_HTTP {
log.BootInfof("[server.startWebServer] will run at http://%s", listenAddr)
err = router.Run(listenAddr)
} else if config.Protocol == settings.SCHEME_HTTPS {
log.BootInfof("[server.startWebServer] will run at https://%s", listenAddr)
err = router.RunTLS(listenAddr, config.CertFile, config.CertKeyFile)
} else {
err = errs.ErrInvalidProtocol
}
if err != nil {
log.BootErrorf("[server.startWebServer] cannot start, because %s", err)
return err
}
return nil
}
func bindMiddleware(fn core.MiddlewareHandlerFunc) gin.HandlerFunc {
return func(c *gin.Context) {
fn(core.WrapContext(c))
}
}
func bindApi(fn core.ApiHandlerFunc) gin.HandlerFunc {
return func(ginCtx *gin.Context) {
c := core.WrapContext(ginCtx)
result, err := fn(c)
if err != nil {
utils.PrintErrorResult(c, err)
} else {
utils.PrintSuccessResult(c, result)
}
}
}

3
go.mod
View File

@@ -4,14 +4,17 @@ go 1.14
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gin-contrib/gzip v0.0.3
github.com/gin-gonic/gin v1.6.3
github.com/go-playground/validator/v10 v10.2.0
github.com/go-sql-driver/mysql v1.5.0
github.com/lib/pq v1.8.0
github.com/mattn/go-sqlite3 v1.14.4
github.com/pquerna/otp v1.2.0
github.com/sirupsen/logrus v1.7.0
github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/stretchr/testify v1.4.0
github.com/urfave/cli v1.22.4
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
gopkg.in/ini.v1 v1.62.0
xorm.io/core v0.7.3

15
go.sum
View File

@@ -1,7 +1,12 @@
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
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/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -10,6 +15,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gin-contrib/gzip v0.0.3 h1:etUaeesHhEORpZMp18zoOhepboiWnFtXrBZxszWUn4k=
github.com/gin-contrib/gzip v0.0.3/go.mod h1:YxxswVZIqOvcHEQpsSn+QF5guQtO1dCfy0shBPy4jFc=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
@@ -62,6 +69,12 @@ github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
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/pquerna/otp v1.2.0 h1:/A3+Jn+cagqayeR3iHs/L62m5ue7710D35zl1zJ1kok=
github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
@@ -79,6 +92,8 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=

31
lab.go
View File

@@ -1 +1,30 @@
package lab
package main
import (
"os"
"github.com/urfave/cli"
"github.com/mayswind/lab/cmd"
)
const LAB_VERSION = "0.1.0"
func main() {
app := cli.NewApp()
app.Name = "lab"
app.Usage = "A lightweight account book app hosted by yourself."
app.Version = LAB_VERSION
app.Commands = []cli.Command{
cmd.WebServer,
cmd.Database,
}
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "conf-path",
Usage: "Custom config `FILE` path",
},
}
app.Run(os.Args)
}

182
pkg/api/authorizations.go Normal file
View File

@@ -0,0 +1,182 @@
package api
import (
"github.com/pquerna/otp/totp"
"github.com/mayswind/lab/pkg/core"
"github.com/mayswind/lab/pkg/errs"
"github.com/mayswind/lab/pkg/log"
"github.com/mayswind/lab/pkg/models"
"github.com/mayswind/lab/pkg/services"
)
type AuthorizationsApi struct {
users *services.UserService
tokens *services.TokenService
twoFactorAuthorizations *services.TwoFactorAuthorizationService
}
var (
Authorizations = &AuthorizationsApi{
users: services.Users,
tokens: services.Tokens,
twoFactorAuthorizations: services.TwoFactorAuthorizations,
}
)
func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (interface{}, *errs.Error) {
var credential models.UserLoginRequest
err := c.ShouldBindJSON(&credential)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] parse request failed, because %s", err.Error())
return nil, errs.ErrLoginNameOrPasswordInvalid
}
user, err := a.users.GetUserByUsernameOrEmailAndPassword(credential.LoginName, credential.Password)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because %s", credential.LoginName, err.Error())
return nil, errs.ErrLoginNameOrPasswordWrong
}
err = a.users.UpdateUserLastLoginTime(user.Uid)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] failed to update last login time for user \"uid:%d\", because %s", user.Uid, err.Error())
}
twoFactorEnable := a.tokens.CurrentConfig().EnableTwoFactor
if twoFactorEnable {
twoFactorEnable, err = a.twoFactorAuthorizations.ExistsTwoFactorSetting(user.Uid)
if err != nil {
log.ErrorfWithRequestId(c, "[authorizations.AuthorizeHandler] failed to check two factor setting for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrSystemError)
}
}
var token string
var claims *core.UserTokenClaims
if twoFactorEnable {
token, claims, err = a.tokens.CreateRequire2FAToken(user, c)
} else {
token, claims, err = a.tokens.CreateToken(user, c)
}
if err != nil {
log.ErrorfWithRequestId(c, "[authorizations.AuthorizeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrTokenGenerating
}
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[authorizations.AuthorizeHandler] user \"uid:%d\" has logined, token type is %d, token will be expired at %d", user.Uid, claims.Type, claims.ExpiresAt)
return token, nil
}
func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (interface{}, *errs.Error) {
var credential models.TwoFactorLoginRequest
err := c.ShouldBindJSON(&credential)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] parse request failed, because %s", err.Error())
return nil, errs.ErrPasscodeInvalid
}
uid := c.GetCurrentUid()
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(uid)
if err != nil {
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get two factor setting for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrSystemError)
}
if !totp.Validate(credential.Passcode, twoFactorSetting.Secret) {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] passcode is invalid for user \"uid:%d\"", uid)
return nil, errs.ErrPasscodeInvalid
}
user, err := a.users.GetUserById(uid)
if err != nil {
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
return nil, errs.ErrUserNotFound
}
oldTokenClaims := c.GetTokenClaims()
err = a.tokens.DeleteTokenByClaims(oldTokenClaims)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to revoke temporary token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
}
token, claims, err := a.tokens.CreateToken(user, c)
if err != nil {
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrTokenGenerating
}
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" has authorized two factor via passcode, token will be expired at %d", user.Uid, claims.ExpiresAt)
return token, nil
}
func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Context) (interface{}, *errs.Error) {
var credential models.TwoFactorRecoveryCodeLoginRequest
err := c.ShouldBindJSON(&credential)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] parse request failed, because %s", err.Error())
return nil, errs.ErrTwoFactorRecoveryCodeInvalid
}
uid := c.GetCurrentUid()
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two factor setting for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrSystemError)
}
if !enableTwoFactor {
return nil, errs.ErrTwoFactorKeyIsNotEnabled
}
user, err := a.users.GetUserById(uid)
if err != nil {
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
return nil, errs.ErrUserNotFound
}
err = a.twoFactorAuthorizations.GetAndUseUserTwoFactorRecoveryCode(uid, credential.RecoveryCode, user.Salt)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two factor recovery code for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrTwoFactorRecoveryCodeNotExist)
}
oldTokenClaims := c.GetTokenClaims()
err = a.tokens.DeleteTokenByClaims(oldTokenClaims)
if err != nil {
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to revoke temporary token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
}
token, claims, err := a.tokens.CreateToken(user, c)
if err != nil {
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.ErrTokenGenerating
}
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" has authorized two factor via recovery code \"%s\", token will be expired at %d", user.Uid, credential.RecoveryCode, claims.ExpiresAt)
return token, nil
}

20
pkg/api/default.go Normal file
View File

@@ -0,0 +1,20 @@
package api
import (
"github.com/mayswind/lab/pkg/core"
"github.com/mayswind/lab/pkg/errs"
)
type DefaultApi struct {}
var (
Default = &DefaultApi{}
)
func (a *DefaultApi) ApiNotFound(c *core.Context) (interface{}, *errs.Error) {
return nil, errs.ErrApiNotFound
}
func (a *DefaultApi) MethodNotAllowed(c *core.Context) (interface{}, *errs.Error) {
return nil, errs.ErrMethodNotAllowed
}

120
pkg/api/tokens.go Normal file
View File

@@ -0,0 +1,120 @@
package api
import (
"github.com/mayswind/lab/pkg/core"
"github.com/mayswind/lab/pkg/errs"
"github.com/mayswind/lab/pkg/log"
"github.com/mayswind/lab/pkg/models"
"github.com/mayswind/lab/pkg/services"
"github.com/mayswind/lab/pkg/utils"
)
type TokensApi struct {
tokens *services.TokenService
users *services.UserService
}
var (
Tokens = &TokensApi{
tokens: services.Tokens,
users: services.Users,
}
)
func (a *TokensApi) TokenListHandler(c *core.Context) (interface{}, *errs.Error) {
uid := c.GetCurrentUid()
tokens, err := a.tokens.GetAllTokensByUid(uid)
if err != nil {
log.ErrorfWithRequestId(c, "[tokens.TokenListHandler] failed to get all tokens for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrOperationFailed
}
tokenResps := make([]*models.TokenInfoResponse, len(tokens))
claims := c.GetTokenClaims()
for i := 0; i < len(tokens); i++ {
token := tokens[i]
tokenResp := &models.TokenInfoResponse{
TokenId: a.tokens.GenerateTokenId(token),
TokenType: token.TokenType,
UserAgent: token.UserAgent,
CreatedAt: token.CreatedUnixTime,
ExpiredAt: token.ExpiredUnixTime,
}
if utils.Int64ToString(token.Uid) == claims.Id && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
tokenResp.IsCurrent = true
}
tokenResps[i] = tokenResp
}
return tokenResps, nil
}
func (a *TokensApi) TokenRevokeHandler(c *core.Context) (interface{}, *errs.Error) {
var tokenRevokeReq models.TokenRevokeRequest
err := c.ShouldBindJSON(&tokenRevokeReq)
if err != nil {
log.WarnfWithRequestId(c, "[tokens.TokenRevokeHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
tokenRecord, err := a.tokens.ParseFromTokenId(tokenRevokeReq.TokenId)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[token.TokenRevokeHandler] failed to parse token \"id:%s\", because %s", tokenRevokeReq.TokenId, err.Error())
}
return nil, errs.Or(err, errs.ErrInvalidTokenId)
}
uid := c.GetCurrentUid()
if tokenRecord.Uid != uid {
log.WarnfWithRequestId(c, "[token.TokenRevokeHandler] token \"id:%s\" is not owned by user \"uid:%d\"", tokenRevokeReq.TokenId, uid)
return nil, errs.ErrInvalidTokenId
}
err = a.tokens.DeleteToken(tokenRecord)
if err != nil {
log.ErrorfWithRequestId(c, "[token.TokenRevokeHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", tokenRevokeReq.TokenId, uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[token.TokenRevokeHandler] user \"uid:%d\" has revoked token \"id:%s\"", uid, tokenRevokeReq.TokenId)
return true, nil
}
func (a *TokensApi) TokenRefreshHandler(c *core.Context) (interface{}, *errs.Error) {
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
if err != nil {
log.WarnfWithRequestId(c, "[token.TokenRefreshHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
return nil, errs.ErrUserNotFound
}
token, claims, err := a.tokens.CreateToken(user, c)
if err != nil {
log.ErrorfWithRequestId(c, "[token.TokenRefreshHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrTokenGenerating)
}
oldTokenClaims := c.GetTokenClaims()
err = a.tokens.DeleteTokenByClaims(oldTokenClaims)
if err != nil {
log.WarnfWithRequestId(c, "[token.TokenRefreshHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
}
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[token.TokenRefreshHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
return token, nil
}

View File

@@ -0,0 +1,278 @@
package api
import (
"bytes"
"encoding/base64"
"image/png"
"time"
"github.com/pquerna/otp/totp"
"github.com/mayswind/lab/pkg/core"
"github.com/mayswind/lab/pkg/errs"
"github.com/mayswind/lab/pkg/log"
"github.com/mayswind/lab/pkg/models"
"github.com/mayswind/lab/pkg/services"
)
type TwoFactorAuthorizationsApi struct {
twoFactorAuthorizations *services.TwoFactorAuthorizationService
users *services.UserService
tokens *services.TokenService
}
var (
TwoFactorAuthorizations = &TwoFactorAuthorizationsApi{
twoFactorAuthorizations: services.TwoFactorAuthorizations,
users: services.Users,
tokens: services.Tokens,
}
)
func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.Context) (interface{}, *errs.Error) {
uid := c.GetCurrentUid()
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(uid)
if err == errs.ErrTwoFactorKeyIsNotEnabled {
statusResp := &models.TwoFactorStatusResponse{
Enable: false,
}
return statusResp, nil
}
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorStatusHandler] failed to get two factor setting, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
statusResp := &models.TwoFactorStatusResponse{
Enable: true,
CreatedAt: twoFactorSetting.CreatedUnixTime,
}
return statusResp, nil
}
func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Context) (interface{}, *errs.Error) {
uid := c.GetCurrentUid()
enabled, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to check two factor setting, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if enabled {
return nil, errs.ErrTwoFactorKeyAlreadyEnabled
}
user, err := a.users.GetUserById(uid)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
key, err := a.twoFactorAuthorizations.GenerateTwoFactorSecret(user)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two factor secret, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
img, err := key.Image(240, 240)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two factor qrcode, because %s", err.Error())
return nil, errs.ErrOperationFailed
}
imgData := &bytes.Buffer{}
if err = png.Encode(imgData, img); err != nil {
return nil, errs.ErrOperationFailed
}
enableResp := &models.TwoFactorEnableResponse{
Secret: key.Secret(),
QRCode: "data:image/png;base64," + base64.StdEncoding.EncodeToString(imgData.Bytes()),
}
return enableResp, nil
}
func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Context) (interface{}, *errs.Error) {
var confirmReq models.TwoFactorEnableConfirmRequest
err := c.ShouldBindJSON(&confirmReq)
if err != nil {
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
exists, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to check two factor setting, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if exists {
return nil, errs.ErrTwoFactorKeyAlreadyEnabled
}
user, err := a.users.GetUserById(uid)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
twoFactorSetting := &models.TwoFactor{
Uid: uid,
Secret: confirmReq.Secret,
}
if !totp.Validate(confirmReq.Passcode, confirmReq.Secret) {
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] passcode is invalid")
return nil, errs.ErrPasscodeInvalid
}
recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to generate two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.twoFactorAuthorizations.CreateTwoFactorRecoveryCodes(uid, recoveryCodes, user.Salt)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.twoFactorAuthorizations.CreateTwoFactorSetting(twoFactorSetting)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two factor setting for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" has enabled two factor authorization", uid)
now := time.Now().Unix()
err = a.tokens.DeleteTokensBeforeTime(uid, now)
if err == nil {
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
} else {
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
}
token, claims, err := a.tokens.CreateToken(user, c)
if err != nil {
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
confirmResp := &models.TwoFactorEnableConfirmResponse{
RecoveryCodes: recoveryCodes,
}
return confirmResp, nil
}
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
confirmResp := &models.TwoFactorEnableConfirmResponse{
Token: token,
RecoveryCodes: recoveryCodes,
}
return confirmResp, nil
}
func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (interface{}, *errs.Error) {
uid := c.GetCurrentUid()
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to check two factor setting, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if !enableTwoFactor {
return nil, errs.ErrTwoFactorKeyIsNotEnabled
}
err = a.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two factor recovery codes for user \"uid:%d\"", uid)
return nil, errs.Or(err, errs.ErrOperationFailed)
}
err = a.twoFactorAuthorizations.DeleteTwoFactorSetting(uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two factor setting for user \"uid:%d\"", uid)
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] user \"uid:%d\" has disabled two factor authorization", uid)
return true, nil
}
func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *core.Context) (interface{}, *errs.Error) {
uid := c.GetCurrentUid()
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to check two factor setting, because %s", err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
if !enableTwoFactor {
return nil, errs.ErrTwoFactorKeyIsNotEnabled
}
recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to generate two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
user, err := a.users.GetUserById(uid)
if err != nil {
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.ErrUserNotFound
}
err = a.twoFactorAuthorizations.CreateTwoFactorRecoveryCodes(uid, recoveryCodes, user.Salt)
if err != nil {
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to create two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
recoveryCodesResp := &models.TwoFactorEnableConfirmResponse{
RecoveryCodes: recoveryCodes,
}
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] user \"uid:%d\" has regenerated two factor recovery codes", uid)
return recoveryCodesResp, nil
}

179
pkg/api/users.go Normal file
View File

@@ -0,0 +1,179 @@
package api
import (
"strings"
"time"
"github.com/mayswind/lab/pkg/core"
"github.com/mayswind/lab/pkg/errs"
"github.com/mayswind/lab/pkg/log"
"github.com/mayswind/lab/pkg/models"
"github.com/mayswind/lab/pkg/services"
"github.com/mayswind/lab/pkg/utils"
)
type UsersApi struct {
users *services.UserService
tokens *services.TokenService
}
var (
Users = &UsersApi{
users: services.Users,
tokens: services.Tokens,
}
)
func (a *UsersApi) UserRegisterHandler(c *core.Context) (interface{}, *errs.Error) {
var userRegisterReq models.UserRegisterRequest
err := c.ShouldBindJSON(&userRegisterReq)
if err != nil {
log.WarnfWithRequestId(c, "[users.UserRegisterHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
userRegisterReq.Username = strings.TrimSpace(userRegisterReq.Username)
userRegisterReq.Email = strings.TrimSpace(userRegisterReq.Email)
userRegisterReq.Nickname = strings.TrimSpace(userRegisterReq.Nickname)
user := &models.User{
Username: userRegisterReq.Username,
Email: userRegisterReq.Email,
Nickname: userRegisterReq.Nickname,
Password: userRegisterReq.Password,
}
err = a.users.CreateUser(user)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserRegisterHandler] failed to create user \"%s\", because %s", user.Username, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[users.UserRegisterHandler] user \"%s\" has registered successfully, uid is %d", user.Username, user.Uid)
token, claims, err := a.tokens.CreateToken(user, c)
if err != nil {
log.WarnfWithRequestId(c, "[users.UserRegisterHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return true, nil
}
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[users.UserRegisterHandler] user \"uid:%d\" has logined, token will be expired at %d", user.Uid, claims.ExpiresAt)
return token, nil
}
func (a *UsersApi) UserProfileHandler(c *core.Context) (interface{}, *errs.Error) {
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[users.UserRegisterHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
userResp := &models.UserProfileResponse{
Uid : utils.Int64ToString(user.Uid),
Username: user.Username,
Email: user.Email,
Nickname: user.Nickname,
Type: user.Type,
CreatedAt: user.CreatedUnixTime,
UpdatedAt: user.UpdatedUnixTime,
LastLoginAt: user.LastLoginUnixTime,
}
return userResp, nil
}
func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs.Error) {
var userUpdateReq models.UserProfileUpdateRequest
err := c.ShouldBindJSON(&userUpdateReq)
if err != nil {
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] parse request failed, because %s", err.Error())
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
}
uid := c.GetCurrentUid()
user, err := a.users.GetUserById(uid)
if err != nil {
if !errs.IsCustomError(err) {
log.ErrorfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to get user, because %s", err.Error())
}
return nil, errs.ErrUserNotFound
}
userUpdateReq.Email = strings.TrimSpace(userUpdateReq.Email)
userUpdateReq.Nickname = strings.TrimSpace(userUpdateReq.Nickname)
anythingUpdate := false
if userUpdateReq.Email != "" && userUpdateReq.Email != user.Email {
anythingUpdate = true
} else {
userUpdateReq.Email = ""
}
if userUpdateReq.Password != "" && !a.users.IsPasswordEqualsUserPassword(userUpdateReq.Password, user) {
anythingUpdate = true
} else {
userUpdateReq.Password = ""
}
if userUpdateReq.Nickname != "" && userUpdateReq.Nickname != user.Nickname {
anythingUpdate = true
} else {
userUpdateReq.Nickname = ""
}
if !anythingUpdate {
return nil, errs.ErrNothingWillBeUpdated
}
user.Email = userUpdateReq.Email
user.Password = userUpdateReq.Password
user.Nickname = userUpdateReq.Nickname
keyProfileUpdated, err := a.users.UpdateUser(user)
if err != nil {
log.ErrorfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrOperationFailed)
}
log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" has updated successfully", user.Uid)
if keyProfileUpdated {
now := time.Now().Unix()
err = a.tokens.DeleteTokensBeforeTime(uid, now)
if err == nil {
log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
} else {
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
}
token, claims, err := a.tokens.CreateToken(user, c)
if err != nil {
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
return true, nil
}
c.SetTokenClaims(claims)
log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
return token, nil
} else {
return true, nil
}
}

View File

@@ -71,3 +71,8 @@ func Or(err error, defaultErr *Error) *Error {
return defaultErr
}
}
func IsCustomError(err error) bool {
_, ok := err.(*Error);
return ok
}

View File

@@ -0,0 +1,79 @@
package middlewares
import (
"time"
"github.com/mayswind/lab/pkg/core"
"github.com/mayswind/lab/pkg/errs"
"github.com/mayswind/lab/pkg/log"
"github.com/mayswind/lab/pkg/services"
"github.com/mayswind/lab/pkg/utils"
)
func JWTAuthorization(c *core.Context) {
claims, err := getTokenClaims(c)
if err != nil {
utils.PrintErrorResult(c, err)
return
}
if claims.Type == core.USER_TOKEN_TYPE_REQUIRE_2FA {
log.WarnfWithRequestId(c, "[authorization.JWTAuthorization] user \"uid:%s\" token requires 2fa", claims.Id)
utils.PrintErrorResult(c, errs.ErrTokenRequire2FA)
return
}
if claims.Type != core.USER_TOKEN_TYPE_NORMAL {
log.WarnfWithRequestId(c, "[authorization.JWTAuthorization] user \"uid:%s\" token type is invalid", claims.Id)
utils.PrintErrorResult(c, errs.ErrInvalidTokenType)
return
}
c.SetTokenClaims(claims)
c.Next()
}
func JWTTwoFactorAuthorization(c *core.Context) {
claims, err := getTokenClaims(c)
if err != nil {
utils.PrintErrorResult(c, err)
return
}
if claims.Type != core.USER_TOKEN_TYPE_REQUIRE_2FA {
log.WarnfWithRequestId(c, "[authorization.JWTTwoFactorAuthorization] user \"uid:%s\" token is not need two factor authorization", claims.Id)
utils.PrintErrorResult(c, errs.ErrTokenNotRequire2FA)
return
}
c.SetTokenClaims(claims)
c.Next()
}
func getTokenClaims(c *core.Context) (*core.UserTokenClaims, *errs.Error) {
token, claims, err := services.Tokens.ParseToken(c)
if err != nil {
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] failed to parse token, because %s", err.Error())
return nil, errs.ErrUnauthorizedAccess
}
if !token.Valid {
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] token is invalid")
return nil, errs.ErrInvalidToken
}
if !claims.VerifyExpiresAt(time.Now().Unix(), true) {
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] token is expired")
return nil, errs.ErrTokenExpired
}
if claims.Id == "" {
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] user id in token is empty")
return nil, errs.ErrInvalidToken
}
return claims, nil
}

View File

@@ -0,0 +1,28 @@
package models
import "github.com/mayswind/lab/pkg/core"
const TOKEN_USER_AGENT_MAX_LENGTH = 255
type TokenRecord struct {
Uid int64 `xorm:"PK"`
UserTokenId int64 `xorm:"PK"`
TokenType core.TokenType `xorm:"TINYINT NOT NULL"`
Secret string `xorm:"VARCHAR(10) NOT NULL"`
UserAgent string `xorm:"VARCHAR(255)"`
CreatedUnixTime int64 `xorm:"PK"`
ExpiredUnixTime int64
}
type TokenRevokeRequest struct {
TokenId string `json:"tokenId" binding:"required,notBlank"`
}
type TokenInfoResponse struct {
TokenId string `json:"tokenId"`
TokenType core.TokenType `json:"tokenType"`
UserAgent string `json:"userAgent"`
CreatedAt int64 `json:"createdAt"`
ExpiredAt int64 `json:"expiredAt"`
IsCurrent bool `json:"isCurrent"`
}

31
pkg/models/two_factor.go Normal file
View File

@@ -0,0 +1,31 @@
package models
type TwoFactor struct {
Uid int64 `xorm:"PK"`
Secret string `xorm:"VARCHAR(80) NOT NULL"`
CreatedUnixTime int64
}
type TwoFactorLoginRequest struct {
Passcode string `json:"passcode" binding:"required,notBlank,len=6"`
}
type TwoFactorEnableConfirmRequest struct {
Secret string `json:"secret" binding:"required,notBlank,len=32"`
Passcode string `json:"passcode" binding:"required,notBlank,len=6"`
}
type TwoFactorEnableResponse struct {
Secret string `json:"secret"`
QRCode string `json:"qrcode"`
}
type TwoFactorEnableConfirmResponse struct {
Token string `json:"token,omitempty"`
RecoveryCodes []string `json:"recoveryCodes"`
}
type TwoFactorStatusResponse struct {
Enable bool `json:"enable"`
CreatedAt int64 `json:"createdAt,omitempty"`
}

View File

@@ -0,0 +1,13 @@
package models
type TwoFactorRecoveryCode struct {
Uid int64 `xorm:"PK"`
RecoveryCode string `xorm:"VARCHAR(64) PK"`
Used bool `xorm:"NOT NULL"`
CreatedUnixTime int64
UsedUnixTime int64
}
type TwoFactorRecoveryCodeLoginRequest struct {
RecoveryCode string `json:"recoveryCode" binding:"required,notBlank,len=11"`
}

282
pkg/services/tokens.go Normal file
View File

@@ -0,0 +1,282 @@
package services
import (
"fmt"
"math"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request"
"xorm.io/xorm"
"github.com/mayswind/lab/pkg/core"
"github.com/mayswind/lab/pkg/datastore"
"github.com/mayswind/lab/pkg/errs"
"github.com/mayswind/lab/pkg/log"
"github.com/mayswind/lab/pkg/models"
"github.com/mayswind/lab/pkg/settings"
"github.com/mayswind/lab/pkg/utils"
)
type TokenService struct {
ServiceUsingDB
ServiceUsingConfig
}
var (
Tokens = &TokenService{
ServiceUsingDB: ServiceUsingDB{
container: datastore.Container,
},
ServiceUsingConfig: ServiceUsingConfig{
container: settings.Container,
},
}
)
func (s *TokenService) GetAllTokensByUid(uid int64) ([]*models.TokenRecord, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
var tokenRecords []*models.TokenRecord
err := s.TokenDB(uid).Cols("uid", "user_token_id", "token_type", "user_agent", "created_unix_time", "expired_unix_time").Where("uid=?", uid).OrderBy("created_unix_time desc").Find(&tokenRecords)
return tokenRecords, err
}
func (s *TokenService) ParseToken(c *core.Context) (*jwt.Token, *core.UserTokenClaims, error) {
claims := &core.UserTokenClaims{}
token, err := request.ParseFromRequest(c.Request, request.AuthorizationHeaderExtractor,
func (token *jwt.Token) (interface{}, error) {
uid, err := utils.StringToInt64(claims.Id)
now := time.Now().Unix()
if err != nil {
log.WarnfWithRequestId(c, "[tokens.ParseToken] user \"uid:%s\" in token is invalid, because %s", claims.Id, err.Error())
return nil, errs.ErrInvalidToken
}
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
if err != nil {
log.WarnfWithRequestId(c, "[tokens.ParseToken] token \"utid:%s\" in token of user \"uid:%s\" is invalid, because %s", claims.UserTokenId, claims.Id, err.Error())
return nil, errs.ErrInvalidUserTokenId
}
tokenRecord, err := s.getTokenRecord(uid, userTokenId, claims.IssuedAt)
if err != nil {
log.WarnfWithRequestId(c, "[tokens.ParseToken] token \"utid:%s\" of user \"uid:%s\" record not found, because %s", claims.UserTokenId, claims.Id, err.Error())
return nil, errs.ErrTokenRecordNotFound
}
if tokenRecord.ExpiredUnixTime < now {
log.WarnfWithRequestId(c, "[tokens.ParseToken] token \"utid:%s\" of user \"uid:%s\" record is expired", claims.UserTokenId, claims.Id)
return nil, errs.ErrTokenExpired
}
return []byte(tokenRecord.Secret), nil
}, request.WithClaims(claims))
if err != nil {
return nil, nil, err
}
return token, claims, err
}
func (s *TokenService) CreateToken(user *models.User, ctx *core.Context) (string, *core.UserTokenClaims, error) {
return s.createToken(user, core.USER_TOKEN_TYPE_NORMAL, s.getUserAgent(ctx), s.CurrentConfig().TokenExpiredTimeDuration)
}
func (s *TokenService) CreateRequire2FAToken(user *models.User, ctx *core.Context) (string, *core.UserTokenClaims, error) {
return s.createToken(user, core.USER_TOKEN_TYPE_REQUIRE_2FA, s.getUserAgent(ctx), s.CurrentConfig().TemporaryTokenExpiredTimeDuration)
}
func (s *TokenService) DeleteToken(tokenRecord *models.TokenRecord) error {
if tokenRecord.Uid <= 0 {
return errs.ErrUserIdInvalid
}
if tokenRecord.UserTokenId <= 0 {
return errs.ErrInvalidUserTokenId
}
return s.TokenDB(tokenRecord.Uid).DoTranscation(func(sess *xorm.Session) error {
deletedRows, err := sess.Where("uid=? AND user_token_id=? AND created_unix_time=?", tokenRecord.Uid, tokenRecord.UserTokenId, tokenRecord.CreatedUnixTime).Delete(&models.TokenRecord{})
if deletedRows < 1 {
return errs.ErrTokenRecordNotFound
}
return err
})
}
func (s *TokenService) DeleteTokenByClaims(claims *core.UserTokenClaims) error {
uid, err := utils.StringToInt64(claims.Id)
if err != nil {
return errs.ErrUserIdInvalid
}
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
if err != nil {
return errs.ErrInvalidUserTokenId
}
return s.DeleteToken(&models.TokenRecord{Uid: uid, UserTokenId: userTokenId, CreatedUnixTime: claims.IssuedAt})
}
func (s *TokenService) DeleteTokensBeforeTime(uid int64, expireTime int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
return s.TokenDB(uid).DoTranscation(func(sess *xorm.Session) error {
_, err := sess.Where("uid=? AND created_unix_time<?", uid, expireTime).Delete(&models.TokenRecord{})
return err
})
}
func (s *TokenService) ParseFromTokenId(tokenId string) (*models.TokenRecord, error) {
pairs := strings.Split(tokenId, ":")
if len(pairs) != 3 {
return nil, errs.ErrInvalidTokenId
}
uid, err := utils.StringToInt64(pairs[0])
if err != nil {
return nil, errs.ErrInvalidTokenId
}
createdUnixTime, err := utils.StringToInt64(pairs[1])
if err != nil {
return nil, errs.ErrInvalidTokenId
}
userTokenId, err := utils.StringToInt64(pairs[2])
if err != nil {
return nil, errs.ErrInvalidTokenId
}
tokenRecord := &models.TokenRecord{
Uid: uid,
UserTokenId: userTokenId,
CreatedUnixTime: createdUnixTime,
}
return tokenRecord, nil
}
func (s *TokenService) GenerateTokenId(tokenRecord *models.TokenRecord) string {
return fmt.Sprintf("%d:%d:%d", tokenRecord.Uid, tokenRecord.CreatedUnixTime, tokenRecord.UserTokenId)
}
func (s *TokenService) createToken(user *models.User, tokenType core.TokenType, userAgent string, expiryDate time.Duration) (string, *core.UserTokenClaims, error) {
var err error
now := time.Now()
tokenRecord := &models.TokenRecord{
Uid: user.Uid,
UserTokenId: s.getUserTokenId(),
TokenType: tokenType,
UserAgent: userAgent,
CreatedUnixTime: now.Unix(),
ExpiredUnixTime: now.Add(expiryDate).Unix(),
}
if tokenRecord.Secret, err = utils.GetRandomString(10); err != nil {
return "", nil, err
}
claims := &core.UserTokenClaims{
UserTokenId: utils.Int64ToString(tokenRecord.UserTokenId),
Username: user.Username,
Type: tokenRecord.TokenType,
StandardClaims: jwt.StandardClaims{
Id: utils.Int64ToString(tokenRecord.Uid),
IssuedAt: tokenRecord.CreatedUnixTime,
ExpiresAt: tokenRecord.ExpiredUnixTime,
},
}
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := jwtToken.SignedString([]byte(tokenRecord.Secret))
err = s.createTokenRecord(tokenRecord)
if err != nil {
return "", nil, err
}
return tokenString, claims, err
}
func (s *TokenService) getTokenRecord(uid int64, userTokenId int64, createUnixTime int64) (*models.TokenRecord, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
if userTokenId <= 0 {
return nil, errs.ErrInvalidUserTokenId
}
tokenRecord := &models.TokenRecord{}
has, err := s.TokenDB(uid).Where("uid=? AND user_token_id=? AND created_unix_time=?", uid, userTokenId, createUnixTime).Limit(1).Get(tokenRecord)
if err != nil {
return nil, err
}
if !has {
return nil, errs.ErrTokenRecordNotFound
}
return tokenRecord, nil
}
func (s *TokenService) createTokenRecord(tokenRecord *models.TokenRecord) error {
if tokenRecord.Uid <= 0 {
return errs.ErrUserIdInvalid
}
if tokenRecord.UserTokenId <= 0 {
return errs.ErrInvalidUserTokenId
}
return s.TokenDB(tokenRecord.Uid).DoTranscation(func(sess *xorm.Session) error {
_, err := sess.Insert(tokenRecord)
return err
})
}
func (s *TokenService) getUserTokenId() int64 {
nanoSeconds := time.Now().Nanosecond()
randomNumber, _ := utils.GetRandomInteger(math.MaxInt32)
userTokenId := (int64(nanoSeconds) << 32) | int64(randomNumber)
return userTokenId
}
func (s *TokenService) getUserAgent(ctx *core.Context) string {
userAgent := ""
if ctx != nil && ctx.Request != nil {
userAgent = ctx.Request.UserAgent()
}
if len(userAgent) > models.TOKEN_USER_AGENT_MAX_LENGTH {
userAgent = utils.SubString(userAgent, 0, models.TOKEN_USER_AGENT_MAX_LENGTH)
}
return userAgent
}

View File

@@ -0,0 +1,204 @@
package services
import (
"time"
"xorm.io/xorm"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/mayswind/lab/pkg/datastore"
"github.com/mayswind/lab/pkg/errs"
"github.com/mayswind/lab/pkg/models"
"github.com/mayswind/lab/pkg/settings"
"github.com/mayswind/lab/pkg/utils"
"github.com/mayswind/lab/pkg/uuid"
)
const (
TWOFACTOR_PERIOD uint = 30 // seconds
TWOFACTOR_SECRET_SIZE uint = 20 // bytes
TWOFACTOR_RECOVERY_CODE_COUNT int = 10
TWOFACTOR_RECOVERY_CODE_LENGTH int = 10 // bytes
)
type TwoFactorAuthorizationService struct {
ServiceUsingDB
ServiceUsingConfig
ServiceUsingUuid
}
var (
TwoFactorAuthorizations = &TwoFactorAuthorizationService{
ServiceUsingDB: ServiceUsingDB{
container: datastore.Container,
},
ServiceUsingConfig: ServiceUsingConfig{
container: settings.Container,
},
ServiceUsingUuid: ServiceUsingUuid{
container: uuid.Container,
},
}
)
func (s *TwoFactorAuthorizationService) GetUserTwoFactorSettingByUid(uid int64) (*models.TwoFactor, error) {
if uid <= 0 {
return nil, errs.ErrUserIdInvalid
}
twoFactor := &models.TwoFactor{}
has, err := s.UserDB().Where("uid=?", uid).Get(twoFactor)
if err != nil {
return nil, err
} else if !has {
return nil, errs.ErrTwoFactorKeyIsNotEnabled
}
twoFactor.Secret, err = utils.DecryptSecret(twoFactor.Secret, s.CurrentConfig().SecretKey)
if err != nil {
return nil, err
}
return twoFactor, nil
}
func (s *TwoFactorAuthorizationService) GenerateTwoFactorSecret(user *models.User) (*otp.Key, error) {
if user == nil {
return nil, errs.ErrUserNotFound
}
key, err := totp.Generate(totp.GenerateOpts{
Issuer: s.CurrentConfig().AppName,
AccountName: user.Username,
Period: TWOFACTOR_PERIOD,
SecretSize: TWOFACTOR_SECRET_SIZE,
})
return key, err
}
func (s *TwoFactorAuthorizationService) CreateTwoFactorSetting(twoFactor *models.TwoFactor) error {
if twoFactor.Uid <= 0 {
return errs.ErrUserIdInvalid
}
var err error
twoFactor.Secret, err = utils.EncyptSecret(twoFactor.Secret, s.CurrentConfig().SecretKey)
if err != nil {
return err
}
twoFactor.CreatedUnixTime = time.Now().Unix()
return s.UserDB().DoTranscation(func(sess *xorm.Session) error {
_, err := sess.Insert(twoFactor)
return err
})
}
func (s *TwoFactorAuthorizationService) DeleteTwoFactorSetting(uid int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
return s.UserDB().DoTranscation(func(sess *xorm.Session) error {
deletedRows, err := sess.Where("uid=?", uid).Delete(&models.TwoFactor{})
if deletedRows < 1 {
return errs.ErrTwoFactorKeyIsNotEnabled
}
return err
})
}
func (s *TwoFactorAuthorizationService) ExistsTwoFactorSetting(uid int64) (bool, error) {
if uid <= 0 {
return false, errs.ErrUserIdInvalid
}
return s.UserDB().Cols("uid").Where("uid=?", uid).Exist(&models.TwoFactor{})
}
func (s *TwoFactorAuthorizationService) GetAndUseUserTwoFactorRecoveryCode(uid int64, recoveryCode string, salt string) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
recoveryCode = utils.EncodePassword(recoveryCode, salt)
exists, err := s.UserDB().Cols("uid", "recovery_code").Where("uid=? AND recovery_code=? AND used=?", uid, recoveryCode, false).Exist(&models.TwoFactorRecoveryCode{})
if err != nil {
return err
} else if !exists {
return errs.ErrTwoFactorRecoveryCodeNotExist
}
return s.UserDB().DoTranscation(func(sess *xorm.Session) error {
_, err := sess.Cols("used", "used_unix_time").Where("uid=? AND recovery_code=?", uid, recoveryCode).Update(&models.TwoFactorRecoveryCode{Used: true, UsedUnixTime: time.Now().Unix()})
return err
})
}
func (s *TwoFactorAuthorizationService) GenerateTwoFactorRecoveryCodes() ([]string, error) {
recoveryCodes := make([]string, TWOFACTOR_RECOVERY_CODE_COUNT)
for i := 0; i < TWOFACTOR_RECOVERY_CODE_COUNT; i++ {
recoveryCode, err := utils.GetRandomNumberOrLetter(TWOFACTOR_RECOVERY_CODE_LENGTH)
if err != nil {
return nil, err
}
recoveryCodes[i] = recoveryCode[:5] + "-" + recoveryCode[5:]
}
return recoveryCodes, nil
}
func (s *TwoFactorAuthorizationService) CreateTwoFactorRecoveryCodes(uid int64, recoveryCodes []string, salt string) error {
twoFactorRecoveryCodes := make([]*models.TwoFactorRecoveryCode, len(recoveryCodes))
for i := 0; i < len(recoveryCodes); i++ {
twoFactorRecoveryCodes[i] = &models.TwoFactorRecoveryCode{
Uid: uid,
Used: false,
RecoveryCode: utils.EncodePassword(recoveryCodes[i], salt),
CreatedUnixTime: time.Now().Unix(),
}
}
return s.UserDB().DoTranscation(func(sess *xorm.Session) error {
_, err := sess.Where("uid=?", uid).Delete(&models.TwoFactorRecoveryCode{})
if err != nil {
return err
}
for i := 0; i < len(twoFactorRecoveryCodes); i++ {
twoFactorRecoveryCode := twoFactorRecoveryCodes[i]
_, err := sess.Insert(twoFactorRecoveryCode)
if err != nil {
return err
}
}
return nil
})
}
func (s *TwoFactorAuthorizationService) DeleteTwoFactorRecoveryCodes(uid int64) error {
if uid <= 0 {
return errs.ErrUserIdInvalid
}
return s.UserDB().DoTranscation(func(sess *xorm.Session) error {
_, err := sess.Where("uid=?", uid).Delete(&models.TwoFactorRecoveryCode{})
return err
})
}

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /