login.go (24953B)
1 package handlers 2 3 import ( 4 "bytes" 5 "dkforest/pkg/cache" 6 "dkforest/pkg/captcha" 7 "dkforest/pkg/config" 8 "dkforest/pkg/database" 9 dutils "dkforest/pkg/database/utils" 10 "dkforest/pkg/managers" 11 "dkforest/pkg/utils" 12 hutils "dkforest/pkg/web/handlers/utils" 13 "fmt" 14 "github.com/asaskevich/govalidator" 15 "github.com/labstack/echo" 16 "github.com/pquerna/otp/totp" 17 "github.com/sirupsen/logrus" 18 "golang.org/x/crypto/bcrypt" 19 "net/http" 20 "strings" 21 "time" 22 ) 23 24 const max2faAttempts = 4 25 26 // partialAuthCache keep track of partial auth token -> user id. 27 // When a user login and have 2fa enabled, we create a "partial" auth cookie. 28 // The token can be used to complete the 2fa authentication. 29 var partialAuthCache = cache.New[*PartialAuthItem](2*time.Minute, time.Hour) 30 31 type PartialAuthItem struct { 32 UserID database.UserID 33 Step PartialAuthStep // Inform which type of 2fa the user is supposed to complete 34 SessionDuration time.Duration 35 Attempt int 36 } 37 38 func NewPartialAuthItem(userID database.UserID, step PartialAuthStep, sessionDuration time.Duration) *PartialAuthItem { 39 return &PartialAuthItem{UserID: userID, Step: step, SessionDuration: sessionDuration} 40 } 41 42 type PartialAuthStep string 43 44 const ( 45 TwoFactorStep PartialAuthStep = "2fa" 46 PgpSignStep PartialAuthStep = "pgp_sign_2fa" 47 PgpStep PartialAuthStep = "pgp_2fa" 48 ) 49 50 // Password recovery flow has 3 steps 51 // 1- Ask for username & captcha & gpg method 52 // 2- Validate gpg token/signature 53 // 3- Reset password 54 // 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. 55 // partialRecoveryCache keeps track of users that are in the process of recovering their password and the step they're at. 56 var ( 57 partialRecoveryCache = cache.New[PartialRecoveryItem](10*time.Minute, time.Hour) 58 ) 59 60 type PartialRecoveryItem struct { 61 UserID database.UserID 62 Step RecoveryStep 63 } 64 65 type RecoveryStep int64 66 67 const ( 68 RecoveryCaptchaCompleted RecoveryStep = iota + 1 69 RecoveryGpgValidated 70 ) 71 72 func firstUseHandler(c echo.Context) error { 73 user := c.Get("authUser").(*database.User) 74 db := c.Get("database").(*database.DkfDB) 75 var data firstUseData 76 if user != nil { 77 return c.Redirect(http.StatusFound, "/") 78 } 79 80 if c.Request().Method == http.MethodGet { 81 //data.Username = "admin" 82 //data.Password = "admin123" 83 //data.RePassword = "admin123" 84 //data.Email = "admin@admin.admin" 85 return c.Render(http.StatusOK, "standalone.first-use", data) 86 } 87 88 data.Username = c.Request().PostFormValue("username") 89 data.Password = c.Request().PostFormValue("password") 90 data.RePassword = c.Request().PostFormValue("repassword") 91 newUser, errs := db.CreateFirstUser(data.Username, data.Password, data.RePassword) 92 data.Errors = errs 93 if errs.HasError() { 94 return c.Render(http.StatusOK, "standalone.first-use", data) 95 } 96 97 _, errs = db.CreateZeroUser() 98 99 config.IsFirstUse.SetFalse() 100 101 session := db.DoCreateSession(newUser.ID, c.Request().UserAgent(), time.Hour*24*30) 102 c.SetCookie(createSessionCookie(session.Token, time.Hour*24*30)) 103 104 return c.Redirect(http.StatusFound, "/") 105 } 106 107 func LoginHandler(c echo.Context) error { 108 109 if config.ProtectHome.IsTrue() { 110 return c.NoContent(http.StatusNotFound) 111 } 112 113 return loginHandler(c) 114 } 115 116 func LoginAttackHandler(c echo.Context) error { 117 key := c.Param("loginToken") 118 loginLink, found := tempLoginCache.Get("login_link") 119 if !found { 120 return c.NoContent(http.StatusNotFound) 121 } 122 // We use the "Dangerous" version of VerifyString, to avoid invalidating the captcha. 123 // This way, the captcha can be used multiple times by different users until it's time has expired. 124 if err := captcha.VerifyStringDangerous(tempLoginStore, loginLink.ID, key); err != nil { 125 // If the captcha was invalid, kill the circuit. 126 hutils.KillCircuit(c) 127 time.Sleep(utils.RandSec(3, 5)) 128 return c.NoContent(http.StatusNotFound) 129 } 130 131 return loginHandler(c) 132 } 133 134 func loginHandler(c echo.Context) error { 135 formName := c.Request().PostFormValue("formName") 136 if formName == "pgp_2fa" { 137 token := c.Request().PostFormValue("token") 138 return SessionsGpgTwoFactorHandler(c, false, token) 139 } else if formName == "pgp_sign_2fa" { 140 token := c.Request().PostFormValue("token") 141 return SessionsGpgSignTwoFactorHandler(c, false, token) 142 } else if formName == "2fa" { 143 token := c.Request().PostFormValue("token") 144 return SessionsTwoFactorHandler(c, false, token) 145 } else if formName == "2fa_recovery" { 146 token := c.Request().PostFormValue("token") 147 return SessionsTwoFactorRecoveryHandler(c, token) 148 } else if formName == "" { 149 return loginFormHandler(c) 150 } 151 return c.Redirect(http.StatusFound, "/") 152 } 153 154 // SessionsGpgTwoFactorHandler ... 155 func SessionsGpgTwoFactorHandler(c echo.Context, step1 bool, token string) error { 156 db := c.Get("database").(*database.DkfDB) 157 item, found := partialAuthCache.Get(token) 158 if !found || item.Step != PgpStep { 159 return c.Redirect(http.StatusFound, "/") 160 } 161 162 user, err := db.GetUserByID(item.UserID) 163 if err != nil { 164 logrus.Errorf("failed to get user %d", item.UserID) 165 return c.Redirect(http.StatusFound, "/") 166 } 167 168 cleanup := func() { 169 pgpTokenCache.Delete(user.ID) 170 partialAuthCache.Delete(token) 171 } 172 173 var data sessionsGpgTwoFactorData 174 data.Token = token 175 176 if step1 { 177 msg, err := generatePgpEncryptedTokenMessage(user.ID, user.GPGPublicKey) 178 if err != nil { 179 data.Error = err.Error() 180 return c.Render(http.StatusOK, "sessions-gpg-two-factor", data) 181 } 182 if expiredTime, _ := utils.GetKeyExpiredTime(user.GPGPublicKey); expiredTime != nil { 183 if expiredTime.AddDate(0, -1, 0).Before(time.Now()) { 184 chatMsg := fmt.Sprintf("Your PGP key expires in less than a month (%s)", expiredTime.Format("Jan 02, 2006 15:04:05")) 185 dutils.ZeroSendMsg(db, user.ID, chatMsg) 186 } 187 } 188 data.EncryptedMessage = msg 189 return c.Render(http.StatusOK, "sessions-gpg-two-factor", data) 190 } 191 192 pgpToken, found := pgpTokenCache.Get(user.ID) 193 if !found { 194 return c.Redirect(http.StatusFound, "/") 195 } 196 data.EncryptedMessage = c.Request().PostFormValue("encrypted_message") 197 data.Code = c.Request().PostFormValue("pgp_code") 198 if data.Code != pgpToken.Value { 199 item.Attempt++ 200 if item.Attempt >= max2faAttempts { 201 cleanup() 202 return c.Redirect(http.StatusFound, "/") 203 } 204 data.ErrorCode = "invalid code" 205 return c.Render(http.StatusOK, "sessions-gpg-two-factor", data) 206 } 207 cleanup() 208 209 if user.HasTotpEnabled() { 210 token := utils.GenerateToken32() 211 partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, TwoFactorStep, item.SessionDuration)) 212 return SessionsTwoFactorHandler(c, true, token) 213 } 214 215 return completeLogin(c, user, item.SessionDuration) 216 } 217 218 // SessionsGpgSignTwoFactorHandler ... 219 func SessionsGpgSignTwoFactorHandler(c echo.Context, step1 bool, token string) error { 220 db := c.Get("database").(*database.DkfDB) 221 item, found := partialAuthCache.Get(token) 222 if !found || item.Step != PgpSignStep { 223 return c.Redirect(http.StatusFound, "/") 224 } 225 226 user, err := db.GetUserByID(item.UserID) 227 if err != nil { 228 logrus.Errorf("failed to get user %d", item.UserID) 229 return c.Redirect(http.StatusFound, "/") 230 } 231 232 cleanup := func() { 233 pgpTokenCache.Delete(user.ID) 234 partialAuthCache.Delete(token) 235 } 236 237 var data sessionsGpgSignTwoFactorData 238 data.Token = token 239 240 if step1 { 241 data.ToBeSignedMessage = generatePgpToBeSignedTokenMessage(user.ID, user.GPGPublicKey) 242 return c.Render(http.StatusOK, "sessions-gpg-sign-two-factor", data) 243 } 244 245 pgpToken, found := pgpTokenCache.Get(user.ID) 246 if !found { 247 return c.Redirect(http.StatusFound, "/") 248 } 249 data.ToBeSignedMessage = c.Request().PostFormValue("to_be_signed_message") 250 data.SignedMessage = c.Request().PostFormValue("signed_message") 251 252 if !utils.PgpCheckSignMessage(pgpToken.PKey, pgpToken.Value, data.SignedMessage) { 253 item.Attempt++ 254 if item.Attempt >= max2faAttempts { 255 cleanup() 256 return c.Redirect(http.StatusFound, "/") 257 } 258 data.ErrorSignedMessage = "invalid signature" 259 return c.Render(http.StatusOK, "sessions-gpg-sign-two-factor", data) 260 } 261 cleanup() 262 263 if user.HasTotpEnabled() { 264 token := utils.GenerateToken32() 265 partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, TwoFactorStep, item.SessionDuration)) 266 return SessionsTwoFactorHandler(c, true, token) 267 } 268 269 return completeLogin(c, user, item.SessionDuration) 270 } 271 272 // SessionsTwoFactorHandler ... 273 func SessionsTwoFactorHandler(c echo.Context, step1 bool, token string) error { 274 db := c.Get("database").(*database.DkfDB) 275 item, found := partialAuthCache.Get(token) 276 if !found || item.Step != TwoFactorStep { 277 return c.Redirect(http.StatusFound, "/") 278 } 279 cleanup := func() { partialAuthCache.Delete(token) } 280 281 var data sessionsTwoFactorData 282 data.Token = token 283 if !step1 { 284 code := c.Request().PostFormValue("code") 285 user, err := db.GetUserByID(item.UserID) 286 if err != nil { 287 logrus.Errorf("failed to get user %d", item.UserID) 288 return c.Redirect(http.StatusFound, "/") 289 } 290 secret := string(user.TwoFactorSecret) 291 if !totp.Validate(code, secret) { 292 item.Attempt++ 293 if item.Attempt >= max2faAttempts { 294 cleanup() 295 return c.Redirect(http.StatusFound, "/") 296 } 297 data.Error = "Two-factor authentication failed." 298 return c.Render(http.StatusOK, "sessions-two-factor", data) 299 } 300 301 cleanup() 302 return completeLogin(c, user, item.SessionDuration) 303 } 304 return c.Render(http.StatusOK, "sessions-two-factor", data) 305 } 306 307 // SessionsTwoFactorRecoveryHandler ... 308 func SessionsTwoFactorRecoveryHandler(c echo.Context, token string) error { 309 db := c.Get("database").(*database.DkfDB) 310 item, found := partialAuthCache.Get(token) 311 if !found { 312 return c.Redirect(http.StatusFound, "/") 313 } 314 cleanup := func() { partialAuthCache.Delete(token) } 315 316 var data sessionsTwoFactorRecoveryData 317 data.Token = token 318 recoveryCode := c.Request().PostFormValue("code") 319 if recoveryCode != "" { 320 user, err := db.GetUserByID(item.UserID) 321 if err != nil { 322 logrus.Errorf("failed to get user %d", item.UserID) 323 return c.Redirect(http.StatusFound, "/") 324 } 325 if err := bcrypt.CompareHashAndPassword([]byte(user.TwoFactorRecovery), []byte(recoveryCode)); err != nil { 326 data.Error = "Recovery code authentication failed" 327 return c.Render(http.StatusOK, "sessions-two-factor-recovery", data) 328 } 329 cleanup() 330 return completeLogin(c, user, item.SessionDuration) 331 } 332 return c.Render(http.StatusOK, "sessions-two-factor-recovery", data) 333 } 334 335 func loginFormHandler(c echo.Context) error { 336 db := c.Get("database").(*database.DkfDB) 337 var data loginData 338 data.Redirect = c.QueryParam("redirect") 339 data.Autofocus = 0 340 data.HomeUsersList = config.HomeUsersList.Load() 341 342 if data.HomeUsersList { 343 data.Online = managers.ActiveUsers.GetActiveUsers() 344 } 345 346 actualLogin := func(username, password string, sessionDuration time.Duration, captchaSolved bool) error { 347 username = strings.TrimSpace(username) 348 user, err := db.GetVerifiedUserByUsername(database.Username(username)) 349 if err != nil { 350 time.Sleep(utils.RandMs(50, 200)) 351 data.Error = "Invalid username/password" 352 return c.Render(http.StatusOK, "standalone.login", data) 353 } 354 355 user.IncrLoginAttempts(db) 356 357 if user.LoginAttempts > 4 && !captchaSolved { 358 data.CaptchaRequired = true 359 data.Autofocus = 2 360 data.Error = "Captcha required" 361 data.CaptchaID, data.CaptchaImg = captcha.New() 362 data.Password = password 363 captchaID := c.Request().PostFormValue("captcha_id") 364 captchaInput := c.Request().PostFormValue("captcha") 365 if captchaInput == "" { 366 return c.Render(http.StatusOK, "standalone.login", data) 367 } else { 368 if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil { 369 data.Error = "Invalid captcha" 370 return c.Render(http.StatusOK, "standalone.login", data) 371 } 372 } 373 } 374 375 if !user.CheckPassword(db, password) { 376 data.Password = "" 377 data.Autofocus = 1 378 data.Error = "Invalid username/password" 379 return c.Render(http.StatusOK, "standalone.login", data) 380 } 381 382 if user.GpgTwoFactorEnabled || user.HasTotpEnabled() { 383 token := utils.GenerateToken32() 384 var twoFactorType PartialAuthStep 385 var twoFactorClb func(echo.Context, bool, string) error 386 if user.GpgTwoFactorEnabled && user.GpgTwoFactorMode { 387 twoFactorType = PgpSignStep 388 twoFactorClb = SessionsGpgSignTwoFactorHandler 389 } else if user.GpgTwoFactorEnabled { 390 twoFactorType = PgpStep 391 twoFactorClb = SessionsGpgTwoFactorHandler 392 } else if user.HasTotpEnabled() { 393 twoFactorType = TwoFactorStep 394 twoFactorClb = SessionsTwoFactorHandler 395 } 396 partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, twoFactorType, sessionDuration)) 397 return twoFactorClb(c, true, token) 398 } 399 400 return completeLogin(c, user, sessionDuration) 401 } 402 403 usernameQuery := c.QueryParam("u") 404 passwordQuery := c.QueryParam("p") 405 if usernameQuery == "darkforestAdmin" && passwordQuery != "" { 406 return actualLogin(usernameQuery, passwordQuery, time.Hour*24, false) 407 } 408 409 if config.ForceLoginCaptcha.IsTrue() { 410 data.CaptchaID, data.CaptchaImg = captcha.New() 411 data.CaptchaRequired = true 412 } 413 414 if c.Request().Method == http.MethodGet { 415 data.SessionDurationSec = 604800 416 return c.Render(http.StatusOK, "standalone.login", data) 417 } 418 419 captchaSolved := false 420 421 data.Username = strings.TrimSpace(c.FormValue("username")) 422 password := c.FormValue("password") 423 data.SessionDurationSec = utils.Clamp(utils.DoParseInt64(c.Request().PostFormValue("session_duration")), 60, utils.OneMonthSecs) 424 sessionDuration := time.Duration(data.SessionDurationSec) * time.Second 425 426 if config.ForceLoginCaptcha.IsTrue() { 427 data.CaptchaRequired = true 428 captchaID := c.Request().PostFormValue("captcha_id") 429 captchaInput := c.Request().PostFormValue("captcha") 430 if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil { 431 data.ErrCaptcha = err.Error() 432 return c.Render(http.StatusOK, "standalone.login", data) 433 } 434 captchaSolved = true 435 } 436 437 return actualLogin(data.Username, password, sessionDuration, captchaSolved) 438 } 439 440 func completeLogin(c echo.Context, user database.User, sessionDuration time.Duration) error { 441 db := c.Get("database").(*database.DkfDB) 442 user.ResetLoginAttempts(db) 443 444 for _, session := range db.GetActiveUserSessions(user.ID) { 445 msg := fmt.Sprintf(`New login`) 446 db.CreateSessionNotification(msg, session.Token) 447 } 448 449 session := db.DoCreateSession(user.ID, c.Request().UserAgent(), sessionDuration) 450 db.CreateSecurityLog(user.ID, database.LoginSecurityLog) 451 c.SetCookie(createSessionCookie(session.Token, sessionDuration)) 452 453 redirectURL := "/" 454 redir := c.QueryParam("redirect") 455 if redir != "" && strings.HasPrefix(redir, "/") { 456 redirectURL = redir 457 } 458 return c.Redirect(http.StatusFound, redirectURL) 459 } 460 461 func LoginCompletedHandler(c echo.Context) error { 462 authUser := c.Get("authUser").(*database.User) 463 var data loginCompletedData 464 data.SecretPhrase = string(authUser.SecretPhrase) 465 data.RedirectURL = "/" 466 redir := c.QueryParam("redirect") 467 if redir != "" && strings.HasPrefix(redir, "/") { 468 data.RedirectURL = redir 469 } 470 return c.Render(http.StatusOK, "login-completed", data) 471 } 472 473 // LogoutHandler for logout route 474 func LogoutHandler(ctx echo.Context) error { 475 authUser := ctx.Get("authUser").(*database.User) 476 db := ctx.Get("database").(*database.DkfDB) 477 c, _ := ctx.Cookie(hutils.AuthCookieName) 478 if err := db.DeleteSessionByToken(c.Value); err != nil { 479 logrus.Error("Failed to remove session from db : ", err) 480 } 481 if authUser.TerminateAllSessionsOnLogout { 482 // Delete active user sessions 483 if err := db.DeleteUserSessions(authUser.ID); err != nil { 484 logrus.Error("failed to delete user sessions : ", err) 485 } 486 } 487 db.CreateSecurityLog(authUser.ID, database.LogoutSecurityLog) 488 ctx.SetCookie(hutils.DeleteCookie(hutils.AuthCookieName)) 489 managers.ActiveUsers.RemoveUser(authUser.ID) 490 if authUser.Temp { 491 if err := db.DB().Where("id = ?", authUser.ID).Unscoped().Delete(&database.User{}).Error; err != nil { 492 logrus.Error(err) 493 } 494 } 495 return ctx.Redirect(http.StatusFound, "/") 496 } 497 498 // ForgotPasswordHandler ... 499 func ForgotPasswordHandler(c echo.Context) error { 500 return waitPageWrapper(c, forgotPasswordHandler, hutils.WaitCookieName) 501 } 502 503 func forgotPasswordHandler(c echo.Context) error { 504 db := c.Get("database").(*database.DkfDB) 505 var data forgotPasswordData 506 data.Redirect = c.QueryParam("redirect") 507 const ( 508 usernameCaptchaStep = iota + 1 509 gpgCodeSignatureStep 510 resetPasswordStep 511 512 forgotPasswordTmplName = "standalone.forgot-password" 513 ) 514 data.Step = usernameCaptchaStep 515 516 data.CaptchaSec = 120 517 data.Frames = generateCssFrames(data.CaptchaSec, nil, true) 518 519 data.CaptchaID, data.CaptchaImg = captcha.New() 520 521 if c.Request().Method == http.MethodGet { 522 return c.Render(http.StatusOK, forgotPasswordTmplName, data) 523 } 524 525 // POST 526 527 formName := c.Request().PostFormValue("form_name") 528 529 if formName == "step1" { 530 // Receive and validate Username/Captcha 531 data.Step = usernameCaptchaStep 532 data.Username = database.Username(c.Request().PostFormValue("username")) 533 captchaID := c.Request().PostFormValue("captcha_id") 534 captchaInput := c.Request().PostFormValue("captcha") 535 data.GpgMode = utils.DoParseBool(c.Request().PostFormValue("gpg_mode")) 536 537 if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil { 538 data.ErrCaptcha = err.Error() 539 return c.Render(http.StatusOK, forgotPasswordTmplName, data) 540 } 541 user, err := db.GetUserByUsername(data.Username) 542 if err != nil { 543 data.UsernameError = "no such user" 544 return c.Render(http.StatusOK, forgotPasswordTmplName, data) 545 } 546 userGPGPublicKey := user.GPGPublicKey 547 if userGPGPublicKey == "" { 548 data.UsernameError = "user has no gpg public key" 549 return c.Render(http.StatusOK, forgotPasswordTmplName, data) 550 } 551 if user.GpgTwoFactorEnabled { 552 data.UsernameError = "user has gpg two-factors enabled" 553 return c.Render(http.StatusOK, forgotPasswordTmplName, data) 554 } 555 556 if data.GpgMode { 557 data.ToBeSignedMessage = generatePgpToBeSignedTokenMessage(user.ID, userGPGPublicKey) 558 559 } else { 560 msg, err := generatePgpEncryptedTokenMessage(user.ID, userGPGPublicKey) 561 if err != nil { 562 data.Error = err.Error() 563 return c.Render(http.StatusOK, forgotPasswordTmplName, data) 564 } 565 data.EncryptedMessage = msg 566 } 567 568 token := utils.GenerateToken32() 569 partialRecoveryCache.SetD(token, PartialRecoveryItem{user.ID, RecoveryCaptchaCompleted}) 570 571 data.Token = token 572 data.Step = gpgCodeSignatureStep 573 return c.Render(http.StatusOK, forgotPasswordTmplName, data) 574 575 } else if formName == "step2" { 576 // Receive and validate GPG code/signature 577 data.Step = gpgCodeSignatureStep 578 579 // Step2 is guarded by the "token" that must be valid 580 data.Token = c.Request().PostFormValue("token") 581 item, found := partialRecoveryCache.Get(data.Token) 582 if !found || item.Step != RecoveryCaptchaCompleted { 583 return c.Redirect(http.StatusFound, "/") 584 } 585 userID := item.UserID 586 587 pgpToken, found := pgpTokenCache.Get(userID) 588 if !found { 589 return c.Redirect(http.StatusFound, "/") 590 } 591 592 data.GpgMode = utils.DoParseBool(c.Request().PostFormValue("gpg_mode")) 593 if data.GpgMode { 594 data.ToBeSignedMessage = c.Request().PostFormValue("to_be_signed_message") 595 data.SignedMessage = c.Request().PostFormValue("signed_message") 596 if !utils.PgpCheckSignMessage(pgpToken.PKey, pgpToken.Value, data.SignedMessage) { 597 data.ErrorSignedMessage = "invalid signature" 598 return c.Render(http.StatusOK, forgotPasswordTmplName, data) 599 } 600 601 } else { 602 data.EncryptedMessage = c.Request().PostFormValue("encrypted_message") 603 data.Code = c.Request().PostFormValue("pgp_code") 604 if data.Code != pgpToken.Value { 605 data.ErrorCode = "invalid code" 606 return c.Render(http.StatusOK, forgotPasswordTmplName, data) 607 } 608 } 609 610 pgpTokenCache.Delete(userID) 611 partialRecoveryCache.SetD(data.Token, PartialRecoveryItem{userID, RecoveryGpgValidated}) 612 613 data.Step = resetPasswordStep 614 return c.Render(http.StatusOK, forgotPasswordTmplName, data) 615 616 } else if formName == "step3" { 617 // Receive and validate new password 618 data.Step = resetPasswordStep 619 620 // Step3 is guarded by the "token" that must be valid 621 token := c.Request().PostFormValue("token") 622 item, found := partialRecoveryCache.Get(token) 623 if !found || item.Step != RecoveryGpgValidated { 624 return c.Redirect(http.StatusFound, "/") 625 } 626 userID := item.UserID 627 user, err := db.GetUserByID(userID) 628 if err != nil { 629 return c.Redirect(http.StatusFound, "/") 630 } 631 632 newPassword := c.Request().PostFormValue("newPassword") 633 rePassword := c.Request().PostFormValue("rePassword") 634 data.NewPassword = newPassword 635 data.RePassword = rePassword 636 637 hashedPassword, err := database.NewPasswordValidator(db, newPassword).CompareWith(rePassword).Hash() 638 if err != nil { 639 data.ErrorNewPassword = err.Error() 640 return c.Render(http.StatusOK, forgotPasswordTmplName, data) 641 } 642 643 if err := user.ChangePassword(db, hashedPassword); err != nil { 644 logrus.Error(err) 645 } 646 db.CreateSecurityLog(user.ID, database.PasswordRecoverySecurityLog) 647 648 partialRecoveryCache.Delete(token) 649 c.SetCookie(hutils.DeleteCookie(hutils.WaitCookieName)) 650 651 return c.Render(http.StatusFound, "flash", FlashResponse{Message: "Password reset done", Redirect: "/login"}) 652 } 653 654 return c.Render(http.StatusOK, "flash", FlashResponse{"should not go here", "/login", "alert-danger"}) 655 } 656 657 func protectHomeHandler(c echo.Context) error { 658 if c.Request().Method == http.MethodPost { 659 return c.NoContent(http.StatusNotFound) 660 } 661 captchaQuery := c.QueryParam("captcha") 662 loginQuery := c.QueryParam("login") 663 signupQuery := c.QueryParam("signup") 664 if captchaQuery != "" { 665 if len(captchaQuery) > 6 || len(loginQuery) > 1 || len(signupQuery) > 1 || 666 !govalidator.IsASCII(captchaQuery) || !govalidator.IsASCII(loginQuery) || !govalidator.IsASCII(signupQuery) { 667 time.Sleep(utils.RandSec(3, 7)) 668 return c.NoContent(http.StatusOK) 669 } 670 redirectTo := "/login/" + captchaQuery 671 if signupQuery == "1" { 672 redirectTo = "/signup/" + captchaQuery 673 } 674 time.Sleep(utils.RandSec(1, 2)) 675 return c.Redirect(http.StatusFound, redirectTo) 676 } 677 loginLink, found := tempLoginCache.Get("login_link") 678 if !found { 679 loginLink.ID, loginLink.Img = captcha.NewWithParams(captcha.Params{Store: tempLoginStore}) 680 loginLink.ValidUntil = time.Now().Add(3 * time.Minute) 681 tempLoginCache.SetD("login_link", loginLink) 682 } 683 684 waitTime := int64(time.Until(loginLink.ValidUntil).Seconds()) 685 686 // Generate css frames 687 frames := generateCssFrames(waitTime, func(i int64) string { 688 return utils.ShortDur(time.Duration(i) * time.Second) 689 }, true) 690 691 time.Sleep(utils.RandSec(1, 2)) 692 bufTmp := make([]byte, 0, 1024*4) 693 buf := bytes.NewBuffer(bufTmp) 694 buf.Write([]byte(`<!DOCTYPE html><html lang="en"><head> 695 <link href="/public/img/favicon.ico" rel="icon" type="image/x-icon" /> 696 <meta charset="UTF-8" /> 697 <meta name="author" content="n0tr1v"> 698 <meta name="language" content="English"> 699 <meta name="revisit-after" content="1 days"> 700 <meta http-equiv="expires" content="0"> 701 <meta http-equiv="pragma" content="no-cache"> 702 <title>DarkForest</title> 703 <style> 704 body, html { height: 100%; width:100%; display:table; background-color: #222; color: white; line-height: 25px; 705 font-family: Lato,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; } 706 body { display:table-cell; vertical-align:middle; } 707 #parent { display: table; width: 100%; } 708 #form_login { display: table; margin: auto; } 709 .captcha-img { transition: transform .2s; } 710 .captcha-img:hover { transform: scale(2.5); } 711 #timer_countdown:before { 712 content: "`)) 713 buf.Write([]byte(utils.ShortDur(time.Duration(waitTime) * time.Second))) 714 buf.Write([]byte(`"; 715 animation: `)) 716 buf.Write([]byte(utils.FormatInt64(waitTime))) 717 buf.Write([]byte(`s 1s forwards timer_countdown_frames; 718 } 719 @keyframes timer_countdown_frames {`)) 720 for _, frame := range frames { 721 buf.Write([]byte(frame)) 722 } 723 buf.Write([]byte(` 724 } 725 </style> 726 </head> 727 <body class="bg"> 728 729 <div id="parent"> 730 <div id="form_login"> 731 <div class="text-center"> 732 <p> 733 To login go to <code>/login/XXXXXX</code><br /> 734 To register go to <code>/signup/XXXXXX</code><br /> 735 (replace X by the numbers in the image)<br /> 736 Link valid for <strong><span id="timer_countdown"></span></strong> 737 </p> 738 <img src="data:image/png;base64,`)) 739 buf.Write([]byte(loginLink.Img)) 740 buf.Write([]byte(`" style="background-color: hsl(0, 0%, 90%);" class="captcha-img" /> 741 <form method="get"> 742 <input type="text" name="captcha" maxlength="6" autofocus /> 743 <button name="login" value="1" type="submit">Login</button> 744 <button name="signup" value="1" type="submit">Register</button> 745 </form> 746 </div> 747 </div> 748 </div> 749 750 </body> 751 </html>`)) 752 return c.HTMLBlob(http.StatusOK, buf.Bytes()) 753 }