dkforest

A forum and chat platform (onion)
git clone https://git.dasho.dev/n0tr1v/dkforest.git
Log | Files | Refs | LICENSE

commit db33df66ead1546d00eae3a1528d3fdc53e8be7d
parent 050d419a8a6eb8d770fb704a13242988501ffcb9
Author: n0tr1v <n0tr1v@protonmail.com>
Date:   Sun,  4 Dec 2022 23:10:38 -0500

simplify recovery workflow

Diffstat:
Mpkg/web/handlers/handlers.go | 48+++++++++++++++++++++++++-----------------------
Mpkg/web/handlers/utils/utils.go | 15+++++++--------
Mpkg/web/public/views/pages/forgot-password.gohtml | 4++--
3 files changed, 34 insertions(+), 33 deletions(-)

diff --git a/pkg/web/handlers/handlers.go b/pkg/web/handlers/handlers.go @@ -578,10 +578,6 @@ func createPartialRecoveryCookie(value string) *http.Cookie { return hutils.CreateCookie(hutils.PartialRecoveryCookieName, value, 10*utils.OneMinuteSecs) } -func createPartialRecovery2Cookie(value string) *http.Cookie { - return hutils.CreateCookie(hutils.PartialRecovery2CookieName, value, 10*utils.OneMinuteSecs) -} - func createPartialSessionCookie(value string) *http.Cookie { return hutils.CreateCookie(hutils.PartialAuthCookieName, value, 10*utils.OneMinuteSecs) } @@ -932,12 +928,21 @@ func validateCaptcha(c echo.Context) error { // 2- Validate gpg token/signature // 3- Reset password // Since the user is not authenticated in any of these steps, we need to guard each steps and ensure the user can access it legitimately. -// partialRecoveryCache keeps track of users that can access step 2 (captcha was completed) -// partialRecovery2Cache keeps track of users that can access step 3 (gpg token/sign validated) -// Note: We cannot reuse the same cache, as a user could complete the captcha and hardcode the request to step 3 directly. +// partialRecoveryCache keeps track of users that are in the process of recovering their password and the step they're at. var ( - partialRecoveryCache = cache1.New[database.UserID](10*time.Minute, time.Hour) - partialRecovery2Cache = cache1.New[database.UserID](10*time.Minute, time.Hour) + partialRecoveryCache = cache1.New[PartialRecoveryItem](10*time.Minute, time.Hour) +) + +type PartialRecoveryItem struct { + UserID database.UserID + Step RecoveryStep +} + +type RecoveryStep int64 + +const ( + RecoveryCaptchaCompleted RecoveryStep = iota + 1 + RecoveryGpgValidated ) func generateCaptchaCssFrames(captchaSec int64) (frames []string) { @@ -1007,7 +1012,7 @@ func ForgotPasswordHandler(c echo.Context) error { } token := utils.GenerateToken32() - partialRecoveryCache.Set(token, user.ID, cache1.DefaultExpiration) + partialRecoveryCache.Set(token, PartialRecoveryItem{user.ID, RecoveryCaptchaCompleted}, cache1.DefaultExpiration) c.SetCookie(createPartialRecoveryCookie(token)) data.Step = 2 @@ -1022,10 +1027,11 @@ func ForgotPasswordHandler(c echo.Context) error { if err != nil { return c.Redirect(http.StatusFound, "/") } - userID, found := partialRecoveryCache.Get(partialRecoveryCookie.Value) - if !found { + item, found := partialRecoveryCache.Get(partialRecoveryCookie.Value) + if !found || item.Step != RecoveryCaptchaCompleted { return c.Redirect(http.StatusFound, "/") } + userID := item.UserID token, found := pgpTokenCache.Get(userID) if !found { @@ -1051,12 +1057,7 @@ func ForgotPasswordHandler(c echo.Context) error { } pgpTokenCache.Delete(userID) - partialRecoveryCache.Delete(partialRecoveryCookie.Value) - c.SetCookie(hutils.DeleteCookie(hutils.PartialRecoveryCookieName)) - - token2 := utils.GenerateToken32() - partialRecovery2Cache.Set(token2, userID, cache1.DefaultExpiration) - c.SetCookie(createPartialRecovery2Cookie(token2)) + partialRecoveryCache.Set(partialRecoveryCookie.Value, PartialRecoveryItem{userID, RecoveryGpgValidated}, cache1.DefaultExpiration) data.Step = 3 return c.Render(http.StatusOK, "forgot-password", data) @@ -1066,14 +1067,15 @@ func ForgotPasswordHandler(c echo.Context) error { data.Step = 3 // Step3 is guarded by the "partial-recovery2-token" cookie that must be valid - partialRecovery2Cookie, err := c.Cookie(hutils.PartialRecovery2CookieName) + partialRecoveryCookie, err := c.Cookie(hutils.PartialRecoveryCookieName) if err != nil { return c.Redirect(http.StatusFound, "/") } - userID, found := partialRecovery2Cache.Get(partialRecovery2Cookie.Value) - if !found { + item, found := partialRecoveryCache.Get(partialRecoveryCookie.Value) + if !found || item.Step != RecoveryGpgValidated { return c.Redirect(http.StatusFound, "/") } + userID := item.UserID user, err := database.GetUserByID(userID) if err != nil { return c.Redirect(http.StatusFound, "/") @@ -1095,8 +1097,8 @@ func ForgotPasswordHandler(c echo.Context) error { } database.CreateSecurityLog(user.ID, database.PasswordRecoverySecurityLog) - partialRecovery2Cache.Delete(partialRecovery2Cookie.Value) - c.SetCookie(hutils.DeleteCookie(hutils.PartialRecovery2CookieName)) + partialRecoveryCache.Delete(partialRecoveryCookie.Value) + c.SetCookie(hutils.DeleteCookie(hutils.PartialRecoveryCookieName)) return c.Render(http.StatusFound, "flash", FlashResponse{Message: "Password reset done", Redirect: "/login"}) } diff --git a/pkg/web/handlers/utils/utils.go b/pkg/web/handlers/utils/utils.go @@ -14,14 +14,13 @@ import ( ) const ( - HBCookieName = "dkft" // dkf troll - SignupCookieName = "signup-token" - AuthCookieName = "auth-token" - PartialAuthCookieName = "partial-auth-token" - PartialRecoveryCookieName = "partial-recovery-token" - PartialRecovery2CookieName = "partial-recovery2-token" - AprilFoolCookieName = "april_fool" - ByteRoadCookieName = "challenge_byte_road_session" + HBCookieName = "dkft" // dkf troll + SignupCookieName = "signup-token" + AuthCookieName = "auth-token" + PartialAuthCookieName = "partial-auth-token" + PartialRecoveryCookieName = "partial-recovery-token" + AprilFoolCookieName = "april_fool" + ByteRoadCookieName = "challenge_byte_road_session" ) func CreateCookie(name, value string, maxAge int64) *http.Cookie { diff --git a/pkg/web/public/views/pages/forgot-password.gohtml b/pkg/web/public/views/pages/forgot-password.gohtml @@ -172,13 +172,13 @@ <div class="row"> <div class="col-sm-12 col-md-10 offset-md-1 "> <div class="form-group"> - <input placeholder="{{ t "New password" . }}" name="newPassword" value="{{ .Data.NewPassword }}" class="form-control{{ if .Data.ErrorNewPassword }} is-invalid{{ end }}" type="password"{{ if .Data.ErrorNewPassword }} autofocus{{ end }} required /> + <input placeholder="{{ t "New password" . }}" name="newPassword" value="{{ .Data.NewPassword }}" class="transparent-input form-control{{ if .Data.ErrorNewPassword }} is-invalid{{ end }}" type="password"{{ if .Data.ErrorNewPassword }} autofocus{{ end }} required /> {{ if .Data.ErrorNewPassword }} <div class="invalid-feedback">{{ .Data.ErrorNewPassword }}</div> {{ end }} </div> <div class="form-group"> - <input placeholder="{{ t "Confirm new password" . }}" name="rePassword" value="{{ .Data.RePassword }}" class="form-control{{ if .Data.ErrorRePassword }} is-invalid{{ end }}" type="password"{{ if .Data.ErrorRePassword }} autofocus{{ end }} /> + <input placeholder="{{ t "Confirm new password" . }}" name="rePassword" value="{{ .Data.RePassword }}" class="transparent-input form-control{{ if .Data.ErrorRePassword }} is-invalid{{ end }}" type="password"{{ if .Data.ErrorRePassword }} autofocus{{ end }} /> {{ if .Data.ErrorRePassword }} <div class="invalid-feedback">{{ .Data.ErrorRePassword }}</div> {{ end }}