1
0
mirror of https://github.com/volatiletech/authboss.git synced 2025-01-04 03:49:32 +02:00

Safety commit

- Add new validation methods
- Cleaned up interactions with validation
- Add required validation
- Add confirm fields to validation
This commit is contained in:
Kris Runzer 2015-01-25 22:58:50 -08:00
parent 042bdba669
commit e660edd428
9 changed files with 276 additions and 102 deletions

View File

@ -36,6 +36,8 @@ type AuthPage struct {
ShowRemember bool
ShowRecover bool
FlashSuccess string
}
type Auth struct {
@ -94,7 +96,14 @@ func (a *Auth) loginHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, r
}
}
a.templates.ExecuteTemplate(w, pageLogin, AuthPage{ShowRemember: a.isRememberLoaded, ShowRecover: a.isRecoverLoaded})
page := AuthPage{ShowRemember: a.isRememberLoaded, ShowRecover: a.isRecoverLoaded}
if msg, ok := ctx.SessionStorer.Get(authboss.FlashSuccessKey); ok {
page.FlashSuccess = msg
ctx.SessionStorer.Del(authboss.FlashSuccessKey)
}
a.templates.ExecuteTemplate(w, pageLogin, page)
case methodPOST:
u, ok := ctx.FirstPostFormValue("username")
if !ok {
@ -103,7 +112,7 @@ func (a *Auth) loginHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, r
if err := a.callbacks.FireBefore(authboss.EventAuth, ctx); err != nil {
w.WriteHeader(http.StatusForbidden)
a.templates.ExecuteTemplate(w, pageLogin, AuthPage{err.Error(), u, a.isRememberLoaded, a.isRecoverLoaded})
a.templates.ExecuteTemplate(w, pageLogin, AuthPage{err.Error(), u, a.isRememberLoaded, a.isRecoverLoaded, ""})
}
p, ok := ctx.FirstPostFormValue("password")
@ -114,7 +123,7 @@ func (a *Auth) loginHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, r
if err := a.authenticate(ctx, u, p); err != nil {
fmt.Fprintln(a.logger, err)
w.WriteHeader(http.StatusForbidden)
a.templates.ExecuteTemplate(w, pageLogin, AuthPage{"invalid username and/or password", u, a.isRememberLoaded, a.isRecoverLoaded})
a.templates.ExecuteTemplate(w, pageLogin, AuthPage{"invalid username and/or password", u, a.isRememberLoaded, a.isRecoverLoaded, ""})
return
}

View File

@ -9,6 +9,10 @@ const (
// the remember module. This serves as a way to force full authentication
// by denying half-authed users acccess to sensitive areas.
HalfAuthKey = "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
FlashErrorKey = "flash_error"
)
// ClientStorer should be able to store values on the clients machine. Cookie and

View File

@ -10,35 +10,38 @@ import (
// 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 string `json:"mount_path" xml:"mountPath"`
MountPath string
// ViewsPath is the path to overiding view template files.
ViewsPath string `json:"views_path" xml:"viewsPath"`
ViewsPath string
// HostName is self explanitory
HostName string
AuthLogoutRoute string `json:"auth_logout_route" xml:"authLogoutRoute"`
AuthLoginSuccessRoute string `json:"auth_login_success_route" xml:"authLoginSuccessRoute"`
AuthLogoutRoute string
AuthLoginSuccessRoute string
ValidateEmail Validator `json:"-" xml:"-"`
ValidateUsername Validator `json:"-" xml:"-"`
ValidatePassword Validator `json:"-" xml:"-"`
RecoverInitiateRedirect string
RecoverInitiateSuccessFlash string
ExpireAfter time.Duration `json:"expire_after" xml:"expireAfter"`
Policies []Validator
ConfirmFields []string
LockAfter int `json:"lock_after" xml:"lockAfter"`
LockWindow time.Duration `json:"lock_window" xml:"lockWindow"`
LockDuration time.Duration `json:"lock_duration" xml:"lockDuration"`
ExpireAfter time.Duration
LockAfter int
LockWindow time.Duration
LockDuration time.Duration
EmailFrom string `json:"email_from" xml:"emailFrom"`
EmailSubjectPrefix string `json:"email_subject_prefix" xml:"emailSubjectPrefix"`
EmailFrom string
EmailSubjectPrefix string
SMTPAddress string `json:"smtp_address" xml:"smtpAddress"`
SMTPAuth smtp.Auth `json:"-" xml:"-"`
SMTPAddress string
SMTPAuth smtp.Auth
Storer Storer `json:"-" xml:"-"`
CookieStoreMaker CookieStoreMaker `json:"-" xml:"-"`
SessionStoreMaker SessionStoreMaker `json:"-" xml:"-"`
LogWriter io.Writer `json:"-" xml:"-"`
Callbacks *Callbacks `json:"-" xml:"-"`
Mailer Mailer `json:"-" xml:"-"`
Storer Storer
CookieStoreMaker CookieStoreMaker
SessionStoreMaker SessionStoreMaker
LogWriter io.Writer
Callbacks *Callbacks
Mailer Mailer
}
// NewConfig creates a new config full of default values ready to override.
@ -50,6 +53,9 @@ func NewConfig() *Config {
AuthLogoutRoute: "/",
AuthLoginSuccessRoute: "/",
RecoverInitiateRedirect: "/login",
RecoverInitiateSuccessFlash: "An email has been sent with further insructions on how to reset your password",
LogWriter: ioutil.Discard,
Callbacks: NewCallbacks(),
Mailer: LogMailer(ioutil.Discard),

View File

@ -1,20 +1,17 @@
package recover
import (
"bytes"
"crypto/md5"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"html/template"
"net/http"
"strings"
"io"
"bytes"
"crypto/md5"
"encoding/base64"
"log"
"gopkg.in/authboss.v0"
"gopkg.in/authboss.v0/internal/views"
)
@ -23,13 +20,14 @@ const (
methodGET = "GET"
methodPOST = "POST"
tplLogin = "login.tpl"
tplRecover = "recover.tpl"
tplRecoverComplete = "recover-complete.tpl"
tplInitEmail = "recover-init.email"
attrUsername = "username"
attrResetToken = "resettoken"
attrEmail = "email"
attrUsername = "username"
attrRecoverToken = "recover_token"
attrEmail = "email"
)
func init() {
@ -38,77 +36,98 @@ func init() {
}
type RecoverPage struct {
Username, ConfirmUsername, Error string
Username, ConfirmUsername string
ErrMap map[string][]string
}
type RecoverModule struct {
templates *template.Template
routes authboss.RouteTable
storageOptions authboss.StorageOptions
storer authboss.Storer
logger io.Writer
fromEmail string
templates *template.Template
routes authboss.RouteTable
storageOptions authboss.StorageOptions
storer authboss.RecoverStorer
logger io.Writer
policies []authboss.Validator
confirmFields []string
hostName string
recoverInitiateRedirect string
recoverInitiateSuccessFlash string
fromEmail string
}
func (m *RecoverModule) Initialize(config *authboss.Config) (err error) {
if config.Storer == nil {
return errors.New("recover: Need a RecoverStorer.")
}
if storer, ok := config.Storer.(authboss.RecoverStorer); !ok {
return errors.New("recover: RecoverStorer required for recover functionality.")
} else {
m.storer = storer
}
if m.templates, err = views.Get(config.ViewsPath, tplRecover, tplRecoverComplete, tplInitEmail); err != nil {
return err
}
m.routes = authboss.RouteTable{
"recover": m.recoverHandlerFunc,
"recover/complete": m.recoverCompleteHandlerFunc,
"recover": m.recoverHandlerFunc,
//"recover/complete": m.recoverCompleteHandlerFunc,
}
m.storageOptions = authboss.StorageOptions{
attrUsername: authboss.String,
attrResetToken: authboss.String,
attrEmail: authboss.String,
attrUsername: authboss.String,
attrRecoverToken: authboss.String,
attrEmail: authboss.String,
}
m.storer = config.Storer
m.logger = config.LogWriter
m.fromEmail = config.RecoverFromEmail
m.hostName = config.HostName
m.recoverInitiateRedirect = config.RecoverInitiateRedirect
m.recoverInitiateSuccessFlash = config.RecoverInitiateSuccessFlash
m.policies = config.Policies
m.confirmFields = config.ConfirmFields
return nil
}
func (m *RecoverModule) Routes() authboss.RouteTable {
return m.routes
}
func (m *RecoverModule) Storage() authboss.StorageOptions {
return m.storageOptions
}
func (m *RecoverModule) Routes() authboss.RouteTable { return m.routes }
func (m *RecoverModule) Storage() authboss.StorageOptions { return m.storageOptions }
func (m *RecoverModule) recoverHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) {
switch r.Method {
case methodGET:
m.templates.ExecuteTemplate(w, tplRecover, nil)
case methodPOST:
username, ok := ctx.FirstPostFormValue("username")
if !ok {
fmt.Fprintln(m.logger, errors.New("recover: Expected postFormValue 'username' to be in the context"))
}
username, _ := ctx.FirstPostFormValue("username")
confirmUsername, _ := ctx.FirstPostFormValue("confirmUsername")
confirmUsername, ok := ctx.FirstPostFormValue("confirmUsername")
if !ok {
fmt.Fprintln(m.logger, errors.New("recover: Expected postFormValue 'confirmUsername' to be in the context"))
}
if err := m.initiateRecover(ctx, username, confirmUsername, r.Host); err != nil {
fmt.Fprintln(m.logger, fmt.Sprintf("recover: %s"), err.Error())
w.WriteHeader(http.StatusBadRequest)
m.templates.ExecuteTemplate(w, tplRecover, RecoverPage{username, confirmUsername, err.Error()})
policies := authboss.FilterValidators(m.policies, "username")
if validationErrs := ctx.Validate(policies, m.confirmFields...); len(validationErrs) > 0 {
err := m.templates.ExecuteTemplate(w, tplRecover, RecoverPage{username, confirmUsername, validationErrs.Map()})
if err != nil {
fmt.Fprintln(m.logger, "recover:", err)
}
return
}
if err := m.initiateRecover(ctx, username, confirmUsername); err != nil {
fmt.Fprintln(m.logger, fmt.Sprintf("recover: %s", err.Error()))
}
ctx.SessionStorer.Put(authboss.FlashSuccessKey, m.recoverInitiateSuccessFlash)
http.Redirect(w, r, m.recoverInitiateRedirect, http.StatusFound)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func (m *RecoverModule) initiateRecover(ctx *authboss.Context, username, confirmUsername, host string) error {
if !strings.EqualFold(username, confirmUsername) {
return errors.New("Confirm username does not match")
func (m *RecoverModule) initiateRecover(ctx *authboss.Context, username, confirmUsername string) (err error) {
if err := ctx.LoadUser(username, m.storer); err != nil {
return err
}
email, ok := ctx.User.String(attrEmail)
if !ok {
return fmt.Errorf("missing attr: %s", email)
}
token := make([]byte, 32)
@ -116,25 +135,8 @@ func (m *RecoverModule) initiateRecover(ctx *authboss.Context, username, confirm
return err
}
if err := ctx.LoadUser(username, m.storer); err != nil {
return err
}
emailInter, ok := ctx.User[attrEmail]
if !ok {
return errors.New("user does not have mapped email")
}
email, ok := emailInter.(string)
if !ok {
return errors.New("user does not have a valid email")
}
// TODO : email regex check on to and from
sum := md5.Sum(token)
ctx.User[attrResetToken] = base64.StdEncoding.EncodeToString(sum[:])
log.Printf("%#v", ctx.User)
ctx.User[attrRecoverToken] = base64.StdEncoding.EncodeToString(sum[:])
if err := ctx.SaveUser(username, m.storer); err != nil {
return err
@ -142,25 +144,63 @@ func (m *RecoverModule) initiateRecover(ctx *authboss.Context, username, confirm
emailBody := &bytes.Buffer{}
if err := m.templates.ExecuteTemplate(emailBody, tplInitEmail, struct{ Link string }{
fmt.Sprintf("%s/recover/complete?token=%s", host, base64.URLEncoding.EncodeToString(token)),
fmt.Sprintf("%s/recover/complete?token=%s", m.hostName, base64.URLEncoding.EncodeToString(sum[:])),
}); err != nil {
return err
}
if err := authboss.SendEmail(email, m.fromEmail, emailBody.Bytes()); err != nil {
fmt.Fprintln(m.logger, err)
}
return nil
return authboss.SendEmail(email, m.fromEmail, emailBody.Bytes())
}
func (m *RecoverModule) recoverCompleteHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) {
/*func (m *RecoverModule) recoverCompleteHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) {
switch r.Method {
case methodGET:
token, ok := ctx.FirstFormValue("token")
if !ok {
fmt.Fprintln(m.logger, "recover: expected value token")
//http.Redirect(w, r, "/", http.StatusFound)
return
}
userAttrs, err := m.verifyToken(token);
if err != nil {
fmt.Fprintf(m.logger, "recover: %s", err)
//http.Redirect(w, r, urlStr, code)
return
}
m.templates.ExecuteTemplate(w, tplRecoverComplete, nil)
case methodPOST:
//if err := completeRecover(ctx); err :=
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func (m *RecoverModule) verifyToken(token) (attrs authboss.Attributes, err) {
decodedToken, err := base64.URLEncoding.DecodeString(token)
if err != nil {
return nil, err
}
sum := md5.Sum(decodedToken)
userInter, err := m.storer.RecoverUser(base64.StdEncoding.EncodeToString(sum[:]))
if err != nil {
return nil, err
}
return authboss.Unbind(userInter), nil
}
func (m *RecoverModule) completeRecover(ctx *authboss.Context, password, confirmPassword string) error {
if password == confirmPassword {
return errors.New("Passwords do not match")
}
return nil
}
*/

View File

@ -7,11 +7,14 @@ import (
"unicode"
)
var blankRegex = regexp.MustCompile(`^\s*$`)
// Rules defines a ruleset by which a string can be validated.
type Rules struct {
// FieldName is the name of the field this is intended to validate.
FieldName string
// MatchError describes the MustMatch regexp to a user.
Required bool
MatchError string
MustMatch *regexp.Regexp
MinLength, MaxLength int
@ -32,9 +35,8 @@ func (r Rules) Errors(toValidate string) ErrorList {
errs := make(ErrorList, 0)
ln := len(toValidate)
if ln == 0 {
errs = append(errs, FieldError{r.FieldName, errors.New("Cannot be blank")})
return errs
if r.Required && (ln == 0 || blankRegex.MatchString(toValidate)) {
return append(errs, FieldError{r.FieldName, errors.New("Cannot be blank")})
}
if r.MustMatch != nil {
@ -64,6 +66,7 @@ func (r Rules) Errors(toValidate string) ErrorList {
if len(errs) == 0 {
return nil
}
return errs
}

View File

@ -14,10 +14,15 @@ func TestRules_Errors(t *testing.T) {
Error string
}{
{
Rules{FieldName: "email"},
Rules{FieldName: "email", Required: true},
"",
"email: Cannot be blank",
},
{
Rules{FieldName: "email", Required: true},
" \t\t\n ",
"email: Cannot be blank",
},
{
Rules{FieldName: "email", MatchError: "Regexp must match!", MustMatch: regexp.MustCompile("abc")},
"hello",
@ -110,7 +115,7 @@ func TestRules_Rules(t *testing.T) {
func TestRules_IsValid(t *testing.T) {
t.Parallel()
r := Rules{FieldName: "email"}
r := Rules{FieldName: "email", Required: true}
if r.IsValid("") {
t.Error("It should not be valid.")
}

View File

@ -49,6 +49,11 @@ type TokenStorer interface {
UseToken(givenKey, token string) (key string, err error)
}
type RecoverStorer interface {
Storer
RecoverUser(recover string) (interface{}, error)
}
// DataType represents the various types that clients must be able to store.
type DataType int
@ -100,6 +105,37 @@ func (a Attributes) Names() []string {
return names
}
// String returns a single value as a string
func (a Attributes) String(key string) (string, bool) {
inter, ok := a[key]
if !ok {
return "", false
}
val, ok := inter.(string)
return val, ok
}
// Int returns a single value as a int
func (a Attributes) Int(key string) (int, bool) {
inter, ok := a[key]
if !ok {
return 0, false
}
val, ok := inter.(int)
return val, ok
}
// DateTime returns a single value as a time.Time
func (a Attributes) DateTime(key string) (time.Time, bool) {
inter, ok := a[key]
if !ok {
var time time.Time
return time, false
}
val, ok := inter.(time.Time)
return val, ok
}
// Bind the data in the attributes to the given struct. This means the
// struct creator must have read the documentation and decided what fields
// will be needed ahead of time.

View File

@ -58,7 +58,7 @@ func (f FieldError) Error() string {
}
// Validate validates a request using the given ruleset.
func (ctx *Context) Validate(ruleset []Validator) ErrorList {
func (ctx *Context) Validate(ruleset []Validator, confirmFields ...string) ErrorList {
errList := make(ErrorList, 0)
for _, validator := range ruleset {
@ -70,5 +70,32 @@ func (ctx *Context) Validate(ruleset []Validator) ErrorList {
}
}
for i := 0; i < len(confirmFields)-1; i += 2 {
main, ok := ctx.FirstPostFormValue(confirmFields[i])
if !ok {
continue
}
confirm, ok := ctx.FirstPostFormValue(confirmFields[i+1])
if !ok || main != confirm {
errList = append(errList, FieldError{confirmFields[i+1], fmt.Errorf("Does not match %s", confirmFields[i])})
}
}
return errList
}
func FilterValidators(validators []Validator, fields ...string) []Validator {
var arr []Validator
for _, validator := range validators {
fieldName := validator.Field()
for _, field := range fields {
if fieldName == field {
arr = append(arr, validator)
}
}
}
return arr
}

View File

@ -91,3 +91,47 @@ func TestValidate(t *testing.T) {
t.Error("Expected no errors for email.")
}
}
func TestValidate_Confirm(t *testing.T) {
t.Parallel()
ctx := mockRequestContext("username", "john", "confirmUsername", "johnny")
errs := ctx.Validate(nil, "username", "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()
if len(errs) != 0 {
t.Error("Expected no errors:", errs)
}
ctx = mockRequestContext("username", "john", "confirmUsername", "john")
errs = ctx.Validate(nil, "username").Map()
if len(errs) != 0 {
t.Error("Expected no errors:", errs)
}
}
func TestFilterValidators(t *testing.T) {
t.Parallel()
validators := []Validator{
mockValidator{
FieldName: "username", Errs: ErrorList{FieldError{"username", errors.New("must be longer than 4")}},
},
mockValidator{
FieldName: "password", Errs: ErrorList{FieldError{"password", errors.New("must be longer than 4")}},
},
}
validators = FilterValidators(validators, "username")
if len(validators) != 1 {
t.Error("Expected length to be 1")
}
if validators[0].Field() != "username" {
t.Error("Expcted validator for field username", validators[0].Field())
}
}