From 6c567cd58c5f7eb01761fe5d5a14b2755bdfa35b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=90=99PiperYxzzy?= Date: Sun, 1 May 2022 19:20:47 +0200 Subject: [PATCH] Verify and password reset * Users can now request a password reset and reset with their token --- controllers/core/core.go | 128 ++++++++++++++++++++++++++++++++++++++- main.go | 5 +- models/auth.go | 4 +- models/user.go | 34 +++++++++++ 4 files changed, 166 insertions(+), 5 deletions(-) diff --git a/controllers/core/core.go b/controllers/core/core.go index 22f2133..68dff2c 100644 --- a/controllers/core/core.go +++ b/controllers/core/core.go @@ -8,6 +8,7 @@ import ( "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" "github.com/google/uuid" + "github.com/yxzzy-wtf/gin-gonic-prepack/database" "github.com/yxzzy-wtf/gin-gonic-prepack/models" "github.com/yxzzy-wtf/gin-gonic-prepack/util" ) @@ -23,6 +24,15 @@ type signup struct { Password string `json:"password" binding:"required"` } +type forgotten struct { + UserKey string `json:"userkey" binding:"required,email"` +} + +type reset struct { + Token string `json:"token" binding:"required"` + NewPassword string `json:"password" binding:"required"` +} + const JwtHeader = "jwt" func UserSignup() gin.HandlerFunc { @@ -53,7 +63,8 @@ func UserSignup() gin.HandlerFunc { } } else { // Send verification - go util.SendEmail("Verify Email", "TODO: generateverification token", u.Email) + verifyToken := u.GetVerificationJwt() + go util.SendEmail("Verify Email", "Helloooo! Go here to verify: http://localhost:9091/v1/verify?verify="+verifyToken, u.Email) } c.JSON(http.StatusOK, util.NextMsg{Next: "verification pending"}) @@ -65,6 +76,7 @@ func UserLogin() gin.HandlerFunc { var loginVals login if err := c.ShouldBind(&loginVals); err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, util.FailMsg{Reason: "Requires username and password"}) + return } u := models.User{} @@ -87,6 +99,120 @@ func UserLogin() gin.HandlerFunc { } } +func UserVerify() gin.HandlerFunc { + return func(c *gin.Context) { + verifyJwt, _ := c.GetQuery("verify") + + claims, err := parseJwt(verifyJwt, models.UserHmac) + if err != nil || claims["role"] != "verify" { + fmt.Println("bad claim or role not 'verify'", err) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + // Yay! Jwt is a verify token, let's verify the linked user + uid, err := uuid.Parse(claims["sub"].(string)) + if err != nil { + fmt.Println("sub should ALWAYS be valid uuid at this point??", err) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + verifying := models.User{ + Auth: models.Auth{ + Base: models.Base{ + Uid: uid, + }, + }, + } + + if err := database.Db.Find(&verifying).Error; err != nil { + fmt.Println("could not find user", err) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + if verifying.Verified { + // User already verified + c.JSON(http.StatusOK, util.NextMsg{Next: "verified"}) + return + } + + verifying.Verified = true + if err := verifying.Save(); err != nil { + fmt.Println("could not verify user", err) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + c.JSON(http.StatusOK, util.NextMsg{Next: "verified"}) + } +} + +func UserForgotPassword() gin.HandlerFunc { + return func(c *gin.Context) { + var forgotVals forgotten + if err := c.ShouldBind(&forgotVals); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, util.FailMsg{Reason: "requires email"}) + return + } + + u := models.User{} + if err := u.ByEmail(forgotVals.UserKey); err == nil { + // Actually send renew token + forgotJwt := u.GetResetPasswordJwt() + go util.SendEmail("Forgot Password", "Token to reset password: "+forgotJwt, u.Email) + } + + c.JSON(http.StatusOK, util.NextMsg{Next: "check email to reset"}) + } +} + +func UserResetForgottenPassword() gin.HandlerFunc { + return func(c *gin.Context) { + var resetVals reset + if err := c.ShouldBind(&resetVals); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, util.FailMsg{Reason: "requires new pass and token"}) + return + } + + claims, err := parseJwt(resetVals.Token, models.UserHmac) + if err != nil || claims["role"] != "reset" { + fmt.Println("bad claim or role not 'reset'", err) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + uid, err := uuid.Parse(claims["sub"].(string)) + if err != nil { + fmt.Println("sub should ALWAYS be valid uuid at this point??", err) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + resetting := models.User{ + Auth: models.Auth{ + Base: models.Base{ + Uid: uid, + }, + }, + } + + if err := database.Db.Find(&resetting).Error; err != nil { + fmt.Println("could not find user", err) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + resetting.SetPassword(resetVals.NewPassword) + if err := resetting.Save(); err != nil { + fmt.Println("could not save user", err) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + c.JSON(http.StatusOK, util.NextMsg{Next: "login"}) + } +} + func AdminLogin() gin.HandlerFunc { return func(c *gin.Context) { var loginVals login diff --git a/main.go b/main.go index a3e9ebb..61ba6d6 100644 --- a/main.go +++ b/main.go @@ -27,9 +27,12 @@ func main() { // Ping functionality v1.GET("/doot", core.Doot()) - // Standard user login + // Standard user signup, verify, login and forgot/reset pw v1.POST("/signup", core.UserSignup()) v1.POST("/login", core.UserLogin()) + v1.GET("/verify", core.UserVerify()) + v1.POST("/forgot", core.UserForgotPassword()) + v1.POST("/reset", core.UserResetForgottenPassword()) v1Sec := v1.Group("/sec", core.UserAuth()) v1Sec.GET("/doot", core.Doot()) diff --git a/models/auth.go b/models/auth.go index 817c5d9..67afd62 100644 --- a/models/auth.go +++ b/models/auth.go @@ -21,8 +21,6 @@ func (a *Auth) SetPassword(pass string) error { return nil } -const VerifiedRequired = false - func (a *Auth) Login(pass string, tfCode string) (error, bool) { if err := a.CheckPassword(pass); err != nil { return err, false @@ -32,7 +30,7 @@ func (a *Auth) Login(pass string, tfCode string) (error, bool) { return err, true } - if !a.Verified && VerifiedRequired { + if !a.Verified { return errors.New("not yet verified"), true } diff --git a/models/user.go b/models/user.go index 9de1873..4449590 100644 --- a/models/user.go +++ b/models/user.go @@ -36,6 +36,40 @@ func (u *User) GetJwt() (string, int) { return jstr, int(userJwtDuration.Seconds()) } +func (u *User) GetVerificationJwt() string { + j := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "sub": u.Uid.String(), + "iat": time.Now().Unix(), + "exp": time.Now().Add(time.Hour * 24).Unix(), + "role": "verify", + }) + + jstr, err := j.SignedString(UserHmac) + if err != nil { + // we should ALWAYS be able to build and sign a str + panic(err) + } + + return jstr +} + +func (u *User) GetResetPasswordJwt() string { + j := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "sub": u.Uid.String(), + "iat": time.Now().Unix(), + "exp": time.Now().Add(time.Minute * 15).Unix(), + "role": "reset", + }) + + jstr, err := j.SignedString(UserHmac) + if err != nil { + // we should ALWAYS be able to build and sign a str + panic(err) + } + + return jstr +} + func (u *User) ByEmail(email string) error { if err := database.Db.Where("email = ?", email).First(&u).Error; err != nil { return errors.New("not found")