Add password hadnelrs
This commit is contained in:
4
go.sum
4
go.sum
@@ -60,7 +60,6 @@ github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CL
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||||
github.com/dgrijalva/jwt-go v1.0.2 h1:KPldsxuKGsS2FPWsNeg9ZO18aCrGKujPoWXn2yo+KQM=
|
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||||
@@ -205,7 +204,6 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
|
|||||||
github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc=
|
github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc=
|
||||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
github.com/patrickmn/go-cache v1.0.0 h1:3gD5McaYs9CxjyK5AXGcq8gdeCARtd/9gJDUvVeaZ0Y=
|
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
@@ -263,7 +261,6 @@ github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHN
|
|||||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||||
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||||
@@ -289,7 +286,6 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC
|
|||||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||||
github.com/vcraescu/go-paginator v1.0.0 h1:ilNmRhlgG8N44LuxfGoPI2u8guXMA6gUqaPGA5BmRFs=
|
github.com/vcraescu/go-paginator v1.0.0 h1:ilNmRhlgG8N44LuxfGoPI2u8guXMA6gUqaPGA5BmRFs=
|
||||||
github.com/vcraescu/go-paginator v1.0.0/go.mod h1:caZCjjt2qcA1O2aDzW7lwAcK4Rxw3LNvdEVF/ONxZWw=
|
github.com/vcraescu/go-paginator v1.0.0/go.mod h1:caZCjjt2qcA1O2aDzW7lwAcK4Rxw3LNvdEVF/ONxZWw=
|
||||||
github.com/wblakecaldwell/profiler v0.0.0-20150908040756-6111ef1313a1 h1:Dz/PRieZRmOhDfOlkVpY1LYYIfNoTJjlDirAlagOr0s=
|
|
||||||
github.com/wblakecaldwell/profiler v0.0.0-20150908040756-6111ef1313a1/go.mod h1:3+0F8oLB1rQlbIcRAuqDgGdzNi9X69un/aPz4cUAFV4=
|
github.com/wblakecaldwell/profiler v0.0.0-20150908040756-6111ef1313a1/go.mod h1:3+0F8oLB1rQlbIcRAuqDgGdzNi9X69un/aPz4cUAFV4=
|
||||||
github.com/writeas/slug v1.2.0 h1:EMQ+cwLiOcA6EtFwUgyw3Ge18x9uflUnOnR6bp/J+/g=
|
github.com/writeas/slug v1.2.0 h1:EMQ+cwLiOcA6EtFwUgyw3Ge18x9uflUnOnR6bp/J+/g=
|
||||||
github.com/writeas/slug v1.2.0/go.mod h1:RE8shOqQP3YhsfsQe0L3RnuejfQ4Mk+JjY5YJQFubfQ=
|
github.com/writeas/slug v1.2.0/go.mod h1:RE8shOqQP3YhsfsQe0L3RnuejfQ4Mk+JjY5YJQFubfQ=
|
||||||
|
|||||||
99
internal/auth_handlers.go
Normal file
99
internal/auth_handlers.go
Normal 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
219
internal/email.go
Normal 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
201
internal/passwd_handlers.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user