1
0
mirror of https://github.com/volatiletech/authboss.git synced 2025-01-10 04:17:59 +02:00

Finish implementing and testing confirm

- Rejig tests to remember to test the smtp mailer
This commit is contained in:
Aaron L 2018-02-27 07:14:30 -08:00
parent 3638e4f047
commit 982025bbc3
12 changed files with 667 additions and 173 deletions

View File

@ -17,6 +17,12 @@ type Config struct {
// AuthLogoutOK is the redirect path after a log out.
AuthLogoutOK string
// ConfirmOK once a user has confirmed their account, where should they go
ConfirmOK string
// ConfirmNotOK is used by the middleware, when a user is still supposed to
// confirm their account, this is where they should be redirected to.
ConfirmNotOK string
// RecoverOK is the redirect path after a successful recovery of a password.
RecoverOK string

View File

@ -3,8 +3,8 @@ package confirm
import (
"context"
"crypto/md5"
"crypto/rand"
"crypto/sha512"
"encoding/base64"
"fmt"
"net/http"
@ -14,45 +14,25 @@ import (
"github.com/pkg/errors"
"github.com/volatiletech/authboss"
"github.com/volatiletech/authboss/internal/response"
)
// Storer and FormValue constants
const (
StoreConfirmToken = "confirm_token"
StoreConfirmed = "confirmed"
// PageConfirm is only really used for the BodyReader
PageConfirm = "confirm"
// EmailConfirmHTML is the name of the html template for e-mails
EmailConfirmHTML = "confirm_html"
// EmailConfirmTxt is the name of the text template for e-mails
EmailConfirmTxt = "confirm_txt"
// FormValueConfirm is the name of the form value for
FormValueConfirm = "cnf"
tplConfirmHTML = "confirm_email.html.tpl"
tplConfirmText = "confirm_email.txt.tpl"
// DataConfirmURL is the name of the e-mail template variable
// that gives the url to send to the user for confirmation.
DataConfirmURL = "url"
)
var (
errUserMissing = errors.New("after registration user must be loaded")
)
// ConfirmStoreLoader allows lookup of users by different parameters.
type ConfirmStoreLoader interface {
authboss.StoreLoader
// ConfirmUser looks up a user by a confirm token. See confirm module for
// attribute names. If the token is not found in the data store,
// simply return nil, ErrUserNotFound.
LoadByConfirmToken(confirmToken string) (ConfirmStorer, error)
}
// ConfirmStorer defines attributes for the confirm module.
type ConfirmStorer interface {
authboss.Storer
PutConfirmed(ctx context.Context, confirmed bool) error
PutConfirmToken(ctx context.Context, token string) error
GetConfirmed(ctx context.Context) (confirmed bool, err error)
GetConfirmToken(ctx context.Context) (token string, err error)
}
func init() {
authboss.RegisterModule("confirm", &Confirm{})
}
@ -62,134 +42,215 @@ type Confirm struct {
*authboss.Authboss
}
// Initialize the module
func (c *Confirm) Initialize(ab *authboss.Authboss) (err error) {
// Init module
func (c *Confirm) Init(ab *authboss.Authboss) (err error) {
c.Authboss = ab
c.Events.After(authboss.EventGetUser, func(ctx context.Context) error {
_, err := c.beforeGet(ctx)
if err = c.Authboss.Config.Core.MailRenderer.Load(EmailConfirmHTML, EmailConfirmTxt); err != nil {
return err
})
c.Events.Before(authboss.EventAuth, c.beforeGet)
c.Events.After(authboss.EventRegister, c.afterRegister)
}
c.Authboss.Config.Core.Router.Get("/confirm", c.Authboss.Config.Core.ErrorHandler.Wrap(c.Get))
c.Events.Before(authboss.EventAuth, c.PreventAuth)
c.Events.After(authboss.EventRegister, c.StartConfirmationWeb)
return nil
}
// Routes for the module
func (c *Confirm) Routes() authboss.RouteTable {
return authboss.RouteTable{
"/confirm": c.confirmHandler,
// PreventAuth stops the EventAuth from succeeding when a user is not confirmed
// This relies on the fact that the context holds the user at this point in time
// loaded by the auth module (or something else).
func (c *Confirm) PreventAuth(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
logger := c.Authboss.RequestLogger(r)
user, err := c.Authboss.CurrentUser(w, r)
if err != nil {
return false, err
}
cuser := authboss.MustBeConfirmable(user)
if cuser.GetConfirmed() {
logger.Infof("user %s was confirmed, allowing auth", user.GetPID())
return false, nil
}
logger.Infof("user %s was not confirmed, preventing auth", user.GetPID())
ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
RedirectPath: c.Authboss.Config.Paths.ConfirmNotOK,
Failure: "Your account has not been confirmed, please check your e-mail.",
}
return true, c.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
}
// Templates returns the list of templates required by this module
func (c *Confirm) Templates() []string {
return []string{tplConfirmHTML, tplConfirmText}
// StartConfirmationWeb hijacks a request and forces a user to be confirmed first
// it's assumed that the current user is loaded into the request context.
func (c *Confirm) StartConfirmationWeb(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
user, err := c.Authboss.CurrentUser(w, r)
if err != nil {
return false, err
}
cuser := authboss.MustBeConfirmable(user)
if err = c.StartConfirmation(r.Context(), cuser, true); err != nil {
return false, err
}
ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
RedirectPath: c.Authboss.Config.Paths.ConfirmNotOK,
Success: "Please verify your account, an e-mail has been sent to you.",
}
return true, c.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
}
func (c *Confirm) beforeGet(ctx context.Context) (authboss.Interrupt, error) {
if confirmed, err := ctx.User.BoolErr(StoreConfirmed); err != nil {
return authboss.InterruptNone, err
} else if !confirmed {
return authboss.InterruptAccountNotConfirmed, nil
}
// StartConfirmation begins confirmation on a user by setting them to require confirmation
// via a created token, and optionally sending them an e-mail.
func (c *Confirm) StartConfirmation(ctx context.Context, user authboss.ConfirmableUser, sendEmail bool) error {
logger := c.Authboss.Logger(ctx)
return authboss.InterruptNone, nil
}
// AfterRegister ensures the account is not activated.
func (c *Confirm) afterRegister(ctx context.Context) error {
if ctx.User == nil {
return errUserMissing
}
token := make([]byte, 32)
if _, err := rand.Read(token); err != nil {
return err
}
sum := md5.Sum(token)
ctx.User[StoreConfirmToken] = base64.StdEncoding.EncodeToString(sum[:])
if err := ctx.SaveUser(); err != nil {
return err
}
email, err := ctx.User.StringErr(authboss.StoreEmail)
hash, token, err := GenerateToken()
if err != nil {
return err
}
goConfirmEmail(c, ctx, email, base64.URLEncoding.EncodeToString(token))
user.PutConfirmed(false)
user.PutConfirmToken(hash)
logger.Infof("generated new confirm token for user: %s", user.GetPID())
if err := c.Authboss.Config.Storage.Server.Save(ctx, user); err != nil {
return errors.Wrap(err, "failed to save user during StartConfirmation, user data may be in weird state")
}
goConfirmEmail(c, ctx, user.GetEmail(), token)
return nil
}
// This is here so it can be mocked out by a test
var goConfirmEmail = func(c *Confirm, ctx context.Context, to, token string) {
if ctx.MailMaker != nil {
c.confirmEmail(ctx, to, token)
} else {
go c.confirmEmail(ctx, to, token)
}
go c.SendConfirmEmail(ctx, to, token)
}
// confirmEmail sends a confirmation e-mail.
func (c *Confirm) confirmEmail(ctx context.Context, to, token string) {
p := path.Join(c.MountPath, "confirm")
url := fmt.Sprintf("%s%s?%s=%s", c.RootURL, p, url.QueryEscape(FormValueConfirm), url.QueryEscape(token))
// SendConfirmEmail sends a confirmation e-mail to a user
func (c *Confirm) SendConfirmEmail(ctx context.Context, to, token string) {
logger := c.Authboss.Logger(ctx)
p := path.Join(c.Config.Paths.Mount, "confirm")
url := fmt.Sprintf("%s%s?%s=%s", c.Paths.RootURL, p, url.QueryEscape(FormValueConfirm), url.QueryEscape(token))
email := authboss.Email{
To: []string{to},
From: c.EmailFrom,
Subject: c.EmailSubjectPrefix + "Confirm New Account",
From: c.Config.Mail.From,
Subject: c.Config.Mail.SubjectPrefix + "Confirm New Account",
}
err := response.Email(ctx.Mailer, email, c.emailHTMLTemplates, tplConfirmHTML, c.emailTextTemplates, tplConfirmText, url)
if err != nil {
fmt.Fprintf(ctx.LogWriter, "confirm: Failed to send e-mail: %v", err)
logger.Infof("sending confirm e-mail to: %s", to)
ro := authboss.EmailResponseOptions{
Data: authboss.NewHTMLData(DataConfirmURL, url),
HTMLTemplate: EmailConfirmHTML,
TextTemplate: EmailConfirmTxt,
}
if err := c.Authboss.Email(ctx, email, ro); err != nil {
logger.Errorf("failed to send confirm e-mail to %s: %+v", to, err)
}
}
func (c *Confirm) confirmHandler(w http.ResponseWriter, r *http.Request) error {
token := r.FormValue(FormValueConfirm)
if len(token) == 0 {
return authboss.ClientDataErr{Name: FormValueConfirm}
}
// Get is a request that confirms a user with a valid token
func (c *Confirm) Get(w http.ResponseWriter, r *http.Request) error {
logger := c.RequestLogger(r)
toHash, err := base64.URLEncoding.DecodeString(token)
validator, err := c.Authboss.Config.Core.BodyReader.Read(PageConfirm, r)
if err != nil {
return authboss.ErrAndRedirect{
Location: "/", Err: errors.Wrapf(err, "token failed to decode %q", token),
}
return err
}
sum := md5.Sum(toHash)
if errs := validator.Validate(); errs != nil {
logger.Infof("validation failed in Confirm.Get, this typically means a bad token: %+v", errs)
ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
Failure: "Invalid confirm token.",
RedirectPath: c.Authboss.Config.Paths.ConfirmNotOK,
}
return c.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
}
dbTok := base64.StdEncoding.EncodeToString(sum[:])
user, err := ctx.Storer.(ConfirmStorer).ConfirmUser(dbTok)
values := authboss.MustHaveConfirmValues(validator)
toHash, err := base64.URLEncoding.DecodeString(values.GetToken())
if err != nil {
logger.Infof("error decoding token in Confirm.Get, this typically means a bad token: %s %+v", values.GetToken(), err)
ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
Failure: "Invalid confirm token.",
RedirectPath: c.Authboss.Config.Paths.ConfirmNotOK,
}
return c.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
}
sum := sha512.Sum512(toHash)
token := base64.StdEncoding.EncodeToString(sum[:])
storer := authboss.EnsureCanConfirm(c.Authboss.Config.Storage.Server)
user, err := storer.LoadByToken(r.Context(), token)
if err == authboss.ErrUserNotFound {
return authboss.ErrAndRedirect{Location: "/", Err: errors.New("token not found")}
logger.Infof("confirm token was not found in database: %s", token)
ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
Failure: "Invalid confirm token.",
RedirectPath: c.Authboss.Config.Paths.ConfirmNotOK,
}
return c.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
} else if err != nil {
return err
}
ctx.User = authboss.Unbind(user)
user.PutConfirmToken("")
user.PutConfirmed(true)
ctx.User[StoreConfirmToken] = ""
ctx.User[StoreConfirmed] = true
key, err := ctx.User.StringErr(c.PrimaryID)
if err != nil {
logger.Infof("user %s confirmed their account", user.GetPID())
if err = c.Authboss.Config.Storage.Server.Save(r.Context(), user); err != nil {
return err
}
if err := ctx.SaveUser(); err != nil {
return err
ro := authboss.RedirectOptions{
Code: http.StatusTemporaryRedirect,
Success: "You have successfully confirmed your account.",
RedirectPath: c.Authboss.Config.Paths.ConfirmOK,
}
ctx.SessionStorer.Put(authboss.SessionKey, key)
response.Redirect(ctx, w, r, c.RegisterOKPath, "You have successfully confirmed your account.", "", true)
return nil
return c.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
}
// Middleware ensures that a user is confirmed, or else it will intercept the request
// and send them to the confirm page, this will load the user if he's not been loaded
// yet from the session.
//
// Panics if the user was not able to be loaded in order to allow a panic handler to show
// a nice error page, also panics if it failed to redirect for whatever reason.
// TODO(aarondl): Document this middleware better
func Middleware(ab *authboss.Authboss, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := ab.LoadCurrentUserP(w, &r)
cu := authboss.MustBeConfirmable(user)
if cu.GetConfirmed() {
next.ServeHTTP(w, r)
return
}
logger := ab.RequestLogger(r)
logger.Infof("user %s prevented from accessing %s: not confirmed", user.GetPID(), r.URL.Path)
})
}
// GenerateToken creates a random token that will be used to confirm the user.
func GenerateToken() (hash string, token string, err error) {
tok := make([]byte, 32)
if _, err := rand.Read(tok); err != nil {
return "", "", err
}
sum := sha512.Sum512(tok)
return base64.StdEncoding.EncodeToString(sum[:]), base64.URLEncoding.EncodeToString(tok[:]), nil
}

View File

@ -2,21 +2,391 @@ package confirm
import (
"bytes"
"crypto/md5"
"context"
"crypto/sha512"
"encoding/base64"
"html/template"
"errors"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
"github.com/pkg/errors"
"github.com/volatiletech/authboss"
"github.com/volatiletech/authboss/internal/mocks"
)
func TestInit(t *testing.T) {
t.Parallel()
ab := authboss.New()
router := &mocks.Router{}
renderer := &mocks.Renderer{}
errHandler := &mocks.ErrorHandler{}
ab.Config.Core.Router = router
ab.Config.Core.MailRenderer = renderer
ab.Config.Core.ErrorHandler = errHandler
c := &Confirm{}
if err := c.Init(ab); err != nil {
t.Fatal(err)
}
if err := renderer.HasLoadedViews(EmailConfirmHTML, EmailConfirmTxt); err != nil {
t.Error(err)
}
if err := router.HasGets("/confirm"); err != nil {
t.Error(err)
}
}
type testHarness struct {
confirm *Confirm
ab *authboss.Authboss
bodyReader *mocks.BodyReader
mailer *mocks.Emailer
redirector *mocks.Redirector
renderer *mocks.Renderer
responder *mocks.Responder
session *mocks.ClientStateRW
storer *mocks.ServerStorer
}
func testSetup() *testHarness {
harness := &testHarness{}
harness.ab = authboss.New()
harness.bodyReader = &mocks.BodyReader{}
harness.mailer = &mocks.Emailer{}
harness.redirector = &mocks.Redirector{}
harness.renderer = &mocks.Renderer{}
harness.responder = &mocks.Responder{}
harness.session = mocks.NewClientRW()
harness.storer = mocks.NewServerStorer()
harness.ab.Paths.ConfirmOK = "/confirm/ok"
harness.ab.Paths.ConfirmNotOK = "/confirm/not/ok"
harness.ab.Config.Core.BodyReader = harness.bodyReader
harness.ab.Config.Core.Logger = mocks.Logger{}
harness.ab.Config.Core.Mailer = harness.mailer
harness.ab.Config.Core.Redirector = harness.redirector
harness.ab.Config.Core.MailRenderer = harness.renderer
harness.ab.Config.Core.Responder = harness.responder
harness.ab.Config.Storage.SessionState = harness.session
harness.ab.Config.Storage.Server = harness.storer
harness.confirm = &Confirm{harness.ab}
return harness
}
func TestPreventAuthAllow(t *testing.T) {
t.Parallel()
harness := testSetup()
user := &mocks.User{
Confirmed: true,
}
r := mocks.Request("GET")
r = r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyUser, user))
w := httptest.NewRecorder()
handled, err := harness.confirm.PreventAuth(w, r, false)
if err != nil {
t.Error(err)
}
if handled {
t.Error("it should not have been handled")
}
}
func TestPreventDisallow(t *testing.T) {
t.Parallel()
harness := testSetup()
user := &mocks.User{
Confirmed: false,
}
r := mocks.Request("GET")
r = r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyUser, user))
w := httptest.NewRecorder()
handled, err := harness.confirm.PreventAuth(w, r, false)
if err != nil {
t.Error(err)
}
if !handled {
t.Error("it should have been handled")
}
if w.Code != http.StatusTemporaryRedirect {
t.Error("redirect did not occur")
}
if p := harness.redirector.Options.RedirectPath; p != "/confirm/not/ok" {
t.Error("redirect path was wrong:", p)
}
}
func TestStartConfirmationWeb(t *testing.T) {
// no t.Parallel(), global var mangling
oldConfirm := goConfirmEmail
goConfirmEmail = func(c *Confirm, ctx context.Context, to, token string) {
c.SendConfirmEmail(ctx, to, token)
}
defer func() {
goConfirmEmail = oldConfirm
}()
harness := testSetup()
user := &mocks.User{Email: "test@test.com"}
harness.storer.Users["test@test.com"] = user
r := mocks.Request("GET")
r = r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyUser, user))
w := httptest.NewRecorder()
handled, err := harness.confirm.StartConfirmationWeb(w, r, false)
if err != nil {
t.Error(err)
}
if !handled {
t.Error("it should always be handled")
}
if w.Code != http.StatusTemporaryRedirect {
t.Error("redirect did not occur")
}
if p := harness.redirector.Options.RedirectPath; p != "/confirm/not/ok" {
t.Error("redirect path was wrong:", p)
}
if to := harness.mailer.Email.To[0]; to != "test@test.com" {
t.Error("mailer sent e-mail to wrong person:", to)
}
}
func TestGetSuccess(t *testing.T) {
t.Parallel()
harness := testSetup()
hash, token, err := GenerateToken()
if err != nil {
t.Fatal(err)
}
user := &mocks.User{Email: "test@test.com", Confirmed: false, ConfirmToken: hash}
harness.storer.Users["test@test.com"] = user
harness.bodyReader.Return = mocks.Values{
Token: token,
}
r := mocks.Request("GET")
w := httptest.NewRecorder()
if err := harness.confirm.Get(w, r); err != nil {
t.Error(err)
}
if w.Code != http.StatusTemporaryRedirect {
t.Error("expected a redirect, got:", w.Code)
}
if p := harness.redirector.Options.RedirectPath; p != harness.ab.Paths.ConfirmOK {
t.Error("redir path was wrong:", p)
}
if len(user.ConfirmToken) != 0 {
t.Error("the confirm token should have been erased")
}
if !user.Confirmed {
t.Error("the user should have been confirmed")
}
}
func TestGetValidationFailure(t *testing.T) {
t.Parallel()
harness := testSetup()
harness.bodyReader.Return = mocks.Values{
Errors: []error{errors.New("fail")},
}
r := mocks.Request("GET")
w := httptest.NewRecorder()
if err := harness.confirm.Get(w, r); err != nil {
t.Error(err)
}
if w.Code != http.StatusTemporaryRedirect {
t.Error("expected a redirect, got:", w.Code)
}
if p := harness.redirector.Options.RedirectPath; p != harness.ab.Paths.ConfirmNotOK {
t.Error("redir path was wrong:", p)
}
if reason := harness.redirector.Options.Failure; reason != "Invalid confirm token." {
t.Error("reason for failure was wrong:", reason)
}
}
func TestGetBase64DecodeFailure(t *testing.T) {
t.Parallel()
harness := testSetup()
harness.bodyReader.Return = mocks.Values{
Token: "5",
}
r := mocks.Request("GET")
w := httptest.NewRecorder()
if err := harness.confirm.Get(w, r); err != nil {
t.Error(err)
}
if w.Code != http.StatusTemporaryRedirect {
t.Error("expected a redirect, got:", w.Code)
}
if p := harness.redirector.Options.RedirectPath; p != harness.ab.Paths.ConfirmNotOK {
t.Error("redir path was wrong:", p)
}
if reason := harness.redirector.Options.Failure; reason != "Invalid confirm token." {
t.Error("reason for failure was wrong:", reason)
}
}
func TestGetUserNotFoundFailure(t *testing.T) {
t.Parallel()
harness := testSetup()
_, token, err := GenerateToken()
if err != nil {
t.Fatal(err)
}
harness.bodyReader.Return = mocks.Values{
Token: token,
}
r := mocks.Request("GET")
w := httptest.NewRecorder()
if err := harness.confirm.Get(w, r); err != nil {
t.Error(err)
}
if w.Code != http.StatusTemporaryRedirect {
t.Error("expected a redirect, got:", w.Code)
}
if p := harness.redirector.Options.RedirectPath; p != harness.ab.Paths.ConfirmNotOK {
t.Error("redir path was wrong:", p)
}
if reason := harness.redirector.Options.Failure; reason != "Invalid confirm token." {
t.Error("reason for failure was wrong:", reason)
}
}
func TestMiddlewareAllow(t *testing.T) {
t.Parallel()
ab := authboss.New()
called := false
server := Middleware(ab, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
}))
user := &mocks.User{
Confirmed: true,
}
r := mocks.Request("GET")
r = r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyUser, user))
w := httptest.NewRecorder()
server.ServeHTTP(w, r)
if !called {
t.Error("The user should have been allowed through")
}
}
func TestMiddlewareDisallow(t *testing.T) {
t.Parallel()
ab := authboss.New()
ab.Config.Core.Logger = mocks.Logger{}
called := false
server := Middleware(ab, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
}))
user := &mocks.User{
Confirmed: false,
}
r := mocks.Request("GET")
r = r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyUser, user))
w := httptest.NewRecorder()
server.ServeHTTP(w, r)
if called {
t.Error("The user should not have been allowed through")
}
}
func TestGenerateToken(t *testing.T) {
t.Parallel()
hash, token, err := GenerateToken()
if err != nil {
t.Error(err)
}
// base64 length: n = 64; 4*(64/3) = 85.3; round to nearest 4: 88
if len(hash) != 88 {
t.Errorf("string length was wrong (%d): %s", len(hash), hash)
}
// base64 length: n = 32; 4*(32/3) = 42.6; round to nearest 4: 44
if len(token) != 44 {
t.Errorf("string length was wrong (%d): %s", len(token), token)
}
rawToken, err := base64.URLEncoding.DecodeString(token)
if err != nil {
t.Error(err)
}
rawHash, err := base64.StdEncoding.DecodeString(hash)
if err != nil {
t.Error(err)
}
checkHash := sha512.Sum512(rawToken)
if 0 != bytes.Compare(checkHash[:], rawHash) {
t.Error("expected hashes to match")
}
}
/*
func setup() *Confirm {
ab := authboss.New()
ab.Storage.Server = mocks.NewMockStorer()
@ -240,3 +610,4 @@ func TestConfirm_Confirm(t *testing.T) {
t.Error("Should have left a nice message.")
}
}
*/

View File

@ -49,6 +49,8 @@ func (a *Authboss) CurrentUserIDP(w http.ResponseWriter, r *http.Request) string
}
// CurrentUser retrieves the current user from the session and the database.
// Before the user is loaded from the database the context key is checked.
// If the session doesn't have the user ID ErrUserNotFound will be returned.
func (a *Authboss) CurrentUser(w http.ResponseWriter, r *http.Request) (User, error) {
if user := r.Context().Value(CTXKeyUser); user != nil {
return user.(User), nil
@ -58,7 +60,7 @@ func (a *Authboss) CurrentUser(w http.ResponseWriter, r *http.Request) (User, er
if err != nil {
return nil, err
} else if len(pid) == 0 {
return nil, nil
return nil, ErrUserNotFound
}
return a.currentUser(r.Context(), pid)

View File

@ -41,5 +41,5 @@ func (e errorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
e.LogWriter.Error(fmt.Sprintf("error at %s: %+v", r.URL.String(), err))
e.LogWriter.Error(fmt.Sprintf("request error from (%s) %s: %+v", r.RemoteAddr, r.URL.String(), err))
}

View File

@ -56,23 +56,6 @@ func TestMailer(t *testing.T) {
}
}
func TestSMTPMailer(t *testing.T) {
t.Parallel()
_ = NewSMTPMailer("server", nil)
recovered := false
defer func() {
recovered = recover() != nil
}()
NewSMTPMailer("", nil)
if !recovered {
t.Error("Should have panicd.")
}
}
func TestBoundary(t *testing.T) {
t.Parallel()

View File

@ -0,0 +1,24 @@
package defaults
import "testing"
func TestSMTPMailer(t *testing.T) {
t.Skip("must implement test against real smtp servers here")
}
func TestSMTPMailerPanic(t *testing.T) {
t.Parallel()
_ = NewSMTPMailer("server", nil)
recovered := false
defer func() {
recovered = recover() != nil
}()
NewSMTPMailer("", nil)
if !recovered {
t.Error("Should have panicked.")
}
}

View File

@ -33,6 +33,7 @@ type User struct {
}
func (m User) GetPID() string { return m.Email }
func (m User) GetEmail() string { return m.Email }
func (m User) GetUsername() string { return m.Username }
func (m User) GetPassword() string { return m.Password }
func (m User) GetRecoverToken() string { return m.RecoverToken }
@ -114,6 +115,17 @@ func (s *ServerStorer) Save(ctx context.Context, user authboss.User) error {
return nil
}
// LoadByToken finds a user by his token
func (s *ServerStorer) LoadByToken(ctx context.Context, token string) (authboss.ConfirmableUser, error) {
for _, v := range s.Users {
if v.ConfirmToken == token {
return v, nil
}
}
return nil, authboss.ErrUserNotFound
}
/*
// TODO(aarondl): What is this?
// AddToken for remember me
@ -428,6 +440,17 @@ func (r *Redirector) Redirect(w http.ResponseWriter, req *http.Request, ro authb
return nil
}
// Emailer that holds the options it was given
type Emailer struct {
Email authboss.Email
}
// Send an e-mail
func (e *Emailer) Send(ctx context.Context, email authboss.Email) error {
e.Email = email
return nil
}
// BodyReader reads the body of a request and returns some values
type BodyReader struct {
Return authboss.Validator
@ -442,6 +465,7 @@ func (b BodyReader) Read(page string, r *http.Request) (authboss.Validator, erro
type Values struct {
PID string
Password string
Token string
Errors []error
}
@ -456,6 +480,11 @@ func (v Values) GetPassword() string {
return v.Password
}
// GetToken from values
func (v Values) GetToken() string {
return v.Token
}
// Validate the values
func (v Values) Validate() []error {
return v.Errors

View File

@ -1,11 +1,31 @@
package authboss
import (
"context"
"net/http"
"github.com/pkg/errors"
)
// HTTPResponder knows how to respond to an HTTP request
// Must consider:
// - Flash messages
// - XSRF handling (template data)
// - Assembling template data from various sources
//
// Authboss controller methods (like the one called in response to POST /auth/login)
// will call this method to write a response to the user.
type HTTPResponder interface {
Respond(w http.ResponseWriter, r *http.Request, code int, templateName string, data HTMLData) error
}
// HTTPRedirector redirects http requests to a different url (must handle both json and html)
// When an authboss controller wants to redirect a user to a different path, it will use
// this interface.
type HTTPRedirector interface {
Redirect(w http.ResponseWriter, r *http.Request, ro RedirectOptions) error
}
// RedirectOptions packages up all the pieces a module needs to write out a
// response.
type RedirectOptions struct {
@ -36,29 +56,8 @@ type EmailResponseOptions struct {
TextTemplate string
}
// HTTPResponder knows how to respond to an HTTP request
// Must consider:
// - Flash messages
// - XSRF handling (template data)
// - Assembling template data from various sources
//
// Authboss controller methods (like the one called in response to POST /auth/login)
// will call this method to write a response to the user.
type HTTPResponder interface {
Respond(w http.ResponseWriter, r *http.Request, code int, templateName string, data HTMLData) error
}
// HTTPRedirector redirects http requests to a different url (must handle both json and html)
// When an authboss controller wants to redirect a user to a different path, it will use
// this interface.
type HTTPRedirector interface {
Redirect(w http.ResponseWriter, r *http.Request, ro RedirectOptions) error
}
// Email renders the e-mail templates and sends it using the mailer.
func (a *Authboss) Email(w http.ResponseWriter, r *http.Request, email Email, ro EmailResponseOptions) error {
ctx := r.Context()
func (a *Authboss) Email(ctx context.Context, email Email, ro EmailResponseOptions) error {
if len(ro.HTMLTemplate) != 0 {
htmlBody, _, err := a.Core.MailRenderer.Render(ctx, ro.HTMLTemplate, ro.Data)
if err != nil {

View File

@ -64,7 +64,7 @@ type CreatingServerStorer interface {
// ConfirmingServerStorer can find a user by a confirm token
type ConfirmingServerStorer interface {
Load(ctx context.Context, token string) (User, error)
LoadByToken(ctx context.Context, token string) (ConfirmableUser, error)
}
// EnsureCanCreate makes sure the server storer supports create operations

View File

@ -30,9 +30,11 @@ type ConfirmableUser interface {
GetConfirmed() (confirmed bool)
GetConfirmToken() (token string)
GetEmail() (email string)
PutConfirmed(confirmed bool)
PutConfirmToken(token string)
PutEmail(email string)
}
// ArbitraryUser allows arbitrary data from the web form through. You should

View File

@ -31,16 +31,6 @@ type UserValuer interface {
GetPassword() string
}
// MustHaveUserValues upgrades a validatable set of values
// to ones specific to the user.
func MustHaveUserValues(v Validator) UserValuer {
if u, ok := v.(UserValuer); ok {
return u
}
panic(fmt.Sprintf("bodyreader returned a type that could not be upgraded to UserValuer: %T", v))
}
// ArbitraryValuer provides the "rest" of the fields
// that aren't strictly needed for anything in particular,
// address, secondary e-mail, etc.
@ -58,3 +48,30 @@ type ArbitraryValuer interface {
GetValues() map[string]string
}
// ConfirmValuer allows us to pull out the token from the request
type ConfirmValuer interface {
Validator
GetToken() string
}
// MustHaveUserValues upgrades a validatable set of values
// to ones specific to an authenticating user.
func MustHaveUserValues(v Validator) UserValuer {
if u, ok := v.(UserValuer); ok {
return u
}
panic(fmt.Sprintf("bodyreader returned a type that could not be upgraded to UserValuer: %T", v))
}
// MustHaveConfirmValues upgrades a validatable set of values
// to ones specific to a user that needs to be confirmed.
func MustHaveConfirmValues(v Validator) ConfirmValuer {
if u, ok := v.(ConfirmValuer); ok {
return u
}
panic(fmt.Sprintf("bodyreader returned a type that could not be upgraded to ConfirmValuer: %T", v))
}