2015-01-12 22:28:42 -08:00
|
|
|
package recover
|
|
|
|
|
|
|
|
import (
|
2015-01-25 22:58:50 -08:00
|
|
|
"bytes"
|
|
|
|
"crypto/md5"
|
2015-01-16 21:49:23 -08:00
|
|
|
"crypto/rand"
|
2015-01-25 22:58:50 -08:00
|
|
|
"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
|
|
|
"html/template"
|
|
|
|
"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"
|
2015-01-30 15:38:28 -08:00
|
|
|
"gopkg.in/authboss.v0/internal/httputil"
|
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"
|
|
|
|
|
2015-01-25 22:58:50 -08:00
|
|
|
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 {
|
2015-01-30 15:38:28 -08:00
|
|
|
templates *template.Template
|
|
|
|
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) {
|
2015-01-25 22:58:50 -08:00
|
|
|
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 {
|
2015-01-25 22:58:50 -08:00
|
|
|
return errors.New("recover: RecoverStorer required for recover functionality.")
|
|
|
|
}
|
|
|
|
|
2015-01-30 15:38:28 -08:00
|
|
|
if m.templates, err = views.Get(config.ViewsPath, tplRecover, tplRecoverComplete, 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
|
|
|
|
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-30 15:38:28 -08:00
|
|
|
execTpl := func(data interface{}) {
|
|
|
|
if err := m.templates.ExecuteTemplate(w, tplRecover, data); err != nil {
|
|
|
|
fmt.Fprintf(m.config.LogWriter, errFormat, "unable to execute template", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-01-12 22:28:42 -08:00
|
|
|
switch r.Method {
|
|
|
|
case methodGET:
|
2015-01-30 15:38:28 -08:00
|
|
|
execTpl(pageRecover{FlashError: httputil.PullFlash(ctx.SessionStorer, authboss.FlashErrorKey)})
|
2015-01-12 22:28:42 -08:00
|
|
|
case methodPOST:
|
2015-01-30 15:38:28 -08:00
|
|
|
// ignore ok checks as we validate these fields anyways
|
2015-01-25 22:58:50 -08:00
|
|
|
username, _ := ctx.FirstPostFormValue("username")
|
|
|
|
confirmUsername, _ := ctx.FirstPostFormValue("confirmUsername")
|
|
|
|
|
2015-01-30 15:38:28 -08:00
|
|
|
policies := authboss.FilterValidators(m.config.Policies, "username")
|
|
|
|
if validationErrs := ctx.Validate(policies, m.config.ConfirmFields...); len(validationErrs) > 0 {
|
|
|
|
fmt.Fprintf(m.config.LogWriter, errFormat, "validation failed", validationErrs)
|
|
|
|
execTpl(pageRecover{username, confirmUsername, validationErrs.Map(), ""})
|
2015-01-25 22:58:50 -08:00
|
|
|
return
|
2015-01-16 21:49:23 -08:00
|
|
|
}
|
2015-01-12 22:28:42 -08:00
|
|
|
|
2015-01-30 15:38:28 -08:00
|
|
|
if err := m.recover(ctx, username); err != nil {
|
|
|
|
// never reveal failed usernames to prevent sniffing
|
|
|
|
fmt.Fprintf(m.config.LogWriter, errFormat, "failed to recover", err)
|
|
|
|
execTpl(pageRecover{username, confirmUsername, nil, m.config.RecoverFailedErrorFlash})
|
|
|
|
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-01-30 15:38:28 -08:00
|
|
|
func (m *RecoverModule) recover(ctx *authboss.Context, username string) (err error) {
|
|
|
|
if err := ctx.LoadUser(username, m.config.Storer); err != nil {
|
2015-01-16 21:49:23 -08:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-01-25 22:58:50 -08:00
|
|
|
token := make([]byte, 32)
|
|
|
|
if _, err := rand.Read(token); err != nil {
|
|
|
|
return err
|
2015-01-18 14:24:20 -08:00
|
|
|
}
|
|
|
|
sum := md5.Sum(token)
|
2015-01-30 15:38:28 -08:00
|
|
|
|
2015-01-25 22:58:50 -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-01-18 14:24:20 -08:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2015-01-30 15:38:28 -08:00
|
|
|
if email, ok := ctx.User.String(attrEmail); !ok {
|
|
|
|
return errors.New("email not found; unable to send email")
|
|
|
|
} else {
|
|
|
|
go m.sendRecoverEmail(email, token)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
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.templates.ExecuteTemplate(htmlEmailBody, tplInitHTMLEmail, data); err != nil {
|
|
|
|
fmt.Fprintf(m.config.LogWriter, errFormat, "failed to build html tpl", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
textEmaiLBody := &bytes.Buffer{}
|
|
|
|
if err := m.templates.ExecuteTemplate(textEmaiLBody, tplInitTextEmail, data); err != nil {
|
|
|
|
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: "Password Reset",
|
|
|
|
TextBody: textEmaiLBody.String(),
|
|
|
|
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
|
|
|
|
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) {
|
2015-01-18 14:24:20 -08:00
|
|
|
switch r.Method {
|
|
|
|
case methodGET:
|
2015-01-30 15:38:28 -08:00
|
|
|
page := pageRecoverComplete{}
|
|
|
|
|
|
|
|
if msg, ok := ctx.SessionStorer.Get(authboss.FlashErrorKey); ok {
|
|
|
|
page.FlashError = msg
|
|
|
|
ctx.SessionStorer.Del(authboss.FlashErrorKey)
|
|
|
|
}
|
2015-01-25 22:58:50 -08:00
|
|
|
|
|
|
|
token, ok := ctx.FirstFormValue("token")
|
|
|
|
if !ok {
|
2015-01-30 15:38:28 -08:00
|
|
|
fmt.Fprintln(m.config.LogWriter, "recover: expected value token")
|
|
|
|
http.Redirect(w, r, "/", http.StatusFound)
|
2015-01-25 22:58:50 -08:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2015-01-30 15:38:28 -08:00
|
|
|
var err error
|
|
|
|
ctx.User, err = m.verifyToken(token)
|
2015-01-25 22:58:50 -08:00
|
|
|
if err != nil {
|
2015-01-30 15:38:28 -08:00
|
|
|
fmt.Fprintln(m.config.LogWriter, "recover:", err)
|
|
|
|
http.Redirect(w, r, "/", http.StatusFound)
|
2015-01-25 22:58:50 -08:00
|
|
|
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
|
|
|
|
}
|
2015-01-25 22:58:50 -08:00
|
|
|
|
2015-01-30 15:38:28 -08:00
|
|
|
page.Token = token
|
|
|
|
if err := m.templates.ExecuteTemplate(w, tplRecoverComplete, pageRecoverComplete{Token: token}); err != nil {
|
|
|
|
fmt.Fprintln(m.config.LogWriter, "recover:", err)
|
|
|
|
}
|
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 value token")
|
|
|
|
}
|
|
|
|
|
|
|
|
var err error
|
|
|
|
ctx.User, err = m.verifyToken(token)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintln(m.config.LogWriter, "recover:", err)
|
|
|
|
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 {
|
|
|
|
err := m.templates.ExecuteTemplate(w, tplRecoverComplete, pageRecoverComplete{Token: token, ErrMap: validationErrs.Map()})
|
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintln(m.config.LogWriter, "recover:", err)
|
|
|
|
}
|
|
|
|
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")
|
|
|
|
err := m.templates.ExecuteTemplate(w, tplRecoverComplete, pageRecoverComplete{Token: token, FlashError: m.config.RecoverFailedErrorFlash})
|
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintln(m.config.LogWriter, "recover:", err)
|
|
|
|
}
|
|
|
|
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)
|
|
|
|
err := m.templates.ExecuteTemplate(w, tplRecoverComplete, pageRecoverComplete{Token: token, FlashError: m.config.RecoverFailedErrorFlash})
|
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintln(m.config.LogWriter, "recover:", err)
|
|
|
|
}
|
|
|
|
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:", err)
|
|
|
|
err := m.templates.ExecuteTemplate(w, tplRecoverComplete, pageRecoverComplete{Token: token, FlashError: m.config.RecoverFailedErrorFlash})
|
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintln(m.config.LogWriter, "recover:", err)
|
|
|
|
}
|
|
|
|
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-25 22:58:50 -08:00
|
|
|
|
2015-01-30 15:38:28 -08:00
|
|
|
func (m *RecoverModule) verifyToken(token string) (attrs authboss.Attributes, err error) {
|
2015-01-25 22:58:50 -08:00
|
|
|
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[:]))
|
2015-01-25 22:58:50 -08:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return authboss.Unbind(userInter), nil
|
|
|
|
}
|