1
0
mirror of https://github.com/volatiletech/authboss.git synced 2025-03-29 22:06:58 +02:00

Rework recover

This commit is contained in:
Kris Runzer 2015-02-23 15:51:42 -08:00
parent ec5da7694e
commit 199d0ec0b3
14 changed files with 266 additions and 1406 deletions

@ -1,6 +1,7 @@
package auth
import (
"errors"
"fmt"
"net/http"
@ -17,18 +18,22 @@ const (
)
func init() {
a := &AuthModule{}
a := &Auth{}
authboss.RegisterModule("auth", a)
}
type AuthModule struct {
type Auth struct {
templates render.Templates
policies []authboss.Validator
isRememberLoaded bool
isRecoverLoaded bool
}
func (a *AuthModule) Initialize() (err error) {
func (a *Auth) Initialize() (err error) {
if authboss.Cfg.Storer == nil {
return errors.New("auth: Need a Storer.")
}
a.templates, err = render.LoadTemplates(authboss.Cfg.Layout, authboss.Cfg.ViewsPath, tplLogin)
if err != nil {
return err
@ -42,21 +47,21 @@ func (a *AuthModule) Initialize() (err error) {
return nil
}
func (a *AuthModule) Routes() authboss.RouteTable {
func (a *Auth) Routes() authboss.RouteTable {
return authboss.RouteTable{
"login": a.loginHandlerFunc,
"logout": a.logoutHandlerFunc,
}
}
func (a *AuthModule) Storage() authboss.StorageOptions {
func (a *Auth) Storage() authboss.StorageOptions {
return authboss.StorageOptions{
authboss.Cfg.PrimaryID: authboss.String,
authboss.StorePassword: authboss.String,
}
}
func (a *AuthModule) loginHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) error {
func (a *Auth) loginHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) error {
switch r.Method {
case methodGET:
if _, ok := ctx.SessionStorer.Get(authboss.SessionKey); ok {
@ -65,7 +70,12 @@ func (a *AuthModule) loginHandlerFunc(ctx *authboss.Context, w http.ResponseWrit
}
}
data := authboss.NewHTMLData("showRemember", a.isRememberLoaded, "showRecover", a.isRecoverLoaded)
data := authboss.NewHTMLData(
"showRemember", a.isRememberLoaded,
"showRecover", a.isRecoverLoaded,
"primaryID", authboss.Cfg.PrimaryID,
"primaryIDValue", "",
)
return a.templates.Render(ctx, w, r, tplLogin, data)
case methodPOST:
interrupted, err := authboss.Cfg.Callbacks.FireBefore(authboss.EventAuth, ctx)
@ -87,8 +97,9 @@ func (a *AuthModule) loginHandlerFunc(ctx *authboss.Context, w http.ResponseWrit
password, _ := ctx.FirstPostFormValue("password")
errData := authboss.NewHTMLData(
"error", "invalid username and/or password",
authboss.Cfg.PrimaryID, key,
"error", fmt.Sprintf("invalid %s and/or password", authboss.Cfg.PrimaryID),
"primaryID", authboss.Cfg.PrimaryID,
"primaryIDValue", key,
"showRemember", a.isRememberLoaded,
"showRecover", a.isRecoverLoaded,
)
@ -130,7 +141,7 @@ func validateCredentials(ctx *authboss.Context, key, password string) error {
return nil
}
func (a *AuthModule) logoutHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) error {
func (a *Auth) logoutHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) error {
switch r.Method {
case methodGET:
ctx.SessionStorer.Del(authboss.SessionKey)

@ -10,11 +10,6 @@ import (
"golang.org/x/crypto/bcrypt"
)
const (
layoutTpl = "layout.tpl"
layoutEmailTpl = "layoutEmail.tpl"
)
// Cfg is the singleton instance of Config
var Cfg *Config = NewConfig()
@ -105,6 +100,7 @@ func NewConfig() *Config {
},
ConfirmFields: []string{
StoreEmail, ConfirmPrefix + StoreEmail,
StoreUsername, ConfirmPrefix + StoreUsername,
StorePassword, ConfirmPrefix + StorePassword,
},

@ -76,7 +76,7 @@ func confirm_email_html_tpl() (*asset, error) {
return nil, err
}
info := bindata_file_info{name: "confirm_email.html.tpl", size: 26, mode: os.FileMode(438), modTime: time.Unix(1424498554, 0)}
info := bindata_file_info{name: "confirm_email.html.tpl", size: 26, mode: os.FileMode(438), modTime: time.Unix(1424471280, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -96,12 +96,12 @@ func confirm_email_txt_tpl() (*asset, error) {
return nil, err
}
info := bindata_file_info{name: "confirm_email.txt.tpl", size: 9, mode: os.FileMode(438), modTime: time.Unix(1424498554, 0)}
info := bindata_file_info{name: "confirm_email.txt.tpl", size: 9, mode: os.FileMode(438), modTime: time.Unix(1424471280, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _login_tpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x7c\x92\x4d\x6e\x83\x30\x10\x85\xf7\x95\x7a\x87\xd1\xec\x5b\x2e\x00\x96\xba\xef\x4f\xd4\xa6\x07\x30\x66\x28\x28\xd8\x83\x06\x3b\x49\x85\xb8\x7b\xed\x12\x12\x90\xaa\xb2\xb2\x9e\x1f\xef\x7b\xcc\x90\xd7\x2c\x16\xb4\xf1\x2d\xbb\x02\xb3\x8e\xbf\x5a\x87\x60\xc9\x37\x5c\x15\xb8\x7b\xfb\xd8\xa3\xba\xbf\x83\xf8\x8c\x63\x5b\xc3\x23\x89\xb0\x4c\xd3\x38\x2e\xa7\xbc\x14\xc8\xd4\x38\x92\xab\xa6\x69\x76\xe6\xad\xeb\x83\x07\xff\xdd\x53\x81\x9e\xce\x1e\xc1\x74\x7a\x18\x0a\x4c\xb0\x07\xc3\xce\x0b\x77\x08\x4e\xdb\x68\x08\x03\x49\x3a\x21\xf4\x9d\x36\xd4\x70\x57\x91\x14\xf8\x79\x95\x8f\xba\x0b\xd1\x17\x91\x8b\x75\x9a\x50\xcd\xdc\x0d\xf0\x42\xec\x23\xea\xc4\x52\xfd\x4b\xbd\x99\x36\xd4\xdd\x22\xff\x95\x3f\xc7\x37\x6d\x55\x91\x5b\x72\x62\xab\xf3\x20\xf5\xeb\xdc\x6a\xd5\x35\xa9\x7b\x3e\x90\x4b\x72\xb6\x99\xe1\xd0\xf0\xe9\x9d\x2c\xd9\x92\xd2\x00\xd7\xe1\xa6\x21\x73\x28\xf9\xbc\xc4\x8b\xbd\x66\x7a\x09\x84\x0a\x96\x17\xe1\x85\xb6\x43\x5f\xf7\x2d\x83\xf7\xec\x2e\x99\x43\x28\x6d\xeb\x51\x3d\xa7\xdd\xe6\xd9\x7c\xb7\xf9\xbe\x75\x2d\xc3\xc7\xdf\x56\x1a\x1a\xa1\x3a\xfe\x12\x32\x4b\xa8\x2e\x77\xf0\x64\x0c\x07\xe7\xf3\x4c\xdf\xd6\x9e\x67\x69\xc8\xea\x27\x00\x00\xff\xff\xe8\x9d\xff\x88\x4e\x02\x00\x00")
var _login_tpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x7c\x92\x4d\x6b\xf3\x30\x0c\x80\xef\x85\xfe\x07\xe3\xfb\xdb\xfc\x81\x24\xf0\xc2\x2e\x83\x7d\x94\xad\xf7\xe1\x38\xca\x62\x1a\x5b\x41\x96\xfb\x41\xc8\x7f\x9f\xbd\xa4\x6b\x02\x63\x39\x05\x49\x7e\xf4\x58\xf2\x30\xec\x9a\x4e\xf9\xf6\xc3\x07\xad\xc1\xfb\x71\xdc\x6e\xf2\x06\xc9\x0a\xa5\xd9\xa0\x2b\x64\xd6\xe1\xa7\x71\x52\x58\xe0\x16\xeb\x42\xee\x5f\xdf\x0f\xb2\xdc\x6e\x44\xfc\x86\xc1\x34\x62\x07\x44\x48\xe3\x18\x51\xf3\x5f\x5e\x91\xc8\xca\x61\x00\x57\x27\x5e\xaa\xcc\x8d\xeb\x03\x0b\xbe\xf6\x50\x48\x86\x0b\x4b\xa1\x63\x5f\x5f\xc8\xd4\xec\x9f\x46\xc7\x84\x9d\x14\x4e\xd9\x58\x10\x51\x3d\x19\xab\xe8\xfa\xf8\x30\x8e\x52\xf4\x9d\xd2\xd0\x62\x57\x03\xa5\x24\x1b\xee\x40\xac\x4b\x4e\xaa\x0b\xd3\xc9\xe0\x81\x12\x26\x46\xcb\xc9\x64\xa5\x30\x3b\xf4\xb1\xf9\x19\xa9\xfe\xd3\xe3\x5e\xb4\x32\xd8\xdf\xc2\xbf\xf1\x27\x7c\x6b\xea\x1a\xdc\xe2\x3e\x17\x4f\xcd\xcb\x64\xb5\x70\x4d\xd1\x03\x1e\xc1\xa5\x70\xb6\x9a\xaa\x6f\xf1\xfc\x06\x16\x6c\x05\x69\xa4\x4b\xb8\x6e\x41\x1f\x2b\xbc\xdc\xf0\x64\x7f\x98\x4c\x01\x64\x29\x6e\x07\xc5\x33\xac\xd7\x50\x05\x66\x74\x33\xc7\x87\xca\x1a\x96\xe5\x53\xda\x70\x9e\x4d\xb9\xd5\x9d\x96\x2a\x1a\x4f\xdf\x26\x4a\xb4\x04\x4d\x7c\x18\x34\x85\x64\x39\xe7\xc4\x7f\xad\x31\x38\xce\x33\x75\x5f\x7e\x9e\xa5\xc1\x96\x5f\x01\x00\x00\xff\xff\xe7\xed\x2e\xa4\x68\x02\x00\x00")
func login_tpl_bytes() ([]byte, error) {
return bindata_read(
@ -116,12 +116,12 @@ func login_tpl() (*asset, error) {
return nil, err
}
info := bindata_file_info{name: "login.tpl", size: 590, mode: os.FileMode(438), modTime: time.Unix(1424567032, 0)}
info := bindata_file_info{name: "login.tpl", size: 616, mode: os.FileMode(438), modTime: time.Unix(1424735294, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _recover_complete_tpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xd4\x54\xc1\x8e\xd3\x30\x10\xbd\xef\x57\x8c\xac\xbd\xa6\xb9\xa3\xa4\x17\x04\x37\x60\x45\xf7\xc0\xd5\x8d\xa7\x1b\x6b\x1d\xdb\x4c\x9c\xc2\xca\xf2\xbf\x33\x8e\x9b\x6e\x89\x5a\x81\x84\x38\x6c\x24\x2b\x9e\xb1\xe7\x79\xde\x7b\xd2\x34\x07\x47\x03\xc8\x2e\x68\x67\x5b\x51\x13\x76\xee\x88\x54\x77\x6e\xf0\x06\x03\x0a\x18\x30\xf4\x4e\xb5\xe2\xe1\xcb\xee\x51\x6c\xef\x80\xbf\x46\x5b\x3f\x05\x08\x2f\x1e\x5b\xd1\x6b\xa5\xd0\x0a\xb0\x72\xe0\x28\xb8\xe7\x1c\x1c\xa5\x99\x38\x8a\x71\xf3\x98\x13\x29\x09\xa8\x4b\x6d\x8c\xf7\x5e\x8e\xe3\x0f\x47\xea\x03\xd1\x08\xef\x5a\xd8\xf0\xe6\x93\xf4\x9b\x25\x9f\x52\x79\x45\xe9\x23\x74\x86\x93\xad\xc8\x4d\x56\x4f\xe4\x26\x1f\xa3\x3e\xc0\x6f\x10\x29\x41\x2f\xc7\x0a\x89\x1c\xc5\x88\x96\xeb\x4f\x7d\xae\x51\xe6\xb6\x0b\xcc\xc5\x8d\xf9\xd6\xe8\xa5\xbd\x72\xad\x92\x4a\x39\x2b\xb6\x8d\x3e\x77\x22\xe1\x20\x2b\xe3\xba\x67\xce\xd6\x9a\x57\x2e\x5d\xa1\x15\x7d\x2e\x7b\xef\x9c\x0d\xe4\x8c\x38\x89\x16\xf0\x67\x58\x24\x5b\xb8\x08\xf0\x46\x76\xd8\x3b\xa3\x90\x58\xef\x73\x9a\xf0\xfb\xa4\x09\xd5\x22\xe1\xfc\x44\xcd\xbc\x5e\xc3\x18\x49\xda\x27\x84\x7b\x56\x21\x4b\xba\x12\xe8\x36\xd7\x1e\x8d\xaf\xf6\x85\x4e\x8c\x9e\xb4\x0d\x33\x48\x4a\x6b\x62\x27\x69\xef\x2e\x5e\x5f\x0c\x65\x72\x07\x4d\xc3\xc3\x0d\x5f\x57\xc7\x7f\x61\xef\x15\xc0\xb7\xee\xf2\x8a\xd2\xca\xec\xf7\xe5\x14\xfe\xcd\xf4\xab\xb2\xfd\x27\xef\xe7\xed\x7e\x0a\xc1\x9d\xf1\xf6\xc1\x02\xaf\x8a\x91\x06\x49\x2f\xf3\xbe\xc0\x9f\xf4\x18\xa7\xfd\xa0\x83\xd8\xee\xe6\x7f\x53\x97\xf2\x3f\xce\x14\x9e\x22\xdf\x76\x5f\x3f\x7e\xe6\x20\x0f\x92\xd7\xd9\x92\xb3\x97\xf3\xa5\xa9\xb3\x0b\xdb\x5f\x01\x00\x00\xff\xff\x30\xbb\xc0\xa5\xd3\x04\x00\x00")
var _recover_complete_tpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xb4\x52\xc1\x6e\xea\x30\x10\xbc\xf3\x15\x2b\xeb\x9d\xf1\xfd\xc9\xc9\x85\x7b\x8b\x5a\xee\x95\x89\x37\xc4\xaa\xb3\x8e\xd6\x0e\x50\x45\xf9\xf7\xda\x04\x08\x6d\xa9\x7a\xa8\x9a\x4b\x3c\xeb\x9d\x19\x8f\x34\xaa\xf6\xdc\x82\xae\xa2\xf5\x54\x08\xc9\x58\xf9\x3d\xb2\xac\x7c\xdb\x39\x8c\x28\xa0\xc5\xd8\x78\x53\x88\xf5\xe3\xf3\x46\x94\x0b\x48\x9f\xb2\xd4\xf5\x11\xe2\x5b\x87\x85\x68\xac\x31\x48\x02\x48\xb7\x09\x45\xff\x9a\xc1\x5e\xbb\x3e\xa1\x61\x58\x9e\x06\xe3\x28\x40\xde\xe1\x76\x3a\x84\x83\x67\x73\x61\xcf\xb8\x73\xba\xc2\xc6\x3b\x83\x9c\xac\xaf\xe3\x59\xf7\xb2\x3a\x49\xab\x2d\x5f\x0c\x86\xe1\x60\x63\x03\x4b\x64\x0e\xe3\x78\x46\xff\x12\x72\x36\x44\xf8\x5f\x80\x25\x83\x47\x58\xc2\xec\x96\xd7\x58\xd3\x0e\xaf\x7b\xe3\xa8\x42\xa7\xa9\x4c\x46\xe9\x28\x4f\xe7\xc9\x63\x18\x90\x4c\x26\xdc\xfe\x7e\x4e\x56\x79\xaa\x2d\xb7\x2f\xdf\x24\x5c\x4d\xd7\x70\x2f\xe9\x99\xba\xfe\x7d\xe0\x2f\x8f\xf8\x83\xe0\x1f\xeb\x90\x64\x8e\x81\xeb\x87\x04\xf2\xbb\xe7\x50\x79\xba\xf9\x5c\x8d\x6d\x1f\xa3\xa7\xb3\x50\xe8\xb7\xad\x8d\xa2\x7c\x9a\x1a\xa9\xe4\x74\x7b\x9b\x5c\x69\x68\x18\xeb\x54\x5b\xe7\x77\x96\x44\xb9\xd2\x54\xa1\x53\x52\x97\x0b\x25\x73\xb1\xcb\xf7\x00\x00\x00\xff\xff\x67\x02\x74\x02\xdf\x02\x00\x00")
func recover_complete_tpl_bytes() ([]byte, error) {
return bindata_read(
@ -136,12 +136,12 @@ func recover_complete_tpl() (*asset, error) {
return nil, err
}
info := bindata_file_info{name: "recover-complete.tpl", size: 1235, mode: os.FileMode(438), modTime: time.Unix(1424498554, 0)}
info := bindata_file_info{name: "recover-complete.tpl", size: 735, mode: os.FileMode(438), modTime: time.Unix(1424735204, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _recover_html_email = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xb2\x29\x2e\x29\xca\xcf\x4b\xb7\xab\xae\xd6\xf3\xc9\xcc\xcb\xae\xad\xb5\xd1\x87\x8a\x00\x02\x00\x00\xff\xff\xe1\x46\x1b\xff\x1a\x00\x00\x00")
var _recover_html_email = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xb2\x29\x2e\x29\xca\xcf\x4b\xb7\xab\xae\xd6\xab\xad\xb5\xd1\x87\xf2\x00\x01\x00\x00\xff\xff\xe7\xfa\xf4\xa7\x16\x00\x00\x00")
func recover_html_email_bytes() ([]byte, error) {
return bindata_read(
@ -156,12 +156,12 @@ func recover_html_email() (*asset, error) {
return nil, err
}
info := bindata_file_info{name: "recover-html.email", size: 26, mode: os.FileMode(438), modTime: time.Unix(1424498554, 0)}
info := bindata_file_info{name: "recover-html.email", size: 22, mode: os.FileMode(438), modTime: time.Unix(1424728125, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _recover_text_email = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xaa\xae\xd6\xf3\xc9\xcc\xcb\xae\xad\x05\x04\x00\x00\xff\xff\x41\xf7\xa1\x3d\x09\x00\x00\x00")
var _recover_text_email = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xaa\xae\xd6\xab\xad\x05\x04\x00\x00\xff\xff\x8e\x60\xe8\x72\x05\x00\x00\x00")
func recover_text_email_bytes() ([]byte, error) {
return bindata_read(
@ -176,12 +176,12 @@ func recover_text_email() (*asset, error) {
return nil, err
}
info := bindata_file_info{name: "recover-text.email", size: 9, mode: os.FileMode(438), modTime: time.Unix(1424498554, 0)}
info := bindata_file_info{name: "recover-text.email", size: 5, mode: os.FileMode(438), modTime: time.Unix(1424728129, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _recover_tpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xd4\x54\x4d\x8f\x9b\x30\x10\xbd\xf3\x2b\xa6\xd6\x5e\xbd\xdc\x2b\xe0\x12\xb5\xb7\x7e\x68\x3f\xa4\x5e\x0d\x98\x60\xc5\xd8\x68\x30\x51\x57\x16\xff\xbd\x43\x0c\x1b\xc2\x92\x36\x6d\xd5\x43\x23\x11\x0d\x66\xfc\x9e\xe7\xbd\xf1\x24\x95\xc5\x06\x44\xe1\x94\x35\x29\x8b\x51\x16\xf6\x28\x91\x41\x23\x5d\x6d\xcb\x94\x7d\xfd\xf2\xf8\xc4\xb2\x08\xe8\x97\x28\xd3\xf6\x0e\xdc\x4b\x2b\x53\xe6\xe4\x77\xc7\xc0\x88\x86\xe2\xbe\x93\x38\x46\x0c\x5a\x2d\x0a\x59\x5b\x5d\x4a\x4c\xd9\xf3\xeb\xf2\x51\xe8\x9e\xf2\xbc\xbf\x9f\x53\x87\x81\x41\x9c\x25\x39\xd2\xff\xcf\xc1\x0b\x6b\x2a\x85\xcd\xf3\x36\xc7\x2e\x7c\x85\x2d\xae\xd5\xce\xb7\x94\x79\xef\x9c\x35\x13\x67\xd7\xe7\x8d\x72\x2c\x7b\x08\x0a\x24\x71\xf8\x3a\xa5\x0a\xa8\x51\x56\x24\x90\xb6\x7b\x65\x58\xb6\x13\xa6\x90\x3a\x89\x45\x16\x25\xf1\x28\x61\x16\x45\x51\xf2\x8e\x73\xf8\x0d\x41\xbd\xbf\x9b\xf5\xf8\x80\xd8\xc1\xfb\x14\xee\x29\xf8\x24\xda\x85\x4e\x81\xbf\x54\x47\x28\xb4\xe8\xba\x94\x8d\xf8\x7c\x8f\xb6\x6f\xbd\x57\x15\x5c\x40\x0c\x03\xd4\xa2\xe3\x12\xd1\xa2\xf7\xd2\x94\x54\x74\xe0\x5a\xa3\x9c\xe4\x0e\x30\x8b\x8c\x53\x56\xd7\x0a\xb3\x91\xc6\x45\x59\x5a\x2a\x3d\x51\xaf\x27\x11\x50\x09\x3e\xf2\xd3\x6a\xac\xe8\x19\xb7\xae\xd0\x82\xaf\xcb\xb3\x93\x2f\x0e\xad\x66\x7f\xd7\x49\x97\xb6\x9e\x6b\x8c\xa9\xc8\xf3\xab\xf7\x28\xcc\x5e\xc2\x1d\x49\x32\xea\xbb\x52\xeb\x7a\xe1\xb5\xd4\x2d\xcf\xb5\x2d\x0e\x2c\xf3\xbe\x45\x65\xdc\x09\x64\x18\xd6\x55\x4e\x3a\x47\x0b\xf6\xd9\xdd\x55\x07\xae\x4d\x7e\xd3\xa0\xbf\xf4\x7a\x03\xf0\x7f\xb7\xfc\xcf\xef\xf7\x6e\xeb\x7e\xdf\xdc\x08\x9b\x52\xfe\xa3\x7e\x18\xc3\x8b\x91\x33\x81\xe6\xce\x00\x3d\x9c\xe0\x1a\x81\x2f\xa7\x38\x70\xdc\x38\x94\x56\x38\x5a\x99\xc3\x55\x90\xeb\x13\x6c\x61\x5a\xd8\x51\xab\xb2\x94\x66\x76\x88\xa4\xfe\xf6\xf8\xf0\xf1\xf3\xa4\xf1\xd9\x80\x71\xf5\xc9\x1e\xa4\x99\xa4\x9f\x26\x21\x70\x9e\xfd\x08\x00\x00\xff\xff\x94\x24\x92\xf9\x57\x06\x00\x00")
var _recover_tpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xb4\x53\x4d\x6f\xe2\x30\x10\xbd\xf3\x2b\x46\x16\x7b\x25\xf7\x55\xe2\x0b\xbb\x87\x5e\x5a\xd4\xa2\x5e\x2b\x93\x4c\x88\x55\xc7\x8e\x26\x13\x0a\x4a\xfd\xdf\x6b\x63\x8a\x48\x2b\x81\x7a\x68\x2e\x99\x8f\x37\x2f\xf3\x9e\x26\x79\xed\xa8\x05\x55\xb2\x76\xb6\x10\x19\x61\xe9\x76\x48\x02\x5a\xe4\xc6\x55\x85\x58\x3d\x3c\xad\x85\x9c\x41\x78\xc6\xf1\x4d\x73\x03\x8b\xda\xa8\xbe\xf9\x4f\xe4\xc8\xfb\x71\x5c\x78\x9f\x6f\x08\x32\x39\x8e\x68\x2b\xef\x8f\xd0\x5c\xdb\x6e\x60\xe0\x43\x87\x85\x60\xdc\xb3\x00\xab\xda\x10\x07\x7c\x47\xba\x55\x74\xb8\xfb\xe7\xbd\x80\xce\xa8\x12\x1b\x67\x2a\xa4\xd8\x64\xcd\x06\x61\x0a\xd9\x29\x33\x7c\x99\x7c\x8e\xa5\xd8\xcb\x64\xfa\xf6\x69\xbf\x79\xa7\x2b\xf8\x5b\x4c\x08\x3e\xb7\x46\xa2\xfe\x9c\xcd\x43\x66\x74\xcf\x11\xac\x6d\x85\x7b\x58\x40\x1c\x8e\x00\x52\x76\x8b\x67\x44\x50\xd7\x77\xca\xca\x24\x34\x3b\xc6\x13\xbd\xd3\xd7\x75\xf5\xa5\xb3\xb5\xa6\xf6\xe5\xaa\x0b\xcb\x04\x82\x5b\x6e\x9c\xc8\x56\x37\x4d\x29\xbf\xb9\x02\xef\x10\x62\xcb\x35\x9c\x57\xfa\xd3\x8b\x9f\x78\x55\xfe\x8e\x59\x8d\xae\x2a\xb4\x17\xc7\xb2\xef\xa9\xbe\x0f\xc9\x54\x7a\xac\xae\xdd\x2b\xda\x24\x37\xf1\x6c\x06\x66\x67\x4f\x44\xfd\xb0\x69\x35\x0b\xf9\x98\xee\x39\xcf\x52\xf7\xd2\x99\x5c\x41\x43\x58\x87\xa3\x37\x6e\xab\xad\x90\x4b\x65\x4b\x34\x79\xa6\xe4\x2c\xcf\xe2\x6f\x21\x3f\x02\x00\x00\xff\xff\x93\x4d\xba\xa6\x1d\x03\x00\x00")
func recover_tpl_bytes() ([]byte, error) {
return bindata_read(
@ -196,7 +196,7 @@ func recover_tpl() (*asset, error) {
return nil, err
}
info := bindata_file_info{name: "recover.tpl", size: 1623, mode: os.FileMode(438), modTime: time.Unix(1424567032, 0)}
info := bindata_file_info{name: "recover.tpl", size: 797, mode: os.FileMode(438), modTime: time.Unix(1424735212, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
@ -216,7 +216,7 @@ func register_html_tpl() (*asset, error) {
return nil, err
}
info := bindata_file_info{name: "register.html.tpl", size: 907, mode: os.FileMode(438), modTime: time.Unix(1424683974, 0)}
info := bindata_file_info{name: "register.html.tpl", size: 907, mode: os.FileMode(438), modTime: time.Unix(1424722297, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}

@ -1,10 +1,10 @@
{{.flash_success}}
<form action="/login" method="POST">
{{if .error}}{{.error}}<br />{{end}}
<input type="text" class="form-control" name="username" placeholder="Username" value="{{.username}}"><br />
<input type="text" class="form-control" name="{{.primaryID}}" placeholder="{{title .primaryID}}" value="{{.username}}"><br />
<input type="password" class="form-control" name="password" placeholder="Password"><br />
<input type="hidden" name="{{.xsrfName}}" value="{{.xsrfToken}}" />
{{if .showRemember}}<input type="checkbox" name="rm" value="true"> Remember Me{{end}}
<br />
<button type="submit">Login</button><br />
{{if .showRecover}}<a href="/recover">Recover Account</a>{{end}}
</form>

@ -1,26 +1,10 @@
<form action="/recover/complete" method="POST">
<input type="hidden" name="token" value="{{.Token}}" />
{{$passwordErrs := .ErrMap.password}}
<div class="form-group{{if $passwordErrs}} has-error{{end}}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock"></i></span>
<input class="form-control" type="text" name="password" placeholder="Password" required />
</div>
{{range $err := $passwordErrs}}
<span class="help-block">{{print $err}}</span>
{{end}}
</div>
{{$confirmPasswordErrs := .ErrMap.confirmPassword}}
<div class="form-group{{if $confirmPasswordErrs}} has-error{{end}}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock"></i></span>
<input class="form-control" type="text" name="confirmPassword" placeholder="Confirm Password" required />
</div>
{{range $err := $confirmPasswordErrs}}
<span class="help-block">{{print $err}}</span>
{{end}}
</div>
<button class="btn btn-primary btn-block" type="submit">Submit</button>
<input type="hidden" name="{{.XSRFName}}" value="{{.XSRFToken}}" />
<input type="hidden" name="token" value="{{.token}}" />
<input type="password" name="password" placeholder="Password" value="{{.password}}" /><br />
{{with .errs}}{{with $errlist := index . "password"}}{{range $errlist}}<span>{{.}}</span><br />{{end}}{{end}}{{end}}
<input type="password" name="confirm_password" placeholder="Confirm Password" value="{{.confirmPassword}}" /><br />
{{with .errs}}{{with $errlist := index . "confirm_password"}}{{range $errlist}}<span>{{.}}</span><br />{{end}}{{end}}{{end}}
<input type="hidden" name="{{.xsrfName}}" value="{{.xsrfToken}}" />
<button type="submit">Recover</button><br />
<a href="/login">Cancel</a>
</form>

@ -1 +1 @@
<strong>{{.Link}}</strong>
<strong>{{.}}</strong>

@ -1 +1 @@
{{.Link}}
{{.}}

@ -1,35 +1,10 @@
<form action="/recover" method="POST">
<input type="text" name="username" placeholder="Username" value="{{.username}}" /><br />
<input type="text" name="confirmUsername" placeholder="Confirm Username" value="{{.confirmUsername}}" /><br />
<button type="submit">Recover</button>
{{with .flashError}}{{.}}<br />{{end}}
<input type="text" name="{{.primaryID}}" placeholder="{{title .primaryID}}" value="{{.primaryIDValue}}" /><br />
{{$pid := .primaryID}}{{with .errs}}{{with $errlist := index . $pid}}{{range $errlist}}<span>{{.}}</span><br />{{end}}{{end}}{{end}}
<input type="text" name="confirm_{{.primaryID}}" placeholder="Confirm {{title .primaryID}}" value="{{.confirmPrimaryIDValue}}" /><br />
{{$cpid := .primaryID | printf "confirm_%s"}}{{with .errs}}{{with $errlist := index . $cpid}}{{range $errlist}}<span>{{.}}</span><br />{{end}}{{end}}{{end}}
<input type="hidden" name="{{.xsrfName}}" value="{{.xsrfToken}}" />
<button type="submit">Recover</button><br />
<a href="/login">Cancel</a>
</form>
<!-- <form action="/recover" method="POST">
{{$usernameErrs := .ErrMap.username}}
<div class="form-group{{if $usernameErrs}} has-error{{end}}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-user"></i></span>
<input class="form-control" type="text" name="username" placeholder="Username" value="{{.Username}}" />
</div>
{{range $err := $usernameErrs}}
<span class="help-block">{{print $err}}</span>
{{end}}
</div>
{{$confirmUsernameErrs := .ErrMap.confirmUsername}}
<div class="form-group{{if $confirmUsernameErrs}} has-error{{end}}">
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-user"></i></span>
<input class="form-control" type="text" name="confirmUsername" placeholder="Confirm Username" value="{{.ConfirmUsername}}" />
</div>
{{range $err := $confirmUsernameErrs}}
<span class="help-block">{{print $err}}</span>
{{end}}
</div>
<button class="btn btn-primary btn-block" type="submit">Recover</button>
<a class="btn btn-link btn-block" type="submit" href="/login">Cancel</a>
<input type="hidden" name="{{.XSRFName}}" value="{{.XSRFToken}}" />
</form> -->
</form>

@ -1,143 +0,0 @@
package recover
import (
"crypto/md5"
"encoding/base64"
"errors"
"fmt"
"net/http"
"time"
"golang.org/x/crypto/bcrypt"
"gopkg.in/authboss.v0"
"gopkg.in/authboss.v0/internal/flashutil"
)
var errRecoveryTokenExpired = errors.New("recovery token expired")
type pageRecoverComplete struct {
Token, Password, ConfirmPassword string
ErrMap map[string][]string
FlashSuccess, FlashError string
XSRFName, XSRFToken string
}
func (m *RecoverModule) recoverCompleteHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) {
switch r.Method {
case methodGET:
_, err := verifyToken(ctx, authboss.Cfg.Storer.(authboss.RecoverStorer))
if err != nil {
if err.Error() == errRecoveryTokenExpired.Error() {
fmt.Fprintln(authboss.Cfg.LogWriter, "recover [token expired]:", err)
ctx.SessionStorer.Put(authboss.FlashErrorKey, authboss.Cfg.RecoverTokenExpiredFlash)
http.Redirect(w, r, "/recover", http.StatusFound)
return
} else {
fmt.Fprintln(authboss.Cfg.LogWriter, "recover:", err)
http.Redirect(w, r, "/", http.StatusFound)
return
}
}
token, _ := ctx.FirstFormValue("token")
page := pageRecoverComplete{
Token: token,
FlashError: flashutil.Pull(ctx.SessionStorer, authboss.FlashErrorKey),
XSRFName: authboss.Cfg.XSRFName,
XSRFToken: authboss.Cfg.XSRFMaker(w, r),
}
m.execTpl(tplRecoverComplete, w, page)
case methodPOST:
errPage := m.recoverComplete(ctx, authboss.Cfg.XSRFName, authboss.Cfg.XSRFMaker(w, r))
if errPage != nil {
m.execTpl(tplRecoverComplete, w, *errPage)
return
}
http.Redirect(w, r, authboss.Cfg.AuthLoginSuccessRoute, http.StatusFound)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
// verifyToken expects a base64.URLEncoded token.
func verifyToken(ctx *authboss.Context, storer authboss.RecoverStorer) (attrs authboss.Attributes, err error) {
token, ok := ctx.FirstFormValue("token")
if !ok {
return nil, errors.New("missing form value: token")
}
decoded, err := base64.URLEncoding.DecodeString(token)
if err != nil {
return nil, err
}
sum := md5.Sum(decoded)
userInter, err := storer.RecoverUser(base64.StdEncoding.EncodeToString(sum[:]))
if err != nil {
return nil, err
}
attrs = authboss.Unbind(userInter)
expiry, ok := attrs.DateTime(attrRecoverTokenExpiry)
if !ok || time.Now().After(expiry) {
return nil, errRecoveryTokenExpired
}
return attrs, nil
}
func (m *RecoverModule) recoverComplete(ctx *authboss.Context, xsrfName, xsrfToken string) (errPage *pageRecoverComplete) {
token, _ := ctx.FirstFormValue("token")
password, _ := ctx.FirstPostFormValue("password")
confirmPassword, _ := ctx.FirstPostFormValue("confirmPassword")
defaultErrPage := &pageRecoverComplete{
Token: token,
Password: password,
ConfirmPassword: confirmPassword,
FlashError: authboss.Cfg.RecoverFailedErrorFlash,
XSRFName: xsrfName,
XSRFToken: xsrfToken,
}
var err error
ctx.User, err = verifyToken(ctx, authboss.Cfg.Storer.(authboss.RecoverStorer))
if err != nil {
fmt.Fprintf(authboss.Cfg.LogWriter, errFormat, "failed to verify token", err)
return defaultErrPage
}
policies := authboss.FilterValidators(authboss.Cfg.Policies, "password")
if validationErrs := ctx.Validate(policies, authboss.Cfg.ConfirmFields...); len(validationErrs) > 0 {
fmt.Fprintf(authboss.Cfg.LogWriter, errFormat, "validation failed", validationErrs)
return &pageRecoverComplete{
Token: token,
Password: password,
ConfirmPassword: confirmPassword,
ErrMap: validationErrs.Map(),
XSRFName: xsrfName,
XSRFToken: xsrfToken,
}
}
encryptedPassword, err := bcrypt.GenerateFromPassword([]byte(password), authboss.Cfg.BCryptCost)
if err != nil {
fmt.Fprintf(authboss.Cfg.LogWriter, errFormat, "failed to encrypt password", err)
return defaultErrPage
}
ctx.User[attrPassword] = string(encryptedPassword)
ctx.User[attrRecoverToken] = ""
var nullTime time.Time
ctx.User[attrRecoverTokenExpiry] = nullTime
username, _ := ctx.User.String(attrUsername)
if err := ctx.SaveUser(); err != nil {
fmt.Fprintf(authboss.Cfg.LogWriter, errFormat, "failed to save user", err)
return defaultErrPage
}
ctx.SessionStorer.Put(authboss.SessionKey, username)
return nil
}

@ -1,370 +0,0 @@
package recover
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"testing"
"time"
"golang.org/x/crypto/bcrypt"
"gopkg.in/authboss.v0"
"gopkg.in/authboss.v0/internal/mocks"
)
const (
testUrlBase64Token = "MTIzNA=="
testStdBase64Token = "gdyb21LQTcIANtvYMT7QVQ=="
)
func Test_recoverCompleteHandlerFunc_GET_TokenExpired(t *testing.T) {
m, logger := testValidRecoverModule()
storer, ok := authboss.Cfg.Storer.(*mocks.MockStorer)
if !ok {
panic("Failed to get storer")
}
storer.Users["b"] = authboss.Attributes{
attrUsername: "b",
attrRecoverToken: testStdBase64Token,
attrRecoverTokenExpiry: time.Now().Add(time.Duration(-24) * time.Hour),
}
w, r, ctx := testHttpRequest("GET", fmt.Sprintf("/recover/complete?token=%s", testUrlBase64Token), nil)
clientStorer := mocks.NewMockClientStorer()
ctx.SessionStorer = clientStorer
m.recoverCompleteHandlerFunc(ctx, w, r)
actualLog, err := ioutil.ReadAll(logger)
if err != nil {
panic(err)
}
if !bytes.Contains(actualLog, []byte("recover [token expired]:")) {
t.Error("Expected logs to start with:", "recover [token expired]:")
}
if flash := clientStorer.Values[authboss.FlashErrorKey]; flash != authboss.Cfg.RecoverTokenExpiredFlash {
t.Error("Unexpected error flash:", flash)
}
if location := w.Header().Get("Location"); location != "/recover" {
t.Error("Unexpected location:", location)
}
}
func Test_recoverCompleteHandlerFunc_GET_OtherErrors(t *testing.T) {
m, logger := testValidRecoverModule()
w, r, ctx := testHttpRequest("GET", "/recover/complete?token=asdf", nil)
m.recoverCompleteHandlerFunc(ctx, w, r)
actualLog, err := ioutil.ReadAll(logger)
if err != nil {
panic(err)
}
if !bytes.Contains(actualLog, []byte("recover:")) {
t.Error("Expected logs to start with:", "recover:")
}
if location := w.Header().Get("Location"); location != "/" {
t.Error("Unexpected location:", location)
}
}
func Test_recoverCompleteHandlerFunc_GET(t *testing.T) {
m, _ := testValidRecoverModule()
storer, ok := authboss.Cfg.Storer.(*mocks.MockStorer)
if !ok {
panic("Failed to get storer")
}
storer.Users["b"] = authboss.Attributes{
attrUsername: "b",
attrRecoverToken: testStdBase64Token,
attrRecoverTokenExpiry: time.Now().Add(time.Duration(24) * time.Hour),
}
w, r, ctx := testHttpRequest("GET", fmt.Sprintf("/recover/complete?token=%s", testUrlBase64Token), nil)
sessionStorer := mocks.NewMockClientStorer()
sessionStorer.Values = map[string]string{authboss.FlashErrorKey: "asdf"}
ctx.SessionStorer = sessionStorer
m.recoverCompleteHandlerFunc(ctx, w, r)
page := pageRecoverComplete{
Token: testUrlBase64Token,
FlashError: "asdf",
XSRFName: authboss.Cfg.XSRFName,
XSRFToken: authboss.Cfg.XSRFMaker(nil, nil),
}
expectedBody := &bytes.Buffer{}
if err := m.templates[tplRecoverComplete].Execute(expectedBody, page); err != nil {
panic(err)
}
if w.Code != http.StatusOK {
t.Error("Unexpected code:", w.Code)
}
if !bytes.Equal(expectedBody.Bytes(), w.Body.Bytes()) {
t.Error("Unexpected body:", w.Body.String())
}
}
func Test_recoverCompleteHandlerFunc_POST_RecoveryCompleteFailed(t *testing.T) {
m, _ := testValidRecoverModule()
w, r, ctx := testHttpRequest(
"POST",
fmt.Sprintf("/recover/complete?token=%s", testUrlBase64Token),
url.Values{"password": []string{"a"}, "confirmPassword": []string{"a"}},
)
tpl := m.templates[tplRecoverComplete]
expectedBody := &bytes.Buffer{}
if err := tpl.Execute(expectedBody, pageRecoverComplete{
Token: testUrlBase64Token,
Password: "a",
ConfirmPassword: "a",
FlashError: authboss.Cfg.RecoverFailedErrorFlash,
XSRFName: authboss.Cfg.XSRFName,
XSRFToken: authboss.Cfg.XSRFMaker(nil, nil),
}); err != nil {
panic(err)
}
// missing storer will cause this to fail
m.recoverCompleteHandlerFunc(ctx, w, r)
if w.Code != http.StatusOK {
t.Error("Unexpected code:", w.Code)
}
if !bytes.Equal(expectedBody.Bytes(), w.Body.Bytes()) {
t.Error("Unexpected body:", w.Body.String())
}
}
func Test_recoverCompleteHandlerFunc_POST(t *testing.T) {
m, _ := testValidRecoverModule()
w, r, ctx := testHttpRequest(
"POST",
fmt.Sprintf("/recover/complete?token=%s", testUrlBase64Token),
url.Values{"password": []string{"a"}, "confirmPassword": []string{"a"}},
)
storer, ok := authboss.Cfg.Storer.(*mocks.MockStorer)
if !ok {
panic("Failed to get storer")
}
storer.Users["a"] = authboss.Attributes{
attrUsername: "a",
attrRecoverToken: testStdBase64Token,
attrRecoverTokenExpiry: time.Now().Add(time.Duration(24) * time.Hour),
}
m.recoverCompleteHandlerFunc(ctx, w, r)
if w.Code != http.StatusFound {
t.Error("Unexpected code:", w.Code)
}
location := w.Header().Get("Location")
if location != "/login" {
t.Error("Unexpected redirect:", location)
}
}
func Test_verifyToken_MissingToken(t *testing.T) {
authboss.NewConfig()
ctx := mocks.MockRequestContext()
_, err := verifyToken(ctx, nil)
if err.Error() != "missing form value: token" {
t.Error("Unexpected error:", err)
}
}
func Test_verifyToken_InvalidToken(t *testing.T) {
authboss.NewConfig()
testValidTestConfig()
storer, ok := authboss.Cfg.Storer.(*mocks.MockStorer)
if !ok {
panic("Failed to get storer")
}
storer.Users["a"] = authboss.Attributes{
attrRecoverToken: testStdBase64Token,
}
ctx := mocks.MockRequestContext("token", "asdf")
_, err := verifyToken(ctx, storer)
if err.Error() != authboss.ErrUserNotFound.Error() {
t.Error("Unexpected error:", err)
}
}
func Test_verifyToken_ExpiredToken(t *testing.T) {
testValidTestConfig()
storer, ok := authboss.Cfg.Storer.(*mocks.MockStorer)
if !ok {
panic("Failed to get storer")
}
storer.Users["a"] = authboss.Attributes{
attrRecoverToken: testStdBase64Token,
attrRecoverTokenExpiry: time.Now().Add(time.Duration(-24) * time.Hour),
}
ctx := mocks.MockRequestContext("token", testUrlBase64Token)
_, err := verifyToken(ctx, storer)
if err.Error() != "recovery token expired" {
t.Error("Unexpected error:", err)
}
}
func Test_verifyToken(t *testing.T) {
testValidTestConfig()
storer, ok := authboss.Cfg.Storer.(*mocks.MockStorer)
if !ok {
panic("Failed to get storer")
}
storer.Users["a"] = authboss.Attributes{
attrRecoverToken: testStdBase64Token,
attrRecoverTokenExpiry: time.Now().Add(time.Duration(24) * time.Hour),
}
ctx := mocks.MockRequestContext("token", testUrlBase64Token)
attrs, err := verifyToken(ctx, storer)
if err != nil {
t.Error("Unexpected error:", err)
}
if attrs == nil {
t.Error("Unexpected nil attrs")
}
}
func Test_recoverComplete_TokenVerificationFails(t *testing.T) {
m, logger := testValidRecoverModule()
ctx := mocks.MockRequestContext()
errPage := m.recoverComplete(ctx, "", "")
if errPage == nil {
t.Error("Expected err page")
}
if !reflect.DeepEqual(*errPage, pageRecoverComplete{FlashError: authboss.Cfg.RecoverFailedErrorFlash}) {
t.Error("Unexpected err page:", errPage)
}
actualLog, err := ioutil.ReadAll(logger)
if err != nil {
panic(err)
}
if !bytes.Contains(actualLog, []byte("recover [failed to verify token]:")) {
t.Error("Expected logs to start with:", "recover [failed to verify token]:")
}
}
func Test_recoverComplete_ValidationFails(t *testing.T) {
m, logger := testValidRecoverModule()
ctx := mocks.MockRequestContext("token", testUrlBase64Token, "password", "a", "confirmPassword", "b")
storer, ok := authboss.Cfg.Storer.(*mocks.MockStorer)
if !ok {
panic("Failed to get storer")
}
storer.Users["b"] = authboss.Attributes{
attrRecoverToken: testStdBase64Token,
attrRecoverTokenExpiry: time.Now().Add(time.Duration(24) * time.Hour),
}
errPage := m.recoverComplete(ctx, "", "")
if errPage == nil {
t.Error("Expected err page")
}
expectedErrPage := pageRecoverComplete{
Token: testUrlBase64Token,
Password: "a",
ConfirmPassword: "b",
ErrMap: map[string][]string{"confirmPassword": []string{"Does not match password"}},
}
if !reflect.DeepEqual(*errPage, expectedErrPage) {
t.Error("Unexpected err page:", errPage)
}
actualLog, err := ioutil.ReadAll(logger)
if err != nil {
panic(err)
}
expetedLog := []byte("recover [validation failed]: confirmPassword: Does not match password\n")
if !bytes.Equal(actualLog, expetedLog) {
t.Error("Expected logs:", string(expetedLog))
}
}
func Test_recoverComplete(t *testing.T) {
m, _ := testValidRecoverModule()
ctx := mocks.MockRequestContext("token", testUrlBase64Token, "password", "a", "confirmPassword", "a")
clientStorer := mocks.NewMockClientStorer()
ctx.SessionStorer = clientStorer
storer, ok := authboss.Cfg.Storer.(*mocks.MockStorer)
if !ok {
panic("Failed to get storer")
}
storer.Users["b"] = authboss.Attributes{
attrUsername: "b",
attrRecoverToken: testStdBase64Token,
attrRecoverTokenExpiry: time.Now().Add(time.Duration(24) * time.Hour),
}
if errPage := m.recoverComplete(ctx, "", ""); errPage != nil {
t.Error("Expected nil err page")
}
password, ok := storer.Users["b"].String(attrPassword)
if !ok {
panic("cannot find password")
}
if err := bcrypt.CompareHashAndPassword([]byte(password), []byte("a")); err != nil {
t.Error("Unexpected encrypted password")
}
recoverToken, ok := storer.Users["b"].String(attrRecoverToken)
if !ok {
panic("cannot find token")
}
if recoverToken != "" {
t.Error("Unexpected recover token:", recoverToken)
}
recoverTokenExpiry, ok := storer.Users["b"].DateTime(attrRecoverTokenExpiry)
if !ok {
panic("cannot find token")
}
var nullTime time.Time
if recoverTokenExpiry != nullTime {
t.Error("Unexpected recover token expiry:", recoverTokenExpiry)
}
}
func Test_recoverCompleteHandlerFunc_OtherMethods(t *testing.T) {
m, _ := testValidRecoverModule()
for i, method := range []string{"HEAD", "PUT", "DELETE", "TRACE", "CONNECT"} {
w, r, _ := testHttpRequest(method, "/recover/complete", nil)
m.recoverHandlerFunc(nil, w, r)
if http.StatusMethodNotAllowed != w.Code {
t.Errorf("%d> Expected status code %d, got %d", i, http.StatusMethodNotAllowed, w.Code)
continue
}
}
}

@ -1,141 +0,0 @@
package recover
import (
"crypto/md5"
"crypto/rand"
"encoding/base64"
"fmt"
"net/http"
"time"
"gopkg.in/authboss.v0"
"gopkg.in/authboss.v0/internal/flashutil"
)
type pageRecover struct {
Username, ConfirmUsername string
ErrMap map[string][]string
FlashSuccess, FlashError string
XSRFName, XSRFToken string
}
func (m *RecoverModule) recoverHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) {
switch r.Method {
case methodGET:
page := pageRecover{
FlashError: flashutil.Pull(ctx.SessionStorer, authboss.FlashErrorKey),
XSRFName: authboss.Cfg.XSRFName,
XSRFToken: authboss.Cfg.XSRFMaker(w, r),
}
m.execTpl(tplRecover, w, page)
case methodPOST:
errPage, _ := m.recover(ctx, authboss.Cfg.XSRFName, authboss.Cfg.XSRFMaker(w, r))
if errPage != nil {
m.execTpl(tplRecover, w, *errPage)
return
}
ctx.SessionStorer.Put(authboss.FlashSuccessKey, authboss.Cfg.RecoverInitiateSuccessFlash)
http.Redirect(w, r, authboss.Cfg.RecoverRedirect, http.StatusFound)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func (m *RecoverModule) recover(ctx *authboss.Context, xsrfName, xsrfToken string) (errPage *pageRecover, emailSent <-chan struct{}) {
username, _ := ctx.FirstPostFormValue("username")
confirmUsername, _ := ctx.FirstPostFormValue("confirmUsername")
policies := authboss.FilterValidators(authboss.Cfg.Policies, "username")
if validationErrs := ctx.Validate(policies, authboss.Cfg.ConfirmFields...); len(validationErrs) > 0 {
fmt.Fprintf(authboss.Cfg.LogWriter, errFormat, "validation failed", validationErrs)
return &pageRecover{
Username: username,
ConfirmUsername: confirmUsername,
ErrMap: validationErrs.Map(),
XSRFName: xsrfName,
XSRFToken: xsrfToken,
}, nil
}
err, emailSent := m.makeAndSendToken(ctx, username)
if err != nil {
fmt.Fprintf(authboss.Cfg.LogWriter, errFormat, "failed to recover", err)
return &pageRecover{
Username: username,
ConfirmUsername: confirmUsername,
FlashError: authboss.Cfg.RecoverFailedErrorFlash,
XSRFName: xsrfName,
XSRFToken: xsrfToken,
}, nil
}
return nil, emailSent
}
func (m *RecoverModule) makeAndSendToken(ctx *authboss.Context, username string) (err error, emailSent <-chan struct{}) {
if err = ctx.LoadUser(username); err != nil {
return err, nil
}
email, ok := ctx.User.String(attrEmail)
if !ok || email == "" {
return fmt.Errorf("email required: %v", attrEmail), nil
}
token := make([]byte, 32)
if _, err = rand.Read(token); err != nil {
return err, nil
}
sum := md5.Sum(token)
ctx.User[attrRecoverToken] = base64.StdEncoding.EncodeToString(sum[:])
ctx.User[attrRecoverTokenExpiry] = time.Now().Add(authboss.Cfg.RecoverTokenDuration)
if err = ctx.SaveUser(); err != nil {
return err, nil
}
return nil, m.sendRecoverEmail(email, token)
}
func (m *RecoverModule) sendRecoverEmail(to string, token []byte) <-chan struct{} {
emailSent := make(chan struct{}, 1)
go func() {
data := struct{ Link string }{fmt.Sprintf("%s/recover/complete?token=%s", authboss.Cfg.HostName, base64.URLEncoding.EncodeToString(token))}
htmlEmailBody, err := m.emailTemplates.ExecuteTemplate(tplInitHTMLEmail, data)
if err != nil {
fmt.Fprintf(authboss.Cfg.LogWriter, errFormat, "failed to build html email", err)
close(emailSent)
return
}
textEmaiLBody, err := m.emailTemplates.ExecuteTemplate(tplInitTextEmail, data)
if err != nil {
fmt.Fprintf(authboss.Cfg.LogWriter, errFormat, "failed to build plaintext email", err)
close(emailSent)
return
}
if err := authboss.Cfg.Mailer.Send(authboss.Email{
To: []string{to},
ToNames: []string{""},
From: authboss.Cfg.EmailFrom,
Subject: authboss.Cfg.EmailSubjectPrefix + "Password Reset",
TextBody: textEmaiLBody.String(),
HTMLBody: htmlEmailBody.String(),
}); err != nil {
fmt.Fprintf(authboss.Cfg.LogWriter, errFormat, "failed to send email", err)
close(emailSent)
return
}
emailSent <- struct{}{}
}()
return emailSent
}

@ -1,385 +0,0 @@
package recover
import (
"bytes"
"encoding/base64"
"fmt"
"html/template"
"io/ioutil"
"net/http"
"net/url"
"testing"
"gopkg.in/authboss.v0"
"gopkg.in/authboss.v0/internal/mocks"
)
func Test_recoverHandlerFunc_GET(t *testing.T) {
m, _ := testValidRecoverModule()
w, r, ctx := testHttpRequest("GET", "/recover", nil)
m.recoverHandlerFunc(ctx, w, r)
expectedBody := &bytes.Buffer{}
err := m.templates[tplRecover].Execute(expectedBody, pageRecover{
XSRFName: authboss.Cfg.XSRFName,
XSRFToken: authboss.Cfg.XSRFMaker(nil, nil),
})
if err != nil {
panic(err)
}
if w.Code != http.StatusOK {
t.Error("Unexpected code:", w.Code)
}
if !bytes.Equal(expectedBody.Bytes(), w.Body.Bytes()) {
t.Error("Unexpected body:", w.Body.String())
}
}
func Test_recoverHandlerFunc_POST_RecoveryFailed(t *testing.T) {
m, _ := testValidRecoverModule()
w, r, ctx := testHttpRequest("POST", "/login", url.Values{"username": []string{"a"}, "confirmUsername": []string{"a"}})
tpl := m.templates[tplRecover]
expectedBody := &bytes.Buffer{}
if err := tpl.Execute(expectedBody, pageRecover{
Username: "a",
ConfirmUsername: "a",
FlashError: authboss.Cfg.RecoverFailedErrorFlash,
XSRFName: authboss.Cfg.XSRFName,
XSRFToken: authboss.Cfg.XSRFMaker(nil, nil),
}); err != nil {
panic(err)
}
// missing storer will cause this to fail
m.recoverHandlerFunc(ctx, w, r)
if w.Code != http.StatusOK {
t.Error("Unexpected code:", w.Code)
}
if !bytes.Equal(expectedBody.Bytes(), w.Body.Bytes()) {
t.Error("Unexpected body:", w.Body.String())
}
}
func Test_recoverHandlerFunc_POST(t *testing.T) {
m, _ := testValidRecoverModule()
w, r, ctx := testHttpRequest("POST", "/login", url.Values{"username": []string{"a"}, "confirmUsername": []string{"a"}})
storer, ok := authboss.Cfg.Storer.(*mocks.MockStorer)
if !ok {
panic("Failed to get storer")
}
storer.Users["a"] = authboss.Attributes{"username": "", "password": "", "email": "a@b.c"}
m.recoverHandlerFunc(ctx, w, r)
if w.Code != http.StatusFound {
t.Error("Unexpected code:", w.Code)
}
location := w.Header().Get("Location")
if location != "/login" {
t.Error("Unexpected redirect:", location)
}
successFlash := ctx.SessionStorer.(*mocks.MockClientStorer).Values[authboss.FlashSuccessKey]
if successFlash != authboss.Cfg.RecoverInitiateSuccessFlash {
t.Error("Unexpected success flash message:", successFlash)
}
}
func Test_recover_UsernameValidationFail(t *testing.T) {
m, logger := testValidRecoverModule()
ctx := mocks.MockRequestContext()
page, emailSent := m.recover(ctx, "", "")
if len(page.ErrMap["username"]) != 1 {
t.Error("Exepted single validation error for username")
}
if page.ErrMap["username"][0] != "Cannot be blank" {
t.Error("Unexpected validation error for username:", page.ErrMap["username"][0])
}
expectedLog := []byte("recover [validation failed]: username: Cannot be blank\n")
actualLog, err := ioutil.ReadAll(logger)
if err != nil {
panic(err)
}
if !bytes.Equal(expectedLog, actualLog) {
t.Errorf("Unexpected logs: %q", string(actualLog))
}
if emailSent != nil {
t.Error("Unexpected sent email")
}
}
func Test_recover_ConfirmUsernameCheckFail(t *testing.T) {
m, logger := testValidRecoverModule()
ctx := mocks.MockRequestContext("username", "a", "confirmUsername", "b")
page, emailSent := m.recover(ctx, "", "")
if len(page.ErrMap["username"]) != 0 {
t.Error("Exepted no validation errors for username")
}
if len(page.ErrMap["confirmUsername"]) != 1 {
t.Error("Exepted single validation error for confirmUsername")
}
if page.ErrMap["confirmUsername"][0] != "Does not match username" {
t.Error("Unexpected validation error for confirmUsername:", page.ErrMap["confirmUsername"][0])
}
expectedLog := []byte("recover [validation failed]: confirmUsername: Does not match username\n")
actualLog, err := ioutil.ReadAll(logger)
if err != nil {
panic(err)
}
if !bytes.Equal(expectedLog, actualLog) {
t.Error("Unexpected logs:", string(actualLog))
}
if emailSent != nil {
t.Error("Unexpected sent email")
}
}
func Test_recover_InvalidUser(t *testing.T) {
m, logger := testValidRecoverModule()
ctx := mocks.MockRequestContext("username", "a", "confirmUsername", "a")
page, emailSent := m.recover(ctx, "", "")
if page.ErrMap != nil {
t.Error("Exepted no validation errors")
}
if page.FlashError != authboss.Cfg.RecoverFailedErrorFlash {
t.Error("Expected flash error:", authboss.Cfg.RecoverFailedErrorFlash)
}
actualLog, err := ioutil.ReadAll(logger)
if err != nil {
panic(err)
}
if !bytes.Contains(actualLog, []byte("recover [failed to recover]:")) {
t.Error("Expected log message starting with:", "recover [failed to recover]:")
}
if emailSent != nil {
t.Error("Unexpected sent email")
}
}
func Test_recover(t *testing.T) {
m, _ := testValidRecoverModule()
storer, ok := authboss.Cfg.Storer.(*mocks.MockStorer)
if !ok {
panic("Failed to get storer")
}
storer.Users["a"] = authboss.Attributes{"username": "", "password": "", "email": "a@b.c"}
ctx := mocks.MockRequestContext("username", "a", "confirmUsername", "a")
page, emailSent := m.recover(ctx, "", "")
if page != nil {
t.Error("Expected nil page")
}
if emailSent == nil {
t.Error("Expected sent email")
}
}
func Test_makeAndSendToken_MissingStorer(t *testing.T) {
m, _ := testValidRecoverModule()
ctx := mocks.MockRequestContext()
err, ch := m.makeAndSendToken(ctx, "a")
if err == nil || err.Error() != authboss.ErrUserNotFound.Error() {
t.Error("Expected error:", authboss.ErrUserNotFound)
}
if ch != nil {
t.Error("Expected nil channel")
}
}
func Test_makeAndSendToken_CheckEmail(t *testing.T) {
m, _ := testValidRecoverModule()
ctx := mocks.MockRequestContext()
storer, ok := authboss.Cfg.Storer.(*mocks.MockStorer)
if !ok {
panic("Failed to get storer")
}
storer.Users["a"] = authboss.Attributes{"username": "", "password": ""}
// missing
err, ch := m.makeAndSendToken(ctx, "a")
expectedErr := fmt.Sprintf("email required: %v", attrEmail)
if err == nil || err.Error() != expectedErr {
t.Error("Expected error:", expectedErr)
}
if ch != nil {
t.Error("Expected nil channel")
}
// empty
storer.Users["a"] = authboss.Attributes{"username": "", "password": "", "email": ""}
err, ch = m.makeAndSendToken(ctx, "a")
if err == nil || err.Error() != expectedErr {
t.Error("Expected error:", expectedErr)
}
if ch != nil {
t.Error("Expected nil channel")
}
}
func Test_makeAndSendToken(t *testing.T) {
m, _ := testValidRecoverModule()
ctx := mocks.MockRequestContext()
storer, ok := authboss.Cfg.Storer.(*mocks.MockStorer)
if !ok {
panic("Failed to get storer")
}
storer.Users["a"] = authboss.Attributes{"username": "a", "password": "b", "email": "a@b.c"}
err, ch := m.makeAndSendToken(ctx, "a")
_, ok = storer.Users["a"][attrRecoverToken]
if !ok {
t.Error("Expected recover token")
}
_, ok = storer.Users["a"][attrRecoverTokenExpiry]
if !ok {
t.Error("Expected recover token expiry")
}
if err != nil {
t.Error("Unexpected error:", err)
}
if ch == nil {
t.Error("Unexpected nil channel")
}
}
func Test_sendRecoverEmail_InvalidTemplates(t *testing.T) {
m, logger := testValidRecoverModule()
failTpl, err := template.New("").Parse("{{.Fail}}")
if err != nil {
panic("Failed to build tpl")
}
// broken html template
originalHtmlEmail := m.emailTemplates[tplInitHTMLEmail]
m.emailTemplates[tplInitHTMLEmail] = failTpl
<-m.sendRecoverEmail("a@b.c", []byte("abc123"))
actualLog, err := ioutil.ReadAll(logger)
if err != nil {
panic(err)
}
if !bytes.Contains(actualLog, []byte("recover [failed to build html email]:")) {
t.Error("Expected log message starting with:", "recover [failed to build html email]:")
}
// broken plain text template
m.emailTemplates[tplInitHTMLEmail] = originalHtmlEmail
m.emailTemplates[tplInitTextEmail] = failTpl
<-m.sendRecoverEmail("a@b.c", []byte("abc123"))
actualLog, err = ioutil.ReadAll(logger)
if err != nil {
panic(err)
}
if !bytes.Contains(actualLog, []byte("recover [failed to build plaintext email]:")) {
t.Error("Expected log message starting with:", "recover [failed to build plaintext email]:")
}
}
func Test_sendRecoverEmail_FailToSend(t *testing.T) {
m, logger := testValidRecoverModule()
mailer := mocks.NewMockMailer()
mailer.SendErr = "explode"
authboss.Cfg.Mailer = mailer
<-m.sendRecoverEmail("a@b.c", []byte("abc123"))
actualLog, err := ioutil.ReadAll(logger)
if err != nil {
panic(err)
}
if !bytes.Contains(actualLog, []byte("recover [failed to send email]:")) {
t.Error("Expecte log message starting with:", "recover [failed to send email]:")
}
}
func Test_sendRecoverEmail(t *testing.T) {
m, _ := testValidRecoverModule()
<-m.sendRecoverEmail("a@b.c", []byte("abc123"))
mailer, ok := authboss.Cfg.Mailer.(*mocks.MockMailer)
if !ok {
panic("Failed to assert mock mailer")
}
sent := mailer.Last
if len(sent.To) != 1 {
t.Error("Expected one to email")
}
if sent.To[0] != "a@b.c" {
t.Error("Unexpected to email:", sent.To[0])
}
if sent.From != authboss.Cfg.EmailFrom {
t.Error("Unexpected from email:", sent.From)
}
if sent.Subject != "Password Reset" {
t.Error("Unexpected subject:", sent.Subject)
}
data := struct {
Link string
}{
fmt.Sprintf("%s/recover/complete?token=%s",
authboss.Cfg.HostName,
base64.URLEncoding.EncodeToString([]byte("abc123")),
),
}
html, err := m.emailTemplates.ExecuteTemplate(tplInitHTMLEmail, data)
if err != nil {
panic(err)
}
test, err := m.emailTemplates.ExecuteTemplate(tplInitTextEmail, data)
if err != nil {
panic(err)
}
if !bytes.Equal(html.Bytes(), []byte(sent.HTMLBody)) {
t.Error("Unexpected html body:", sent.HTMLBody)
}
if !bytes.Equal(test.Bytes(), []byte(sent.TextBody)) {
t.Error("Unexpected text body:", sent.TextBody)
}
}
func Test_recoverHandlerFunc_OtherMethods(t *testing.T) {
m, _ := testValidRecoverModule()
for i, method := range []string{"HEAD", "PUT", "DELETE", "TRACE", "CONNECT"} {
w, r, _ := testHttpRequest(method, "/recover", nil)
m.recoverHandlerFunc(nil, w, r)
if http.StatusMethodNotAllowed != w.Code {
t.Errorf("%d> Expected status code %d, got %d", i, http.StatusMethodNotAllowed, w.Code)
continue
}
}
}

@ -1,13 +1,17 @@
package recover
import (
"crypto/md5"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"time"
"golang.org/x/crypto/bcrypt"
"gopkg.in/authboss.v0"
"gopkg.in/authboss.v0/internal/views"
"gopkg.in/authboss.v0/internal/render"
)
const (
@ -20,26 +24,26 @@ const (
tplInitHTMLEmail = "recover-html.email"
tplInitTextEmail = "recover-text.email"
attrUsername = "username"
attrRecoverToken = "recover_token"
attrRecoverTokenExpiry = "recover_token_expiry"
attrEmail = "email"
attrPassword = "password"
errFormat = "recover [%s]: %s\n"
storeUsername = "username"
storeRecoverToken = "recover_token"
storeRecoverTokenExpiry = "recover_token_expiry"
storeEmail = "email"
storePassword = "password"
)
var errRecoveryTokenExpired = errors.New("recovery token expired")
func init() {
m := &RecoverModule{}
m := &Recover{}
authboss.RegisterModule("recover", m)
}
type RecoverModule struct {
templates views.Templates
emailTemplates views.Templates
type Recover struct {
templates render.Templates
emailTemplates render.Templates
}
func (m *RecoverModule) Initialize() (err error) {
func (r *Recover) Initialize() (err error) {
if authboss.Cfg.Storer == nil {
return errors.New("recover: Need a RecoverStorer.")
}
@ -48,49 +52,217 @@ func (m *RecoverModule) Initialize() (err error) {
return errors.New("recover: RecoverStorer required for recover functionality.")
}
if authboss.Cfg.Layout == nil {
return errors.New("recover: Layout required for Recover functionallity.")
}
if m.templates, err = views.Get(authboss.Cfg.Layout, authboss.Cfg.ViewsPath, tplRecover, tplRecoverComplete); err != nil {
r.templates, err = render.LoadTemplates(authboss.Cfg.Layout, authboss.Cfg.ViewsPath, tplRecover, tplRecoverComplete)
if err != nil {
return err
}
if authboss.Cfg.LayoutEmail == nil {
return errors.New("recover: LayoutEmail required for Recover functionallity.")
}
if m.emailTemplates, err = views.Get(authboss.Cfg.LayoutEmail, authboss.Cfg.ViewsPath, tplInitHTMLEmail, tplInitTextEmail); err != nil {
r.emailTemplates, err = render.LoadTemplates(authboss.Cfg.LayoutEmail, authboss.Cfg.ViewsPath, tplInitHTMLEmail, tplInitTextEmail)
if err != nil {
return err
}
return nil
}
func (m *RecoverModule) Routes() authboss.RouteTable {
func (r *Recover) Routes() authboss.RouteTable {
return authboss.RouteTable{
"recover": m.recoverHandlerFunc,
"recover/complete": m.recoverCompleteHandlerFunc,
"recover": r.startHandlerFunc,
"recover/complete": r.completeHandlerFunc,
}
}
func (m *RecoverModule) Storage() authboss.StorageOptions {
func (r *Recover) Storage() authboss.StorageOptions {
return authboss.StorageOptions{
attrUsername: authboss.String,
attrRecoverToken: authboss.String,
attrEmail: authboss.String,
attrRecoverTokenExpiry: authboss.String,
attrPassword: authboss.String,
storeUsername: authboss.String,
storeRecoverToken: authboss.String,
storeEmail: authboss.String,
storeRecoverTokenExpiry: authboss.String,
storePassword: authboss.String,
}
}
func (m *RecoverModule) execTpl(tpl string, w http.ResponseWriter, data interface{}) {
buffer, err := m.templates.ExecuteTemplate(tpl, data)
func (r *Recover) startHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, req *http.Request) error {
switch req.Method {
case methodGET:
data := authboss.NewHTMLData(
"primaryID", authboss.Cfg.PrimaryID,
"primaryIDValue", "",
"confirmPrimaryIDValue", "",
)
return r.templates.Render(ctx, w, req, tplRecover, data)
case methodPOST:
primaryID, _ := ctx.FirstPostFormValue(authboss.Cfg.PrimaryID)
confirmPrimaryID, _ := ctx.FirstPostFormValue(fmt.Sprintf("confirm_%s", authboss.Cfg.PrimaryID))
errData := authboss.NewHTMLData(
"primaryID", authboss.Cfg.PrimaryID,
"primaryIDValue", primaryID,
"confirmPrimaryIDValue", confirmPrimaryID,
)
policies := authboss.FilterValidators(authboss.Cfg.Policies, authboss.Cfg.PrimaryID)
if validationErrs := ctx.Validate(policies, authboss.Cfg.ConfirmFields...).Map(); len(validationErrs) > 0 {
fmt.Fprintln(authboss.Cfg.LogWriter, "recover: form validation failed:", validationErrs)
errData.MergeKV("errs", validationErrs)
return r.templates.Render(ctx, w, req, tplRecover, errData)
}
if err := ctx.LoadUser(primaryID); err == authboss.ErrUserNotFound {
errData.MergeKV("flashError", authboss.Cfg.RecoverFailedErrorFlash)
return r.templates.Render(ctx, w, req, tplRecover, errData)
} else if err != nil {
return err
}
email, err := ctx.User.StringErr(storeEmail)
if err != nil {
return err
}
encodedToken, encodedChecksum, err := newToken()
if err != nil {
return err
}
ctx.User[storeRecoverToken] = encodedChecksum
ctx.User[storeRecoverTokenExpiry] = time.Now().Add(authboss.Cfg.RecoverTokenDuration)
if err := ctx.SaveUser(); err != nil {
return err
}
go goRecoverEmail(r, email, encodedToken)
ctx.SessionStorer.Put(authboss.FlashSuccessKey, authboss.Cfg.RecoverInitiateSuccessFlash)
http.Redirect(w, req, authboss.Cfg.RecoverRedirect, http.StatusFound)
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) {
url := fmt.Sprintf("%s/recover/complete?token=%s", authboss.Cfg.HostName, encodedToken)
email := authboss.Email{
To: []string{to},
From: authboss.Cfg.EmailFrom,
Subject: authboss.Cfg.EmailSubjectPrefix + "Password Reset",
}
if err := r.emailTemplates.RenderEmail(email, tplInitHTMLEmail, tplInitTextEmail, url); err != nil {
fmt.Fprintln(authboss.Cfg.LogWriter, "recover: failed to send recover email:", err)
}
}
func (r *Recover) completeHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, req *http.Request) (err error) {
switch req.Method {
case methodGET:
_, err = verifyToken(ctx)
if err == errRecoveryTokenExpired {
return authboss.ErrAndRedirect{err, "/recover", "", authboss.Cfg.RecoverTokenExpiredFlash}
} else if err != nil {
return authboss.ErrAndRedirect{err, "/", "", ""}
}
token, _ := ctx.FirstFormValue("token")
data := authboss.NewHTMLData("token", token)
return r.templates.Render(ctx, w, req, tplRecoverComplete, data)
case methodPOST:
token, err := ctx.FirstFormValueErr("token")
if err != nil {
return err
}
password, _ := ctx.FirstPostFormValue("password")
confirmPassword, _ := ctx.FirstPostFormValue("confirmPassword")
policies := authboss.FilterValidators(authboss.Cfg.Policies, "password")
if validationErrs := ctx.Validate(policies, authboss.Cfg.ConfirmFields...).Map(); len(validationErrs) > 0 {
fmt.Fprintln(authboss.Cfg.LogWriter, "recover: form validation failed:", validationErrs)
data := authboss.NewHTMLData(
"token", token,
"password", password,
"confirmPassword", confirmPassword,
"errs", validationErrs,
)
return r.templates.Render(ctx, w, req, tplRecoverComplete, data)
}
if ctx.User, err = verifyToken(ctx); err != nil {
return err
}
encryptedPassword, err := bcrypt.GenerateFromPassword([]byte(password), authboss.Cfg.BCryptCost)
if err != nil {
return err
}
ctx.User[storePassword] = string(encryptedPassword)
ctx.User[storeRecoverToken] = ""
var nullTime time.Time
ctx.User[storeRecoverTokenExpiry] = nullTime
primaryID, err := ctx.User.StringErr(authboss.Cfg.PrimaryID)
if err != nil {
return err
}
if err := ctx.SaveUser(); err != nil {
return err
}
ctx.SessionStorer.Put(authboss.SessionKey, primaryID)
http.Redirect(w, req, authboss.Cfg.AuthLoginSuccessRoute, http.StatusFound)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
return nil
}
// verifyToken expects a base64.URLEncoded token.
func verifyToken(ctx *authboss.Context) (attrs authboss.Attributes, err error) {
token, err := ctx.FirstFormValueErr("token")
if err != nil {
fmt.Fprintf(authboss.Cfg.LogWriter, errFormat, "unable to execute template", err)
w.WriteHeader(http.StatusInternalServerError)
return
return nil, err
}
if _, err := io.Copy(w, buffer); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
decoded, err := base64.URLEncoding.DecodeString(token)
if err != nil {
return nil, err
}
sum := md5.Sum(decoded)
storer := authboss.Cfg.Storer.(authboss.RecoverStorer)
userInter, err := storer.RecoverUser(base64.StdEncoding.EncodeToString(sum[:]))
if err != nil {
return nil, err
}
attrs = authboss.Unbind(userInter)
expiry, ok := attrs.DateTime(storeRecoverTokenExpiry)
if !ok || time.Now().After(expiry) {
return nil, errRecoveryTokenExpired
}
return attrs, nil
}

@ -1,239 +0,0 @@
package recover
import (
"bytes"
"html/template"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"gopkg.in/authboss.v0"
"gopkg.in/authboss.v0/internal/mocks"
"gopkg.in/authboss.v0/internal/views"
)
func Test_Initialize(t *testing.T) {
authboss.NewConfig()
m := &RecoverModule{}
authboss.Cfg.Storer = nil
if err := m.Initialize(); err == nil {
t.Error("Expected error")
} else if err.Error() != "recover: Need a RecoverStorer." {
t.Error("Got error but wrong reason:", err)
}
authboss.Cfg.Storer = mocks.MockFailStorer{}
if err := m.Initialize(); err == nil {
t.Error("Expected error")
} else if err.Error() != "recover: RecoverStorer required for recover functionality." {
t.Error("Got error but wrong reason:", err)
}
authboss.Cfg.Storer = mocks.NewMockStorer()
authboss.Cfg.Layout = nil
if err := m.Initialize(); err == nil {
t.Error("Expected error")
} else if err.Error() != "recover: Layout required for Recover functionallity." {
t.Error("Got error but wrong reason:", err)
}
var err error
authboss.Cfg.Layout, err = template.New("").Parse(`{{template "authboss" .}}`)
if err != nil {
t.Fatal("Unexpected error:", err)
}
authboss.Cfg.LayoutEmail = nil
if err := m.Initialize(); err == nil {
t.Error("Expected error:", err)
} else if err.Error() != "recover: LayoutEmail required for Recover functionallity." {
t.Error("Got error but wrong reason:", err)
}
authboss.Cfg.LayoutEmail, err = template.New("").Parse(`{{template "authboss" .}}`)
if err != nil {
t.Fatal("Unexpected error:", err)
}
if err := m.Initialize(); err != nil {
t.Error("Unexpected error:", err)
}
}
func testValidTestConfig() {
cfg := authboss.Config{
Storer: mocks.NewMockStorer(),
RecoverRedirect: "/login",
RecoverInitiateSuccessFlash: "sf",
RecoverTokenExpiredFlash: "exf",
RecoverFailedErrorFlash: "errf",
Policies: []authboss.Validator{
authboss.Rules{
FieldName: "username",
Required: true,
},
authboss.Rules{
FieldName: "password",
Required: true,
},
},
ConfirmFields: []string{"username", "confirmUsername", "password", "confirmPassword"},
LogWriter: &bytes.Buffer{},
Mailer: &mocks.MockMailer{},
EmailFrom: "auth@boss.com",
HostName: "localhost",
RecoverTokenDuration: time.Duration(24) * time.Hour,
BCryptCost: 4,
AuthLoginSuccessRoute: "/login",
XSRFName: "rofl",
XSRFMaker: func(_ http.ResponseWriter, _ *http.Request) string {
return "lawl"
},
}
var err error
if cfg.Layout, err = views.AssetToTemplate("layout.tpl"); err != nil {
panic(err)
}
if cfg.LayoutEmail, err = views.AssetToTemplate("layoutEmail.tpl"); err != nil {
panic(err)
}
authboss.Cfg = &cfg
}
func testValidRecoverModule() (*RecoverModule, *bytes.Buffer) {
testValidTestConfig()
m := &RecoverModule{}
if err := m.Initialize(); err != nil {
panic(err)
}
return m, authboss.Cfg.LogWriter.(*bytes.Buffer)
}
func Test_Routes(t *testing.T) {
testValidTestConfig()
m, _ := testValidRecoverModule()
routes := m.Routes()
if _, ok := routes["recover"]; !ok {
t.Error("Expected route: recover")
}
if _, ok := routes["recover/complete"]; !ok {
t.Error("Expected route: recover/complete")
}
}
func Test_Storage(t *testing.T) {
testValidTestConfig()
m, _ := testValidRecoverModule()
tests := []struct {
Attr string
Kind authboss.DataType
}{
{attrUsername, authboss.String},
{attrRecoverToken, authboss.String},
{attrEmail, authboss.String},
{attrRecoverTokenExpiry, authboss.String},
{attrPassword, authboss.String},
}
options := m.Storage()
for i, test := range tests {
if kind, ok := options[test.Attr]; !ok {
t.Errorf("%s> Expected attr: %s", i, test.Attr)
} else if kind != test.Kind {
t.Errorf("%s> Expected DataType: %s", i, test.Kind)
}
}
}
func testHttpRequest(method, url string, data url.Values) (*httptest.ResponseRecorder, *http.Request, *authboss.Context) {
var body io.Reader
if method != "GET" {
body = strings.NewReader(data.Encode())
}
r, err := http.NewRequest(method, url, body)
if err != nil {
panic(err)
}
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
ctx, err := authboss.ContextFromRequest(r)
if err != nil {
panic(err)
}
sessionStorer := mocks.NewMockClientStorer()
ctx.SessionStorer = sessionStorer
return w, r, ctx
}
func Test_execTpl_TemplateExectionFail(t *testing.T) {
testValidTestConfig()
m, logger := testValidRecoverModule()
w := httptest.NewRecorder()
failTpl, err := template.New("").Parse("{{.Fail}}")
if err != nil {
panic("Failed to build tpl")
}
m.templates["fail.tpl"] = failTpl
m.execTpl("fail.tpl", w, pageRecover{})
if w.Code != http.StatusInternalServerError {
t.Error("Unexpected code:", w.Code)
}
actualLog, err := ioutil.ReadAll(logger)
if err != nil {
panic(err)
}
if !bytes.Contains(actualLog, []byte("recover [unable to execute template]:")) {
t.Error("Expected log message starting with:", "recover [unable to execute template]:")
}
}
func Test_execTpl(t *testing.T) {
testValidTestConfig()
m, _ := testValidRecoverModule()
w := httptest.NewRecorder()
page := pageRecover{"bobby", "bob", nil, "", authboss.Cfg.RecoverFailedErrorFlash, "", ""}
m.execTpl(tplRecover, w, page)
tpl := m.templates[tplRecover]
expectedBody := &bytes.Buffer{}
if err := tpl.Execute(expectedBody, page); err != nil {
panic(err)
}
if w.Code != http.StatusOK {
t.Error("Unexpected code:", w.Code)
}
if !bytes.Equal(expectedBody.Bytes(), w.Body.Bytes()) {
t.Error("Unexpected body:", w.Body.String())
}
}