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:
parent
adaf5a9192
commit
6f3e7ca54a
@ -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
|
||||
|
||||
|
99
authboss.go
99
authboss.go
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
14
config.go
14
config.go
@ -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 {
|
||||
|
@ -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)))
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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)))
|
||||
|
||||
|
@ -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)))
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user