1
0
mirror of https://github.com/volatiletech/authboss.git synced 2024-11-28 08:58:38 +02:00

Add PrimaryID to the system.

- Fix #17
This commit is contained in:
Aaron 2015-02-22 13:16:11 -08:00
parent 4eeb21e16d
commit 001810bf7c
12 changed files with 96 additions and 84 deletions

View File

@ -14,9 +14,6 @@ const (
methodPOST = "POST"
tplLogin = "login.tpl"
storeUsername = "username"
storePassword = "password"
)
func init() {
@ -37,7 +34,7 @@ func (a *AuthModule) Initialize() (err error) {
return err
}
a.policies = authboss.FilterValidators(authboss.Cfg.Policies, "username", "password")
a.policies = authboss.FilterValidators(authboss.Cfg.Policies, authboss.Cfg.PrimaryID, authboss.StorePassword)
a.isRememberLoaded = authboss.IsLoaded("remember")
a.isRecoverLoaded = authboss.IsLoaded("recover")
@ -54,8 +51,8 @@ func (a *AuthModule) Routes() authboss.RouteTable {
func (a *AuthModule) Storage() authboss.StorageOptions {
return authboss.StorageOptions{
storeUsername: authboss.String,
storePassword: authboss.String,
authboss.Cfg.PrimaryID: authboss.String,
authboss.StorePassword: authboss.String,
}
}
@ -63,7 +60,7 @@ func (a *AuthModule) loginHandlerFunc(ctx *authboss.Context, w http.ResponseWrit
switch r.Method {
case methodGET:
if _, ok := ctx.SessionStorer.Get(authboss.SessionKey); ok {
if halfAuthed, ok := ctx.SessionStorer.Get(authboss.HalfAuthKey); !ok || halfAuthed == "false" {
if halfAuthed, ok := ctx.SessionStorer.Get(authboss.SessionHalfAuthKey); !ok || halfAuthed == "false" {
http.Redirect(w, r, authboss.Cfg.AuthLoginSuccessRoute, http.StatusFound)
}
}
@ -86,12 +83,12 @@ func (a *AuthModule) loginHandlerFunc(ctx *authboss.Context, w http.ResponseWrit
return nil
}
username, _ := ctx.FirstPostFormValue("username")
key, _ := ctx.FirstPostFormValue(authboss.Cfg.PrimaryID)
password, _ := ctx.FirstPostFormValue("password")
errData := authboss.NewHTMLData(
"error", "invalid username and/or password",
"username", username,
authboss.Cfg.PrimaryID, key,
"showRemember", a.isRememberLoaded,
"showRecover", a.isRecoverLoaded,
)
@ -101,12 +98,12 @@ func (a *AuthModule) loginHandlerFunc(ctx *authboss.Context, w http.ResponseWrit
return a.templates.Render(ctx, w, r, tplLogin, errData)
}
if err := validateCredentials(ctx, username, password); err != nil {
if err := validateCredentials(ctx, key, password); err != nil {
fmt.Fprintln(authboss.Cfg.LogWriter, "auth: failed to validate credentials:", err)
return a.templates.Render(ctx, w, r, tplLogin, errData)
}
ctx.SessionStorer.Put(authboss.SessionKey, username)
ctx.SessionStorer.Put(authboss.SessionKey, key)
authboss.Cfg.Callbacks.FireAfter(authboss.EventAuth, ctx)
http.Redirect(w, r, authboss.Cfg.AuthLoginSuccessRoute, http.StatusFound)
default:
@ -116,12 +113,12 @@ func (a *AuthModule) loginHandlerFunc(ctx *authboss.Context, w http.ResponseWrit
return nil
}
func validateCredentials(ctx *authboss.Context, username, password string) error {
if err := ctx.LoadUser(username); err != nil {
func validateCredentials(ctx *authboss.Context, key, password string) error {
if err := ctx.LoadUser(key); err != nil {
return err
}
actualPassword, err := ctx.User.StringErr(storePassword)
actualPassword, err := ctx.User.StringErr(authboss.StorePassword)
if err != nil {
return err
}

View File

@ -8,7 +8,7 @@ const (
// HalfAuthKey is used for sessions that have been authenticated by
// the remember module. This serves as a way to force full authentication
// by denying half-authed users acccess to sensitive areas.
HalfAuthKey = "halfauth"
SessionHalfAuthKey = "halfauth"
// FlashSuccessKey is used for storing sucess flash messages on the session
FlashSuccessKey = "flash_success"
// FlashErrorKey is used for storing sucess flash messages on the session

View File

@ -5,6 +5,7 @@ import (
"io"
"io/ioutil"
"net/smtp"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
@ -20,15 +21,19 @@ var Cfg *Config = NewConfig()
// Config holds all the configuration for both authboss and it's modules.
type Config struct {
// MountPath is the path to mount the router at.
// MountPath is the path to mount authboss's routes at (eg /auth).
MountPath string
// ViewsPath is the path to overiding view template files.
// ViewsPath is the path to search for overridden templates.
ViewsPath string
// HostName is self explanitory
// HostName is the host of the web application (eg https://www.happiness.com:8080) for e-mail url generation.
HostName string
// BCryptPasswordCost is self explanitory.
// BCryptCost is the cost of the bcrypt password hashing function.
BCryptCost int
// PrimaryID is the primary identifier of the user. Set to one of:
// authboss.StoreEmail, authboss.StoreUsername (StoreEmail is default)
PrimaryID string
Layout *template.Template
LayoutEmail *template.Template
LayoutDataMaker ViewDataMaker
@ -45,7 +50,8 @@ type Config struct {
Policies []Validator
ConfirmFields []string
ExpireAfter time.Duration
ExpireAfter time.Duration
LockAfter int
LockWindow time.Duration
LockDuration time.Duration
@ -73,6 +79,8 @@ func NewConfig() *Config {
HostName: "localhost:8080",
BCryptCost: bcrypt.DefaultCost,
PrimaryID: StoreEmail,
Layout: template.Must(template.New("").Parse(`<html><body>{{template "authboss" .}}</body></html>`)),
LayoutEmail: template.Must(template.New("").Parse(`<html><body>{{template "authboss" .}}</body></html>`)),
@ -96,7 +104,10 @@ func NewConfig() *Config {
AllowWhitespace: false,
},
},
ConfirmFields: []string{"username", "confirmUsername", "password", "confirmPassword"},
ConfirmFields: []string{
StoreEmail, "confirm" + strings.Title(StoreEmail),
StorePassword, "confirm" + strings.Title(StorePassword),
},
RecoverRedirect: "/login",
RecoverInitiateSuccessFlash: "An email has been sent with further insructions on how to reset your password",

View File

@ -98,6 +98,7 @@ func TestConfirm_AfterRegister(t *testing.T) {
log := &bytes.Buffer{}
authboss.Cfg.LogWriter = log
authboss.Cfg.Mailer = authboss.LogMailer(log)
authboss.Cfg.PrimaryID = authboss.StoreUsername
sentEmail := false
@ -110,7 +111,7 @@ func TestConfirm_AfterRegister(t *testing.T) {
t.Error("Expected it to die with user error:", err)
}
ctx.User = authboss.Attributes{authboss.StoreUsername: "uname"}
ctx.User = authboss.Attributes{authboss.Cfg.PrimaryID: "username"}
if err := c.AfterRegister(ctx); err == nil || err.(authboss.AttributeErr).Name != "email" {
t.Error("Expected it to die with e-mail address error:", err)
}

View File

@ -130,7 +130,7 @@ func (c *Context) SaveUser() error {
return errors.New("User not initialized.")
}
key, ok := c.User.String("username")
key, ok := c.User.String(Cfg.PrimaryID)
if !ok {
return errors.New("User improperly initialized, primary ID missing")
}

View File

@ -41,14 +41,14 @@ func TestContext_SaveUser(t *testing.T) {
ctx := NewContext()
storer := mockStorer{}
Cfg.Storer = storer
ctx.User = Attributes{"username": "joe", "email": "hello@joe.com", "password": "mysticalhash"}
ctx.User = Attributes{StoreUsername: "joe", StoreEmail: "hello@joe.com", StorePassword: "mysticalhash"}
err := ctx.SaveUser()
if err != nil {
t.Error("Unexpected error:", err)
}
attr, ok := storer["joe"]
attr, ok := storer["hello@joe.com"]
if !ok {
t.Error("Could not find joe!")
}

View File

@ -74,7 +74,7 @@ func TestExpire_Middleware(t *testing.T) {
authboss.NewConfig()
session := mocks.NewMockClientStorer()
session.Values = map[string]string{
authboss.SessionKey: "username",
authboss.SessionKey: "email@email.com",
}
maker := func(w http.ResponseWriter, r *http.Request) authboss.ClientStorer { return session }

View File

@ -49,15 +49,15 @@ func TestAfterAuth(t *testing.T) {
storer := mocks.NewMockStorer()
authboss.Cfg.Storer = storer
ctx.User = authboss.Attributes{"username": "username"}
ctx.User = authboss.Attributes{authboss.Cfg.PrimaryID: "john@john.com"}
if err := lock.AfterAuth(ctx); err != nil {
t.Error(err)
}
if storer.Users["username"][StoreAttemptNumber].(int) != 0 {
if storer.Users["john@john.com"][StoreAttemptNumber].(int) != 0 {
t.Error("StoreAttemptNumber set incorrectly.")
}
if _, ok := storer.Users["username"][StoreAttemptTime].(time.Time); !ok {
if _, ok := storer.Users["john@john.com"][StoreAttemptTime].(time.Time); !ok {
t.Error("StoreAttemptTime not set.")
}
}
@ -74,35 +74,37 @@ func TestAfterAuthFail_Lock(t *testing.T) {
authboss.Cfg.LockWindow = 30 * time.Minute
authboss.Cfg.LockAfter = 3
ctx.User = map[string]interface{}{"username": "username"}
email := "john@john.com"
ctx.User = map[string]interface{}{authboss.Cfg.PrimaryID: email}
old = time.Now().UTC().Add(-1 * time.Hour)
for i := 0; i < 3; i++ {
if lockedIntf, ok := storer.Users["username"][StoreLocked]; ok && lockedIntf.(bool) {
if lockedIntf, ok := storer.Users["john@john.com"][StoreLocked]; ok && lockedIntf.(bool) {
t.Errorf("%d: User should not be locked.", i)
}
if err := lock.AfterAuthFail(ctx); err != nil {
t.Error(err)
}
if val := storer.Users["username"][StoreAttemptNumber].(int); val != i+1 {
if val := storer.Users[email][StoreAttemptNumber].(int); val != i+1 {
t.Errorf("%d: StoreAttemptNumber set incorrectly: %v", i, val)
}
if current, ok = storer.Users["username"][StoreAttemptTime].(time.Time); !ok || old.After(current) {
if current, ok = storer.Users[email][StoreAttemptTime].(time.Time); !ok || old.After(current) {
t.Error("%d: StoreAttemptTime not set correctly: %v", i, current)
}
current = old
}
if !storer.Users["username"][StoreLocked].(bool) {
if !storer.Users[email][StoreLocked].(bool) {
t.Error("User should be locked.")
}
if val := storer.Users["username"][StoreAttemptNumber].(int); val != 3 {
if val := storer.Users[email][StoreAttemptNumber].(int); val != 3 {
t.Error("StoreAttemptNumber set incorrectly:", val)
}
if _, ok = storer.Users["username"][StoreAttemptTime].(time.Time); !ok {
if _, ok = storer.Users[email][StoreAttemptTime].(time.Time); !ok {
t.Error("StoreAttemptTime not set correctly.")
}
}
@ -120,21 +122,22 @@ func TestAfterAuthFail_Reset(t *testing.T) {
old = time.Now().UTC().Add(-time.Hour)
email := "john@john.com"
ctx.User = map[string]interface{}{
"username": "username",
StoreAttemptNumber: 2,
StoreAttemptTime: old,
StoreLocked: false,
authboss.Cfg.PrimaryID: email,
StoreAttemptNumber: 2,
StoreAttemptTime: old,
StoreLocked: false,
}
lock.AfterAuthFail(ctx)
if val := storer.Users["username"][StoreAttemptNumber].(int); val != 0 {
if val := storer.Users[email][StoreAttemptNumber].(int); val != 0 {
t.Error("StoreAttemptNumber set incorrectly:", val)
}
if current, ok = storer.Users["username"][StoreAttemptTime].(time.Time); !ok || current.Before(old) {
if current, ok = storer.Users[email][StoreAttemptTime].(time.Time); !ok || current.Before(old) {
t.Error("StoreAttemptTime not set correctly.")
}
if locked := storer.Users["username"][StoreLocked].(bool); locked {
if locked := storer.Users[email][StoreLocked].(bool); locked {
t.Error("StoreLocked not set correctly:", locked)
}
}
@ -156,17 +159,18 @@ func TestLock(t *testing.T) {
authboss.Cfg.Storer = storer
lock := Lock{}
storer.Users["username"] = map[string]interface{}{
"username": "username",
"password": "password",
email := "john@john.com"
storer.Users[email] = map[string]interface{}{
authboss.Cfg.PrimaryID: email,
"password": "password",
}
err := lock.Lock("username")
err := lock.Lock(email)
if err != nil {
t.Error(err)
}
if locked := storer.Users["username"][StoreLocked].(bool); !locked {
if locked := storer.Users[email][StoreLocked].(bool); !locked {
t.Error("User should be locked.")
}
}
@ -178,25 +182,26 @@ func TestUnlock(t *testing.T) {
lock := Lock{}
authboss.Cfg.LockWindow = 1 * time.Hour
storer.Users["username"] = map[string]interface{}{
"username": "username",
"password": "password",
"locked": true,
email := "john@john.com"
storer.Users[email] = map[string]interface{}{
authboss.Cfg.PrimaryID: email,
"password": "password",
"locked": true,
}
err := lock.Unlock("username")
err := lock.Unlock(email)
if err != nil {
t.Error(err)
}
attemptTime := storer.Users["username"][StoreAttemptTime].(time.Time)
attemptTime := storer.Users[email][StoreAttemptTime].(time.Time)
if attemptTime.After(time.Now().UTC().Add(-authboss.Cfg.LockWindow)) {
t.Error("StoreLocked not set correctly:", attemptTime)
}
if number := storer.Users["username"][StoreAttemptNumber].(int); number != 0 {
if number := storer.Users[email][StoreAttemptNumber].(int); number != 0 {
t.Error("StoreLocked not set correctly:", number)
}
if locked := storer.Users["username"][StoreLocked].(bool); locked {
if locked := storer.Users[email][StoreLocked].(bool); locked {
t.Error("User should not be locked.")
}
}

View File

@ -68,7 +68,7 @@ func (r *Remember) AfterAuth(ctx *authboss.Context) error {
return errUserMissing
}
key, err := ctx.User.StringErr("username")
key, err := ctx.User.StringErr(authboss.Cfg.PrimaryID)
if err != nil {
return err
}
@ -140,7 +140,7 @@ func (r *Remember) Auth(
}
// Ensure a half-auth.
sstorer.Put(authboss.HalfAuthKey, "true")
sstorer.Put(authboss.SessionHalfAuthKey, "true")
// Log the user in.
sstorer.Put(authboss.SessionKey, key)

View File

@ -52,7 +52,7 @@ func TestAfterAuth(t *testing.T) {
ctx.SessionStorer = session
ctx.CookieStorer = cookies
ctx.User = authboss.Attributes{"username": "testuser"}
ctx.User = authboss.Attributes{authboss.Cfg.PrimaryID: "test@email.com"}
if err := R.AfterAuth(ctx); err != nil {
t.Error(err)
@ -109,7 +109,7 @@ func TestAuth(t *testing.T) {
t.Error("Unexpected error:", err)
}
if session.Values[authboss.HalfAuthKey] != "true" {
if session.Values[authboss.SessionHalfAuthKey] != "true" {
t.Error("The user should have been half-authed.")
}

View File

@ -14,8 +14,6 @@ const (
StoreEmail = "email"
StoreUsername = "username"
StorePassword = "password"
// UserKey is used to uniquely identify the user.
StoreKey = StoreEmail
)
var (

View File

@ -22,9 +22,9 @@ func TestErrorList_Map(t *testing.T) {
errAsploded := "asploded"
errList := ErrorList{
FieldError{"username", errors.New(errNotLong)},
FieldError{"username", errors.New(errEmail)},
FieldError{"password", errors.New(errNotLong)},
FieldError{StoreUsername, errors.New(errNotLong)},
FieldError{StoreUsername, errors.New(errEmail)},
FieldError{StorePassword, errors.New(errNotLong)},
errors.New(errAsploded),
}
@ -33,7 +33,7 @@ func TestErrorList_Map(t *testing.T) {
t.Error("Wrong number of fields:", len(m))
}
usernameErrs := m["username"]
usernameErrs := m[StoreUsername]
if len(usernameErrs) != 2 {
t.Error("Wrong number of username errors:", len(usernameErrs))
}
@ -44,7 +44,7 @@ func TestErrorList_Map(t *testing.T) {
t.Error("Wrong username error at 1:", usernameErrs[1])
}
passwordErrs := m["password"]
passwordErrs := m[StorePassword]
if len(passwordErrs) != 1 {
t.Error("Wrong number of password errors:", len(passwordErrs))
}
@ -64,30 +64,30 @@ func TestErrorList_Map(t *testing.T) {
func TestValidate(t *testing.T) {
t.Parallel()
ctx := mockRequestContext("username", "john", "email", "john@john.com")
ctx := mockRequestContext(StoreUsername, "john", StoreEmail, "john@john.com")
errList := ctx.Validate([]Validator{
mockValidator{
FieldName: "username",
Errs: ErrorList{FieldError{"username", errors.New("must be longer than 4")}},
FieldName: StoreUsername,
Errs: ErrorList{FieldError{StoreUsername, errors.New("must be longer than 4")}},
},
mockValidator{
FieldName: "missing_field",
Errs: ErrorList{FieldError{"missing_field", errors.New("Expected field to exist.")}},
},
mockValidator{
FieldName: "email", Errs: nil,
FieldName: StoreEmail, Errs: nil,
},
})
errs := errList.Map()
if errs["username"][0] != "must be longer than 4" {
t.Error("Expected a different error for username:", errs["username"][0])
if errs[StoreUsername][0] != "must be longer than 4" {
t.Error("Expected a different error for username:", errs[StoreUsername][0])
}
if errs["missing_field"][0] != "Expected field to exist." {
t.Error("Expected a different error for missing_field:", errs["missing_field"][0])
}
if _, ok := errs["email"]; ok {
if _, ok := errs[StoreEmail]; ok {
t.Error("Expected no errors for email.")
}
}
@ -95,20 +95,20 @@ func TestValidate(t *testing.T) {
func TestValidate_Confirm(t *testing.T) {
t.Parallel()
ctx := mockRequestContext("username", "john", "confirmUsername", "johnny")
errs := ctx.Validate(nil, "username", "confirmUsername").Map()
ctx := mockRequestContext(StoreUsername, "john", "confirmUsername", "johnny")
errs := ctx.Validate(nil, StoreUsername, "confirmUsername").Map()
if errs["confirmUsername"][0] != "Does not match username" {
t.Error("Expected a different error for confirmUsername:", errs["confirmUsername"][0])
}
ctx = mockRequestContext("username", "john", "confirmUsername", "john")
errs = ctx.Validate(nil, "username", "confirmUsername").Map()
ctx = mockRequestContext(StoreUsername, "john", "confirmUsername", "john")
errs = ctx.Validate(nil, StoreUsername, "confirmUsername").Map()
if len(errs) != 0 {
t.Error("Expected no errors:", errs)
}
ctx = mockRequestContext("username", "john", "confirmUsername", "john")
errs = ctx.Validate(nil, "username").Map()
ctx = mockRequestContext(StoreUsername, "john", "confirmUsername", "john")
errs = ctx.Validate(nil, StoreUsername).Map()
if len(errs) != 0 {
t.Error("Expected no errors:", errs)
}
@ -119,19 +119,19 @@ func TestFilterValidators(t *testing.T) {
validators := []Validator{
mockValidator{
FieldName: "username", Errs: ErrorList{FieldError{"username", errors.New("must be longer than 4")}},
FieldName: StoreUsername, Errs: ErrorList{FieldError{StoreUsername, errors.New("must be longer than 4")}},
},
mockValidator{
FieldName: "password", Errs: ErrorList{FieldError{"password", errors.New("must be longer than 4")}},
FieldName: StorePassword, Errs: ErrorList{FieldError{StorePassword, errors.New("must be longer than 4")}},
},
}
validators = FilterValidators(validators, "username")
validators = FilterValidators(validators, StoreUsername)
if len(validators) != 1 {
t.Error("Expected length to be 1")
}
if validators[0].Field() != "username" {
if validators[0].Field() != StoreUsername {
t.Error("Expcted validator for field username", validators[0].Field())
}
}