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

Add more flexibility to authboss.Middleware

- Add requirements and responses for the authboss middleware. This lets
  us later add new types that don't break the API instead of a list of
  bools.
This commit is contained in:
Aaron L 2018-12-10 23:00:27 -08:00
parent adaf5a9192
commit 6f3e7ca54a
9 changed files with 168 additions and 30 deletions

View File

@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Add e-mail confirmation before 2fa setup feature - Add e-mail confirmation before 2fa setup feature
- Add config value TwoFactorEmailAuthRequired - Add config value TwoFactorEmailAuthRequired
- Add a more flexible way of adding behaviors and requirements to
authboss.Middleware. This API is at authboss.Middleware2 temporarily
until we can make a breaking change.
### Fixed ### Fixed
@ -31,6 +34,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Deprecate the config field ConfirmMethod in favor of MailRouteMethod. See - Deprecate the config field ConfirmMethod in favor of MailRouteMethod. See
documentation for these config fields to understand how to use them now. documentation for these config fields to understand how to use them now.
- Deprecate Middleware/MountedMiddleware for Middleware2 and MountedMiddleware2
as these new APIs are more flexible. When v3 hits (Mounted)Middleware2 will
become just (Mounted)Middleware.
- Deprecate RoutesRedirectOnUnauthed in favor of ResponseOnUnauthed
## [2.1.0] - 2018-10-28 ## [2.1.0] - 2018-10-28

View File

@ -82,38 +82,96 @@ func (a *Authboss) UpdatePassword(ctx context.Context, user AuthableUser, newPas
return rmStorer.DelRememberTokens(ctx, user.GetPID()) return rmStorer.DelRememberTokens(ctx, user.GetPID())
} }
// Middleware prevents someone from accessing a route that should be // MWRequirements are user requirements for authboss.Middleware
// only allowed for users who are logged in. // in order to access the routes in protects. Requirements is a bit-set integer
// It allows the user through if they are logged in (SessionKey). // to be able to easily combine requirements like so:
// //
// If redirectToLogin is true, the user will be redirected to the // authboss.RequireFullAuth | authboss.Require2FA
// login page, otherwise they will get a 404. type MWRequirements int
// The redirect goes to: mountPath/login, this means it's expected that
// the auth module is loaded if this is set to true. // MWRespondOnFailure tells authboss.Middleware how to respond to
// // a failure to meet the requirements.
// If forceFullAuth is true then half-authed users (SessionHalfAuth) type MWRespondOnFailure int
// are not allowed through, otherwise a half-authed user will be allowed through.
// // Middleware requirements
// If force2fa is true, then users must have been logged in const (
// with 2fa (Session2FA) otherwise they will not be allowed through. RequireNone MWRequirements = 0x00
// RequireFullAuth means half-authed users will also be rejected
RequireFullAuth MWRequirements = 0x01
// Require2FA means that users who have not authed with 2fa will
// be rejected.
Require2FA MWRequirements = 0x02
)
// Middleware response types
const (
// RespondNotFound does not allow users who are not logged in to know a
// route exists by responding with a 404.
RespondNotFound MWRespondOnFailure = iota
// RespondRedirect redirects users to the login page
RespondRedirect
// RespondUnauthorized provides a 401, this allows users to know the page
// exists unlike the 404 option.
RespondUnauthorized
)
// Middleware is deprecated. See Middleware2.
func Middleware(ab *Authboss, redirectToLogin bool, forceFullAuth bool, force2fa bool) func(http.Handler) http.Handler { func Middleware(ab *Authboss, redirectToLogin bool, forceFullAuth bool, force2fa bool) func(http.Handler) http.Handler {
return MountedMiddleware(ab, false, redirectToLogin, forceFullAuth, force2fa) return MountedMiddleware(ab, false, redirectToLogin, forceFullAuth, force2fa)
} }
// MountedMiddleware hides an option from typical users in "mountPathed". // MountedMiddleware is deprecated. See MountedMiddleware2.
func MountedMiddleware(ab *Authboss, mountPathed, redirectToLogin, forceFullAuth, force2fa bool) func(http.Handler) http.Handler {
var reqs MWRequirements
failResponse := RespondNotFound
if forceFullAuth {
reqs |= RequireFullAuth
}
if force2fa {
reqs |= Require2FA
}
if redirectToLogin {
failResponse = RespondRedirect
}
return MountedMiddleware2(ab, mountPathed, reqs, failResponse)
}
// Middleware2 prevents someone from accessing a route that should be
// only allowed for users who are logged in.
// It allows the user through if they are logged in (SessionKey is present in
// the session).
//
// requirements are set by logical or'ing together requirements. eg:
//
// authboss.RequireFullAuth | authboss.Require2FA
//
// failureResponse is how the middleware rejects the users that don't meet
// the criteria. This should be chosen from the MWRespondOnFailure constants.
func Middleware2(ab *Authboss, requirements MWRequirements, failureResponse MWRespondOnFailure) func(http.Handler) http.Handler {
return MountedMiddleware2(ab, false, requirements, failureResponse)
}
// MountedMiddleware2 hides an option from typical users in "mountPathed".
// Normal routes should never need this only authboss routes (since they // Normal routes should never need this only authboss routes (since they
// are behind mountPath typically). This method is exported only for use // are behind mountPath typically). This method is exported only for use
// by Authboss modules, normal users should use Middleware instead. // by Authboss modules, normal users should use Middleware instead.
// //
// If mountPathed is true, then before redirecting to a URL it will add // If mountPathed is true, then before redirecting to a URL it will add
// the mountpath to the front of it. // the mountpath to the front of it.
func MountedMiddleware(ab *Authboss, mountPathed, redirectToLogin, forceFullAuth, force2fa bool) func(http.Handler) http.Handler { func MountedMiddleware2(ab *Authboss, mountPathed bool, reqs MWRequirements, failResponse MWRespondOnFailure) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := ab.RequestLogger(r) log := ab.RequestLogger(r)
fail := func(w http.ResponseWriter, r *http.Request) { fail := func(w http.ResponseWriter, r *http.Request) {
if redirectToLogin { switch failResponse {
case RespondNotFound:
log.Infof("not found for unauthorized user at: %s", r.URL.Path)
w.WriteHeader(http.StatusNotFound)
case RespondUnauthorized:
log.Infof("unauthorized for unauthorized user at: %s", r.URL.Path)
w.WriteHeader(http.StatusUnauthorized)
case RespondRedirect:
log.Infof("redirecting unauthorized user to login from: %s", r.URL.Path) log.Infof("redirecting unauthorized user to login from: %s", r.URL.Path)
vals := make(url.Values) vals := make(url.Values)
@ -134,12 +192,9 @@ func MountedMiddleware(ab *Authboss, mountPathed, redirectToLogin, forceFullAuth
} }
return return
} }
log.Infof("not found for unauthorized user at: %s", r.URL.Path)
w.WriteHeader(http.StatusNotFound)
} }
if forceFullAuth && !IsFullyAuthed(r) || force2fa && !IsTwoFactored(r) { if hasBit(reqs, RequireFullAuth) && !IsFullyAuthed(r) || hasBit(reqs, Require2FA) && !IsTwoFactored(r) {
fail(w, r) fail(w, r)
return return
} }
@ -157,3 +212,7 @@ func MountedMiddleware(ab *Authboss, mountPathed, redirectToLogin, forceFullAuth
}) })
} }
} }
func hasBit(reqs, req MWRequirements) bool {
return reqs&req == req
}

View File

@ -5,8 +5,6 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/davecgh/go-spew/spew"
) )
func TestAuthBossInit(t *testing.T) { func TestAuthBossInit(t *testing.T) {
@ -137,8 +135,6 @@ func TestAuthbossMiddleware(t *testing.T) {
rec, called, hadUser := setupMore(false, false, false, false) rec, called, hadUser := setupMore(false, false, false, false)
spew.Dump(ab.Storage)
if rec.Code != http.StatusNotFound { if rec.Code != http.StatusNotFound {
t.Error("wrong code:", rec.Code) t.Error("wrong code:", rec.Code)
} }
@ -149,6 +145,40 @@ func TestAuthbossMiddleware(t *testing.T) {
t.Error("should not have had user") t.Error("should not have had user")
} }
}) })
t.Run("Reject401", func(t *testing.T) {
ab.Storage.SessionState = mockClientStateReadWriter{}
r := httptest.NewRequest("GET", "/super/secret", nil)
rec := httptest.NewRecorder()
w := ab.NewResponse(rec)
var err error
r, err = ab.LoadClientState(w, r)
if err != nil {
t.Fatal(err)
}
var mid func(http.Handler) http.Handler
mid = Middleware2(ab, RequireNone, RespondUnauthorized)
var called, hadUser bool
server := mid(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
called = true
hadUser = r.Context().Value(CTXKeyUser) != nil
w.WriteHeader(http.StatusOK)
}))
server.ServeHTTP(w, r)
if rec.Code != http.StatusUnauthorized {
t.Error("wrong code:", rec.Code)
}
if called {
t.Error("should not have been called")
}
if hadUser {
t.Error("should not have had user")
}
})
t.Run("RejectRedirect", func(t *testing.T) { t.Run("RejectRedirect", func(t *testing.T) {
redir := &testRedirector{} redir := &testRedirector{}
ab.Config.Core.Redirector = redir ab.Config.Core.Redirector = redir

View File

@ -96,7 +96,7 @@ type Config struct {
// //
// This configuration setting deprecates ConfirmMethod. // This configuration setting deprecates ConfirmMethod.
// If ConfirmMethod is set to the default value (GET) then // If ConfirmMethod is set to the default value (GET) then
// MailRouteMethod is used. If ConfirMethod is not the default value // MailRouteMethod is used. If ConfirmMethod is not the default value
// then it is used until Authboss v3 when only MailRouteMethod will be // then it is used until Authboss v3 when only MailRouteMethod will be
// used. // used.
MailRouteMethod string MailRouteMethod string
@ -138,6 +138,7 @@ type Config struct {
// a qr code for google authenticator. // a qr code for google authenticator.
TOTP2FAIssuer string TOTP2FAIssuer string
// DEPRECATED: See ResponseOnUnauthed
// RoutesRedirectOnUnauthed controls whether or not a user is redirected // RoutesRedirectOnUnauthed controls whether or not a user is redirected
// or given a 404 when they are unauthenticated and attempting to access // or given a 404 when they are unauthenticated and attempting to access
// a route that's login-protected inside Authboss itself. // a route that's login-protected inside Authboss itself.
@ -145,6 +146,17 @@ type Config struct {
// their routes and this is the redirectToLogin parameter in that // their routes and this is the redirectToLogin parameter in that
// middleware that they pass through. // middleware that they pass through.
RoutesRedirectOnUnauthed bool RoutesRedirectOnUnauthed bool
// ResponseOnUnauthed controls how a user is responded to when
// attempting to access a route that's login-protected inside Authboss
// itself. The otp/twofactor modules all use authboss.Middleware2 to
// protect their routes and this is the failResponse parameter in that
// middleware that they pass through.
//
// This deprecates RoutesRedirectOnUnauthed. If RoutesRedirectOnUnauthed
// is true, the value of this will be set to RespondRedirect until
// authboss v3.
ResponseOnUnauthed MWRespondOnFailure
} }
Mail struct { Mail struct {

View File

@ -75,7 +75,13 @@ func (o *OTP) Init(ab *authboss.Authboss) (err error) {
o.Authboss.Config.Core.Router.Get("/otp/login", o.Authboss.Core.ErrorHandler.Wrap(o.LoginGet)) o.Authboss.Config.Core.Router.Get("/otp/login", o.Authboss.Core.ErrorHandler.Wrap(o.LoginGet))
o.Authboss.Config.Core.Router.Post("/otp/login", o.Authboss.Core.ErrorHandler.Wrap(o.LoginPost)) o.Authboss.Config.Core.Router.Post("/otp/login", o.Authboss.Core.ErrorHandler.Wrap(o.LoginPost))
middleware := authboss.MountedMiddleware(ab, true, ab.Config.Modules.RoutesRedirectOnUnauthed, false, false) var unauthedResponse authboss.MWRespondOnFailure
if ab.Config.Modules.ResponseOnUnauthed != 0 {
unauthedResponse = ab.Config.Modules.ResponseOnUnauthed
} else if ab.Config.Modules.RoutesRedirectOnUnauthed {
unauthedResponse = authboss.RespondRedirect
}
middleware := authboss.MountedMiddleware2(ab, true, authboss.RequireNone, unauthedResponse)
o.Authboss.Config.Core.Router.Get("/otp/add", middleware(o.Authboss.Core.ErrorHandler.Wrap(o.AddGet))) o.Authboss.Config.Core.Router.Get("/otp/add", middleware(o.Authboss.Core.ErrorHandler.Wrap(o.AddGet)))
o.Authboss.Config.Core.Router.Post("/otp/add", middleware(o.Authboss.Core.ErrorHandler.Wrap(o.AddPost))) o.Authboss.Config.Core.Router.Post("/otp/add", middleware(o.Authboss.Core.ErrorHandler.Wrap(o.AddPost)))

View File

@ -98,7 +98,13 @@ func (s *SMS) Setup() error {
return errors.New("must have SMS.Sender set") return errors.New("must have SMS.Sender set")
} }
abmw := authboss.MountedMiddleware(s.Authboss, true, s.Authboss.Config.Modules.RoutesRedirectOnUnauthed, false, false) var unauthedResponse authboss.MWRespondOnFailure
if s.Config.Modules.ResponseOnUnauthed != 0 {
unauthedResponse = s.Config.Modules.ResponseOnUnauthed
} else if s.Config.Modules.RoutesRedirectOnUnauthed {
unauthedResponse = authboss.RespondRedirect
}
abmw := authboss.MountedMiddleware2(s.Authboss, true, authboss.RequireFullAuth, unauthedResponse)
var middleware, verified func(func(w http.ResponseWriter, r *http.Request) error) http.Handler var middleware, verified func(func(w http.ResponseWriter, r *http.Request) error) http.Handler
middleware = func(handler func(http.ResponseWriter, *http.Request) error) http.Handler { middleware = func(handler func(http.ResponseWriter, *http.Request) error) http.Handler {

View File

@ -67,7 +67,13 @@ type TOTP struct {
// Setup the module // Setup the module
func (t *TOTP) Setup() error { func (t *TOTP) Setup() error {
abmw := authboss.MountedMiddleware(t.Authboss, true, t.Authboss.Config.Modules.RoutesRedirectOnUnauthed, true, false) var unauthedResponse authboss.MWRespondOnFailure
if t.Config.Modules.ResponseOnUnauthed != 0 {
unauthedResponse = t.Config.Modules.ResponseOnUnauthed
} else if t.Config.Modules.RoutesRedirectOnUnauthed {
unauthedResponse = authboss.RespondRedirect
}
abmw := authboss.MountedMiddleware2(t.Authboss, true, authboss.RequireFullAuth, unauthedResponse)
var middleware, verified func(func(w http.ResponseWriter, r *http.Request) error) http.Handler var middleware, verified func(func(w http.ResponseWriter, r *http.Request) error) http.Handler
middleware = func(handler func(http.ResponseWriter, *http.Request) error) http.Handler { middleware = func(handler func(http.ResponseWriter, *http.Request) error) http.Handler {

View File

@ -18,7 +18,13 @@ type Recovery struct {
// Setup the module to provide recovery regeneration routes // Setup the module to provide recovery regeneration routes
func (rc *Recovery) Setup() error { func (rc *Recovery) Setup() error {
middleware := authboss.MountedMiddleware(rc.Authboss, true, rc.Authboss.Config.Modules.RoutesRedirectOnUnauthed, true, false) var unauthedResponse authboss.MWRespondOnFailure
if rc.Config.Modules.ResponseOnUnauthed != 0 {
unauthedResponse = rc.Config.Modules.ResponseOnUnauthed
} else if rc.Config.Modules.RoutesRedirectOnUnauthed {
unauthedResponse = authboss.RespondRedirect
}
middleware := authboss.MountedMiddleware2(rc.Authboss, true, authboss.RequireFullAuth, unauthedResponse)
rc.Authboss.Core.Router.Get("/2fa/recovery/regen", middleware(rc.Authboss.Core.ErrorHandler.Wrap(rc.GetRegen))) rc.Authboss.Core.Router.Get("/2fa/recovery/regen", middleware(rc.Authboss.Core.ErrorHandler.Wrap(rc.GetRegen)))
rc.Authboss.Core.Router.Post("/2fa/recovery/regen", middleware(rc.Authboss.Core.ErrorHandler.Wrap(rc.PostRegen))) rc.Authboss.Core.Router.Post("/2fa/recovery/regen", middleware(rc.Authboss.Core.ErrorHandler.Wrap(rc.PostRegen)))

View File

@ -38,7 +38,13 @@ func SetupEmailVerify(ab *authboss.Authboss, twofactorKind, setupURL string) (Em
TwofactorSetupURL: setupURL, TwofactorSetupURL: setupURL,
} }
middleware := authboss.MountedMiddleware(ab, true, ab.Config.Modules.RoutesRedirectOnUnauthed, true, false) var unauthedResponse authboss.MWRespondOnFailure
if ab.Config.Modules.ResponseOnUnauthed != 0 {
unauthedResponse = ab.Config.Modules.ResponseOnUnauthed
} else if ab.Config.Modules.RoutesRedirectOnUnauthed {
unauthedResponse = authboss.RespondRedirect
}
middleware := authboss.MountedMiddleware2(ab, true, authboss.RequireFullAuth, unauthedResponse)
e.Authboss.Core.Router.Get("/2fa/"+twofactorKind+"/email/verify", middleware(ab.Core.ErrorHandler.Wrap(e.GetStart))) e.Authboss.Core.Router.Get("/2fa/"+twofactorKind+"/email/verify", middleware(ab.Core.ErrorHandler.Wrap(e.GetStart)))
e.Authboss.Core.Router.Post("/2fa/"+twofactorKind+"/email/verify", middleware(ab.Core.ErrorHandler.Wrap(e.PostStart))) e.Authboss.Core.Router.Post("/2fa/"+twofactorKind+"/email/verify", middleware(ab.Core.ErrorHandler.Wrap(e.PostStart)))