mirror of
https://github.com/volatiletech/authboss.git
synced 2025-09-16 09:06:20 +02:00
@@ -339,7 +339,8 @@ func TestAuthPostUserNotFound(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
w := harness.ab.NewResponse(resp)
|
||||
|
||||
// This event is really the only thing that separates "user not found" from "bad password"
|
||||
// This event is really the only thing that separates "user not found"
|
||||
// from "bad password"
|
||||
var afterCalled bool
|
||||
harness.ab.Events.After(authboss.EventAuthFail, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
|
||||
afterCalled = true
|
||||
|
10
authboss.go
10
authboss.go
@@ -53,12 +53,14 @@ func (a *Authboss) Init(modulesToLoad ...string) error {
|
||||
}
|
||||
|
||||
// UpdatePassword updates the password field of a user using the same semantics
|
||||
// that register/auth do to create and verify passwords. It saves this using the storer.
|
||||
// that register/auth do to create and verify passwords. It saves this using
|
||||
// the storer.
|
||||
//
|
||||
// In addition to that, it also invalidates any remember me tokens, if the storer supports
|
||||
// that kind of operation.
|
||||
// In addition to that, it also invalidates any remember me tokens, if the
|
||||
// storer supports that kind of operation.
|
||||
//
|
||||
// If it's also desirable to log the user out, use: authboss.DelKnown(Session|Cookie)
|
||||
// If it's also desirable to log the user out, use:
|
||||
// authboss.DelKnown(Session|Cookie)
|
||||
func (a *Authboss) UpdatePassword(ctx context.Context, user AuthableUser, newPassword string) error {
|
||||
pass, err := bcrypt.GenerateFromPassword([]byte(newPassword), a.Config.Modules.BCryptCost)
|
||||
if err != nil {
|
||||
|
@@ -13,13 +13,15 @@ const (
|
||||
// the remember module. This serves as a way to force full authentication
|
||||
// by denying half-authed users acccess to sensitive areas.
|
||||
SessionHalfAuthKey = "halfauth"
|
||||
// SessionLastAction is the session key to retrieve the last action of a user.
|
||||
// SessionLastAction is the session key to retrieve the
|
||||
// last action of a user.
|
||||
SessionLastAction = "last_action"
|
||||
// Session2FA is set when a user has been authenticated with a second factor
|
||||
Session2FA = "twofactor"
|
||||
// SessionOAuth2State is the xsrf protection key for oauth.
|
||||
SessionOAuth2State = "oauth2_state"
|
||||
// SessionOAuth2Params is the additional settings for oauth like redirection/remember.
|
||||
// SessionOAuth2Params is the additional settings for oauth
|
||||
// like redirection/remember.
|
||||
SessionOAuth2Params = "oauth2_params"
|
||||
|
||||
// CookieRemember is used for cookies and form input names.
|
||||
@@ -94,8 +96,9 @@ type ClientStateResponseWriter struct {
|
||||
sessionStateEvents []ClientStateEvent
|
||||
}
|
||||
|
||||
// LoadClientStateMiddleware wraps all requests with the ClientStateResponseWriter
|
||||
// as well as loading the current client state into the context for use.
|
||||
// LoadClientStateMiddleware wraps all requests with the
|
||||
// ClientStateResponseWriter as well as loading the current client
|
||||
// state into the context for use.
|
||||
func (a *Authboss) LoadClientStateMiddleware(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
writer := a.NewResponse(w)
|
||||
@@ -121,8 +124,8 @@ func (a *Authboss) NewResponse(w http.ResponseWriter) *ClientStateResponseWriter
|
||||
}
|
||||
}
|
||||
|
||||
// LoadClientState loads the state from sessions and cookies into the ResponseWriter
|
||||
// for later use.
|
||||
// LoadClientState loads the state from sessions and cookies
|
||||
// into the ResponseWriter for later use.
|
||||
func (a *Authboss) LoadClientState(w http.ResponseWriter, r *http.Request) (*http.Request, error) {
|
||||
if a.Storage.SessionState != nil {
|
||||
state, err := a.Storage.SessionState.ReadState(r)
|
||||
|
93
config.go
93
config.go
@@ -14,16 +14,18 @@ type Config struct {
|
||||
Mount string
|
||||
|
||||
// NotAuthorized is the default URL to kick users back to when
|
||||
// they attempt an action that requires them to be logged in and they're not auth'd
|
||||
// they attempt an action that requires them to be logged in and
|
||||
// they're not auth'd
|
||||
NotAuthorized string
|
||||
|
||||
// AuthLoginOK is the redirect path after a successful authentication.
|
||||
AuthLoginOK string
|
||||
|
||||
// ConfirmOK once a user has confirmed their account, where should they go
|
||||
// ConfirmOK once a user has confirmed their account
|
||||
// this says where they should 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 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
|
||||
|
||||
// LockNotOK is a path to go to when the user fails
|
||||
@@ -34,7 +36,8 @@ type Config struct {
|
||||
|
||||
// OAuth2LoginOK is the redirect path after a successful oauth2 login
|
||||
OAuth2LoginOK string
|
||||
// OAuth2LoginNotOK is the redirect path after an unsuccessful oauth2 login
|
||||
// OAuth2LoginNotOK is the redirect path after
|
||||
// an unsuccessful oauth2 login
|
||||
OAuth2LoginNotOK string
|
||||
|
||||
// RecoverOK is the redirect path after a successful recovery of a password.
|
||||
@@ -43,7 +46,9 @@ type Config struct {
|
||||
// RegisterOK is the redirect path after a successful registration.
|
||||
RegisterOK string
|
||||
|
||||
// RootURL is the scheme+host+port of the web application (eg https://www.happiness.com:8080) for url generation. No trailing slash.
|
||||
// RootURL is the scheme+host+port of the web application
|
||||
// (eg https://www.happiness.com:8080) for url generation.
|
||||
// No trailing slash.
|
||||
RootURL string
|
||||
}
|
||||
|
||||
@@ -51,14 +56,14 @@ type Config struct {
|
||||
// BCryptCost is the cost of the bcrypt password hashing function.
|
||||
BCryptCost int
|
||||
|
||||
// ConfirmMethod controls which http method confirm expects. This is because
|
||||
// typically this is a GET request since it's a link from an e-mail, but in
|
||||
// api-like cases it needs to be able to be a post since there's data that
|
||||
// must be sent to it.
|
||||
// ConfirmMethod controls which http method confirm expects.
|
||||
// This is because typically this is a GET request since it's a link
|
||||
// from an e-mail, but in api-like cases it needs to be able to be a
|
||||
// post since there's data that must be sent to it.
|
||||
ConfirmMethod string
|
||||
|
||||
// ExpireAfter controls the time an account is idle before being logged out
|
||||
// by the ExpireMiddleware.
|
||||
// ExpireAfter controls the time an account is idle before being
|
||||
// logged out by the ExpireMiddleware.
|
||||
ExpireAfter time.Duration
|
||||
|
||||
// LockAfter this many tries.
|
||||
@@ -68,41 +73,48 @@ type Config struct {
|
||||
// LockDuration is how long an account is locked for.
|
||||
LockDuration time.Duration
|
||||
|
||||
// LogoutMethod is the method the logout route should use (default should be DELETE)
|
||||
// LogoutMethod is the method the logout route should use
|
||||
// (default should be DELETE)
|
||||
LogoutMethod string
|
||||
|
||||
// RegisterPreserveFields are fields used with registration that are to be rendered when
|
||||
// post fails in a normal way (for example validation errors), they will be passed
|
||||
// back in the data of the response under the key DataPreserve which will be a map[string]string.
|
||||
// RegisterPreserveFields are fields used with registration that are
|
||||
// to be rendered when post fails in a normal way
|
||||
// (for example validation errors), they will be passed back in the
|
||||
// data of the response under the key DataPreserve which
|
||||
// will be a map[string]string.
|
||||
//
|
||||
// All fields that are to be preserved must be able to be returned by the ArbitraryValuer.GetValues()
|
||||
// All fields that are to be preserved must be able to be returned by
|
||||
// the ArbitraryValuer.GetValues()
|
||||
//
|
||||
// This means in order to have a field named "address" you would need to have that returned by
|
||||
// the ArbitraryValuer.GetValues() method and then it would be available to be whitelisted by this
|
||||
// This means in order to have a field named "address" you would need
|
||||
// to have that returned by the ArbitraryValuer.GetValues() method and
|
||||
// then it would be available to be whitelisted by this
|
||||
// configuration variable.
|
||||
RegisterPreserveFields []string
|
||||
|
||||
// RecoverTokenDuration controls how long a token sent via email for password
|
||||
// recovery is valid for.
|
||||
// RecoverTokenDuration controls how long a token sent via
|
||||
// email for password recovery is valid for.
|
||||
RecoverTokenDuration time.Duration
|
||||
// RecoverLoginAfterRecovery says for the recovery module after a user has successfully
|
||||
// recovered the password, are they simply logged in, or are they redirected to
|
||||
// the login page with an "updated password" message.
|
||||
// RecoverLoginAfterRecovery says for the recovery module after a
|
||||
// user has successfully recovered the password, are they simply
|
||||
// logged in, or are they redirected to the login page with an
|
||||
// "updated password" message.
|
||||
RecoverLoginAfterRecovery bool
|
||||
|
||||
// OAuth2Providers lists all providers that can be used. See
|
||||
// OAuthProvider documentation for more details.
|
||||
OAuth2Providers map[string]OAuth2Provider
|
||||
|
||||
// TOTP2FAIssuer is the issuer that appears in the url when scanning a qr code
|
||||
// for google authenticator.
|
||||
// TOTP2FAIssuer is the issuer that appears in the url when scanning
|
||||
// a qr code for google authenticator.
|
||||
TOTP2FAIssuer string
|
||||
|
||||
// RoutesRedirectOnUnauthed controls whether or not a user is redirected or given
|
||||
// a 404 when they are unauthenticated and attempting to access a route that's
|
||||
// login-protected inside Authboss itself. The otp/twofactor modules all use
|
||||
// authboss.Middleware to protect their routes and this is the
|
||||
// redirectToLogin parameter in that middleware that they pass through.
|
||||
// RoutesRedirectOnUnauthed controls whether or not a user is redirected
|
||||
// or given a 404 when they are unauthenticated and attempting to access
|
||||
// a route that's login-protected inside Authboss itself.
|
||||
// The otp/twofactor modules all use authboss.Middleware to protect
|
||||
// their routes and this is the redirectToLogin parameter in that
|
||||
// middleware that they pass through.
|
||||
RoutesRedirectOnUnauthed bool
|
||||
}
|
||||
|
||||
@@ -110,8 +122,8 @@ type Config struct {
|
||||
// RootURL is a full path to an application that is hosting a front-end
|
||||
// Typically using a combination of Paths.RootURL and Paths.Mount
|
||||
// MailRoot will be assembled if not set.
|
||||
// Typically looks something like: https://our-front-end.com/authenication
|
||||
// No trailing slash
|
||||
// Typically looks like: https://our-front-end.com/authenication
|
||||
// No trailing slash.
|
||||
RootURL string
|
||||
|
||||
// From is the email address authboss e-mails come from.
|
||||
@@ -124,12 +136,13 @@ type Config struct {
|
||||
}
|
||||
|
||||
Storage struct {
|
||||
// Storer is the interface through which Authboss accesses the web apps database
|
||||
// for user operations.
|
||||
// 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.
|
||||
// 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
|
||||
@@ -150,12 +163,12 @@ type Config struct {
|
||||
// http request.
|
||||
Responder HTTPResponder
|
||||
|
||||
// Redirector can redirect a response, similar to Responder but responsible
|
||||
// only for redirection.
|
||||
// Redirector can redirect a response, similar to Responder but
|
||||
// responsible only for redirection.
|
||||
Redirector HTTPRedirector
|
||||
|
||||
// BodyReader reads validatable data from the body of a request to be able
|
||||
// to get data from the user's client.
|
||||
// BodyReader reads validatable data from the body of a request to
|
||||
// be able to get data from the user's client.
|
||||
BodyReader BodyReader
|
||||
|
||||
// ViewRenderer loads the templates for the application.
|
||||
|
@@ -96,8 +96,8 @@ func (c *Confirm) PreventAuth(w http.ResponseWriter, r *http.Request, handled bo
|
||||
return true, c.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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(r)
|
||||
if err != nil {
|
||||
@@ -117,8 +117,8 @@ func (c *Confirm) StartConfirmationWeb(w http.ResponseWriter, r *http.Request, h
|
||||
return true, c.Authboss.Config.Core.Redirector.Redirect(w, r, ro)
|
||||
}
|
||||
|
||||
// StartConfirmation begins confirmation on a user by setting them to require confirmation
|
||||
// via a created token, and optionally sending them an e-mail.
|
||||
// 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)
|
||||
|
||||
@@ -260,12 +260,13 @@ func (c *Confirm) invalidToken(w http.ResponseWriter, r *http.Request) error {
|
||||
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.
|
||||
// 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.
|
||||
// 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.
|
||||
func Middleware(ab *authboss.Authboss) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -291,9 +292,11 @@ func Middleware(ab *authboss.Authboss) func(http.Handler) http.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateConfirmCreds generates pieces needed for user confirmy
|
||||
// selector: hash of the first half of a 64 byte value (to be stored in the database and used in SELECT query)
|
||||
// verifier: hash of the second half of a 64 byte value (to be stored in database but never used in SELECT query)
|
||||
// GenerateConfirmCreds generates pieces needed for user confirm
|
||||
// selector: hash of the first half of a 64 byte value
|
||||
// (to be stored in the database and used in SELECT query)
|
||||
// verifier: hash of the second half of a 64 byte value
|
||||
// (to be stored in database but never used in SELECT query)
|
||||
// token: the user-facing base64 encoded selector+verifier
|
||||
func GenerateConfirmCreds() (selector, verifier, token string, err error) {
|
||||
rawToken := make([]byte, confirmTokenSize)
|
||||
|
@@ -17,7 +17,8 @@ func NewResponder(renderer authboss.Renderer) *Responder {
|
||||
}
|
||||
|
||||
// Respond to an HTTP request. It's main job is to merge data that comes in from
|
||||
// various middlewares via the context with the data sent by the controller and render that.
|
||||
// various middlewares via the context with the data sent by the controller and
|
||||
// render that.
|
||||
func (r *Responder) Respond(w http.ResponseWriter, req *http.Request, code int, page string, data authboss.HTMLData) error {
|
||||
ctxData := req.Context().Value(authboss.CTXKeyData)
|
||||
if ctxData != nil {
|
||||
|
@@ -17,7 +17,8 @@ import (
|
||||
// NewSMTPMailer creates an SMTP Mailer to send emails with.
|
||||
// An example usage might be something like:
|
||||
//
|
||||
// NewSMTPMailer("smtp.gmail.com", smtp.PlainAuth("", "admin@yoursite.com", "password", "smtp.gmail.com"))
|
||||
// NewSMTPMailer("smtp.gmail.com",
|
||||
// smtp.PlainAuth("", "admin@yoursite.com", "password", "smtp.gmail.com"))
|
||||
func NewSMTPMailer(server string, auth smtp.Auth) *SMTPMailer {
|
||||
if len(server) == 0 {
|
||||
panic("SMTP Mailer must be created with a server string.")
|
||||
|
10
html_data.go
10
html_data.go
@@ -32,8 +32,8 @@ const (
|
||||
// keyed by the field name, and the value is the field value.
|
||||
DataPreserve = "preserve"
|
||||
// DataModules contains a map[string]bool of which modules are loaded
|
||||
// The bool is largely extraneous and can be ignored, if the module is loaded
|
||||
// it will be present in the map, if not it will be missing.
|
||||
// The bool is largely extraneous and can be ignored, if the module is
|
||||
// loaded it will be present in the map, if not it will be missing.
|
||||
DataModules = "modules"
|
||||
)
|
||||
|
||||
@@ -41,7 +41,8 @@ const (
|
||||
type HTMLData map[string]interface{}
|
||||
|
||||
// NewHTMLData creates HTMLData from key-value pairs. The input is a key-value
|
||||
// slice, where odd elements are keys, and the following even element is their value.
|
||||
// slice, where odd elements are keys, and the following even element
|
||||
// is their value.
|
||||
func NewHTMLData(data ...interface{}) HTMLData {
|
||||
if len(data)%2 != 0 {
|
||||
panic("it should be a key value list of arguments.")
|
||||
@@ -71,7 +72,8 @@ func (h HTMLData) Merge(other HTMLData) HTMLData {
|
||||
}
|
||||
|
||||
// MergeKV adds extra key-values to the HTMLData. The input is a key-value
|
||||
// slice, where odd elements are keys, and the following even element is their value.
|
||||
// slice, where odd elements are keys, and the following even element
|
||||
// is their value.
|
||||
func (h HTMLData) MergeKV(data ...interface{}) HTMLData {
|
||||
if len(data)%2 != 0 {
|
||||
panic("It should be a key value list of arguments.")
|
||||
|
@@ -266,7 +266,8 @@ func (s *ServerStorer) SaveOAuth2(ctx context.Context, user authboss.OAuth2User)
|
||||
u := user.(*User)
|
||||
|
||||
pid := authboss.MakeOAuth2PID(u.OAuth2Provider, u.OAuth2UID)
|
||||
// Since we don't have to differentiate between insert/update in a map, we just overwrite
|
||||
// Since we don't have to differentiate between
|
||||
// insert/update in a map, we just overwrite
|
||||
s.Users[pid] = u
|
||||
return nil
|
||||
}
|
||||
@@ -329,7 +330,8 @@ func (s *ServerStorer) UseRememberToken(ctx context.Context, givenKey, token str
|
||||
return authboss.ErrTokenNotFound
|
||||
}
|
||||
|
||||
// FailStorer is used for testing module initialize functions that recover more than the base storer
|
||||
// FailStorer is used for testing module initialize functions that
|
||||
// recover more than the base storer
|
||||
type FailStorer struct {
|
||||
User
|
||||
}
|
||||
|
@@ -148,9 +148,10 @@ func (l *Lock) Unlock(ctx context.Context, key string) error {
|
||||
return l.Authboss.Config.Storage.Server.Save(ctx, lu)
|
||||
}
|
||||
|
||||
// Middleware ensures that a user is not locked, or else it will intercept the request
|
||||
// and send them to the configured LockNotOK page, this will load the user if he's not been loaded
|
||||
// yet from the session. And panics if it cannot load the user.
|
||||
// Middleware ensures that a user is not locked, or else it will intercept
|
||||
// the request and send them to the configured LockNotOK page, this will load
|
||||
// the user if he's not been loaded yet from the session. And panics if it
|
||||
// cannot load the user.
|
||||
func Middleware(ab *authboss.Authboss) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
@@ -109,7 +109,8 @@ func TestLogoutLogout(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
w := h.ab.NewResponse(resp)
|
||||
|
||||
// This enables the logging portion, which is debatable-y not useful in a log out method
|
||||
// This enables the logging portion
|
||||
// which is debatable-y not useful in a log out method
|
||||
user := &mocks.User{Email: "test@test.com"}
|
||||
r = r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyUser, user))
|
||||
|
||||
|
20
module.go
20
module.go
@@ -51,13 +51,13 @@ func (a *Authboss) IsLoaded(mod string) bool {
|
||||
}
|
||||
|
||||
// loadModule loads a particular module. It uses reflection to create a new
|
||||
// instance of the module type. The original value is copied, but not deep copied
|
||||
// so care should be taken to make sure most initialization happens inside the Initialize()
|
||||
// method of the module.
|
||||
// instance of the module type. The original value is copied, but not deep
|
||||
// copied so care should be taken to make sure most initialization happens
|
||||
// inside the Initialize() method of the module.
|
||||
//
|
||||
// This method exists so many copies of authboss can be loaded and initialized at the same time
|
||||
// if we didn't use this, then the registeredModules instances of the modules would end up used
|
||||
// by the first instance of authboss.
|
||||
// This method exists so many copies of authboss can be loaded and initialized
|
||||
// at the same time if we didn't use this, then the registeredModules
|
||||
// instances of the modules would end up used by the first instance of authboss.
|
||||
func (a *Authboss) loadModule(name string) error {
|
||||
module, ok := registeredModules[name]
|
||||
if !ok {
|
||||
@@ -87,15 +87,15 @@ func (a *Authboss) loadModule(name string) error {
|
||||
|
||||
// ModuleListMiddleware puts a map in the data that can be used
|
||||
// to provide the renderer with information about which pieces of the
|
||||
// views to show. The bool is extraneous, as presence in the map is the indication
|
||||
// of wether or not the module is loaded.
|
||||
// views to show. The bool is extraneous, as presence in the map is
|
||||
// the indication of wether or not the module is loaded.
|
||||
// Data looks like:
|
||||
// map[modulename] = true
|
||||
//
|
||||
// oauth2 providers are also listed here using the syntax:
|
||||
// oauth2.google for an example. Be careful since this doesn't actually mean
|
||||
// that the oauth2 module has been loaded so you should do a conditional that checks
|
||||
// for both.
|
||||
// that the oauth2 module has been loaded so you should do a conditional
|
||||
// that checks for both.
|
||||
func ModuleListMiddleware(ab *Authboss) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
@@ -5,28 +5,31 @@
|
||||
// The general flow looks like this:
|
||||
// 1. User goes to Start handler and has his session packed with goodies
|
||||
// then redirects to the OAuth service.
|
||||
// 2. OAuth service returns to OAuthCallback which extracts state and parameters
|
||||
// and generally checks that everything is ok. It uses the token received to
|
||||
// get an access token from the oauth2 library
|
||||
// 3. Calls the OAuth2Provider.FindUserDetails which should return the user's details
|
||||
// in a generic form.
|
||||
// 4. Passes the user details into the OAuth2ServerStorer.NewFromOAuth2 in order
|
||||
// to create a user object we can work with.
|
||||
// 2. OAuth service returns to OAuthCallback which extracts state and
|
||||
// parameters and generally checks that everything is ok. It uses the
|
||||
// token received to get an access token from the oauth2 library
|
||||
// 3. Calls the OAuth2Provider.FindUserDetails which should return the user's
|
||||
// details in a generic form.
|
||||
// 4. Passes the user details into the OAuth2ServerStorer.NewFromOAuth2 in
|
||||
// order to create a user object we can work with.
|
||||
// 5. Saves the user in the database, logs them in, redirects.
|
||||
//
|
||||
// In order to do this there are a number of parts:
|
||||
// 1. The configuration of a provider (handled by authboss.Config.Modules.OAuth2Providers)
|
||||
// 2. The flow of redirection of client, parameter passing etc (handled by this package)
|
||||
// 3. The HTTP call to the service once a token has been retrieved to get user details
|
||||
// (handled by OAuth2Provider.FindUserDetails)
|
||||
// 4. The creation of a user from the user details returned from the FindUserDetails
|
||||
// (authboss.OAuth2ServerStorer)
|
||||
// 1. The configuration of a provider
|
||||
// (handled by authboss.Config.Modules.OAuth2Providers).
|
||||
// 2. The flow of redirection of client, parameter passing etc
|
||||
// (handled by this package)
|
||||
// 3. The HTTP call to the service once a token has been retrieved to
|
||||
// get user details (handled by OAuth2Provider.FindUserDetails)
|
||||
// 4. The creation of a user from the user details returned from the
|
||||
// FindUserDetails (authboss.OAuth2ServerStorer)
|
||||
//
|
||||
// Of these parts, the responsibility of the authboss library consumer is on 1, 3, and 4.
|
||||
// Configuration of providers that should be used is totally up to the consumer. The FindUserDetails
|
||||
// function is typically up to the user, but we have some basic ones included in this package too.
|
||||
// The creation of users from the FindUserDetail's map[string]string return is handled as part
|
||||
// of the implementation of the OAuth2ServerStorer.
|
||||
// Of these parts, the responsibility of the authboss library consumer
|
||||
// is on 1, 3, and 4. Configuration of providers that should be used is totally
|
||||
// up to the consumer. The FindUserDetails function is typically up to the
|
||||
// user, but we have some basic ones included in this package too.
|
||||
// The creation of users from the FindUserDetail's map[string]string return
|
||||
// is handled as part of the implementation of the OAuth2ServerStorer.
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
@@ -73,7 +76,8 @@ func (o *OAuth2) Init(ab *authboss.Authboss) error {
|
||||
o.Authboss = ab
|
||||
|
||||
// Do annoying sorting on keys so we can have predictible
|
||||
// route registration (both for consistency inside the router but also for tests -_-)
|
||||
// route registration (both for consistency inside the router but
|
||||
// also for tests -_-)
|
||||
var keys []string
|
||||
for k := range o.Authboss.Config.Modules.OAuth2Providers {
|
||||
keys = append(keys, k)
|
||||
|
@@ -30,7 +30,8 @@ type googleMeResponse struct {
|
||||
// testing
|
||||
var clientGet = (*http.Client).Get
|
||||
|
||||
// GoogleUserDetails can be used as a FindUserDetails function for an authboss.OAuth2Provider
|
||||
// GoogleUserDetails can be used as a FindUserDetails function
|
||||
// for an authboss.OAuth2Provider
|
||||
func GoogleUserDetails(ctx context.Context, cfg oauth2.Config, token *oauth2.Token) (map[string]string, error) {
|
||||
client := cfg.Client(ctx, token)
|
||||
resp, err := clientGet(client, googleInfoEndpoint)
|
||||
@@ -61,7 +62,8 @@ type facebookMeResponse struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// FacebookUserDetails can be used as a FindUserDetails function for an authboss.OAuth2Provider
|
||||
// FacebookUserDetails can be used as a FindUserDetails function
|
||||
// for an authboss.OAuth2Provider
|
||||
func FacebookUserDetails(ctx context.Context, cfg oauth2.Config, token *oauth2.Token) (map[string]string, error) {
|
||||
client := cfg.Client(ctx, token)
|
||||
resp, err := clientGet(client, facebookInfoEndpoint)
|
||||
|
@@ -12,8 +12,8 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
// This has an extra parameter that the Google client wouldn't normally get, but it'll safely be
|
||||
// ignored.
|
||||
// This has an extra parameter that the Google client wouldn't normally
|
||||
// get, but it'll safely be ignored.
|
||||
clientGet = func(_ *http.Client, url string) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
Body: ioutil.NopCloser(strings.NewReader(`{"id":"id", "email":"email", "name": "name"}`)),
|
||||
|
@@ -359,7 +359,8 @@ func TestAuthPostUserNotFound(t *testing.T) {
|
||||
resp := httptest.NewRecorder()
|
||||
w := harness.ab.NewResponse(resp)
|
||||
|
||||
// This event is really the only thing that separates "user not found" from "bad password"
|
||||
// This event is really the only thing that separates
|
||||
// "user not found" from "bad password"
|
||||
var afterCalled bool
|
||||
harness.ab.Events.After(authboss.EventAuthFail, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
|
||||
afterCalled = true
|
||||
|
@@ -20,7 +20,8 @@ type User interface {
|
||||
|
||||
// GetRecoveryCodes retrieves a CSV string of bcrypt'd recovery codes
|
||||
GetRecoveryCodes() string
|
||||
// PutRecoveryCodes uses a single string to store many bcrypt'd recovery codes
|
||||
// PutRecoveryCodes uses a single string to store many
|
||||
// bcrypt'd recovery codes
|
||||
PutRecoveryCodes(codes string)
|
||||
}
|
||||
|
||||
@@ -147,8 +148,8 @@ func BCryptRecoveryCodes(codes []string) ([]string, error) {
|
||||
return cryptedCodes, nil
|
||||
}
|
||||
|
||||
// UseRecoveryCode deletes the code that was used from the string slice and returns it
|
||||
// the bool is true if a code was used
|
||||
// UseRecoveryCode deletes the code that was used from the string slice and
|
||||
// returns it, the bool is true if a code was used
|
||||
func UseRecoveryCode(codes []string, inputCode string) ([]string, bool) {
|
||||
input := []byte(inputCode)
|
||||
use := -1
|
||||
|
@@ -155,8 +155,8 @@ func (r *Recover) SendRecoverEmail(ctx context.Context, to, encodedToken string)
|
||||
}
|
||||
}
|
||||
|
||||
// EndGet shows a password recovery form, and it should have the token that the user
|
||||
// brought in the query parameters in it on submission.
|
||||
// EndGet shows a password recovery form, and it should have the token that
|
||||
// the user brought in the query parameters in it on submission.
|
||||
func (r *Recover) EndGet(w http.ResponseWriter, req *http.Request) error {
|
||||
validatable, err := r.Authboss.Core.BodyReader.Read(PageRecoverMiddle, req)
|
||||
if err != nil {
|
||||
@@ -282,8 +282,10 @@ func (r *Recover) mailURL(token string) string {
|
||||
}
|
||||
|
||||
// GenerateRecoverCreds generates pieces needed for user recovery
|
||||
// selector: hash of the first half of a 64 byte value (to be stored in the database and used in SELECT query)
|
||||
// verifier: hash of the second half of a 64 byte value (to be stored in database but never used in SELECT query)
|
||||
// selector: hash of the first half of a 64 byte value
|
||||
// (to be stored in the database and used in SELECT query)
|
||||
// verifier: hash of the second half of a 64 byte value
|
||||
// (to be stored in database but never used in SELECT query)
|
||||
// token: the user-facing base64 encoded selector+verifier
|
||||
func GenerateRecoverCreds() (selector, verifier, token string, err error) {
|
||||
rawToken := make([]byte, recoverTokenSize)
|
||||
|
@@ -128,8 +128,8 @@ func (r *Register) Post(w http.ResponseWriter, req *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Log the user in, but only if the response wasn't handled previously by a module
|
||||
// like confirm.
|
||||
// Log the user in, but only if the response wasn't handled previously
|
||||
// by a module like confirm.
|
||||
authboss.PutSession(w, authboss.SessionKey, pid)
|
||||
|
||||
logger.Infof("registered and logged in user %s", pid)
|
||||
@@ -141,7 +141,8 @@ func (r *Register) Post(w http.ResponseWriter, req *http.Request) error {
|
||||
return r.Config.Core.Redirector.Redirect(w, req, ro)
|
||||
}
|
||||
|
||||
// hasString checks to see if a sorted (ascending) array of strings contains a string
|
||||
// hasString checks to see if a sorted (ascending) array of
|
||||
// strings contains a string
|
||||
func hasString(arr []string, s string) bool {
|
||||
index := sort.SearchStrings(arr, s)
|
||||
if index < 0 || index >= len(arr) {
|
||||
|
@@ -196,7 +196,8 @@ func TestRegisterPostValidationFailure(t *testing.T) {
|
||||
|
||||
h := testSetup()
|
||||
|
||||
// Ensure the below is sorted, the sort normally happens in Init() that we don't call
|
||||
// Ensure the below is sorted, the sort normally happens in Init()
|
||||
// that we don't call
|
||||
h.ab.Modules.RegisterPreserveFields = []string{"another", "email"}
|
||||
h.bodyReader.Return = mocks.ArbValues{
|
||||
Values: map[string]string{
|
||||
@@ -249,7 +250,8 @@ func TestRegisterPostUserExists(t *testing.T) {
|
||||
|
||||
h := testSetup()
|
||||
|
||||
// Ensure the below is sorted, the sort normally happens in Init() that we don't call
|
||||
// Ensure the below is sorted, the sort normally happens in Init()
|
||||
// that we don't call
|
||||
h.ab.Modules.RegisterPreserveFields = []string{"another", "email"}
|
||||
h.storer.Users["test@test.com"] = &mocks.User{}
|
||||
h.bodyReader.Return = mocks.ArbValues{
|
||||
|
17
response.go
17
response.go
@@ -20,15 +20,15 @@ const (
|
||||
// - 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.
|
||||
// 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.
|
||||
// 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
|
||||
}
|
||||
@@ -46,8 +46,8 @@ type RedirectOptions struct {
|
||||
|
||||
// When a request should redirect a user somewhere on completion, these
|
||||
// should be set. RedirectURL tells it where to go. And optionally set
|
||||
// FollowRedirParam to override the RedirectURL if the form parameter defined
|
||||
// by FormValueRedirect is passed in the request.
|
||||
// FollowRedirParam to override the RedirectURL if the form parameter
|
||||
// defined by FormValueRedirect is passed in the request.
|
||||
//
|
||||
// Redirecting works differently whether it's an API request or not.
|
||||
// If it's an API request, then it will leave the URL in a "redirect"
|
||||
@@ -63,7 +63,8 @@ type EmailResponseOptions struct {
|
||||
TextTemplate string
|
||||
}
|
||||
|
||||
// Email renders the e-mail templates for the given email and sends it using the mailer.
|
||||
// Email renders the e-mail templates for the given email and
|
||||
// sends it using the mailer.
|
||||
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)
|
||||
|
39
storage.go
39
storage.go
@@ -14,12 +14,13 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrUserFound should be returned from Create (see ConfirmUser) when the primaryID
|
||||
// of the record is found.
|
||||
// ErrUserFound should be returned from Create (see ConfirmUser)
|
||||
// when the primaryID of the record is found.
|
||||
ErrUserFound = errors.New("user found")
|
||||
// ErrUserNotFound should be returned from Get when the record is not found.
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
// ErrTokenNotFound should be returned from UseToken when the record is not found.
|
||||
// ErrTokenNotFound should be returned from UseToken when the
|
||||
// record is not found.
|
||||
ErrTokenNotFound = errors.New("token not found")
|
||||
)
|
||||
|
||||
@@ -56,23 +57,24 @@ type OAuth2ServerStorer interface {
|
||||
// of details returned from OAuth2Provider.FindUserDetails
|
||||
// A more in-depth explanation is that once we've got an access token
|
||||
// for the service in question (say a service that rhymes with book)
|
||||
// the FindUserDetails function does an http request to a known endpoint that
|
||||
// provides details about the user, those details are captured in a generic
|
||||
// way as map[string]string and passed into this function to be turned
|
||||
// into a real user.
|
||||
// the FindUserDetails function does an http request to a known endpoint
|
||||
// that provides details about the user, those details are captured in a
|
||||
// generic way as map[string]string and passed into this function to be
|
||||
// turned into a real user.
|
||||
//
|
||||
// It's possible that the user exists in the database already, and so
|
||||
// an attempt should be made to look that user up using the details.
|
||||
// Any details that have changed should be updated. Do not save the user
|
||||
// since that will be done by a later call to OAuth2ServerStorer.SaveOAuth2()
|
||||
// since that will be done later by OAuth2ServerStorer.SaveOAuth2()
|
||||
NewFromOAuth2(ctx context.Context, provider string, details map[string]string) (OAuth2User, error)
|
||||
|
||||
// SaveOAuth2 has different semantics from the typical ServerStorer.Save, in this case
|
||||
// we want to insert a user if they do not exist. The difference must be made clear because
|
||||
// in the non-oauth2 case, we know exactly when we want to Create vs Update. However
|
||||
// since we're simply trying to persist a user that may have been in our database, but if not
|
||||
// should already be (since you can think of the operation as a caching of what's on the oauth2 provider's
|
||||
// servers).
|
||||
// SaveOAuth2 has different semantics from the typical ServerStorer.Save,
|
||||
// in this case we want to insert a user if they do not exist.
|
||||
// The difference must be made clear because in the non-oauth2 case,
|
||||
// we know exactly when we want to Create vs Update. However since we're
|
||||
// simply trying to persist a user that may have been in our database,
|
||||
// but if not should already be (since you can think of the operation as
|
||||
// a caching of what's on the oauth2 provider's servers).
|
||||
SaveOAuth2(ctx context.Context, user OAuth2User) error
|
||||
}
|
||||
|
||||
@@ -117,7 +119,8 @@ func EnsureCanCreate(storer ServerStorer) CreatingServerStorer {
|
||||
return s
|
||||
}
|
||||
|
||||
// EnsureCanConfirm makes sure the server storer supports confirm-lookup operations
|
||||
// EnsureCanConfirm makes sure the server storer supports
|
||||
// confirm-lookup operations
|
||||
func EnsureCanConfirm(storer ServerStorer) ConfirmingServerStorer {
|
||||
s, ok := storer.(ConfirmingServerStorer)
|
||||
if !ok {
|
||||
@@ -127,7 +130,8 @@ func EnsureCanConfirm(storer ServerStorer) ConfirmingServerStorer {
|
||||
return s
|
||||
}
|
||||
|
||||
// EnsureCanRecover makes sure the server storer supports confirm-lookup operations
|
||||
// EnsureCanRecover makes sure the server storer supports
|
||||
// confirm-lookup operations
|
||||
func EnsureCanRecover(storer ServerStorer) RecoveringServerStorer {
|
||||
s, ok := storer.(RecoveringServerStorer)
|
||||
if !ok {
|
||||
@@ -147,7 +151,8 @@ func EnsureCanRemember(storer ServerStorer) RememberingServerStorer {
|
||||
return s
|
||||
}
|
||||
|
||||
// EnsureCanOAuth2 makes sure the server storer supports oauth2 creation and lookup
|
||||
// EnsureCanOAuth2 makes sure the server storer supports
|
||||
// oauth2 creation and lookup
|
||||
func EnsureCanOAuth2(storer ServerStorer) OAuth2ServerStorer {
|
||||
s, ok := storer.(OAuth2ServerStorer)
|
||||
if !ok {
|
||||
|
@@ -9,8 +9,8 @@ const (
|
||||
ConfirmPrefix = "confirm_"
|
||||
)
|
||||
|
||||
// Validator takes a form name and a set of inputs and returns any validation errors
|
||||
// for the inputs.
|
||||
// Validator takes a form name and a set of inputs and returns any
|
||||
// validation errors for the inputs.
|
||||
type Validator interface {
|
||||
// Validate makes the type validate itself and return
|
||||
// a list of validation errors.
|
||||
@@ -26,8 +26,8 @@ type FieldError interface {
|
||||
Err() error
|
||||
}
|
||||
|
||||
// ErrorMap is a shortcut to change []error into ErrorList and call Map on it since
|
||||
// this is a common operation.
|
||||
// ErrorMap is a shortcut to change []error into ErrorList and call Map on it
|
||||
// since this is a common operation.
|
||||
func ErrorMap(e []error) map[string][]string {
|
||||
return ErrorList(e).Map()
|
||||
}
|
||||
|
Reference in New Issue
Block a user