1
0
mirror of https://github.com/volatiletech/authboss.git synced 2025-02-03 13:21:22 +02:00
authboss/recover/recover.go

313 lines
8.6 KiB
Go
Raw Normal View History

// Package recover implements password reset via e-mail.
2015-01-12 22:28:42 -08:00
package recover
import (
2015-02-23 15:51:42 -08:00
"crypto/md5"
"crypto/rand"
"encoding/base64"
2015-01-12 22:28:42 -08:00
"errors"
2015-01-16 21:49:23 -08:00
"fmt"
2015-01-12 22:28:42 -08:00
"net/http"
"net/url"
2015-03-07 19:45:48 -08:00
"path"
2015-02-23 15:51:42 -08:00
"time"
2015-01-12 22:28:42 -08:00
2015-02-23 15:51:42 -08:00
"golang.org/x/crypto/bcrypt"
2015-01-12 22:28:42 -08:00
"gopkg.in/authboss.v0"
"gopkg.in/authboss.v0/internal/response"
2015-01-12 22:28:42 -08:00
)
2015-03-16 14:42:45 -07:00
// Storage constants
const (
StoreRecoverToken = "recover_token"
StoreRecoverTokenExpiry = "recover_token_expiry"
)
const (
formValueToken = "token"
)
2015-01-12 22:28:42 -08:00
const (
methodGET = "GET"
methodPOST = "POST"
tplLogin = "login.html.tpl"
tplRecover = "recover.html.tpl"
tplRecoverComplete = "recover_complete.html.tpl"
tplInitHTMLEmail = "recover_email.html.tpl"
tplInitTextEmail = "recover_email.txt.tpl"
2015-01-12 22:28:42 -08:00
recoverInitiateSuccessFlash = "An email has been sent with further instructions on how to reset your password"
recoverTokenExpiredFlash = "Account recovery request has expired. Please try again."
recoverFailedErrorFlash = "Account recovery has failed. Please contact tech support."
2015-01-12 22:28:42 -08:00
)
2015-02-23 15:51:42 -08:00
var errRecoveryTokenExpired = errors.New("recovery token expired")
2015-02-24 11:04:27 -08:00
// RecoverStorer must be implemented in order to satisfy the recover module's
// storage requirements.
type RecoverStorer interface {
authboss.Storer
// RecoverUser looks a user up by a recover token. See recover module for
// attribute names. If the key is not found in the data store,
// simply return nil, ErrUserNotFound.
RecoverUser(recoverToken string) (interface{}, error)
}
2015-01-12 22:28:42 -08:00
func init() {
2015-02-23 15:51:42 -08:00
m := &Recover{}
2015-01-12 22:28:42 -08:00
authboss.RegisterModule("recover", m)
}
2015-03-16 14:42:45 -07:00
// Recover module
2015-02-23 15:51:42 -08:00
type Recover struct {
2015-03-31 15:27:47 -07:00
*authboss.Authboss
templates response.Templates
emailHTMLTemplates response.Templates
emailTextTemplates response.Templates
2015-01-18 14:24:20 -08:00
}
2015-01-12 22:28:42 -08:00
2015-03-16 14:42:45 -07:00
// Initialize module
2015-03-31 15:27:47 -07:00
func (r *Recover) Initialize(ab *authboss.Authboss) (err error) {
r.Authboss = ab
if r.Storer == nil {
2015-03-16 14:42:45 -07:00
return errors.New("recover: Need a RecoverStorer")
}
2015-03-31 15:27:47 -07:00
if _, ok := r.Storer.(RecoverStorer); !ok {
2015-03-16 14:42:45 -07:00
return errors.New("recover: RecoverStorer required for recover functionality")
}
2015-03-31 15:27:47 -07:00
if len(r.XSRFName) == 0 {
return errors.New("auth: XSRFName must be set")
}
2015-03-31 15:27:47 -07:00
if r.XSRFMaker == nil {
return errors.New("auth: XSRFMaker must be defined")
}
2015-03-31 15:27:47 -07:00
r.templates, err = response.LoadTemplates(r.Authboss, r.Layout, r.ViewsPath, tplRecover, tplRecoverComplete)
2015-02-23 15:51:42 -08:00
if err != nil {
return err
}
2015-03-31 15:27:47 -07:00
r.emailHTMLTemplates, err = response.LoadTemplates(r.Authboss, r.LayoutHTMLEmail, r.ViewsPath, tplInitHTMLEmail)
if err != nil {
return err
}
2015-03-31 15:27:47 -07:00
r.emailTextTemplates, err = response.LoadTemplates(r.Authboss, r.LayoutTextEmail, r.ViewsPath, tplInitTextEmail)
2015-02-23 15:51:42 -08:00
if err != nil {
2015-01-18 14:24:20 -08:00
return err
2015-01-12 22:28:42 -08:00
}
return nil
}
2015-03-16 14:42:45 -07:00
// Routes for module
2015-02-23 15:51:42 -08:00
func (r *Recover) Routes() authboss.RouteTable {
2015-01-30 15:38:28 -08:00
return authboss.RouteTable{
"/recover": r.startHandlerFunc,
"/recover/complete": r.completeHandlerFunc,
2015-01-30 15:38:28 -08:00
}
}
2015-03-16 14:42:45 -07:00
// Storage requirements
2015-02-23 15:51:42 -08:00
func (r *Recover) Storage() authboss.StorageOptions {
2015-01-30 15:38:28 -08:00
return authboss.StorageOptions{
2015-03-31 15:27:47 -07:00
r.PrimaryID: authboss.String,
2015-02-24 10:12:23 -08:00
authboss.StoreEmail: authboss.String,
authboss.StorePassword: authboss.String,
StoreRecoverToken: authboss.String,
StoreRecoverTokenExpiry: authboss.String,
2015-02-23 15:51:42 -08:00
}
}
func (rec *Recover) startHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) error {
switch r.Method {
2015-02-23 15:51:42 -08:00
case methodGET:
data := authboss.NewHTMLData(
2015-03-31 15:27:47 -07:00
"primaryID", rec.PrimaryID,
2015-02-23 15:51:42 -08:00
"primaryIDValue", "",
"confirmPrimaryIDValue", "",
)
return rec.templates.Render(ctx, w, r, tplRecover, data)
2015-02-23 15:51:42 -08:00
case methodPOST:
primaryID := r.FormValue(rec.PrimaryID)
confirmPrimaryID := r.FormValue(fmt.Sprintf("confirm_%s", rec.PrimaryID))
2015-02-23 15:51:42 -08:00
errData := authboss.NewHTMLData(
2015-03-31 15:27:47 -07:00
"primaryID", rec.PrimaryID,
2015-02-23 15:51:42 -08:00
"primaryIDValue", primaryID,
"confirmPrimaryIDValue", confirmPrimaryID,
)
2015-03-31 15:27:47 -07:00
policies := authboss.FilterValidators(rec.Policies, rec.PrimaryID)
if validationErrs := authboss.Validate(r, policies, rec.PrimaryID, authboss.ConfirmPrefix+rec.PrimaryID).Map(); len(validationErrs) > 0 {
2015-02-23 15:51:42 -08:00
errData.MergeKV("errs", validationErrs)
return rec.templates.Render(ctx, w, r, tplRecover, errData)
2015-02-23 15:51:42 -08:00
}
2015-02-26 08:26:44 -08:00
// redirect to login when user not found to prevent username sniffing
2015-02-23 15:51:42 -08:00
if err := ctx.LoadUser(primaryID); err == authboss.ErrUserNotFound {
2015-03-31 15:27:47 -07:00
return authboss.ErrAndRedirect{err, rec.RecoverOKPath, recoverInitiateSuccessFlash, ""}
2015-02-23 15:51:42 -08:00
} else if err != nil {
return err
}
2015-02-24 10:12:23 -08:00
email, err := ctx.User.StringErr(authboss.StoreEmail)
2015-02-23 15:51:42 -08:00
if err != nil {
return err
}
encodedToken, encodedChecksum, err := newToken()
if err != nil {
return err
}
2015-02-24 10:12:23 -08:00
ctx.User[StoreRecoverToken] = encodedChecksum
2015-03-31 15:27:47 -07:00
ctx.User[StoreRecoverTokenExpiry] = time.Now().Add(rec.RecoverTokenDuration)
2015-02-23 15:51:42 -08:00
if err := ctx.SaveUser(); err != nil {
return err
}
2015-02-26 08:26:44 -08:00
goRecoverEmail(rec, email, encodedToken)
2015-02-23 15:51:42 -08:00
ctx.SessionStorer.Put(authboss.FlashSuccessKey, recoverInitiateSuccessFlash)
2015-03-31 15:27:47 -07:00
response.Redirect(ctx, w, r, rec.RecoverOKPath, "", "", true)
2015-02-23 15:51:42 -08:00
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
return nil
}
func newToken() (encodedToken, encodedChecksum string, err error) {
token := make([]byte, 32)
if _, err = rand.Read(token); err != nil {
return "", "", err
}
sum := md5.Sum(token)
return base64.URLEncoding.EncodeToString(token), base64.StdEncoding.EncodeToString(sum[:]), nil
}
var goRecoverEmail = func(r *Recover, to, encodedToken string) {
go r.sendRecoverEmail(to, encodedToken)
}
func (r *Recover) sendRecoverEmail(to, encodedToken string) {
2015-03-31 15:27:47 -07:00
p := path.Join(r.MountPath, "recover/complete")
query := url.Values{formValueToken: []string{encodedToken}}
url := fmt.Sprintf("%s%s?%s", r.RootURL, p, query.Encode())
2015-02-23 15:51:42 -08:00
email := authboss.Email{
To: []string{to},
2015-03-31 15:27:47 -07:00
From: r.EmailFrom,
Subject: r.EmailSubjectPrefix + "Password Reset",
2015-02-23 15:51:42 -08:00
}
2015-03-31 15:27:47 -07:00
if err := response.Email(r.Mailer, email, r.emailHTMLTemplates, tplInitHTMLEmail, r.emailTextTemplates, tplInitTextEmail, url); err != nil {
fmt.Fprintln(r.LogWriter, "recover: failed to send recover email:", err)
2015-02-23 15:51:42 -08:00
}
}
func (r *Recover) completeHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, req *http.Request) (err error) {
switch req.Method {
case methodGET:
_, err = verifyToken(ctx, req)
2015-02-23 15:51:42 -08:00
if err == errRecoveryTokenExpired {
return authboss.ErrAndRedirect{err, "/recover", "", recoverTokenExpiredFlash}
2015-02-23 15:51:42 -08:00
} else if err != nil {
return authboss.ErrAndRedirect{err, "/", "", ""}
}
token := req.FormValue(formValueToken)
data := authboss.NewHTMLData(formValueToken, token)
2015-02-23 15:51:42 -08:00
return r.templates.Render(ctx, w, req, tplRecoverComplete, data)
case methodPOST:
token := req.FormValue(formValueToken)
if len(token) == 0 {
return authboss.ClientDataErr{formValueToken}
2015-02-23 15:51:42 -08:00
}
password := req.FormValue(authboss.StorePassword)
//confirmPassword, _ := ctx.FirstPostFormValue("confirmPassword")
2015-02-23 15:51:42 -08:00
policies := authboss.FilterValidators(r.Policies, authboss.StorePassword)
if validationErrs := authboss.Validate(req, policies, authboss.StorePassword, authboss.ConfirmPrefix+authboss.StorePassword).Map(); len(validationErrs) > 0 {
2015-02-23 15:51:42 -08:00
data := authboss.NewHTMLData(
formValueToken, token,
2015-02-23 15:51:42 -08:00
"errs", validationErrs,
)
return r.templates.Render(ctx, w, req, tplRecoverComplete, data)
}
if ctx.User, err = verifyToken(ctx, req); err != nil {
2015-02-23 15:51:42 -08:00
return err
}
2015-03-31 15:27:47 -07:00
encryptedPassword, err := bcrypt.GenerateFromPassword([]byte(password), r.BCryptCost)
2015-02-23 15:51:42 -08:00
if err != nil {
return err
}
ctx.User[authboss.StorePassword] = string(encryptedPassword)
ctx.User[StoreRecoverToken] = ""
2015-02-23 15:51:42 -08:00
var nullTime time.Time
ctx.User[StoreRecoverTokenExpiry] = nullTime
2015-02-23 15:51:42 -08:00
2015-03-31 15:27:47 -07:00
primaryID, err := ctx.User.StringErr(r.PrimaryID)
2015-02-23 15:51:42 -08:00
if err != nil {
return err
}
if err := ctx.SaveUser(); err != nil {
return err
}
2015-03-31 15:27:47 -07:00
if err := r.Callbacks.FireAfter(authboss.EventPasswordReset, ctx); err != nil {
return err
}
2015-02-23 15:51:42 -08:00
ctx.SessionStorer.Put(authboss.SessionKey, primaryID)
2015-03-31 15:27:47 -07:00
response.Redirect(ctx, w, req, r.AuthLoginOKPath, "", "", true)
2015-02-23 15:51:42 -08:00
default:
w.WriteHeader(http.StatusMethodNotAllowed)
2015-01-30 15:38:28 -08:00
}
2015-02-23 15:51:42 -08:00
return nil
2015-01-30 15:38:28 -08:00
}
2015-02-23 15:51:42 -08:00
// verifyToken expects a base64.URLEncoded token.
func verifyToken(ctx *authboss.Context, r *http.Request) (attrs authboss.Attributes, err error) {
token := r.FormValue(formValueToken)
if len(token) == 0 {
return nil, authboss.ClientDataErr{token}
2015-02-23 15:51:42 -08:00
}
decoded, err := base64.URLEncoding.DecodeString(token)
2015-02-08 23:08:33 -08:00
if err != nil {
2015-02-23 15:51:42 -08:00
return nil, err
2015-02-08 23:08:33 -08:00
}
2015-02-23 15:51:42 -08:00
sum := md5.Sum(decoded)
2015-03-31 15:27:47 -07:00
storer := ctx.Storer.(RecoverStorer)
2015-02-23 15:51:42 -08:00
userInter, err := storer.RecoverUser(base64.StdEncoding.EncodeToString(sum[:]))
if err != nil {
return nil, err
2015-02-08 23:08:33 -08:00
}
2015-02-23 15:51:42 -08:00
attrs = authboss.Unbind(userInter)
expiry, ok := attrs.DateTime(StoreRecoverTokenExpiry)
2015-02-23 15:51:42 -08:00
if !ok || time.Now().After(expiry) {
return nil, errRecoveryTokenExpired
}
return attrs, nil
2015-02-08 23:08:33 -08:00
}