From c8e741d46a5fe4d77aa04abb06e38d7cd76ca200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Schro=CC=88ter?= Date: Sun, 4 Oct 2020 04:18:58 +0200 Subject: [PATCH] Create site to let user reset password without knowing the old one --- assets/web/forgotPassword.tmpl | 126 +++++++++++++++++++++++++++++++++ endpoint/user/endpoint.go | 83 ++++++++++++++++++++++ setup/setup.go | 4 ++ 3 files changed, 213 insertions(+) create mode 100644 assets/web/forgotPassword.tmpl diff --git a/assets/web/forgotPassword.tmpl b/assets/web/forgotPassword.tmpl new file mode 100644 index 0000000..9dbeb1e --- /dev/null +++ b/assets/web/forgotPassword.tmpl @@ -0,0 +1,126 @@ + + + + + + + Akamu Account Password Reset + + + + + +
+ {{if .Done}} + Your password reset was successful. + +

You can log in with your new password in the App.

+ {{else if .Valid}} + Set a new password. + + {{range $_, $e := .Errors}} +

{{ $e }}

+ {{ end }} + +
+

+
+ +

+

+
+ +

+ +
+ + {{else if .ExpiredTime}} + Your link has expired. + +

+ Your link has expired since your request is older than 24h.
+ Just create a new request to reset your password. +

+ + {{else if .ExpiredUsed}} + You already updated your password. + +

+ Your link has expired since you already set a new password.
+ If you did not change your password since you requested a password + reset, please contact + support@akamu.de. +

+ + {{else if .Failure}} + An error occurred. + +

+ Please try to reset your password again.
+ If it repeatedly failes, contact the support at + support@akamu.de. +

+ + {{else}} + Your request seems to be invalid. + +

+ Check that you called the link as in your email.
+ Get additional help from + support@akamu.de. +

+ {{ end }} +
+ + diff --git a/endpoint/user/endpoint.go b/endpoint/user/endpoint.go index 107c563..0e2b25a 100755 --- a/endpoint/user/endpoint.go +++ b/endpoint/user/endpoint.go @@ -15,6 +15,7 @@ import ( "github.com/getsentry/raven-go" "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" "gitlab.akamu.de/akamu/game-server-go/schemas" ) @@ -372,6 +373,88 @@ func ForgotPassword(repository UserQuery, tokenFactory otp.TokenFactory, templat } } +func GetResetPassword(repository UserQuery, tokenFactory otp.TokenFactory) gin.HandlerFunc { + return func(ctx *gin.Context) { + token, ok := ctx.GetQuery("token") + + if !ok { + ctx.HTML(http.StatusBadRequest, "forgotPassword.tmpl", nil) + return + } + + // Validate and get payload from token + userID, hash, err := tokenFactory.ParsePasswordResetToken(token) + if err == otp.ErrTokenExpired { + ctx.HTML(http.StatusGone, "forgotPassword.tmpl", gin.H{"ExpiredTime": true}) + return + } else if err != nil { + ctx.HTML(http.StatusBadRequest, "forgotPassword.tmpl", nil) + return + } + + // Check if user already updated the password + if cred, err := repository.SelectCredentialsByField(FieldID, userID); err != nil { + ctx.HTML(http.StatusInternalServerError, "forgotPassword.tmpl", gin.H{"Failure": true}) + return + } else if cred.PasswordCipher != hash { + ctx.HTML(http.StatusGone, "forgotPassword.tmpl", gin.H{"ExpiredUsed": true}) + return + } + + ctx.HTML(http.StatusOK, "forgotPassword.tmpl", gin.H{"Valid": true}) + } +} + +func PostResetPassword(repository UserQuery, tokenFactory otp.TokenFactory) gin.HandlerFunc { + return func(ctx *gin.Context) { + token, ok := ctx.GetQuery("token") + + if !ok { + ctx.HTML(http.StatusBadRequest, "forgotPassword.tmpl", nil) + return + } + + userID, hash, err := tokenFactory.ParsePasswordResetToken(token) + if err == otp.ErrTokenExpired { + ctx.HTML(http.StatusGone, "forgotPassword.tmpl", gin.H{"ExpiredTime": true}) + return + } else if err != nil { + ctx.HTML(http.StatusBadRequest, "forgotPassword.tmpl", nil) + return + } + + var formData struct { + Password string `form:"password" binding:"required,password"` + Confirm string `form:"confirm-password" binding:"required,eqfield=Password"` + } + + if err := ctx.ShouldBind(&formData); err != nil { + errs := err.(validator.ValidationErrors) + ctx.HTML(http.StatusBadRequest, "forgotPassword.tmpl", gin.H{"Valid": true, "Errors": errs.Translate(endpoint.GetTranslator())}) + return + } + + // Check that the password has not been updated since creation of the token + if cred, err := repository.SelectCredentialsByField(FieldID, userID); err != nil { + raven.CaptureError(err, nil) + ctx.HTML(http.StatusInternalServerError, "forgotPassword.tmpl", gin.H{"Failure": true}) + return + } else if cred.PasswordCipher != hash { + ctx.HTML(http.StatusGone, "forgotPassword.tmpl", gin.H{"ExpiredUsed": true}) + return + } + + // Update password + if err := repository.UpdatePassword(userID, formData.Password); err != nil { + raven.CaptureError(err, nil) + ctx.HTML(http.StatusInternalServerError, "forgotPassword.tmpl", gin.H{"Failure": true}) + return + } + + ctx.HTML(http.StatusOK, "forgotPassword.tmpl", gin.H{"Done": true}) + } +} + func sendRegistrationVerification(tokenFactory otp.TokenFactory, templatemail *sendmail.TemplateMail, userID uint32, username, email string) error { // Create token token, err := tokenFactory.CreateConfirmRegistrationToken(userID, email) diff --git a/setup/setup.go b/setup/setup.go index 24abeca..4c56cc6 100644 --- a/setup/setup.go +++ b/setup/setup.go @@ -279,6 +279,10 @@ func SetupRoutes(courseRepository course.CourseQuery, flashcardRepository flashc ctx.Header("Allow", "POST") ctx.Status(200) }) + + router.GET("/forgotpassword/reset", user.GetResetPassword(userRepository, tokenFactory)) + router.POST("/forgotpassword/reset", user.PostResetPassword(userRepository, tokenFactory)) + router.PATCH("/refreshjwt", authMiddleware.RefreshHandler) router.OPTIONS("/refreshjwt", func(ctx *gin.Context) { ctx.Header("Allow", "PATCH") -- GitLab