package account import ( "net/http" "strings" "code.nonshy.com/nonshy/website/pkg/config" "code.nonshy.com/nonshy/website/pkg/log" "code.nonshy.com/nonshy/website/pkg/middleware" "code.nonshy.com/nonshy/website/pkg/models" "code.nonshy.com/nonshy/website/pkg/ratelimit" "code.nonshy.com/nonshy/website/pkg/session" "code.nonshy.com/nonshy/website/pkg/templates" "golang.org/x/crypto/bcrypt" ) // Login controller. func Login() http.HandlerFunc { tmpl := templates.Must("account/login.html") tmpl2fa := templates.Must("account/two_factor_login.html") return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var next = r.FormValue("next") // Posting? if r.Method == http.MethodPost { var ( // Collect form fields. username = strings.ToLower(r.PostFormValue("username")) password = r.PostFormValue("password") ) // Rate limit login attempts by email or username they are trying (whether it exists or not). limiter := &ratelimit.Limiter{ Namespace: "login", ID: username, Limit: config.LoginRateLimit, Window: config.LoginRateLimitWindow, CooldownAt: config.LoginRateLimitCooldownAt, Cooldown: config.LoginRateLimitCooldown, } if err := limiter.Ping(); err != nil { session.FlashError(w, r, err.Error()) templates.Redirect(w, r.URL.Path) return } // Look up their account. user, err := models.FindUser(username) if err != nil { // The user wasn't found, but still hash the incoming password to take time: // so a mischievous user can't infer whether the username was valid based // on the server response time. bcrypt.GenerateFromPassword([]byte(password), config.BcryptCost) session.FlashError(w, r, "Incorrect username or password.") templates.Redirect(w, r.URL.Path) return } // Verify password. if err := user.CheckPassword(password); err != nil { session.FlashError(w, r, "Incorrect username or password.") templates.Redirect(w, r.URL.Path) return } // Is their account banned or disabled? if user.Status != models.UserStatusActive { session.FlashError(w, r, "Your account has been %s. If you believe this was done in error, please contact support.", user.Status) templates.Redirect(w, r.URL.Path) return } // Maintenance mode check. if middleware.LoginMaintenance(user, w, r) { return } // Clear their login rate limiter. limiter.Clear() // Does the user have Two-Factor Auth enabled? var ( tf = models.Get2FA(user.ID) twoFactorOK bool // has successfully entered the code ) if tf.Enabled { // Are they submitting the 2FA code? var ( intent = r.PostFormValue("intent") code = strings.ReplaceAll(r.PostFormValue("code"), " ", "") ) // Validate the submitted code. if intent == "two-factor" { // Verify the TOTP code. if err := tf.Validate(code); err != nil { session.FlashError(w, r, "Invalid authentication code; please try again.") } else { // We're in! twoFactorOK = true } } // Show the 2FA login form. if !twoFactorOK { var vars = map[string]interface{}{ "Next": next, "Username": username, "Password": password, } if err := tmpl2fa.Execute(w, r, vars); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } return } } // OK. Log in the user's session. session.LoginUser(w, r, user) // Clear their rate limiter. if err := limiter.Clear(); err != nil { log.Error("Failed to clear login rate limiter: %s", err) } // Redirect to their dashboard. session.Flash(w, r, "Login successful.") if strings.HasPrefix(next, "/") { templates.Redirect(w, next) } else { templates.Redirect(w, "/me") } return } var vars = map[string]interface{}{ "Next": next, } if err := tmpl.Execute(w, r, vars); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } }) } // Logout controller. func Logout() http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { session.Flash(w, r, "You have been successfully logged out.") session.LogoutUser(w, r) templates.Redirect(w, "/") }) }