commit db33df66ead1546d00eae3a1528d3fdc53e8be7d
parent 050d419a8a6eb8d770fb704a13242988501ffcb9
Author: n0tr1v <n0tr1v@protonmail.com>
Date: Sun, 4 Dec 2022 23:10:38 -0500
simplify recovery workflow
Diffstat:
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 }}