1
0
mirror of https://github.com/volatiletech/authboss.git synced 2025-01-26 05:27:33 +02:00
authboss/recover/recover.go

298 lines
9.4 KiB
Go
Raw Normal View History

2015-01-12 22:28:42 -08:00
package recover
import (
"bytes"
"crypto/md5"
2015-01-16 21:49:23 -08:00
"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"
2015-01-30 15:38:28 -08:00
"time"
2015-01-12 22:28:42 -08:00
2015-01-30 15:38:28 -08:00
"golang.org/x/crypto/bcrypt"
2015-01-16 21:49:23 -08:00
2015-01-12 22:28:42 -08:00
"gopkg.in/authboss.v0"
"gopkg.in/authboss.v0/internal/flashutil"
2015-01-18 14:24:20 -08:00
"gopkg.in/authboss.v0/internal/views"
2015-01-12 22:28:42 -08:00
)
const (
methodGET = "GET"
methodPOST = "POST"
tplLogin = "login.tpl"
2015-01-18 14:24:20 -08:00
tplRecover = "recover.tpl"
tplRecoverComplete = "recover-complete.tpl"
2015-01-30 15:38:28 -08:00
tplInitHTMLEmail = "recover-html.email"
tplInitTextEmail = "recover-text.email"
2015-01-12 22:28:42 -08:00
2015-01-30 15:38:28 -08:00
attrUsername = "username"
attrRecoverToken = "recover_token"
attrRecoverTokenExpiry = "recover_token_expiry"
attrEmail = "email"
attrPassword = "password"
errFormat = "recover [%s]: %s\n"
2015-01-12 22:28:42 -08:00
)
func init() {
m := &RecoverModule{}
authboss.RegisterModule("recover", m)
}
type RecoverModule struct {
templates views.Templates
emailTemplates views.Templates
config *authboss.Config
2015-01-18 14:24:20 -08:00
}
2015-01-12 22:28:42 -08:00
2015-01-18 14:24:20 -08:00
func (m *RecoverModule) Initialize(config *authboss.Config) (err error) {
if config.Storer == nil {
return errors.New("recover: Need a RecoverStorer.")
}
2015-01-30 15:38:28 -08:00
if _, ok := config.Storer.(authboss.RecoverStorer); !ok {
return errors.New("recover: RecoverStorer required for recover functionality.")
}
2015-02-05 10:30:57 -08:00
if config.Layout == nil {
return errors.New("recover: Layout required for Recover functionallity.")
}
if m.templates, err = views.Get(config.Layout, config.ViewsPath, tplRecover, tplRecoverComplete); err != nil {
return err
}
2015-02-05 10:30:57 -08:00
if config.LayoutEmail == nil {
return errors.New("recover: LayoutEmail required for Recover functionallity.")
}
if m.emailTemplates, err = views.Get(config.LayoutEmail, config.ViewsPath, tplInitHTMLEmail, tplInitTextEmail); err != nil {
2015-01-18 14:24:20 -08:00
return err
2015-01-12 22:28:42 -08:00
}
2015-01-30 15:38:28 -08:00
m.config = config
2015-01-12 22:28:42 -08:00
return nil
}
2015-01-30 15:38:28 -08:00
func (m *RecoverModule) Routes() authboss.RouteTable {
return authboss.RouteTable{
"recover": m.recoverHandlerFunc,
"recover/complete": m.recoverCompleteHandlerFunc,
}
}
func (m *RecoverModule) Storage() authboss.StorageOptions {
return authboss.StorageOptions{
attrUsername: authboss.String,
attrRecoverToken: authboss.String,
attrEmail: authboss.String,
attrRecoverTokenExpiry: authboss.String,
attrPassword: authboss.String,
}
}
type pageRecover struct {
Username, ConfirmUsername string
ErrMap map[string][]string
FlashSuccess string
2015-01-30 15:38:28 -08:00
FlashError string
}
2015-01-12 22:28:42 -08:00
2015-01-16 21:49:23 -08:00
func (m *RecoverModule) recoverHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) {
2015-01-12 22:28:42 -08:00
switch r.Method {
case methodGET:
2015-02-05 10:30:57 -08:00
m.execTpl(w, pageRecover{FlashError: flashutil.Pull(ctx.SessionStorer, authboss.FlashErrorKey)})
2015-01-12 22:28:42 -08:00
case methodPOST:
2015-02-05 10:30:57 -08:00
if page := m.recover(ctx); page != nil {
m.execTpl(w, page)
return
2015-01-16 21:49:23 -08:00
}
2015-01-30 15:38:28 -08:00
ctx.SessionStorer.Put(authboss.FlashSuccessKey, m.config.RecoverInitiateSuccessFlash)
http.Redirect(w, r, m.config.RecoverRedirect, http.StatusFound)
2015-01-12 22:28:42 -08:00
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
2015-02-05 10:30:57 -08:00
func (m *RecoverModule) execTpl(w http.ResponseWriter, data interface{}) {
if err := m.templates.ExecuteTemplate(w, tplRecover, data); err != nil {
fmt.Fprintf(m.config.LogWriter, errFormat, "unable to execute template", err)
}
}
func (m *RecoverModule) recover(ctx *authboss.Context) *pageRecover {
username, _ := ctx.FirstPostFormValue("username")
confirmUsername, _ := ctx.FirstPostFormValue("confirmUsername")
policies := authboss.FilterValidators(m.config.Policies, "username")
if validationErrs := ctx.Validate(policies, m.config.ConfirmFields...); len(validationErrs) > 0 {
return m.prepareRecoverPage(username, confirmUsername, "", "validation failed", validationErrs.Map())
}
2015-01-30 15:38:28 -08:00
if err := ctx.LoadUser(username, m.config.Storer); err != nil {
2015-02-05 10:30:57 -08:00
return m.prepareRecoverPage(username, confirmUsername, m.config.RecoverFailedErrorFlash, "failed to recover", nil)
2015-01-16 21:49:23 -08:00
}
token := make([]byte, 32)
if _, err := rand.Read(token); err != nil {
2015-02-05 10:30:57 -08:00
return m.prepareRecoverPage(username, confirmUsername, m.config.RecoverFailedErrorFlash, "failed to recover", nil)
2015-01-18 14:24:20 -08:00
}
sum := md5.Sum(token)
2015-01-30 15:38:28 -08:00
ctx.User[attrRecoverToken] = base64.StdEncoding.EncodeToString(sum[:])
2015-01-30 15:38:28 -08:00
ctx.User[attrRecoverTokenExpiry] = time.Now().Add(m.config.RecoverTokenDuration)
2015-01-18 14:24:20 -08:00
2015-01-30 15:38:28 -08:00
if err := ctx.SaveUser(username, m.config.Storer); err != nil {
2015-02-05 10:30:57 -08:00
return m.prepareRecoverPage(username, confirmUsername, m.config.RecoverFailedErrorFlash, "failed to recover", nil)
2015-01-18 14:24:20 -08:00
}
2015-02-05 10:30:57 -08:00
/*if email, ok := ctx.User.String(attrEmail); !ok {
return m.prepareRecoverPage(username, confirmUsername, m.config.RecoverFailedErrorFlash, "failed to recover", nil)
2015-01-30 15:38:28 -08:00
} else {
go m.sendRecoverEmail(email, token)
2015-02-05 10:30:57 -08:00
}*/
2015-01-30 15:38:28 -08:00
return nil
}
2015-02-05 10:30:57 -08:00
func (m *RecoverModule) prepareRecoverPage(username, confirmUsername, flashError, message string, validationErrs map[string][]string) *pageRecover {
fmt.Fprintf(m.config.LogWriter, errFormat, message, validationErrs)
return &pageRecover{username, confirmUsername, validationErrs, "", flashError}
}
2015-01-30 15:38:28 -08:00
func (m *RecoverModule) sendRecoverEmail(to string, token []byte) {
data := struct{ Link string }{fmt.Sprintf("%s/recover/complete?token=%s", m.config.HostName, base64.URLEncoding.EncodeToString(token))}
htmlEmailBody := &bytes.Buffer{}
if err := m.emailTemplates.ExecuteTemplate(htmlEmailBody, tplInitHTMLEmail, data); err != nil {
2015-01-30 15:38:28 -08:00
fmt.Fprintf(m.config.LogWriter, errFormat, "failed to build html tpl", err)
}
textEmailBody := &bytes.Buffer{}
if err := m.emailTemplates.ExecuteTemplate(textEmaiLBody, tplInitTextEmail, data); err != nil {
2015-01-30 15:38:28 -08:00
fmt.Fprintf(m.config.LogWriter, errFormat, "failed to build plaintext tpl", err)
}
if err := m.config.Mailer.Send(authboss.Email{
To: []string{to},
ToNames: []string{""},
From: m.config.EmailFrom,
Subject: m.config.EmailSubjectPrefix + "Password Reset",
TextBody: textEmailBody.String(),
2015-01-30 15:38:28 -08:00
HTMLBody: htmlEmailBody.String(),
2015-01-18 14:24:20 -08:00
}); err != nil {
2015-01-30 15:38:28 -08:00
fmt.Fprintf(m.config.LogWriter, errFormat, "failed to send email", err)
2015-01-18 14:24:20 -08:00
}
2015-01-30 15:38:28 -08:00
}
2015-01-18 14:24:20 -08:00
2015-01-30 15:38:28 -08:00
type pageRecoverComplete struct {
Token string
ErrMap map[string][]string
FlashSuccess string
FlashError string
2015-01-12 22:28:42 -08:00
}
2015-01-18 14:24:20 -08:00
2015-01-30 15:38:28 -08:00
func (m *RecoverModule) recoverCompleteHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) {
execTpl := func(name string, data interface{}) {
if err := m.templates.ExecuteTemplate(w, name, data); err != nil {
fmt.Fprintf(m.config.LogWriter, errFormat, "unable to execute template", err)
2015-01-30 15:38:28 -08:00
}
}
switch r.Method {
case methodGET:
token, ok := ctx.FirstFormValue("token")
if !ok {
fmt.Fprintln(m.config.LogWriter, "recover: expected form value token")
2015-01-30 15:38:28 -08:00
http.Redirect(w, r, "/", http.StatusFound)
return
}
2015-01-30 15:38:28 -08:00
var err error
ctx.User, err = m.verifyToken(token)
if err != nil {
2015-01-30 15:38:28 -08:00
fmt.Fprintln(m.config.LogWriter, "recover:", err)
http.Redirect(w, r, "/", http.StatusFound)
return
}
2015-01-30 15:38:28 -08:00
expiry, ok := ctx.User.DateTime(attrRecoverTokenExpiry)
if !ok || time.Now().After(expiry) {
fmt.Fprintln(m.config.LogWriter, "recover: token has expired:", expiry)
ctx.SessionStorer.Put(authboss.FlashErrorKey, m.config.RecoverTokenExpiredFlash)
http.Redirect(w, r, "/recover", http.StatusFound)
return
}
execTpl(tplRecoverComplete, pageRecoverComplete{
FlashError: flashutil.Pull(ctx.SessionStorer, authboss.FlashErrorKey),
Token: token,
})
2015-01-18 14:24:20 -08:00
case methodPOST:
2015-01-30 15:38:28 -08:00
token, ok := ctx.FirstFormValue("token")
if !ok {
fmt.Fprintln(m.config.LogWriter, "recover: expected form value token")
http.Redirect(w, r, "/", http.StatusFound)
return
2015-01-30 15:38:28 -08:00
}
var err error
ctx.User, err = m.verifyToken(token)
if err != nil {
fmt.Fprintln(m.config.LogWriter, "recover 1234:", err)
2015-01-30 15:38:28 -08:00
http.Redirect(w, r, "/", http.StatusFound)
return
}
policies := authboss.FilterValidators(m.config.Policies, "password")
if validationErrs := ctx.Validate(policies, m.config.ConfirmFields...); len(validationErrs) > 0 {
execTpl(tplRecoverComplete, pageRecoverComplete{Token: token, ErrMap: validationErrs.Map()})
2015-01-30 15:38:28 -08:00
return
}
password, _ := ctx.FirstFormValue("password")
encryptedPassword, err := bcrypt.GenerateFromPassword([]byte(password), m.config.BCryptCost)
if err != nil {
fmt.Fprintln(m.config.LogWriter, "recover: failed to encrypt password")
execTpl(tplRecoverComplete, pageRecoverComplete{Token: token, FlashError: m.config.RecoverFailedErrorFlash})
2015-01-30 15:38:28 -08:00
return
}
ctx.User[attrPassword] = string(encryptedPassword)
username, ok := ctx.User.String(attrUsername)
if !ok {
fmt.Println(m.config.LogWriter, "reover: expected user attribue missing:", attrUsername)
execTpl(tplRecoverComplete, pageRecoverComplete{Token: token, FlashError: m.config.RecoverFailedErrorFlash})
2015-01-30 15:38:28 -08:00
return
}
ctx.User[attrRecoverToken] = ""
ctx.User[attrRecoverTokenExpiry] = time.Now().UTC()
if err := ctx.SaveUser(username, m.config.Storer); err != nil {
fmt.Fprintln(m.config.LogWriter, "recover asdf:", err)
execTpl(tplRecoverComplete, pageRecoverComplete{Token: token, FlashError: m.config.RecoverFailedErrorFlash})
2015-01-30 15:38:28 -08:00
return
}
ctx.SessionStorer.Put(authboss.SessionKey, username)
http.Redirect(w, r, m.config.AuthLoginSuccessRoute, http.StatusFound)
2015-01-18 14:24:20 -08:00
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
2015-01-30 15:38:28 -08:00
func (m *RecoverModule) verifyToken(token string) (attrs authboss.Attributes, err error) {
decodedToken, err := base64.URLEncoding.DecodeString(token)
if err != nil {
return nil, err
}
sum := md5.Sum(decodedToken)
2015-01-30 15:38:28 -08:00
userInter, err := m.config.Storer.(authboss.RecoverStorer).RecoverUser(base64.StdEncoding.EncodeToString(sum[:]))
if err != nil {
return nil, err
}
return authboss.Unbind(userInter), nil
}