package main import ( "encoding/base64" "fmt" "html/template" "net/http" "net/url" "strings" qrcode "github.com/skip2/go-qrcode" ) func (s *Server) setEmail(w http.ResponseWriter, r *http.Request) { u := s.require(w, r) if u == nil { return } email := strings.TrimSpace(strings.ToLower(r.FormValue("email"))) if err := s.store.SetEmail(u.ID, email); err != nil { http.Redirect(w, r, "/settings?error="+template.URLQueryEscaper(err.Error()), http.StatusSeeOther) return } if email == "" { http.Redirect(w, r, "/settings", http.StatusSeeOther) return } if s.mailer == nil { http.Redirect(w, r, "/settings?error=Mail+is+not+configured+on+this+server", http.StatusSeeOther) return } if err := s.sendVerification(*u, email); err != nil { http.Redirect(w, r, "/settings?error="+template.URLQueryEscaper("Could not send mail: "+err.Error()), http.StatusSeeOther) return } http.Redirect(w, r, "/settings?sent=1", http.StatusSeeOther) } func (s *Server) verifyEmail(w http.ResponseWriter, r *http.Request) { user, email, ok := s.store.UseMailToken(r.PathValue("token"), "verify") if !ok { s.fail(w, r, 400, "That verification link is invalid or has expired") return } s.store.MarkVerified(user, email) s.render(w, r, "message", page{Title: "Email verified", Data: map[string]any{"Heading": "Email verified", "Text": "Thanks, " + email + " is now confirmed. Password resets and notifications will go there."}}) } func (s *Server) setNotify(w http.ResponseWriter, r *http.Request) { u := s.require(w, r) if u == nil { return } s.store.SetNotify(u.ID, r.FormValue("notify") == "on") http.Redirect(w, r, "/settings", http.StatusSeeOther) } func (s *Server) resetForm(w http.ResponseWriter, r *http.Request) { s.render(w, r, "reset", page{Title: "Reset password", Data: map[string]any{"Mail": s.mailer != nil, "Sent": r.URL.Query().Get("sent")}}) } func (s *Server) resetRequest(w http.ResponseWriter, r *http.Request) { ip := clientIP(r) if s.limiter.blocked(ip) { s.fail(w, r, 429, "Too many attempts, try again in a few minutes") return } s.limiter.fail(ip) who := strings.TrimSpace(r.FormValue("who")) u, err := s.store.UserByName(who) if err != nil { u, err = s.store.UserByEmail(who) } if err == nil && s.mailer != nil && !s.limiter.blocked("reset:"+u.Name) { if email := s.store.Account(u.ID).Email; email != "" && s.store.Account(u.ID).Verified { s.limiter.fail("reset:" + u.Name) token, _ := s.store.CreateMailToken(u.ID, "reset", email) link := fmt.Sprintf("https://%s/reset/%s", s.host, token) s.notify(email, "Password reset for "+s.host, fmt.Sprintf("Hello ~%s,\n\nSomeone asked to reset the password for your account. Open this link to choose a new one:\n\n%s\n\nThe link is valid for 24 hours. If that was not you, ignore this message and nothing changes.\n", u.Name, link)) } } http.Redirect(w, r, "/reset?sent=1", http.StatusSeeOther) } func (s *Server) resetTokenForm(w http.ResponseWriter, r *http.Request) { s.render(w, r, "resetpassword", page{Title: "Choose a new password", Data: map[string]any{"Token": r.PathValue("token")}}) } func (s *Server) resetPassword(w http.ResponseWriter, r *http.Request) { user, _, ok := s.store.UseMailToken(r.PathValue("token"), "reset") if !ok { s.fail(w, r, 400, "That reset link is invalid or has expired") return } if err := s.store.SetPassword(user, r.FormValue("password")); err != nil { w.WriteHeader(400) s.render(w, r, "resetpassword", page{Title: "Choose a new password", Error: err.Error(), Data: map[string]any{"Token": r.PathValue("token")}}) return } s.store.DeleteSessions(user) http.Redirect(w, r, "/login", http.StatusSeeOther) } func (s *Server) pendingUser(r *http.Request) (User, bool) { c, err := r.Cookie("pending") if err != nil { return User{}, false } var user int64 var created int64 err = s.store.db.QueryRow(`select user, created from mailtokens where token = ? and kind = 'totp'`, c.Value).Scan(&user, &created) if err != nil { return User{}, false } u, err := s.store.userByID(user) return u, err == nil } func (s *Server) totpForm(w http.ResponseWriter, r *http.Request) { if _, ok := s.pendingUser(r); !ok { http.Redirect(w, r, "/login", http.StatusSeeOther) return } s.render(w, r, "totp", page{Title: "Two factor"}) } func (s *Server) totpLogin(w http.ResponseWriter, r *http.Request) { u, ok := s.pendingUser(r) if !ok { http.Redirect(w, r, "/login", http.StatusSeeOther) return } ip, who := clientIP(r), "totp:"+u.Name if s.limiter.blocked(ip) || s.limiter.blocked(who) { s.fail(w, r, 429, "Too many attempts, try again in a few minutes") return } a := s.store.Account(u.ID) code := r.FormValue("code") if !totpValid(a.TOTP, code) { if left, used := useRecovery(a.Recovery, code); used { s.store.SetTOTP(u.ID, a.TOTP, left) } else { s.limiter.fail(ip) s.limiter.fail(who) w.WriteHeader(401) s.render(w, r, "totp", page{Title: "Two factor", Error: "Wrong code"}) return } } if c, err := r.Cookie("pending"); err == nil { s.store.db.Exec(`delete from mailtokens where token = ?`, c.Value) } http.SetCookie(w, &http.Cookie{Name: "pending", Value: "", Path: "/", MaxAge: -1}) s.startSession(w, u) http.Redirect(w, r, "/", http.StatusSeeOther) } func (s *Server) setupTOTP(w http.ResponseWriter, r *http.Request) { u := s.require(w, r) if u == nil { return } if _, ok := s.store.CheckPassword(u.Name, r.FormValue("password")); !ok { http.Redirect(w, r, "/settings/security?error=Wrong+password", http.StatusSeeOther) return } secret := newSecret() token, _ := s.store.CreateMailToken(u.ID, "totpsetup", secret) uri := fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s", url.PathEscape(s.host), url.PathEscape(u.Name), secret, url.QueryEscape(s.host)) png, _ := qrcode.Encode(uri, qrcode.Medium, 200) s.render(w, r, "totpsetup", page{Title: "Enable two factor", User: u, Data: map[string]any{"Settings": "security", "Secret": secret, "Token": token, "QR": template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(png)), "URI": uri}}) } func (s *Server) enableTOTP(w http.ResponseWriter, r *http.Request) { u := s.require(w, r) if u == nil { return } var secret string var created int64 err := s.store.db.QueryRow(`select email, created from mailtokens where token = ? and kind = 'totpsetup' and user = ?`, r.FormValue("token"), u.ID).Scan(&secret, &created) if err != nil || !totpValid(secret, r.FormValue("code")) { http.Redirect(w, r, "/settings/security?error=That+code+did+not+match%2C+start+again", http.StatusSeeOther) return } s.store.db.Exec(`delete from mailtokens where token = ?`, r.FormValue("token")) codes, hashes := newRecoveryCodes() s.store.SetTOTP(u.ID, secret, hashes) s.render(w, r, "recovery", page{Title: "Recovery codes", User: u, Data: map[string]any{"Settings": "security", "Codes": codes}}) } func (s *Server) disableTOTP(w http.ResponseWriter, r *http.Request) { u := s.require(w, r) if u == nil { return } if _, ok := s.store.CheckPassword(u.Name, r.FormValue("password")); !ok { http.Redirect(w, r, "/settings/security?error=Wrong+password", http.StatusSeeOther) return } s.store.SetTOTP(u.ID, "", "") http.Redirect(w, r, "/settings/security", http.StatusSeeOther) } func (s *Server) sendVerification(u User, email string) error { token, err := s.store.CreateMailToken(u.ID, "verify", email) if err != nil { return err } link := fmt.Sprintf("https://%s/verify/%s", s.host, token) return s.mailer.Send(email, "Verify your email for "+s.host, fmt.Sprintf("Hello ~%s,\n\nOpen this link to confirm your email address:\n\n%s\n\nOnce it is confirmed you can reset your password from https://%s/reset if you ever lose it. The link is valid for 24 hours. If you did not ask for this, ignore it.\n", u.Name, link, s.host)) }