Add password hadnelrs

This commit is contained in:
James Mills
2021-01-30 15:38:26 +10:00
parent f724228782
commit 61163cd2bf
4 changed files with 519 additions and 4 deletions

99
internal/auth_handlers.go Normal file
View File

@@ -0,0 +1,99 @@
package internal
import (
"net/http"
"time"
"github.com/julienschmidt/httprouter"
log "github.com/sirupsen/logrus"
"git.mills.io/prologic/spyda/internal/session"
)
// LoginHandler ...
func (s *Server) LoginHandler() httprouter.Handle {
// #239: Throttle failed login attempts and lock user account.
failures := NewTTLCache(5 * time.Minute)
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
ctx := NewContext(s.config, s.db, r)
if r.Method == "GET" {
s.render("login", w, ctx)
return
}
username := NormalizeUsername(r.FormValue("username"))
password := r.FormValue("password")
rememberme := r.FormValue("rememberme") == "on"
// Error: no username or password provided
if username == "" || password == "" {
log.Warn("no username or password provided")
http.Redirect(w, r, "/login", http.StatusFound)
return
}
// Lookup user
user, err := s.db.GetUser(username)
if err != nil {
ctx.Error = true
ctx.Message = "Invalid username! Hint: Register an account?"
s.render("error", w, ctx)
return
}
// #239: Throttle failed login attempts and lock user account.
if failures.Get(user.Username) > MaxFailedLogins {
ctx.Error = true
ctx.Message = "Too many failed login attempts. Account temporarily locked! Please try again later."
s.render("error", w, ctx)
return
}
// Validate cleartext password against KDF hash
err = s.pm.CheckPassword(user.Password, password)
if err != nil {
// #239: Throttle failed login attempts and lock user account.
failed := failures.Inc(user.Username)
time.Sleep(time.Duration(IntPow(2, failed)) * time.Second)
ctx.Error = true
ctx.Message = "Invalid password! Hint: Reset your password?"
s.render("error", w, ctx)
return
}
// #239: Throttle failed login attempts and lock user account.
failures.Reset(user.Username)
// Login successful
log.Infof("login successful: %s", username)
// Lookup session
sess := r.Context().Value(session.SessionKey)
if sess == nil {
log.Warn("no session found")
http.Redirect(w, r, "/login", http.StatusFound)
return
}
// Authorize session
_ = sess.(*session.Session).Set("username", username)
// Persist session?
if rememberme {
_ = sess.(*session.Session).Set("persist", "1")
}
http.Redirect(w, r, RedirectRefererURL(r, s.config, "/"), http.StatusFound)
}
}
// LogoutHandler ...
func (s *Server) LogoutHandler() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
s.sm.Delete(w, r)
http.Redirect(w, r, "/", http.StatusFound)
}
}

219
internal/email.go Normal file
View File

@@ -0,0 +1,219 @@
package internal
import (
"bytes"
"errors"
"fmt"
"strings"
"text/template"
"github.com/go-mail/mail"
log "github.com/sirupsen/logrus"
)
var (
ErrSendingEmail = errors.New("error: unable to send email")
passwordResetEmailTemplate = template.Must(template.New("email").Parse(`Hello {{ .Username }},
You have requested to have your password on {{ .Pod }} reset for your account.
**IMPORTANT:** If this was __NOT__ initiated by you, please ignore this email and contract support!
To reset your password, please visit the following link:
{{ .BaseURL}}/newPassword?token={{ .Token }}
Kind regards,
{{ .Pod}} Support
`))
supportRequestEmailTemplate = template.Must(template.New("email").Parse(`Hello {{ .AdminUser }},
{{ .Name }} <{{ .Email }} from {{ .Pod }} has sent the following support request:
> Subject: {{ .Subject }}
>
{{ .Message }}
Kind regards,
{{ .Pod}} Support
`))
reportAbuseEmailTemplate = template.Must(template.New("email").Parse(`Hello {{ .AdminUser }},
{{ .Name }} <{{ .Email }} from {{ .Pod }} has sent the following abuse report:
> Category: {{ .Category }}
>
{{ .Message }}
The offending user/feed in question is:
- Nick: {{ .Nick }}
- URL: {{ .URL }}
Kind regards,
{{ .Pod }} Support
`))
)
type PasswordResetEmailContext struct {
Pod string
BaseURL string
Token string
Username string
}
type SupportRequestEmailContext struct {
Pod string
AdminUser string
Name string
Email string
Subject string
Message string
}
type ReportAbuseEmailContext struct {
Pod string
AdminUser string
Nick string
URL string
Name string
Email string
Category string
Message string
}
// indents a block of text with an indent string
func Indent(text, indent string) string {
if text[len(text)-1:] == "\n" {
result := ""
for _, j := range strings.Split(text[:len(text)-1], "\n") {
result += indent + j + "\n"
}
return result
}
result := ""
for _, j := range strings.Split(strings.TrimRight(text, "\n"), "\n") {
result += indent + j + "\n"
}
return result[:len(result)-1]
}
func SendEmail(conf *Config, recipients []string, replyTo, subject string, body string) error {
m := mail.NewMessage()
m.SetHeader("From", conf.SMTPFrom)
m.SetHeader("To", recipients...)
m.SetHeader("Reply-To", replyTo)
m.SetHeader("Subject", subject)
m.SetBody("text/plain", body)
d := mail.NewDialer(conf.SMTPHost, conf.SMTPPort, conf.SMTPUser, conf.SMTPPass)
err := d.DialAndSend(m)
if err != nil {
log.WithError(err).Error("SendEmail() failed")
return ErrSendingEmail
}
return nil
}
func SendPasswordResetEmail(conf *Config, user *User, email, token string) error {
recipients := []string{email}
subject := fmt.Sprintf(
"[%s]: Password Reset Request for %s",
conf.Name, user.Username,
)
ctx := PasswordResetEmailContext{
Pod: conf.Name,
BaseURL: conf.BaseURL,
Token: token,
Username: user.Username,
}
buf := &bytes.Buffer{}
if err := passwordResetEmailTemplate.Execute(buf, ctx); err != nil {
log.WithError(err).Error("error rendering email template")
return err
}
if err := SendEmail(conf, recipients, conf.SMTPFrom, subject, buf.String()); err != nil {
log.WithError(err).Errorf("error sending new token to %s", recipients[0])
return err
}
return nil
}
func SendSupportRequestEmail(conf *Config, name, email, subject, message string) error {
recipients := []string{conf.AdminEmail, email}
emailSubject := fmt.Sprintf(
"[%s Support Request]: %s",
conf.Name, subject,
)
ctx := SupportRequestEmailContext{
Pod: conf.Name,
AdminUser: conf.AdminUser,
Name: name,
Email: email,
Subject: subject,
Message: Indent(message, "> "),
}
buf := &bytes.Buffer{}
if err := supportRequestEmailTemplate.Execute(buf, ctx); err != nil {
log.WithError(err).Error("error rendering email template")
return err
}
if err := SendEmail(conf, recipients, email, emailSubject, buf.String()); err != nil {
log.WithError(err).Errorf("error sending support request to %s", recipients[0])
return err
}
return nil
}
func SendReportAbuseEmail(conf *Config, nick, url, name, email, category, message string) error {
recipients := []string{conf.AdminEmail, email}
emailSubject := fmt.Sprintf(
"[%s Report Abuse]: %s",
conf.Name, category,
)
ctx := ReportAbuseEmailContext{
Pod: conf.Name,
AdminUser: conf.AdminUser,
Nick: nick,
URL: url,
Name: name,
Email: email,
Category: category,
Message: Indent(message, "> "),
}
buf := &bytes.Buffer{}
if err := reportAbuseEmailTemplate.Execute(buf, ctx); err != nil {
log.WithError(err).Error("error rendering email template")
return err
}
if err := SendEmail(conf, recipients, email, emailSubject, buf.String()); err != nil {
log.WithError(err).Errorf("error sending report abuse to %s", recipients[0])
return err
}
return nil
}

201
internal/passwd_handlers.go Normal file
View File

@@ -0,0 +1,201 @@
package internal
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/julienschmidt/httprouter"
log "github.com/sirupsen/logrus"
)
// ResetPasswordHandler ...
func (s *Server) ResetPasswordHandler() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
ctx := NewContext(s.config, s.db, r)
if r.Method == "GET" {
ctx.Title = "Reset password"
s.render("resetPassword", w, ctx)
return
}
username := NormalizeUsername(r.FormValue("username"))
email := strings.TrimSpace(r.FormValue("email"))
recovery := fmt.Sprintf("email:%s", FastHash(email))
if err := ValidateUsername(username); err != nil {
ctx.Error = true
ctx.Message = fmt.Sprintf("Username validation failed: %s", err.Error())
s.render("error", w, ctx)
return
}
// Check if user exist
if !s.db.HasUser(username) {
ctx.Error = true
ctx.Message = "User not found!"
s.render("error", w, ctx)
return
}
// Get user object from DB
user, err := s.db.GetUser(username)
if err != nil {
ctx.Error = true
ctx.Message = "Error loading user"
s.render("error", w, ctx)
return
}
if recovery != user.Recovery {
ctx.Error = true
ctx.Message = "Error! The email address you supplied does not match what you registered with :/"
s.render("error", w, ctx)
return
}
// Create magic link expiry time
now := time.Now()
secs := now.Unix()
expiresAfterSeconds := int64(600) // Link expires after 10 minutes
expiryTime := secs + expiresAfterSeconds
// Create magic link
token := jwt.NewWithClaims(
jwt.SigningMethodHS256,
jwt.MapClaims{"username": username, "expiresAt": expiryTime},
)
tokenString, err := token.SignedString([]byte(s.config.MagicLinkSecret))
if err != nil {
ctx.Error = true
ctx.Message = err.Error()
s.render("error", w, ctx)
return
}
if err := SendPasswordResetEmail(s.config, user, email, tokenString); err != nil {
log.WithError(err).Errorf("unable to send reset password email to %s", user.Username)
ctx.Error = true
ctx.Message = err.Error()
s.render("error", w, ctx)
return
}
log.Infof("reset password email sent for %s", user.Username)
// Show success msg
ctx.Error = false
ctx.Message = "Password request request sent! Please check your email and follow the instructions"
s.render("error", w, ctx)
}
}
// ResetPasswordMagicLinkHandler ...
func (s *Server) ResetPasswordMagicLinkHandler() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
ctx := NewContext(s.config, s.db, r)
// Get token from query string
tokens, ok := r.URL.Query()["token"]
// Check if valid token
if !ok || len(tokens[0]) < 1 {
ctx.Error = true
ctx.Message = "Invalid token"
s.render("error", w, ctx)
return
}
tokenEmail := tokens[0]
ctx.PasswordResetToken = tokenEmail
// Show newPassword page
s.render("newPassword", w, ctx)
}
}
// NewPasswordHandler ...
func (s *Server) NewPasswordHandler() httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
ctx := NewContext(s.config, s.db, r)
if r.Method == "GET" {
return
}
password := r.FormValue("password")
tokenEmail := r.FormValue("token")
// Check if token is valid
token, err := jwt.Parse(tokenEmail, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return []byte(s.config.MagicLinkSecret), nil
})
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
var username = fmt.Sprintf("%v", claims["username"])
var expiresAt int = int(claims["expiresAt"].(float64))
now := time.Now()
secs := now.Unix()
// Check token expiry
if secs > int64(expiresAt) {
ctx.Error = true
ctx.Message = "Token expires"
s.render("error", w, ctx)
return
}
user, err := s.db.GetUser(username)
if err != nil {
ctx.Error = true
ctx.Message = "Error loading user"
s.render("error", w, ctx)
return
}
// Reset password
if password != "" {
hash, err := s.pm.CreatePassword(password)
if err != nil {
ctx.Error = true
ctx.Message = "Error loading user"
s.render("error", w, ctx)
return
}
user.Password = hash
// Save user
if err := s.db.SetUser(username, user); err != nil {
ctx.Error = true
ctx.Message = "Error loading user"
s.render("error", w, ctx)
return
}
}
log.Infof("password changed: %v", user)
// Show success msg
ctx.Error = false
ctx.Message = "Password reset successfully."
s.render("error", w, ctx)
} else {
ctx.Error = true
ctx.Message = err.Error()
s.render("error", w, ctx)
return
}
}
}