mirror of
https://github.com/volatiletech/authboss.git
synced 2025-09-16 09:06:20 +02:00
Add confirm module beginnings.
- Fix some inconsistencies in expire and lock. - Add bool type to storer.
This commit is contained in:
165
confirm/confirm.go
Normal file
165
confirm/confirm.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// Package confirm implements user confirming after N bad sign-in attempts.
|
||||
package confirm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"gopkg.in/authboss.v0"
|
||||
"gopkg.in/authboss.v0/internal/views"
|
||||
)
|
||||
|
||||
const (
|
||||
UserConfirmToken = "confirm_token"
|
||||
UserConfirmed = "confirmed"
|
||||
|
||||
FormValueConfirm = "cnf"
|
||||
|
||||
tplConfirmHTML = "confirm_email.html.tpl"
|
||||
tplConfirmText = "confirm_email.text.tpl"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotConfirmed happens when the account is there, but
|
||||
// not yet confirmed.
|
||||
ErrNotConfirmed = errors.New("Account is not confirmed.")
|
||||
)
|
||||
|
||||
// C is the singleton instance of the confirm module which will have been
|
||||
// configured and ready to use after authboss.Init()
|
||||
var C *Confirm
|
||||
|
||||
func init() {
|
||||
C = &Confirm{}
|
||||
authboss.RegisterModule("confirm", C)
|
||||
}
|
||||
|
||||
type Confirm struct {
|
||||
logger io.Writer
|
||||
|
||||
config *authboss.Config
|
||||
emailTemplates views.Templates
|
||||
}
|
||||
|
||||
func (c *Confirm) Initialize(config *authboss.Config) (err error) {
|
||||
if config.Storer == nil {
|
||||
return errors.New("confirm: Need a Storer.")
|
||||
}
|
||||
|
||||
c.logger = config.LogWriter
|
||||
c.config = config
|
||||
|
||||
c.emailTemplates, err = views.Get(config.LayoutEmail, config.ViewsPath, tplConfirmHTML, tplConfirmText)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config.Callbacks.Before(authboss.EventGet, c.BeforeGet)
|
||||
config.Callbacks.After(authboss.EventRegister, c.AfterRegister)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Confirm) Routes() authboss.RouteTable {
|
||||
return authboss.RouteTable{
|
||||
"/confirm": c.confirmHandler,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Confirm) Storage() authboss.StorageOptions {
|
||||
return authboss.StorageOptions{
|
||||
UserConfirmToken: authboss.String,
|
||||
UserConfirmed: authboss.Bool,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Confirm) BeforeGet(ctx *authboss.Context) error {
|
||||
if intf, ok := ctx.User[UserConfirmed]; ok {
|
||||
if confirmed, ok := intf.(bool); ok && confirmed {
|
||||
return ErrNotConfirmed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AfterRegister ensures the account is not activated.
|
||||
func (c *Confirm) AfterRegister(ctx *authboss.Context) {
|
||||
if ctx.User == nil {
|
||||
fmt.Fprintln(c.logger, "confirm: user not loaded in after register callback")
|
||||
}
|
||||
|
||||
token := make([]byte, 32)
|
||||
if _, err := rand.Read(token); err != nil {
|
||||
fmt.Fprintln(c.logger, "confirm: failed to produce random token:", err)
|
||||
}
|
||||
sum := md5.Sum(token)
|
||||
|
||||
ctx.User[UserConfirmToken] = base64.StdEncoding.EncodeToString(sum[:])
|
||||
|
||||
if err := ctx.SaveUser(username, c.config.Storer); err != nil {
|
||||
fmt.Fprintln(c.logger, "confirm: failed to save user:", err)
|
||||
}
|
||||
|
||||
if email, ok := ctx.User.String("email"); !ok {
|
||||
fmt.Fprintln(c.logger, "confirm: user has no e-mail address to send to, could not send confirm e-mail")
|
||||
} else {
|
||||
go c.confirmEmail(email, base64.URLEncoding.EncodeToString(sum[:]))
|
||||
}
|
||||
}
|
||||
|
||||
// confirmEmail sends a confirmation e-mail.
|
||||
func (c *Confirm) confirmEmail(to, token string) {
|
||||
url := fmt.Sprintf("%s/recover/complete?token=%s", c.config.HostName, token)
|
||||
|
||||
htmlEmailBody := &bytes.Buffer{}
|
||||
if err := c.emailTemplates.ExecuteTemplate(htmlEmailBody, tplConfirmHTML, url); err != nil {
|
||||
fmt.Fprintln(c.logger, "confirm: failed to build html template:", err)
|
||||
}
|
||||
|
||||
textEmailBody := &bytes.Buffer{}
|
||||
if err := c.emailTemplates.ExecuteTemplate(textEmailBody, tplConfirmText, url); err != nil {
|
||||
fmt.Fprintln(c.logger, "confirm: failed to build plaintext template:", err)
|
||||
}
|
||||
|
||||
if err := m.config.Mailer.Send(authboss.Email{
|
||||
To: []string{to},
|
||||
From: c.config.EmailFrom,
|
||||
Subject: c.config.EmailSubjectPrefix + "Confirm New Account",
|
||||
TextBody: textEmailBody.String(),
|
||||
HTMLBody: htmlEmailBody.String(),
|
||||
}); err != nil {
|
||||
fmt.Fprintln(c.logger, "confirm: failed to build plaintext template:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Confirm) confirmHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := authboss.ContextFromRequest(r)
|
||||
|
||||
u, err := ctx.LoadUser(authboss.SessionKey, c.config.Storer)
|
||||
if err != nil {
|
||||
// 500
|
||||
}
|
||||
|
||||
ctx.FirstFormValue(FormValueConfirm)
|
||||
|
||||
token, ok := ctx.User.String(UserConfirmToken)
|
||||
if !ok {
|
||||
// Redirect no error
|
||||
}
|
||||
|
||||
tok, err := base64.URLEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
// Redirect no error
|
||||
}
|
||||
|
||||
dbTok := base64.StdEncoding.EncodeToString(tok)
|
||||
|
||||
// Redirect to / with flash message.
|
||||
// Log user in.
|
||||
// Overwrite dbTok with empty string.
|
||||
}
|
@@ -1,4 +1,6 @@
|
||||
// Package expire implements user
|
||||
// Package expire implements user session timeouts.
|
||||
// To take advantage of this the expire.Middleware must be installed
|
||||
// into your http stack.
|
||||
package expire
|
||||
|
||||
import (
|
||||
@@ -71,9 +73,9 @@ type middleware struct {
|
||||
next http.Handler
|
||||
}
|
||||
|
||||
// TouchMiddleware ensures that the user's expiry information is kept up-to-date
|
||||
// Middleware ensures that the user's expiry information is kept up-to-date
|
||||
// on each request.
|
||||
func TouchMiddleware(sessionMaker authboss.SessionStoreMaker, next http.Handler) http.Handler {
|
||||
func Middleware(sessionMaker authboss.SessionStoreMaker, next http.Handler) http.Handler {
|
||||
return middleware{sessionMaker, next}
|
||||
}
|
||||
|
||||
|
@@ -88,7 +88,7 @@ func TestExpire_Middleware(t *testing.T) {
|
||||
maker := func(w http.ResponseWriter, r *http.Request) authboss.ClientStorer { return session }
|
||||
|
||||
handler := new(testHandler)
|
||||
touch := TouchMiddleware(maker, handler)
|
||||
touch := Middleware(maker, handler)
|
||||
|
||||
touch.ServeHTTP(nil, nil)
|
||||
if !*handler {
|
||||
|
@@ -30,7 +30,7 @@ func init() {
|
||||
}
|
||||
|
||||
type Lock struct {
|
||||
storer authboss.TokenStorer
|
||||
storer authboss.Storer
|
||||
logger io.Writer
|
||||
|
||||
attempts int
|
||||
@@ -66,13 +66,14 @@ func (l *Lock) Storage() authboss.StorageOptions {
|
||||
return authboss.StorageOptions{
|
||||
UserAttemptNumber: authboss.Integer,
|
||||
UserAttemptTime: authboss.DateTime,
|
||||
UserLocked: authboss.Bool,
|
||||
}
|
||||
}
|
||||
|
||||
// BeforeAuth ensures the account is not locked.
|
||||
func (l *Lock) BeforeAuth(ctx *authboss.Context) error {
|
||||
if ctx.User == nil {
|
||||
return nil
|
||||
return errors.New("lock: user not loaded in before auth callback")
|
||||
}
|
||||
|
||||
if intf, ok := ctx.User[UserLocked]; ok {
|
||||
@@ -87,7 +88,7 @@ func (l *Lock) BeforeAuth(ctx *authboss.Context) error {
|
||||
// AfterAuth resets the attempt number field.
|
||||
func (l *Lock) AfterAuth(ctx *authboss.Context) {
|
||||
if ctx.User == nil {
|
||||
return
|
||||
fmt.Fprintln(l.logger, "lock: user not loaded in after auth callback")
|
||||
}
|
||||
|
||||
var username string
|
||||
|
@@ -28,6 +28,7 @@ func setup(keyValuePairs interface{}) {
|
||||
|
||||
func TestBeforeAuth(t *testing.T) {
|
||||
ctx := authboss.NewContext()
|
||||
L.logger = ioutil.Discard
|
||||
|
||||
if nil != L.BeforeAuth(ctx) {
|
||||
t.Error("Expected it to break early.")
|
||||
|
@@ -168,7 +168,7 @@ func (m *RecoverModule) sendRecoverEmail(to string, token []byte) {
|
||||
fmt.Fprintf(m.config.LogWriter, errFormat, "failed to build html tpl", err)
|
||||
}
|
||||
|
||||
textEmaiLBody := &bytes.Buffer{}
|
||||
textEmailBody := &bytes.Buffer{}
|
||||
if err := m.emailTemplates.ExecuteTemplate(textEmaiLBody, tplInitTextEmail, data); err != nil {
|
||||
fmt.Fprintf(m.config.LogWriter, errFormat, "failed to build plaintext tpl", err)
|
||||
}
|
||||
@@ -177,8 +177,8 @@ func (m *RecoverModule) sendRecoverEmail(to string, token []byte) {
|
||||
To: []string{to},
|
||||
ToNames: []string{""},
|
||||
From: m.config.EmailFrom,
|
||||
Subject: "Password Reset",
|
||||
TextBody: textEmaiLBody.String(),
|
||||
Subject: m.config.EmailSubjectPrefix + "Password Reset",
|
||||
TextBody: textEmailBody.String(),
|
||||
HTMLBody: htmlEmailBody.String(),
|
||||
}); err != nil {
|
||||
fmt.Fprintf(m.config.LogWriter, errFormat, "failed to send email", err)
|
||||
|
24
storer.go
24
storer.go
@@ -64,6 +64,7 @@ type DataType int
|
||||
const (
|
||||
Integer DataType = iota
|
||||
String
|
||||
Bool
|
||||
DateTime
|
||||
)
|
||||
|
||||
@@ -75,6 +76,8 @@ func (d DataType) String() string {
|
||||
return "Integer"
|
||||
case String:
|
||||
return "String"
|
||||
case Bool:
|
||||
return "Bool"
|
||||
case DateTime:
|
||||
return "DateTime"
|
||||
}
|
||||
@@ -129,6 +132,18 @@ func (a Attributes) Int(key string) (int, bool) {
|
||||
return val, ok
|
||||
}
|
||||
|
||||
// Bool returns a single value as a bool.
|
||||
func (a Attributes) Bool(key string) (val bool, ok bool) {
|
||||
var inter interface{}
|
||||
inter, ok = a[key]
|
||||
if !ok {
|
||||
return val, ok
|
||||
}
|
||||
|
||||
val, ok = inter.(bool)
|
||||
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]
|
||||
@@ -177,6 +192,11 @@ func (a Attributes) Bind(strct interface{}) error {
|
||||
return fmt.Errorf("Bind: Field %s's type should be %s but was %s", k, reflect.String.String(), fieldType)
|
||||
}
|
||||
field.SetString(val)
|
||||
case bool:
|
||||
if fieldKind != reflect.Bool {
|
||||
return fmt.Errorf("Bind: Field %s's type should be %s but was %s", k, reflect.Bool.String(), fieldType)
|
||||
}
|
||||
field.SetBool(val)
|
||||
case time.Time:
|
||||
timeType := dateTimeType
|
||||
if fieldType != timeType {
|
||||
@@ -213,9 +233,7 @@ func Unbind(intf interface{}) Attributes {
|
||||
if field.Type() == dateTimeType {
|
||||
attr[name] = field.Interface()
|
||||
}
|
||||
case reflect.Int:
|
||||
attr[name] = field.Interface()
|
||||
case reflect.String:
|
||||
case reflect.Bool, reflect.String, reflect.Int:
|
||||
attr[name] = field.Interface()
|
||||
}
|
||||
}
|
||||
|
@@ -10,11 +10,12 @@ func TestAttributes_Names(t *testing.T) {
|
||||
attr := Attributes{
|
||||
"integer": 5,
|
||||
"string": "string",
|
||||
"bool": true,
|
||||
"date_time": time.Now(),
|
||||
}
|
||||
names := attr.Names()
|
||||
|
||||
found := map[string]bool{"integer": false, "string": false, "date_time": false}
|
||||
found := map[string]bool{"integer": false, "string": false, "bool": false, "date_time": false}
|
||||
for _, n := range names {
|
||||
found[n] = true
|
||||
}
|
||||
@@ -30,11 +31,12 @@ func TestAttributeMeta_Names(t *testing.T) {
|
||||
meta := AttributeMeta{
|
||||
"integer": Integer,
|
||||
"string": String,
|
||||
"bool": Bool,
|
||||
"date_time": DateTime,
|
||||
}
|
||||
names := meta.Names()
|
||||
|
||||
found := map[string]bool{"integer": false, "string": false, "date_time": false}
|
||||
found := map[string]bool{"integer": false, "string": false, "bool": false, "date_time": false}
|
||||
for _, n := range names {
|
||||
found[n] = true
|
||||
}
|
||||
@@ -53,6 +55,9 @@ func TestDataType_String(t *testing.T) {
|
||||
if String.String() != "String" {
|
||||
t.Error("Expected String:", String)
|
||||
}
|
||||
if Bool.String() != "Bool" {
|
||||
t.Error("Expected Bool:", String)
|
||||
}
|
||||
if DateTime.String() != "DateTime" {
|
||||
t.Error("Expected DateTime:", DateTime)
|
||||
}
|
||||
@@ -61,17 +66,20 @@ func TestDataType_String(t *testing.T) {
|
||||
func TestAttributes_Bind(t *testing.T) {
|
||||
anInteger := 5
|
||||
aString := "string"
|
||||
aBool := true
|
||||
aTime := time.Now()
|
||||
|
||||
data := Attributes{
|
||||
"integer": anInteger,
|
||||
"string": aString,
|
||||
"bool": aBool,
|
||||
"date_time": aTime,
|
||||
}
|
||||
|
||||
s := struct {
|
||||
Integer int
|
||||
String string
|
||||
Bool bool
|
||||
DateTime time.Time
|
||||
}{}
|
||||
|
||||
@@ -85,6 +93,9 @@ func TestAttributes_Bind(t *testing.T) {
|
||||
if s.String != aString {
|
||||
t.Error("String was not set.")
|
||||
}
|
||||
if s.Bool != aBool {
|
||||
t.Error("Bool was not set.")
|
||||
}
|
||||
if s.DateTime != aTime {
|
||||
t.Error("DateTime was not set.")
|
||||
}
|
||||
@@ -132,6 +143,13 @@ func TestAttributes_BindTypeFail(t *testing.T) {
|
||||
String int
|
||||
}{},
|
||||
},
|
||||
{
|
||||
Attr: Attributes{"bool": true},
|
||||
Err: "should be bool",
|
||||
ToBind: &struct {
|
||||
Bool string
|
||||
}{},
|
||||
},
|
||||
{
|
||||
Attr: Attributes{"date": time.Time{}},
|
||||
Err: "should be time.Time",
|
||||
@@ -155,16 +173,17 @@ func TestAttributes_Unbind(t *testing.T) {
|
||||
s1 := struct {
|
||||
Integer int
|
||||
String string
|
||||
Bool bool
|
||||
Time time.Time
|
||||
|
||||
SomethingElse1 int32
|
||||
SomethingElse2 *Config
|
||||
|
||||
unexported int
|
||||
}{5, "string", time.Now(), 5, nil, 5}
|
||||
}{5, "string", true, time.Now(), 5, nil, 5}
|
||||
|
||||
attr := Unbind(&s1)
|
||||
if len(attr) != 3 {
|
||||
if len(attr) != 4 {
|
||||
t.Error("Expected three fields, got:", len(attr))
|
||||
}
|
||||
|
||||
@@ -184,6 +203,14 @@ func TestAttributes_Unbind(t *testing.T) {
|
||||
t.Error("Underlying value is wrong:", val)
|
||||
}
|
||||
|
||||
if v, ok := attr["bool"]; !ok {
|
||||
t.Error("Could not find String entry.")
|
||||
} else if val, ok := v.(bool); !ok {
|
||||
t.Errorf("Underlying type is wrong: %T", v)
|
||||
} else if s1.Bool != val {
|
||||
t.Error("Underlying value is wrong:", val)
|
||||
}
|
||||
|
||||
if v, ok := attr["time"]; !ok {
|
||||
t.Error("Could not find Time entry.")
|
||||
} else if val, ok := v.(time.Time); !ok {
|
||||
|
Reference in New Issue
Block a user