1
0
mirror of https://github.com/volatiletech/authboss.git synced 2025-02-01 13:17:43 +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 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
@ -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
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

View File

@ -82,38 +82,96 @@ func (a *Authboss) UpdatePassword(ctx context.Context, user AuthableUser, newPas
return rmStorer.DelRememberTokens(ctx, user.GetPID())
}
// Middleware 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).
// MWRequirements are user requirements for authboss.Middleware
// in order to access the routes in protects. Requirements is a bit-set integer
// to be able to easily combine requirements like so:
//
// If redirectToLogin is true, the user will be redirected to the
// login page, otherwise they will get a 404.
// The redirect goes to: mountPath/login, this means it's expected that
// the auth module is loaded if this is set to true.
//
// If forceFullAuth is true then half-authed users (SessionHalfAuth)
// are not allowed through, otherwise a half-authed user will be allowed through.
//
// If force2fa is true, then users must have been logged in
// with 2fa (Session2FA) otherwise they will not be allowed through.
// authboss.RequireFullAuth | authboss.Require2FA
type MWRequirements int
// MWRespondOnFailure tells authboss.Middleware how to respond to
// a failure to meet the requirements.
type MWRespondOnFailure int
// Middleware requirements
const (
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 {
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
// are behind mountPath typically). This method is exported only for use
// by Authboss modules, normal users should use Middleware instead.
//
// If mountPathed is true, then before redirecting to a URL it will add
// 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 http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := ab.RequestLogger(r)
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)
vals := make(url.Values)
@ -134,12 +192,9 @@ func MountedMiddleware(ab *Authboss, mountPathed, redirectToLogin, forceFullAuth
}
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)
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/httptest"
"testing"
"github.com/davecgh/go-spew/spew"
)
func TestAuthBossInit(t *testing.T) {
@ -137,8 +135,6 @@ func TestAuthbossMiddleware(t *testing.T) {
rec, called, hadUser := setupMore(false, false, false, false)
spew.Dump(ab.Storage)
if rec.Code != http.StatusNotFound {
t.Error("wrong code:", rec.Code)
}
@ -149,6 +145,40 @@ func TestAuthbossMiddleware(t *testing.T) {
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) {
redir := &testRedirector{}
ab.Config.Core.Redirector = redir

View File

@ -96,7 +96,7 @@ type Config struct {
//
// This configuration setting deprecates ConfirmMethod.
// 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
// used.
MailRouteMethod string
@ -138,6 +138,7 @@ type Config struct {
// a qr code for google authenticator.
TOTP2FAIssuer string
// DEPRECATED: See ResponseOnUnauthed
// 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.
@ -145,6 +146,17 @@ type Config struct {
// their routes and this is the redirectToLogin parameter in that
// middleware that they pass through.
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 {

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.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.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")
}
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
middleware = func(handler func(http.ResponseWriter, *http.Request) error) http.Handler {

View File

@ -67,7 +67,13 @@ type TOTP struct {
// Setup the module
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
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
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.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,
}
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.Post("/2fa/"+twofactorKind+"/email/verify", middleware(ab.Core.ErrorHandler.Wrap(e.PostStart)))