mirror of
				https://github.com/volatiletech/authboss.git
				synced 2025-10-30 23:47:59 +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:
		
							
								
								
									
										20
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -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. | ||||
|  | ||||
| **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. | ||||
|  | ||||
| ## 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 | ||||
| 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) | ||||
|   | ||||
| @@ -13,9 +13,6 @@ import ( | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	methodGET  = "GET" | ||||
| 	methodPOST = "POST" | ||||
|  | ||||
| 	tplLogin = "login.html.tpl" | ||||
| ) | ||||
|  | ||||
| @@ -26,7 +23,6 @@ func init() { | ||||
| // Auth module | ||||
| type Auth struct { | ||||
| 	*authboss.Authboss | ||||
| 	templates response.Templates | ||||
| } | ||||
|  | ||||
| // Initialize module | ||||
|   | ||||
| @@ -21,7 +21,7 @@ func testSetup() (a *Auth, s *mocks.MockStorer) { | ||||
| 	ab := authboss.New() | ||||
| 	ab.LogWriter = ioutil.Discard | ||||
| 	ab.Layout = template.Must(template.New("").Parse(`{{template "authboss" .}}`)) | ||||
| 	ab.Storer = s | ||||
| 	ab.Storage.Server = s | ||||
| 	ab.XSRFName = "xsrf" | ||||
| 	ab.XSRFMaker = func(_ http.ResponseWriter, _ *http.Request) string { | ||||
| 		return "xsrfvalue" | ||||
| @@ -293,7 +293,7 @@ func TestAuth_validateCredentials(t *testing.T) { | ||||
|  | ||||
| 	ab := authboss.New() | ||||
| 	storer := mocks.NewMockStorer() | ||||
| 	ab.Storer = storer | ||||
| 	ab.Storage.Server = storer | ||||
|  | ||||
| 	ctx := ab.NewContext() | ||||
| 	storer.Users["john"] = authboss.Attributes{"password": "$2a$10$pgFsuQwdhwOdZp/v52dvHeEi53ZaI7dGmtwK4bAzGGN5A4nT6doqm"} | ||||
|   | ||||
							
								
								
									
										33
									
								
								authboss.go
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								authboss.go
									
									
									
									
									
								
							| @@ -11,34 +11,37 @@ import "github.com/pkg/errors" | ||||
| // Authboss contains a configuration and other details for running. | ||||
| type Authboss struct { | ||||
| 	Config | ||||
| 	loadedModules map[string]bool | ||||
|  | ||||
| 	viewRenderer Renderer | ||||
| 	mailRenderer Renderer | ||||
| 	loadedModules map[string]Moduler | ||||
| } | ||||
|  | ||||
| // New makes a new instance of authboss with a default | ||||
| // configuration. | ||||
| func New() *Authboss { | ||||
| 	ab := &Authboss{} | ||||
| 	ab.loadedModules = make(map[string]Moduler) | ||||
|  | ||||
| 	ab.Config.Defaults() | ||||
| 	return ab | ||||
| } | ||||
|  | ||||
| // Init authboss, modules, renderers | ||||
| func (a *Authboss) Init() error { | ||||
| 	//TODO(aarondl): Figure the template names out along with new "module" loading. | ||||
| 	views := []string{"all"} | ||||
|  | ||||
| 	var err error | ||||
| 	a.viewRenderer, err = a.Config.ViewLoader.Init(views) | ||||
| 	if err != nil { | ||||
| 		return errors.Wrap(err, "failed to load the view renderer") | ||||
| func (a *Authboss) Init(modulesToLoad ...string) error { | ||||
| 	if len(modulesToLoad) == 0 { | ||||
| 		modulesToLoad = RegisteredModules() | ||||
| 	} | ||||
|  | ||||
| 	a.mailRenderer, err = a.Config.MailViewLoader.Init(views) | ||||
| 	if err != nil { | ||||
| 		return errors.Wrap(err, "failed to load the mail view renderer") | ||||
| 	for _, name := range modulesToLoad { | ||||
| 		mod, ok := registeredModules[name] | ||||
| 		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 | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| package authboss | ||||
|  | ||||
| import ( | ||||
| 	"io/ioutil" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| @@ -9,9 +8,6 @@ func TestAuthBossInit(t *testing.T) { | ||||
| 	t.Parallel() | ||||
|  | ||||
| 	ab := New() | ||||
| 	ab.LogWriter = ioutil.Discard | ||||
| 	ab.ViewLoader = mockRenderLoader{} | ||||
| 	ab.MailViewLoader = mockRenderLoader{} | ||||
| 	err := ab.Init() | ||||
| 	if err != nil { | ||||
| 		t.Error("Unexpected error:", err) | ||||
|   | ||||
| @@ -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 | ||||
| func (a *Authboss) LoadClientState(w http.ResponseWriter, r *http.Request) (*http.Request, error) { | ||||
| 	if a.SessionStateStorer != nil { | ||||
| 		state, err := a.SessionStateStorer.ReadState(w, r) | ||||
| 	if a.Storage.SessionState != nil { | ||||
| 		state, err := a.Storage.SessionState.ReadState(w, r) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} 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) | ||||
| 		r = r.WithContext(ctx) | ||||
| 	} | ||||
| 	if a.CookieStateStorer != nil { | ||||
| 		state, err := a.CookieStateStorer.ReadState(w, r) | ||||
| 	if a.Storage.CookieState != nil { | ||||
| 		state, err := a.Storage.CookieState.ReadState(w, r) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} else if state == nil { | ||||
| @@ -184,14 +184,14 @@ func (c *ClientStateResponseWriter) putClientState() error { | ||||
| 		cookie = cookieStateIntf.(ClientState) | ||||
| 	} | ||||
|  | ||||
| 	if c.ab.SessionStateStorer != nil { | ||||
| 		err := c.ab.SessionStateStorer.WriteState(c, session, c.sessionStateEvents) | ||||
| 	if c.ab.Storage.SessionState != nil { | ||||
| 		err := c.ab.Storage.SessionState.WriteState(c, session, c.sessionStateEvents) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	if c.ab.CookieStateStorer != nil { | ||||
| 		err := c.ab.CookieStateStorer.WriteState(c, cookie, c.cookieStateEvents) | ||||
| 	if c.ab.Storage.CookieState != nil { | ||||
| 		err := c.ab.Storage.CookieState.WriteState(c, cookie, c.cookieStateEvents) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|   | ||||
| @@ -12,8 +12,8 @@ func TestStateGet(t *testing.T) { | ||||
| 	t.Parallel() | ||||
|  | ||||
| 	ab := New() | ||||
| 	ab.SessionStateStorer = newMockClientStateRW("one", "two") | ||||
| 	ab.CookieStateStorer = newMockClientStateRW("three", "four") | ||||
| 	ab.Storage.SessionState = newMockClientStateRW("one", "two") | ||||
| 	ab.Storage.CookieState = newMockClientStateRW("three", "four") | ||||
|  | ||||
| 	r := httptest.NewRequest("GET", "/", nil) | ||||
| 	w := ab.NewResponse(httptest.NewRecorder(), r) | ||||
| @@ -36,7 +36,7 @@ func TestStateResponseWriterDoubleWritePanic(t *testing.T) { | ||||
| 	t.Parallel() | ||||
|  | ||||
| 	ab := New() | ||||
| 	ab.SessionStateStorer = newMockClientStateRW("one", "two") | ||||
| 	ab.Storage.SessionState = newMockClientStateRW("one", "two") | ||||
|  | ||||
| 	r := httptest.NewRequest("GET", "/", nil) | ||||
| 	w := ab.NewResponse(httptest.NewRecorder(), r) | ||||
| @@ -58,8 +58,8 @@ func TestStateResponseWriterLastSecondWriteWithPrevious(t *testing.T) { | ||||
| 	t.Parallel() | ||||
|  | ||||
| 	ab := New() | ||||
| 	ab.SessionStateStorer = newMockClientStateRW("one", "two") | ||||
| 	ab.CookieStateStorer = newMockClientStateRW("three", "four") | ||||
| 	ab.Storage.SessionState = newMockClientStateRW("one", "two") | ||||
| 	ab.Storage.CookieState = newMockClientStateRW("three", "four") | ||||
|  | ||||
| 	r := httptest.NewRequest("GET", "/", nil) | ||||
| 	var w http.ResponseWriter = httptest.NewRecorder() | ||||
| @@ -85,7 +85,7 @@ func TestStateResponseWriterLastSecondWriteHeader(t *testing.T) { | ||||
| 	t.Parallel() | ||||
|  | ||||
| 	ab := New() | ||||
| 	ab.SessionStateStorer = newMockClientStateRW() | ||||
| 	ab.Storage.SessionState = newMockClientStateRW() | ||||
|  | ||||
| 	r := httptest.NewRequest("GET", "/", nil) | ||||
| 	w := ab.NewResponse(httptest.NewRecorder(), r) | ||||
| @@ -103,7 +103,7 @@ func TestStateResponseWriterLastSecondWriteWrite(t *testing.T) { | ||||
| 	t.Parallel() | ||||
|  | ||||
| 	ab := New() | ||||
| 	ab.SessionStateStorer = newMockClientStateRW() | ||||
| 	ab.Storage.SessionState = newMockClientStateRW() | ||||
|  | ||||
| 	r := httptest.NewRequest("GET", "/", nil) | ||||
| 	w := ab.NewResponse(httptest.NewRecorder(), r) | ||||
| @@ -155,7 +155,7 @@ func TestFlashClearer(t *testing.T) { | ||||
| 	t.Parallel() | ||||
|  | ||||
| 	ab := New() | ||||
| 	ab.SessionStateStorer = newMockClientStateRW(FlashSuccessKey, "a", FlashErrorKey, "b") | ||||
| 	ab.Storage.SessionState = newMockClientStateRW(FlashSuccessKey, "a", FlashErrorKey, "b") | ||||
|  | ||||
| 	r := httptest.NewRequest("GET", "/", nil) | ||||
| 	w := ab.NewResponse(httptest.NewRecorder(), r) | ||||
|   | ||||
							
								
								
									
										161
									
								
								config.go
									
									
									
									
									
								
							
							
						
						
									
										161
									
								
								config.go
									
									
									
									
									
								
							| @@ -7,90 +7,107 @@ import ( | ||||
|  | ||||
| // Config holds all the configuration for both authboss and it's modules. | ||||
| type Config struct { | ||||
| 	// MountPath is the path to mount authboss's routes at (eg /auth). | ||||
| 	MountPath string | ||||
| 	// ViewsPath is the path to search for overridden templates. | ||||
| 	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 | ||||
| 	Paths struct { | ||||
| 		// Mount is the path to mount authboss's routes at (eg /auth). | ||||
| 		Mount string | ||||
|  | ||||
| 	// PrimaryID is the primary identifier of the user. Set to one of: | ||||
| 	// authboss.StoreEmail, authboss.StoreUsername (StoreEmail is default) | ||||
| 	PrimaryID string | ||||
| 		// AuthLoginOK is the redirect path after a successful authentication. | ||||
| 		AuthLoginOK 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. | ||||
| 	ViewLoader RenderLoader | ||||
| 	// 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 | ||||
| 		// RecoverOK is the redirect path after a successful recovery of a password. | ||||
| 		RecoverOK string | ||||
|  | ||||
| 	// OAuth2Providers lists all providers that can be used. See | ||||
| 	// OAuthProvider documentation for more details. | ||||
| 	OAuth2Providers map[string]OAuth2Provider | ||||
| 		// RegisterOK is the redirect path after a successful registration. | ||||
| 		RegisterOK string | ||||
|  | ||||
| 	// AuthLoginOKPath is the redirect path after a successful authentication. | ||||
| 	AuthLoginOKPath string | ||||
| 	// AuthLoginFailPath is the redirect path after a failed authentication. | ||||
| 	AuthLoginFailPath string | ||||
| 	// AuthLogoutOKPath is the redirect path after a log out. | ||||
| 	AuthLogoutOKPath 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 | ||||
| 	} | ||||
|  | ||||
| 	// RecoverOKPath is the redirect path after a successful recovery of a password. | ||||
| 	RecoverOKPath string | ||||
| 	// RecoverTokenDuration controls how long a token sent via email for password | ||||
| 	// recovery is valid for. | ||||
| 	RecoverTokenDuration time.Duration | ||||
| 	Modules struct { | ||||
| 		// BCryptCost is the cost of the bcrypt password hashing function. | ||||
| 		BCryptCost int | ||||
|  | ||||
| 	// RegisterOKPath is the redirect path after a successful registration. | ||||
| 	RegisterOKPath string | ||||
| 		// OAuth2Providers lists all providers that can be used. See | ||||
| 		// OAuthProvider documentation for more details. | ||||
| 		OAuth2Providers map[string]OAuth2Provider | ||||
|  | ||||
| 	// Policies control validation of form fields and are automatically run | ||||
| 	// against form posts that include the fields. | ||||
| 	Policies []Validator | ||||
| 	// 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 | ||||
| 		// 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 | ||||
| 	// by the ExpireMiddleware. | ||||
| 	ExpireAfter time.Duration | ||||
| 		// ExpireAfter controls the time an account is idle before being logged out | ||||
| 		// by the ExpireMiddleware. | ||||
| 		ExpireAfter time.Duration | ||||
|  | ||||
| 	// LockAfter this many tries. | ||||
| 	LockAfter int | ||||
| 	// LockWindow is the waiting time before the number of attemps are reset. | ||||
| 	LockWindow time.Duration | ||||
| 	// LockDuration is how long an account is locked for. | ||||
| 	LockDuration time.Duration | ||||
| 		// RecoverTokenDuration controls how long a token sent via email for password | ||||
| 		// recovery is valid for. | ||||
| 		RecoverTokenDuration time.Duration | ||||
|  | ||||
| 	// EmailFrom is the email address authboss e-mails come from. | ||||
| 	EmailFrom string | ||||
| 	// EmailSubjectPrefix is used to add something to the front of the authboss | ||||
| 	// email subjects. | ||||
| 	EmailSubjectPrefix string | ||||
| 		// LockAfter this many tries. | ||||
| 		LockAfter int | ||||
| 		// LockWindow is the waiting time before the number of attemps are reset. | ||||
| 		LockWindow time.Duration | ||||
| 		// LockDuration is how long an account is locked for. | ||||
| 		LockDuration time.Duration | ||||
| 	} | ||||
|  | ||||
| 	// Storer is the interface through which Authboss accesses the web apps database | ||||
| 	// for user operations. | ||||
| 	Storer ServerStorer | ||||
| 	Mail struct { | ||||
| 		// From is the email address authboss e-mails come from. | ||||
| 		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 | ||||
| 	// storing cookies for the given response, and reading them from the request. | ||||
| 	CookieStateStorer ClientStateReadWriter | ||||
| 	// SessionStateStorer must be defined to provide an interface capable of | ||||
| 	// storing session-only values for the given response, and reading them | ||||
| 	// from the request. | ||||
| 	SessionStateStorer ClientStateReadWriter | ||||
| 	// LogWriter is written to when errors occur, as well as on startup to show | ||||
| 	// which modules are loaded and which routes they registered. By default | ||||
| 	// writes to io.Discard. | ||||
| 	LogWriter io.Writer | ||||
| 	// Mailer is the mailer being used to send e-mails out. Authboss defines two loggers for use | ||||
| 	// LogMailer and SMTPMailer, the default is a LogMailer to io.Discard. | ||||
| 	Mailer Mailer | ||||
| 	Storage struct { | ||||
| 		// Storer is the interface through which Authboss accesses the web apps database | ||||
| 		// for user operations. | ||||
| 		Server ServerStorer | ||||
|  | ||||
| 		// CookieState must be defined to provide an interface capapable of | ||||
| 		// storing cookies for the given response, and reading them from the request. | ||||
| 		CookieState ClientStateReadWriter | ||||
| 		// SessionState must be defined to provide an interface capable of | ||||
| 		// storing session-only values for the given response, and reading them | ||||
| 		// from the request. | ||||
| 		SessionState ClientStateReadWriter | ||||
| 	} | ||||
|  | ||||
| 	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. | ||||
|   | ||||
| @@ -19,7 +19,7 @@ import ( | ||||
|  | ||||
| func setup() *Confirm { | ||||
| 	ab := authboss.New() | ||||
| 	ab.Storer = mocks.NewMockStorer() | ||||
| 	ab.Storage.Server = mocks.NewMockStorer() | ||||
| 	ab.LayoutHTMLEmail = template.Must(template.New("").Parse(`email ^_^`)) | ||||
| 	ab.LayoutTextEmail = template.Must(template.New("").Parse(`email`)) | ||||
|  | ||||
|   | ||||
| @@ -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) { | ||||
| 	user, err := a.Storer.Load(ctx, pid) | ||||
| 	user, err := a.Storage.Server.Load(ctx, pid) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|   | ||||
| @@ -17,8 +17,8 @@ func loadClientStateP(ab *Authboss, w http.ResponseWriter, r *http.Request) *htt | ||||
|  | ||||
| func testSetupContext() (*Authboss, *http.Request) { | ||||
| 	ab := New() | ||||
| 	ab.SessionStateStorer = newMockClientStateRW(SessionKey, "george-pid") | ||||
| 	ab.Storer = mockServerStorer{ | ||||
| 	ab.Storage.SessionState = newMockClientStateRW(SessionKey, "george-pid") | ||||
| 	ab.Storage.Server = mockServerStorer{ | ||||
| 		"george-pid": mockUser{Email: "george-pid", Password: "unreadable"}, | ||||
| 	} | ||||
| 	r := loadClientStateP(ab, nil, httptest.NewRequest("GET", "/", nil)) | ||||
| @@ -39,8 +39,8 @@ func testSetupContextCached() (*Authboss, mockUser, *http.Request) { | ||||
|  | ||||
| func testSetupContextPanic() *Authboss { | ||||
| 	ab := New() | ||||
| 	ab.SessionStateStorer = newMockClientStateRW(SessionKey, "george-pid") | ||||
| 	ab.Storer = mockServerStorer{} | ||||
| 	ab.Storage.SessionState = newMockClientStateRW(SessionKey, "george-pid") | ||||
| 	ab.Storage.Server = mockServerStorer{} | ||||
|  | ||||
| 	return ab | ||||
| } | ||||
| @@ -80,7 +80,7 @@ func TestCurrentUserIDP(t *testing.T) { | ||||
|  | ||||
| 	ab := testSetupContextPanic() | ||||
| 	// Overwrite the setup functions state storer | ||||
| 	ab.SessionStateStorer = newMockClientStateRW() | ||||
| 	ab.Storage.SessionState = newMockClientStateRW() | ||||
|  | ||||
| 	defer func() { | ||||
| 		if recover().(error) != ErrUserNotFound { | ||||
| @@ -101,7 +101,7 @@ func TestCurrentUser(t *testing.T) { | ||||
| 		t.Error(err) | ||||
| 	} | ||||
|  | ||||
| 	if got, err := user.GetEmail(context.TODO()); err != nil { | ||||
| 	if got, err := user.GetPID(context.TODO()); err != nil { | ||||
| 		t.Error(err) | ||||
| 	} else if got != "george-pid" { | ||||
| 		t.Error("got:", got) | ||||
| @@ -118,7 +118,7 @@ func TestCurrentUserContext(t *testing.T) { | ||||
| 		t.Error(err) | ||||
| 	} | ||||
|  | ||||
| 	if got, err := user.GetEmail(context.TODO()); err != nil { | ||||
| 	if got, err := user.GetPID(context.TODO()); err != nil { | ||||
| 		t.Error(err) | ||||
| 	} else if got != "george-pid" { | ||||
| 		t.Error("got:", got) | ||||
| @@ -198,7 +198,7 @@ func TestLoadCurrentUser(t *testing.T) { | ||||
| 		t.Error(err) | ||||
| 	} | ||||
|  | ||||
| 	if got, err := user.GetEmail(context.TODO()); err != nil { | ||||
| 	if got, err := user.GetPID(context.TODO()); err != nil { | ||||
| 		t.Error(err) | ||||
| 	} else if got != "george-pid" { | ||||
| 		t.Error("got:", got) | ||||
|   | ||||
| @@ -16,6 +16,10 @@ type testRenderer struct { | ||||
| 	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) { | ||||
| 	return t.Callback(ctx, name, data) | ||||
| } | ||||
| @@ -189,8 +193,8 @@ func TestResponseRedirectNonAPI(t *testing.T) { | ||||
| 	w := httptest.NewRecorder() | ||||
|  | ||||
| 	ab := authboss.New() | ||||
| 	ab.Config.SessionStateStorer = mocks.NewClientRW() | ||||
| 	ab.Config.CookieStateStorer = mocks.NewClientRW() | ||||
| 	ab.Config.Storage.SessionState = mocks.NewClientRW() | ||||
| 	ab.Config.Storage.CookieState = mocks.NewClientRW() | ||||
| 	aw := ab.NewResponse(w, r) | ||||
|  | ||||
| 	ro := authboss.RedirectOptions{ | ||||
| @@ -228,8 +232,8 @@ func TestResponseRedirectNonAPIFollowRedir(t *testing.T) { | ||||
| 	w := httptest.NewRecorder() | ||||
|  | ||||
| 	ab := authboss.New() | ||||
| 	ab.Config.SessionStateStorer = mocks.NewClientRW() | ||||
| 	ab.Config.CookieStateStorer = mocks.NewClientRW() | ||||
| 	ab.Config.Storage.SessionState = mocks.NewClientRW() | ||||
| 	ab.Config.Storage.CookieState = mocks.NewClientRW() | ||||
| 	aw := ab.NewResponse(w, r) | ||||
|  | ||||
| 	ro := authboss.RedirectOptions{ | ||||
|   | ||||
| @@ -61,7 +61,7 @@ func (a *Authboss) ExpireMiddleware(next http.Handler) http.Handler { | ||||
| // below it. | ||||
| func (m expireMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	if _, ok := GetSession(r, SessionKey); ok { | ||||
| 		ttl := timeToExpiry(r, m.ab.ExpireAfter) | ||||
| 		ttl := timeToExpiry(r, m.ab.Modules.ExpireAfter) | ||||
| 		if ttl == 0 { | ||||
| 			DelSession(w, SessionKey) | ||||
| 			DelSession(w, SessionLastAction) | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import ( | ||||
|  | ||||
| func TestExpireIsExpired(t *testing.T) { | ||||
| 	ab := New() | ||||
| 	ab.SessionStateStorer = newMockClientStateRW( | ||||
| 	ab.Storage.SessionState = newMockClientStateRW( | ||||
| 		SessionKey, "username", | ||||
| 		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 | ||||
| 	nowTime = func() time.Time { | ||||
| 		return time.Now().UTC().Add(ab.ExpireAfter * 2) | ||||
| 		return time.Now().UTC().Add(ab.Modules.ExpireAfter * 2) | ||||
| 	} | ||||
| 	defer func() { | ||||
| 		nowTime = time.Now | ||||
| @@ -72,8 +72,8 @@ func TestExpireIsExpired(t *testing.T) { | ||||
|  | ||||
| func TestExpireNotExpired(t *testing.T) { | ||||
| 	ab := New() | ||||
| 	ab.Config.ExpireAfter = time.Hour | ||||
| 	ab.SessionStateStorer = newMockClientStateRW( | ||||
| 	ab.Config.Modules.ExpireAfter = time.Hour | ||||
| 	ab.Storage.SessionState = newMockClientStateRW( | ||||
| 		SessionKey, "username", | ||||
| 		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 | ||||
| 	newTime := time.Now().UTC().Add(ab.ExpireAfter / 2) | ||||
| 	newTime := time.Now().UTC().Add(ab.Modules.ExpireAfter / 2) | ||||
| 	nowTime = func() time.Time { | ||||
| 		return newTime | ||||
| 	} | ||||
|   | ||||
| @@ -31,7 +31,7 @@ type User struct { | ||||
| } | ||||
|  | ||||
| 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) GetRecoverToken(context.Context) (string, error) { return m.RecoverToken, nil } | ||||
| func (m User) GetRecoverTokenExpiry(context.Context) (time.Time, error) { | ||||
|   | ||||
| @@ -58,7 +58,7 @@ func TestAfterAuth(t *testing.T) { | ||||
| 	} | ||||
|  | ||||
| 	storer := mocks.NewMockStorer() | ||||
| 	ab.Storer = storer | ||||
| 	ab.Storage.Server = storer | ||||
| 	ctx.User = authboss.Attributes{ab.PrimaryID: "john@john.com"} | ||||
|  | ||||
| 	if err := lock.afterAuth(ctx); err != nil { | ||||
| @@ -81,7 +81,7 @@ func TestAfterAuthFail_Lock(t *testing.T) { | ||||
|  | ||||
| 	ctx := ab.NewContext() | ||||
| 	storer := mocks.NewMockStorer() | ||||
| 	ab.Storer = storer | ||||
| 	ab.Storage.Server = storer | ||||
| 	lock := Lock{ab} | ||||
| 	ab.LockWindow = 30 * time.Minute | ||||
| 	ab.LockDuration = 30 * time.Minute | ||||
| @@ -133,7 +133,7 @@ func TestAfterAuthFail_Reset(t *testing.T) { | ||||
| 	storer := mocks.NewMockStorer() | ||||
| 	lock := Lock{ab} | ||||
| 	ab.LockWindow = 30 * time.Minute | ||||
| 	ab.Storer = storer | ||||
| 	ab.Storage.Server = storer | ||||
|  | ||||
| 	old = time.Now().UTC().Add(-time.Hour) | ||||
|  | ||||
| @@ -175,7 +175,7 @@ func TestLock(t *testing.T) { | ||||
|  | ||||
| 	ab := authboss.New() | ||||
| 	storer := mocks.NewMockStorer() | ||||
| 	ab.Storer = storer | ||||
| 	ab.Storage.Server = storer | ||||
| 	lock := Lock{ab} | ||||
|  | ||||
| 	email := "john@john.com" | ||||
| @@ -199,7 +199,7 @@ func TestUnlock(t *testing.T) { | ||||
|  | ||||
| 	ab := authboss.New() | ||||
| 	storer := mocks.NewMockStorer() | ||||
| 	ab.Storer = storer | ||||
| 	ab.Storage.Server = storer | ||||
| 	lock := Lock{ab} | ||||
| 	ab.LockWindow = 1 * time.Hour | ||||
|  | ||||
|   | ||||
| @@ -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 { | ||||
| 	e, err := user.GetEmail(ctx) | ||||
| 	e, err := user.GetPID(ctx) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| @@ -39,7 +39,7 @@ func (m mockServerStorer) Save(ctx context.Context, user User) error { | ||||
| 	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 | ||||
| 	return nil | ||||
| } | ||||
| @@ -53,7 +53,7 @@ func (m mockUser) PutPassword(ctx context.Context, password string) error { | ||||
| 	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 | ||||
| } | ||||
|  | ||||
| @@ -153,16 +153,14 @@ func newMockAPIRequest(postKeyValues ...string) *http.Request { | ||||
| 	return req | ||||
| } | ||||
|  | ||||
| type mockRenderLoader struct{} | ||||
|  | ||||
| func (m mockRenderLoader) Init(names []string) (Renderer, error) { | ||||
| 	return mockRenderer{}, nil | ||||
| } | ||||
|  | ||||
| type mockRenderer struct { | ||||
| 	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) { | ||||
| 	if len(m.expectName) != 0 && m.expectName != name { | ||||
| 		panic(fmt.Sprintf("want template name: %s, but got: %s", m.expectName, name)) | ||||
|   | ||||
							
								
								
									
										45
									
								
								module.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								module.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										70
									
								
								module_test.go
									
									
									
									
									
										Normal 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) | ||||
| 	} | ||||
| } | ||||
| @@ -29,7 +29,7 @@ func testSetup() (r *Recover, s *mocks.MockStorer, l *bytes.Buffer) { | ||||
| 	ab.Layout = template.Must(template.New("").Parse(`{{template "authboss" .}}`)) | ||||
| 	ab.LayoutHTMLEmail = template.Must(template.New("").Parse(`<strong>{{template "authboss" .}}</strong>`)) | ||||
| 	ab.LayoutTextEmail = template.Must(template.New("").Parse(`{{template "authboss" .}}`)) | ||||
| 	ab.Storer = s | ||||
| 	ab.Storage.Server = s | ||||
| 	ab.XSRFName = "xsrf" | ||||
| 	ab.XSRFMaker = func(_ http.ResponseWriter, _ *http.Request) string { | ||||
| 		return "xsrfvalue" | ||||
|   | ||||
| @@ -22,7 +22,7 @@ func setup() *Register { | ||||
| 		return "xsrfvalue" | ||||
| 	} | ||||
| 	ab.ConfirmFields = []string{"password", "confirm_password"} | ||||
| 	ab.Storer = mocks.NewMockStorer() | ||||
| 	ab.Storage.Server = mocks.NewMockStorer() | ||||
|  | ||||
| 	reg := Register{} | ||||
| 	if err := reg.Initialize(ab); err != nil { | ||||
| @@ -34,7 +34,7 @@ func setup() *Register { | ||||
|  | ||||
| func TestRegister(t *testing.T) { | ||||
| 	ab := authboss.New() | ||||
| 	ab.Storer = mocks.NewMockStorer() | ||||
| 	ab.Storage.Server = mocks.NewMockStorer() | ||||
| 	r := Register{} | ||||
| 	if err := r.Initialize(ab); err != nil { | ||||
| 		t.Error(err) | ||||
|   | ||||
| @@ -19,13 +19,13 @@ func TestInitialize(t *testing.T) { | ||||
| 		t.Error("Expected error about token storers.") | ||||
| 	} | ||||
|  | ||||
| 	ab.Storer = mocks.MockFailStorer{} | ||||
| 	ab.Storage.Server = mocks.MockFailStorer{} | ||||
| 	err = r.Initialize(ab) | ||||
| 	if err == nil { | ||||
| 		t.Error("Expected error about token storers.") | ||||
| 	} | ||||
|  | ||||
| 	ab.Storer = mocks.NewMockStorer() | ||||
| 	ab.Storage.Server = mocks.NewMockStorer() | ||||
| 	err = r.Initialize(ab) | ||||
| 	if err != nil { | ||||
| 		t.Error("Unexpected error:", err) | ||||
|   | ||||
							
								
								
									
										11
									
								
								renderer.go
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								renderer.go
									
									
									
									
									
								
							| @@ -2,14 +2,11 @@ package authboss | ||||
|  | ||||
| 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. | ||||
| 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) | ||||
| } | ||||
|   | ||||
							
								
								
									
										10
									
								
								response.go
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								response.go
									
									
									
									
									
								
							| @@ -48,10 +48,10 @@ type HTTPResponder interface { | ||||
| 	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 | ||||
| // this interface. | ||||
| type Redirector interface { | ||||
| type HTTPRedirector interface { | ||||
| 	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() | ||||
|  | ||||
| 	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 { | ||||
| 			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 { | ||||
| 		textBody, _, err := a.mailRenderer.Render(ctx, ro.TextTemplate, ro.Data) | ||||
| 		textBody, _, err := a.Core.MailRenderer.Render(ctx, ro.TextTemplate, ro.Data) | ||||
| 		if err != nil { | ||||
| 			return errors.Wrap(err, "failed to render e-mail text body") | ||||
| 		} | ||||
| 		email.TextBody = string(textBody) | ||||
| 	} | ||||
|  | ||||
| 	return a.Mailer.Send(ctx, email) | ||||
| 	return a.Core.Mailer.Send(ctx, email) | ||||
| } | ||||
|   | ||||
| @@ -45,13 +45,14 @@ type ServerStorer interface { | ||||
|  | ||||
| // User has functions for each piece of data it requires. | ||||
| // 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 { | ||||
| 	PutEmail(ctx context.Context, email string) error | ||||
| 	PutUsername(ctx context.Context, username string) error | ||||
| 	PutPID(ctx context.Context, pid string) error | ||||
| 	PutPassword(ctx context.Context, password string) error | ||||
|  | ||||
| 	GetEmail(ctx context.Context) (email string, err error) | ||||
| 	GetUsername(ctx context.Context) (username string, err error) | ||||
| 	GetPID(ctx context.Context) (pid string, err error) | ||||
| 	GetPassword(ctx context.Context) (password string, err error) | ||||
| } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user