1
0
mirror of https://github.com/volatiletech/authboss.git synced 2025-02-09 13:47:09 +02:00

Get tests working after latest refactors

- Change changelog format to use keepachangelog standard
- Refactor the config to be made of substructs to help organize all the
  pieces
- Add the new interfaces to the configuration
- Clean up module loading (no unnecessary reflection to create new value)
- Change User interface to have a Get/SetPID not E-mail/Username, this
  way we don't ever have to refer to one or the other, we just always
  assume pid. In the case of Confirm/Recover we'll have to make a GetEmail
  or there won't be a way for us to get the e-mail to send to.
- Delete the xsrf nonsense in the core
This commit is contained in:
Aaron L 2018-02-01 15:42:48 -08:00
parent cbfc1d8388
commit de1c2ed081
28 changed files with 313 additions and 178 deletions

View File

@ -1,7 +1,20 @@
Changelog # Changelog
=========
## 2015-08-02 Change the way Bind/Unbind works The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [2.0.0] - 2018-01-??
### Added
### Changed
### Deprecated
### Removed
### Fixed
### Security
## 2015-08-02
### Changed
This change is potentially breaking, it did break the sample since the supporting struct was wrong for the data we were using. This change is potentially breaking, it did break the sample since the supporting struct was wrong for the data we were using.
**Lock:** The documentation was updated to reflect that the struct value for AttemptNumber is indeed an int64. **Lock:** The documentation was updated to reflect that the struct value for AttemptNumber is indeed an int64.
@ -11,6 +24,7 @@ and make them into a map. Now the field list will contain all types found in the
the type in the attribute map matches what's in the struct before assignment. the type in the attribute map matches what's in the struct before assignment.
## 2015-04-01 Refactor for Multi-tenancy ## 2015-04-01 Refactor for Multi-tenancy
### Changed
This breaking change allows multiple sites running off the same code base to each use different configurations of Authboss. To migrate This breaking change allows multiple sites running off the same code base to each use different configurations of Authboss. To migrate
your code simply use authboss.New() to get an instance of Authboss and all the old things that used to be in the authboss package are your code simply use authboss.New() to get an instance of Authboss and all the old things that used to be in the authboss package are
now there. See [this commit to the sample](https://github.com/volatiletech/authboss-sample/commit/eea55fc3b03855d4e9fb63577d72ce8ff0cd4079) now there. See [this commit to the sample](https://github.com/volatiletech/authboss-sample/commit/eea55fc3b03855d4e9fb63577d72ce8ff0cd4079)

View File

@ -13,9 +13,6 @@ import (
) )
const ( const (
methodGET = "GET"
methodPOST = "POST"
tplLogin = "login.html.tpl" tplLogin = "login.html.tpl"
) )
@ -26,7 +23,6 @@ func init() {
// Auth module // Auth module
type Auth struct { type Auth struct {
*authboss.Authboss *authboss.Authboss
templates response.Templates
} }
// Initialize module // Initialize module

View File

@ -21,7 +21,7 @@ func testSetup() (a *Auth, s *mocks.MockStorer) {
ab := authboss.New() ab := authboss.New()
ab.LogWriter = ioutil.Discard ab.LogWriter = ioutil.Discard
ab.Layout = template.Must(template.New("").Parse(`{{template "authboss" .}}`)) ab.Layout = template.Must(template.New("").Parse(`{{template "authboss" .}}`))
ab.Storer = s ab.Storage.Server = s
ab.XSRFName = "xsrf" ab.XSRFName = "xsrf"
ab.XSRFMaker = func(_ http.ResponseWriter, _ *http.Request) string { ab.XSRFMaker = func(_ http.ResponseWriter, _ *http.Request) string {
return "xsrfvalue" return "xsrfvalue"
@ -293,7 +293,7 @@ func TestAuth_validateCredentials(t *testing.T) {
ab := authboss.New() ab := authboss.New()
storer := mocks.NewMockStorer() storer := mocks.NewMockStorer()
ab.Storer = storer ab.Storage.Server = storer
ctx := ab.NewContext() ctx := ab.NewContext()
storer.Users["john"] = authboss.Attributes{"password": "$2a$10$pgFsuQwdhwOdZp/v52dvHeEi53ZaI7dGmtwK4bAzGGN5A4nT6doqm"} storer.Users["john"] = authboss.Attributes{"password": "$2a$10$pgFsuQwdhwOdZp/v52dvHeEi53ZaI7dGmtwK4bAzGGN5A4nT6doqm"}

View File

@ -11,34 +11,37 @@ import "github.com/pkg/errors"
// Authboss contains a configuration and other details for running. // Authboss contains a configuration and other details for running.
type Authboss struct { type Authboss struct {
Config Config
loadedModules map[string]bool loadedModules map[string]Moduler
viewRenderer Renderer
mailRenderer Renderer
} }
// New makes a new instance of authboss with a default // New makes a new instance of authboss with a default
// configuration. // configuration.
func New() *Authboss { func New() *Authboss {
ab := &Authboss{} ab := &Authboss{}
ab.loadedModules = make(map[string]Moduler)
ab.Config.Defaults() ab.Config.Defaults()
return ab return ab
} }
// Init authboss, modules, renderers // Init authboss, modules, renderers
func (a *Authboss) Init() error { func (a *Authboss) Init(modulesToLoad ...string) error {
//TODO(aarondl): Figure the template names out along with new "module" loading. if len(modulesToLoad) == 0 {
views := []string{"all"} modulesToLoad = RegisteredModules()
var err error
a.viewRenderer, err = a.Config.ViewLoader.Init(views)
if err != nil {
return errors.Wrap(err, "failed to load the view renderer")
} }
a.mailRenderer, err = a.Config.MailViewLoader.Init(views) for _, name := range modulesToLoad {
if err != nil { mod, ok := registeredModules[name]
return errors.Wrap(err, "failed to load the mail view renderer") if !ok {
return errors.Errorf("module %s was supposed to be loaded but is not registered", name)
}
a.loadedModules[name] = mod
// Initialize the module
if err := mod.Init(a); err != nil {
return errors.Wrapf(err, "failed to init module: %s", name)
}
} }
return nil return nil

View File

@ -1,7 +1,6 @@
package authboss package authboss
import ( import (
"io/ioutil"
"testing" "testing"
) )
@ -9,9 +8,6 @@ func TestAuthBossInit(t *testing.T) {
t.Parallel() t.Parallel()
ab := New() ab := New()
ab.LogWriter = ioutil.Discard
ab.ViewLoader = mockRenderLoader{}
ab.MailViewLoader = mockRenderLoader{}
err := ab.Init() err := ab.Init()
if err != nil { if err != nil {
t.Error("Unexpected error:", err) t.Error("Unexpected error:", err)

View File

@ -95,8 +95,8 @@ func (a *Authboss) NewResponse(w http.ResponseWriter, r *http.Request) *ClientSt
// LoadClientState loads the state from sessions and cookies into the request context // LoadClientState loads the state from sessions and cookies into the request context
func (a *Authboss) LoadClientState(w http.ResponseWriter, r *http.Request) (*http.Request, error) { func (a *Authboss) LoadClientState(w http.ResponseWriter, r *http.Request) (*http.Request, error) {
if a.SessionStateStorer != nil { if a.Storage.SessionState != nil {
state, err := a.SessionStateStorer.ReadState(w, r) state, err := a.Storage.SessionState.ReadState(w, r)
if err != nil { if err != nil {
return nil, err return nil, err
} else if state == nil { } else if state == nil {
@ -106,8 +106,8 @@ func (a *Authboss) LoadClientState(w http.ResponseWriter, r *http.Request) (*htt
ctx := context.WithValue(r.Context(), ctxKeySessionState, state) ctx := context.WithValue(r.Context(), ctxKeySessionState, state)
r = r.WithContext(ctx) r = r.WithContext(ctx)
} }
if a.CookieStateStorer != nil { if a.Storage.CookieState != nil {
state, err := a.CookieStateStorer.ReadState(w, r) state, err := a.Storage.CookieState.ReadState(w, r)
if err != nil { if err != nil {
return nil, err return nil, err
} else if state == nil { } else if state == nil {
@ -184,14 +184,14 @@ func (c *ClientStateResponseWriter) putClientState() error {
cookie = cookieStateIntf.(ClientState) cookie = cookieStateIntf.(ClientState)
} }
if c.ab.SessionStateStorer != nil { if c.ab.Storage.SessionState != nil {
err := c.ab.SessionStateStorer.WriteState(c, session, c.sessionStateEvents) err := c.ab.Storage.SessionState.WriteState(c, session, c.sessionStateEvents)
if err != nil { if err != nil {
return err return err
} }
} }
if c.ab.CookieStateStorer != nil { if c.ab.Storage.CookieState != nil {
err := c.ab.CookieStateStorer.WriteState(c, cookie, c.cookieStateEvents) err := c.ab.Storage.CookieState.WriteState(c, cookie, c.cookieStateEvents)
if err != nil { if err != nil {
return err return err
} }

View File

@ -12,8 +12,8 @@ func TestStateGet(t *testing.T) {
t.Parallel() t.Parallel()
ab := New() ab := New()
ab.SessionStateStorer = newMockClientStateRW("one", "two") ab.Storage.SessionState = newMockClientStateRW("one", "two")
ab.CookieStateStorer = newMockClientStateRW("three", "four") ab.Storage.CookieState = newMockClientStateRW("three", "four")
r := httptest.NewRequest("GET", "/", nil) r := httptest.NewRequest("GET", "/", nil)
w := ab.NewResponse(httptest.NewRecorder(), r) w := ab.NewResponse(httptest.NewRecorder(), r)
@ -36,7 +36,7 @@ func TestStateResponseWriterDoubleWritePanic(t *testing.T) {
t.Parallel() t.Parallel()
ab := New() ab := New()
ab.SessionStateStorer = newMockClientStateRW("one", "two") ab.Storage.SessionState = newMockClientStateRW("one", "two")
r := httptest.NewRequest("GET", "/", nil) r := httptest.NewRequest("GET", "/", nil)
w := ab.NewResponse(httptest.NewRecorder(), r) w := ab.NewResponse(httptest.NewRecorder(), r)
@ -58,8 +58,8 @@ func TestStateResponseWriterLastSecondWriteWithPrevious(t *testing.T) {
t.Parallel() t.Parallel()
ab := New() ab := New()
ab.SessionStateStorer = newMockClientStateRW("one", "two") ab.Storage.SessionState = newMockClientStateRW("one", "two")
ab.CookieStateStorer = newMockClientStateRW("three", "four") ab.Storage.CookieState = newMockClientStateRW("three", "four")
r := httptest.NewRequest("GET", "/", nil) r := httptest.NewRequest("GET", "/", nil)
var w http.ResponseWriter = httptest.NewRecorder() var w http.ResponseWriter = httptest.NewRecorder()
@ -85,7 +85,7 @@ func TestStateResponseWriterLastSecondWriteHeader(t *testing.T) {
t.Parallel() t.Parallel()
ab := New() ab := New()
ab.SessionStateStorer = newMockClientStateRW() ab.Storage.SessionState = newMockClientStateRW()
r := httptest.NewRequest("GET", "/", nil) r := httptest.NewRequest("GET", "/", nil)
w := ab.NewResponse(httptest.NewRecorder(), r) w := ab.NewResponse(httptest.NewRecorder(), r)
@ -103,7 +103,7 @@ func TestStateResponseWriterLastSecondWriteWrite(t *testing.T) {
t.Parallel() t.Parallel()
ab := New() ab := New()
ab.SessionStateStorer = newMockClientStateRW() ab.Storage.SessionState = newMockClientStateRW()
r := httptest.NewRequest("GET", "/", nil) r := httptest.NewRequest("GET", "/", nil)
w := ab.NewResponse(httptest.NewRecorder(), r) w := ab.NewResponse(httptest.NewRecorder(), r)
@ -155,7 +155,7 @@ func TestFlashClearer(t *testing.T) {
t.Parallel() t.Parallel()
ab := New() ab := New()
ab.SessionStateStorer = newMockClientStateRW(FlashSuccessKey, "a", FlashErrorKey, "b") ab.Storage.SessionState = newMockClientStateRW(FlashSuccessKey, "a", FlashErrorKey, "b")
r := httptest.NewRequest("GET", "/", nil) r := httptest.NewRequest("GET", "/", nil)
w := ab.NewResponse(httptest.NewRecorder(), r) w := ab.NewResponse(httptest.NewRecorder(), r)

161
config.go
View File

@ -7,90 +7,107 @@ import (
// Config holds all the configuration for both authboss and it's modules. // Config holds all the configuration for both authboss and it's modules.
type Config struct { type Config struct {
// MountPath is the path to mount authboss's routes at (eg /auth). Paths struct {
MountPath string // Mount is the path to mount authboss's routes at (eg /auth).
// ViewsPath is the path to search for overridden templates. Mount string
ViewsPath string
// RootURL is the scheme+host+port of the web application (eg https://www.happiness.com:8080) for url generation. No trailing slash.
RootURL string
// BCryptCost is the cost of the bcrypt password hashing function.
BCryptCost int
// PrimaryID is the primary identifier of the user. Set to one of: // AuthLoginOK is the redirect path after a successful authentication.
// authboss.StoreEmail, authboss.StoreUsername (StoreEmail is default) AuthLoginOK string
PrimaryID string // AuthLoginFail is the redirect path after a failed authentication.
AuthLoginFail string
// AuthLogoutOK is the redirect path after a log out.
AuthLogoutOK string
// ViewLoader loads the templates for the application. // RecoverOK is the redirect path after a successful recovery of a password.
ViewLoader RenderLoader RecoverOK string
// MailViewLoader loads the templates for mail. If this is nil, it will
// fall back to using the Renderer created from the ViewLoader instead.
MailViewLoader RenderLoader
// OAuth2Providers lists all providers that can be used. See // RegisterOK is the redirect path after a successful registration.
// OAuthProvider documentation for more details. RegisterOK string
OAuth2Providers map[string]OAuth2Provider
// AuthLoginOKPath is the redirect path after a successful authentication. // RootURL is the scheme+host+port of the web application (eg https://www.happiness.com:8080) for url generation. No trailing slash.
AuthLoginOKPath string RootURL string
// AuthLoginFailPath is the redirect path after a failed authentication. }
AuthLoginFailPath string
// AuthLogoutOKPath is the redirect path after a log out.
AuthLogoutOKPath string
// RecoverOKPath is the redirect path after a successful recovery of a password. Modules struct {
RecoverOKPath string // BCryptCost is the cost of the bcrypt password hashing function.
// RecoverTokenDuration controls how long a token sent via email for password BCryptCost int
// recovery is valid for.
RecoverTokenDuration time.Duration
// RegisterOKPath is the redirect path after a successful registration. // OAuth2Providers lists all providers that can be used. See
RegisterOKPath string // OAuthProvider documentation for more details.
OAuth2Providers map[string]OAuth2Provider
// Policies control validation of form fields and are automatically run // PreserveFields are fields used with registration that are to be rendered when
// against form posts that include the fields. // post fails.
Policies []Validator PreserveFields []string
// ConfirmFields are fields that are supposed to be submitted with confirmation
// fields alongside them, passwords, emails etc.
ConfirmFields []string
// PreserveFields are fields used with registration that are to be rendered when
// post fails.
PreserveFields []string
// ExpireAfter controls the time an account is idle before being logged out // ExpireAfter controls the time an account is idle before being logged out
// by the ExpireMiddleware. // by the ExpireMiddleware.
ExpireAfter time.Duration ExpireAfter time.Duration
// LockAfter this many tries. // RecoverTokenDuration controls how long a token sent via email for password
LockAfter int // recovery is valid for.
// LockWindow is the waiting time before the number of attemps are reset. RecoverTokenDuration time.Duration
LockWindow time.Duration
// LockDuration is how long an account is locked for.
LockDuration time.Duration
// EmailFrom is the email address authboss e-mails come from. // LockAfter this many tries.
EmailFrom string LockAfter int
// EmailSubjectPrefix is used to add something to the front of the authboss // LockWindow is the waiting time before the number of attemps are reset.
// email subjects. LockWindow time.Duration
EmailSubjectPrefix string // LockDuration is how long an account is locked for.
LockDuration time.Duration
}
// Storer is the interface through which Authboss accesses the web apps database Mail struct {
// for user operations. // From is the email address authboss e-mails come from.
Storer ServerStorer From string
// SubjectPrefix is used to add something to the front of the authboss
// email subjects.
SubjectPrefix string
}
// CookieStateStorer must be defined to provide an interface capapable of Storage struct {
// storing cookies for the given response, and reading them from the request. // Storer is the interface through which Authboss accesses the web apps database
CookieStateStorer ClientStateReadWriter // for user operations.
// SessionStateStorer must be defined to provide an interface capable of Server ServerStorer
// storing session-only values for the given response, and reading them
// from the request. // CookieState must be defined to provide an interface capapable of
SessionStateStorer ClientStateReadWriter // storing cookies for the given response, and reading them from the request.
// LogWriter is written to when errors occur, as well as on startup to show CookieState ClientStateReadWriter
// which modules are loaded and which routes they registered. By default // SessionState must be defined to provide an interface capable of
// writes to io.Discard. // storing session-only values for the given response, and reading them
LogWriter io.Writer // from the request.
// Mailer is the mailer being used to send e-mails out. Authboss defines two loggers for use SessionState ClientStateReadWriter
// LogMailer and SMTPMailer, the default is a LogMailer to io.Discard. }
Mailer Mailer
Core struct {
// Router is the entity that controls all routing to authboss routes
// modules will register their routes with it.
Router Router
// Responder takes a generic response from a controller and prepares
// the response, uses a renderer to create the body, and replies to the
// http request.
Responder HTTPResponder
// Redirector can redirect a response, similar to Responder but responsible
// only for redirection.
Redirector HTTPRedirector
// Validator helps validate an http request, it's given a name that describes
// the form it's validating so that conditional logic may be applied.
Validator Validator
// ViewRenderer loads the templates for the application.
ViewRenderer Renderer
// MailRenderer loads the templates for mail. If this is nil, it will
// fall back to using the Renderer created from the ViewLoader instead.
MailRenderer Renderer
// Mailer is the mailer being used to send e-mails out via smtp
Mailer Mailer
// LogWriter is written to when errors occur
LogWriter io.Writer
}
} }
// Defaults sets the configuration's default values. // Defaults sets the configuration's default values.

View File

@ -19,7 +19,7 @@ import (
func setup() *Confirm { func setup() *Confirm {
ab := authboss.New() ab := authboss.New()
ab.Storer = mocks.NewMockStorer() ab.Storage.Server = mocks.NewMockStorer()
ab.LayoutHTMLEmail = template.Must(template.New("").Parse(`email ^_^`)) ab.LayoutHTMLEmail = template.Must(template.New("").Parse(`email ^_^`))
ab.LayoutTextEmail = template.Must(template.New("").Parse(`email`)) ab.LayoutTextEmail = template.Must(template.New("").Parse(`email`))

View File

@ -76,7 +76,7 @@ func (a *Authboss) CurrentUserP(w http.ResponseWriter, r *http.Request) User {
} }
func (a *Authboss) currentUser(ctx context.Context, pid string) (User, error) { func (a *Authboss) currentUser(ctx context.Context, pid string) (User, error) {
user, err := a.Storer.Load(ctx, pid) user, err := a.Storage.Server.Load(ctx, pid)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -17,8 +17,8 @@ func loadClientStateP(ab *Authboss, w http.ResponseWriter, r *http.Request) *htt
func testSetupContext() (*Authboss, *http.Request) { func testSetupContext() (*Authboss, *http.Request) {
ab := New() ab := New()
ab.SessionStateStorer = newMockClientStateRW(SessionKey, "george-pid") ab.Storage.SessionState = newMockClientStateRW(SessionKey, "george-pid")
ab.Storer = mockServerStorer{ ab.Storage.Server = mockServerStorer{
"george-pid": mockUser{Email: "george-pid", Password: "unreadable"}, "george-pid": mockUser{Email: "george-pid", Password: "unreadable"},
} }
r := loadClientStateP(ab, nil, httptest.NewRequest("GET", "/", nil)) r := loadClientStateP(ab, nil, httptest.NewRequest("GET", "/", nil))
@ -39,8 +39,8 @@ func testSetupContextCached() (*Authboss, mockUser, *http.Request) {
func testSetupContextPanic() *Authboss { func testSetupContextPanic() *Authboss {
ab := New() ab := New()
ab.SessionStateStorer = newMockClientStateRW(SessionKey, "george-pid") ab.Storage.SessionState = newMockClientStateRW(SessionKey, "george-pid")
ab.Storer = mockServerStorer{} ab.Storage.Server = mockServerStorer{}
return ab return ab
} }
@ -80,7 +80,7 @@ func TestCurrentUserIDP(t *testing.T) {
ab := testSetupContextPanic() ab := testSetupContextPanic()
// Overwrite the setup functions state storer // Overwrite the setup functions state storer
ab.SessionStateStorer = newMockClientStateRW() ab.Storage.SessionState = newMockClientStateRW()
defer func() { defer func() {
if recover().(error) != ErrUserNotFound { if recover().(error) != ErrUserNotFound {
@ -101,7 +101,7 @@ func TestCurrentUser(t *testing.T) {
t.Error(err) t.Error(err)
} }
if got, err := user.GetEmail(context.TODO()); err != nil { if got, err := user.GetPID(context.TODO()); err != nil {
t.Error(err) t.Error(err)
} else if got != "george-pid" { } else if got != "george-pid" {
t.Error("got:", got) t.Error("got:", got)
@ -118,7 +118,7 @@ func TestCurrentUserContext(t *testing.T) {
t.Error(err) t.Error(err)
} }
if got, err := user.GetEmail(context.TODO()); err != nil { if got, err := user.GetPID(context.TODO()); err != nil {
t.Error(err) t.Error(err)
} else if got != "george-pid" { } else if got != "george-pid" {
t.Error("got:", got) t.Error("got:", got)
@ -198,7 +198,7 @@ func TestLoadCurrentUser(t *testing.T) {
t.Error(err) t.Error(err)
} }
if got, err := user.GetEmail(context.TODO()); err != nil { if got, err := user.GetPID(context.TODO()); err != nil {
t.Error(err) t.Error(err)
} else if got != "george-pid" { } else if got != "george-pid" {
t.Error("got:", got) t.Error("got:", got)

View File

@ -16,6 +16,10 @@ type testRenderer struct {
Callback func(context.Context, string, authboss.HTMLData) ([]byte, string, error) Callback func(context.Context, string, authboss.HTMLData) ([]byte, string, error)
} }
func (t testRenderer) Load(names ...string) error {
return nil
}
func (t testRenderer) Render(ctx context.Context, name string, data authboss.HTMLData) ([]byte, string, error) { func (t testRenderer) Render(ctx context.Context, name string, data authboss.HTMLData) ([]byte, string, error) {
return t.Callback(ctx, name, data) return t.Callback(ctx, name, data)
} }
@ -189,8 +193,8 @@ func TestResponseRedirectNonAPI(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
ab := authboss.New() ab := authboss.New()
ab.Config.SessionStateStorer = mocks.NewClientRW() ab.Config.Storage.SessionState = mocks.NewClientRW()
ab.Config.CookieStateStorer = mocks.NewClientRW() ab.Config.Storage.CookieState = mocks.NewClientRW()
aw := ab.NewResponse(w, r) aw := ab.NewResponse(w, r)
ro := authboss.RedirectOptions{ ro := authboss.RedirectOptions{
@ -228,8 +232,8 @@ func TestResponseRedirectNonAPIFollowRedir(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
ab := authboss.New() ab := authboss.New()
ab.Config.SessionStateStorer = mocks.NewClientRW() ab.Config.Storage.SessionState = mocks.NewClientRW()
ab.Config.CookieStateStorer = mocks.NewClientRW() ab.Config.Storage.CookieState = mocks.NewClientRW()
aw := ab.NewResponse(w, r) aw := ab.NewResponse(w, r)
ro := authboss.RedirectOptions{ ro := authboss.RedirectOptions{

View File

@ -61,7 +61,7 @@ func (a *Authboss) ExpireMiddleware(next http.Handler) http.Handler {
// below it. // below it.
func (m expireMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (m expireMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if _, ok := GetSession(r, SessionKey); ok { if _, ok := GetSession(r, SessionKey); ok {
ttl := timeToExpiry(r, m.ab.ExpireAfter) ttl := timeToExpiry(r, m.ab.Modules.ExpireAfter)
if ttl == 0 { if ttl == 0 {
DelSession(w, SessionKey) DelSession(w, SessionKey)
DelSession(w, SessionLastAction) DelSession(w, SessionLastAction)

View File

@ -10,7 +10,7 @@ import (
func TestExpireIsExpired(t *testing.T) { func TestExpireIsExpired(t *testing.T) {
ab := New() ab := New()
ab.SessionStateStorer = newMockClientStateRW( ab.Storage.SessionState = newMockClientStateRW(
SessionKey, "username", SessionKey, "username",
SessionLastAction, time.Now().UTC().Format(time.RFC3339), SessionLastAction, time.Now().UTC().Format(time.RFC3339),
) )
@ -26,7 +26,7 @@ func TestExpireIsExpired(t *testing.T) {
// No t.Parallel() - Also must be after refreshExpiry() call // No t.Parallel() - Also must be after refreshExpiry() call
nowTime = func() time.Time { nowTime = func() time.Time {
return time.Now().UTC().Add(ab.ExpireAfter * 2) return time.Now().UTC().Add(ab.Modules.ExpireAfter * 2)
} }
defer func() { defer func() {
nowTime = time.Now nowTime = time.Now
@ -72,8 +72,8 @@ func TestExpireIsExpired(t *testing.T) {
func TestExpireNotExpired(t *testing.T) { func TestExpireNotExpired(t *testing.T) {
ab := New() ab := New()
ab.Config.ExpireAfter = time.Hour ab.Config.Modules.ExpireAfter = time.Hour
ab.SessionStateStorer = newMockClientStateRW( ab.Storage.SessionState = newMockClientStateRW(
SessionKey, "username", SessionKey, "username",
SessionLastAction, time.Now().UTC().Format(time.RFC3339), SessionLastAction, time.Now().UTC().Format(time.RFC3339),
) )
@ -90,7 +90,7 @@ func TestExpireNotExpired(t *testing.T) {
} }
// No t.Parallel() - Also must be after refreshExpiry() call // No t.Parallel() - Also must be after refreshExpiry() call
newTime := time.Now().UTC().Add(ab.ExpireAfter / 2) newTime := time.Now().UTC().Add(ab.Modules.ExpireAfter / 2)
nowTime = func() time.Time { nowTime = func() time.Time {
return newTime return newTime
} }

View File

@ -31,7 +31,7 @@ type User struct {
} }
func (m User) GetUsername(context.Context) (string, error) { return m.Username, nil } func (m User) GetUsername(context.Context) (string, error) { return m.Username, nil }
func (m User) GetEmail(context.Context) (string, error) { return m.Email, nil } func (m User) GetPID(context.Context) (string, error) { return m.Email, nil }
func (m User) GetPassword(context.Context) (string, error) { return m.Password, nil } func (m User) GetPassword(context.Context) (string, error) { return m.Password, nil }
func (m User) GetRecoverToken(context.Context) (string, error) { return m.RecoverToken, nil } func (m User) GetRecoverToken(context.Context) (string, error) { return m.RecoverToken, nil }
func (m User) GetRecoverTokenExpiry(context.Context) (time.Time, error) { func (m User) GetRecoverTokenExpiry(context.Context) (time.Time, error) {

View File

@ -58,7 +58,7 @@ func TestAfterAuth(t *testing.T) {
} }
storer := mocks.NewMockStorer() storer := mocks.NewMockStorer()
ab.Storer = storer ab.Storage.Server = storer
ctx.User = authboss.Attributes{ab.PrimaryID: "john@john.com"} ctx.User = authboss.Attributes{ab.PrimaryID: "john@john.com"}
if err := lock.afterAuth(ctx); err != nil { if err := lock.afterAuth(ctx); err != nil {
@ -81,7 +81,7 @@ func TestAfterAuthFail_Lock(t *testing.T) {
ctx := ab.NewContext() ctx := ab.NewContext()
storer := mocks.NewMockStorer() storer := mocks.NewMockStorer()
ab.Storer = storer ab.Storage.Server = storer
lock := Lock{ab} lock := Lock{ab}
ab.LockWindow = 30 * time.Minute ab.LockWindow = 30 * time.Minute
ab.LockDuration = 30 * time.Minute ab.LockDuration = 30 * time.Minute
@ -133,7 +133,7 @@ func TestAfterAuthFail_Reset(t *testing.T) {
storer := mocks.NewMockStorer() storer := mocks.NewMockStorer()
lock := Lock{ab} lock := Lock{ab}
ab.LockWindow = 30 * time.Minute ab.LockWindow = 30 * time.Minute
ab.Storer = storer ab.Storage.Server = storer
old = time.Now().UTC().Add(-time.Hour) old = time.Now().UTC().Add(-time.Hour)
@ -175,7 +175,7 @@ func TestLock(t *testing.T) {
ab := authboss.New() ab := authboss.New()
storer := mocks.NewMockStorer() storer := mocks.NewMockStorer()
ab.Storer = storer ab.Storage.Server = storer
lock := Lock{ab} lock := Lock{ab}
email := "john@john.com" email := "john@john.com"
@ -199,7 +199,7 @@ func TestUnlock(t *testing.T) {
ab := authboss.New() ab := authboss.New()
storer := mocks.NewMockStorer() storer := mocks.NewMockStorer()
ab.Storer = storer ab.Storage.Server = storer
lock := Lock{ab} lock := Lock{ab}
ab.LockWindow = 1 * time.Hour ab.LockWindow = 1 * time.Hour

View File

@ -29,7 +29,7 @@ func (m mockServerStorer) Load(ctx context.Context, key string) (User, error) {
} }
func (m mockServerStorer) Save(ctx context.Context, user User) error { func (m mockServerStorer) Save(ctx context.Context, user User) error {
e, err := user.GetEmail(ctx) e, err := user.GetPID(ctx)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -39,7 +39,7 @@ func (m mockServerStorer) Save(ctx context.Context, user User) error {
return nil return nil
} }
func (m mockUser) PutEmail(ctx context.Context, email string) error { func (m mockUser) PutPID(ctx context.Context, email string) error {
m.Email = email m.Email = email
return nil return nil
} }
@ -53,7 +53,7 @@ func (m mockUser) PutPassword(ctx context.Context, password string) error {
return nil return nil
} }
func (m mockUser) GetEmail(ctx context.Context) (email string, err error) { func (m mockUser) GetPID(ctx context.Context) (email string, err error) {
return m.Email, nil return m.Email, nil
} }
@ -153,16 +153,14 @@ func newMockAPIRequest(postKeyValues ...string) *http.Request {
return req return req
} }
type mockRenderLoader struct{}
func (m mockRenderLoader) Init(names []string) (Renderer, error) {
return mockRenderer{}, nil
}
type mockRenderer struct { type mockRenderer struct {
expectName string expectName string
} }
func (m mockRenderer) Load(names ...string) error {
return nil
}
func (m mockRenderer) Render(ctx context.Context, name string, data HTMLData) ([]byte, string, error) { func (m mockRenderer) Render(ctx context.Context, name string, data HTMLData) ([]byte, string, error) {
if len(m.expectName) != 0 && m.expectName != name { if len(m.expectName) != 0 && m.expectName != name {
panic(fmt.Sprintf("want template name: %s, but got: %s", m.expectName, name)) panic(fmt.Sprintf("want template name: %s, but got: %s", m.expectName, name))

45
module.go Normal file
View File

@ -0,0 +1,45 @@
package authboss
var registeredModules = make(map[string]Moduler)
// Moduler should be implemented by all the authboss modules.
type Moduler interface {
// Init the module
Init(*Authboss) error
}
// RegisterModule with the core providing all the necessary information to
// integrate into authboss.
func RegisterModule(name string, m Moduler) {
registeredModules[name] = m
}
// RegisteredModules returns a list of modules that are currently registered.
func RegisteredModules() []string {
mods := make([]string, len(registeredModules))
i := 0
for k := range registeredModules {
mods[i] = k
i++
}
return mods
}
// LoadedModules returns a list of modules that are currently loaded.
func (a *Authboss) LoadedModules() []string {
mods := make([]string, len(a.loadedModules))
i := 0
for k := range a.loadedModules {
mods[i] = k
i++
}
return mods
}
// IsLoaded checks if a specific module is loaded.
func (a *Authboss) IsLoaded(mod string) bool {
_, ok := a.loadedModules[mod]
return ok
}

70
module_test.go Normal file
View File

@ -0,0 +1,70 @@
package authboss
import (
"net/http"
"testing"
)
const (
testModName = "testmodule"
)
var (
testMod = &testModule{}
)
func init() {
RegisterModule(testModName, testMod)
}
type testModule struct {
}
func testHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("testhandler", "test")
}
func (t *testModule) Init(a *Authboss) error { return nil }
func TestRegister(t *testing.T) {
t.Parallel()
// RegisterModule called by init()
if _, ok := registeredModules[testModName]; !ok {
t.Error("Expected module to be saved.")
}
}
func TestLoadedModules(t *testing.T) {
t.Parallel()
// RegisterModule called by init()
registered := RegisteredModules()
if len(registered) != 1 {
t.Error("Expected only a single module to be loaded.")
} else {
found := false
for _, name := range registered {
if name == testModName {
found = true
break
}
}
if !found {
t.Error("It should have found the module:", registered)
}
}
}
func TestIsLoaded(t *testing.T) {
t.Parallel()
ab := New()
if err := ab.Init(); err != nil {
t.Error(err)
}
if loaded := ab.LoadedModules(); len(loaded) == 0 || loaded[0] != testModName {
t.Error("Loaded modules wrong:", loaded)
}
}

View File

@ -29,7 +29,7 @@ func testSetup() (r *Recover, s *mocks.MockStorer, l *bytes.Buffer) {
ab.Layout = template.Must(template.New("").Parse(`{{template "authboss" .}}`)) ab.Layout = template.Must(template.New("").Parse(`{{template "authboss" .}}`))
ab.LayoutHTMLEmail = template.Must(template.New("").Parse(`<strong>{{template "authboss" .}}</strong>`)) ab.LayoutHTMLEmail = template.Must(template.New("").Parse(`<strong>{{template "authboss" .}}</strong>`))
ab.LayoutTextEmail = template.Must(template.New("").Parse(`{{template "authboss" .}}`)) ab.LayoutTextEmail = template.Must(template.New("").Parse(`{{template "authboss" .}}`))
ab.Storer = s ab.Storage.Server = s
ab.XSRFName = "xsrf" ab.XSRFName = "xsrf"
ab.XSRFMaker = func(_ http.ResponseWriter, _ *http.Request) string { ab.XSRFMaker = func(_ http.ResponseWriter, _ *http.Request) string {
return "xsrfvalue" return "xsrfvalue"

View File

@ -22,7 +22,7 @@ func setup() *Register {
return "xsrfvalue" return "xsrfvalue"
} }
ab.ConfirmFields = []string{"password", "confirm_password"} ab.ConfirmFields = []string{"password", "confirm_password"}
ab.Storer = mocks.NewMockStorer() ab.Storage.Server = mocks.NewMockStorer()
reg := Register{} reg := Register{}
if err := reg.Initialize(ab); err != nil { if err := reg.Initialize(ab); err != nil {
@ -34,7 +34,7 @@ func setup() *Register {
func TestRegister(t *testing.T) { func TestRegister(t *testing.T) {
ab := authboss.New() ab := authboss.New()
ab.Storer = mocks.NewMockStorer() ab.Storage.Server = mocks.NewMockStorer()
r := Register{} r := Register{}
if err := r.Initialize(ab); err != nil { if err := r.Initialize(ab); err != nil {
t.Error(err) t.Error(err)

View File

@ -19,13 +19,13 @@ func TestInitialize(t *testing.T) {
t.Error("Expected error about token storers.") t.Error("Expected error about token storers.")
} }
ab.Storer = mocks.MockFailStorer{} ab.Storage.Server = mocks.MockFailStorer{}
err = r.Initialize(ab) err = r.Initialize(ab)
if err == nil { if err == nil {
t.Error("Expected error about token storers.") t.Error("Expected error about token storers.")
} }
ab.Storer = mocks.NewMockStorer() ab.Storage.Server = mocks.NewMockStorer()
err = r.Initialize(ab) err = r.Initialize(ab)
if err != nil { if err != nil {
t.Error("Unexpected error:", err) t.Error("Unexpected error:", err)

View File

@ -2,14 +2,11 @@ package authboss
import "context" import "context"
// RenderLoader is an object that understands how to load display templates.
// It's possible that Init() is a no-op if the responses are JSON or anything
// else.
type RenderLoader interface {
Init(names []string) (Renderer, error)
}
// Renderer is a type that can render a given template with some data. // Renderer is a type that can render a given template with some data.
type Renderer interface { type Renderer interface {
// Load the given templates, will most likely be called multiple times
Load(name ...string) error
// Render the given template
Render(ctx context.Context, name string, data HTMLData) (output []byte, contentType string, err error) Render(ctx context.Context, name string, data HTMLData) (output []byte, contentType string, err error)
} }

View File

@ -48,10 +48,10 @@ type HTTPResponder interface {
Respond(w http.ResponseWriter, r *http.Request, code int, templateName string, data HTMLData) error Respond(w http.ResponseWriter, r *http.Request, code int, templateName string, data HTMLData) error
} }
// Redirector redirects http requests to a different url (must handle both json and html) // 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 // When an authboss controller wants to redirect a user to a different path, it will use
// this interface. // this interface.
type Redirector interface { type HTTPRedirector interface {
Redirect(w http.ResponseWriter, r *http.Request, ro RedirectOptions) error Redirect(w http.ResponseWriter, r *http.Request, ro RedirectOptions) error
} }
@ -60,7 +60,7 @@ func (a *Authboss) Email(w http.ResponseWriter, r *http.Request, email Email, ro
ctx := r.Context() ctx := r.Context()
if len(ro.HTMLTemplate) != 0 { if len(ro.HTMLTemplate) != 0 {
htmlBody, _, err := a.mailRenderer.Render(ctx, ro.HTMLTemplate, ro.Data) htmlBody, _, err := a.Core.MailRenderer.Render(ctx, ro.HTMLTemplate, ro.Data)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to render e-mail html body") return errors.Wrap(err, "failed to render e-mail html body")
} }
@ -68,12 +68,12 @@ func (a *Authboss) Email(w http.ResponseWriter, r *http.Request, email Email, ro
} }
if len(ro.TextTemplate) != 0 { if len(ro.TextTemplate) != 0 {
textBody, _, err := a.mailRenderer.Render(ctx, ro.TextTemplate, ro.Data) textBody, _, err := a.Core.MailRenderer.Render(ctx, ro.TextTemplate, ro.Data)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to render e-mail text body") return errors.Wrap(err, "failed to render e-mail text body")
} }
email.TextBody = string(textBody) email.TextBody = string(textBody)
} }
return a.Mailer.Send(ctx, email) return a.Core.Mailer.Send(ctx, email)
} }

View File

@ -45,13 +45,14 @@ type ServerStorer interface {
// User has functions for each piece of data it requires. // User has functions for each piece of data it requires.
// Data should not be persisted on each function call. // Data should not be persisted on each function call.
// User has a PID (primary ID) that is used on the site as
// a single unique identifier to any given user (very typically e-mail
// or username).
type User interface { type User interface {
PutEmail(ctx context.Context, email string) error PutPID(ctx context.Context, pid string) error
PutUsername(ctx context.Context, username string) error
PutPassword(ctx context.Context, password string) error PutPassword(ctx context.Context, password string) error
GetEmail(ctx context.Context) (email string, err error) GetPID(ctx context.Context) (pid string, err error)
GetUsername(ctx context.Context) (username string, err error)
GetPassword(ctx context.Context) (password string, err error) GetPassword(ctx context.Context) (password string, err error)
} }

View File

@ -1,6 +0,0 @@
package authboss
import "net/http"
// XSRF returns a token that should be written to forms to prevent xsrf attacks.
type XSRF func(http.ResponseWriter, *http.Request) (token string)