mirror of
https://github.com/volatiletech/authboss.git
synced 2024-11-24 08:42:17 +02:00
226 lines
7.0 KiB
Go
226 lines
7.0 KiB
Go
/*
|
|
Package authboss is a modular authentication system for the web. It tries to
|
|
remove as much boilerplate and "hard things" as possible so that each time you
|
|
start a new web project in Go, you can plug it in, configure and be off to the
|
|
races without having to think about how to store passwords or remember tokens.
|
|
*/
|
|
package authboss
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
|
|
"github.com/friendsofgo/errors"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// Authboss contains a configuration and other details for running.
|
|
type Authboss struct {
|
|
Config
|
|
Events *Events
|
|
|
|
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.Events = NewEvents()
|
|
|
|
ab.Config.Defaults()
|
|
return ab
|
|
}
|
|
|
|
// Init authboss, modules, renderers
|
|
func (a *Authboss) Init(modulesToLoad ...string) error {
|
|
if len(modulesToLoad) == 0 {
|
|
modulesToLoad = RegisteredModules()
|
|
}
|
|
|
|
for _, name := range modulesToLoad {
|
|
if err := a.loadModule(name); err != nil {
|
|
return errors.Errorf("module %s failed to load: %+v", name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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.
|
|
//
|
|
// 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)
|
|
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 {
|
|
return err
|
|
}
|
|
|
|
user.PutPassword(string(pass))
|
|
|
|
storer := a.Config.Storage.Server
|
|
if err := storer.Save(ctx, user); err != nil {
|
|
return err
|
|
}
|
|
|
|
rmStorer, ok := storer.(RememberingServerStorer)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
return rmStorer.DelRememberTokens(ctx, user.GetPID())
|
|
}
|
|
|
|
// VerifyPassword uses authboss mechanisms to check that a password is correct.
|
|
// Returns nil on success otherwise there will be an error. Simply a helper
|
|
// to do the bcrypt comparison.
|
|
func VerifyPassword(user AuthableUser, password string) error {
|
|
return bcrypt.CompareHashAndPassword([]byte(user.GetPassword()), []byte(password))
|
|
}
|
|
|
|
// 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:
|
|
//
|
|
// 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 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 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) {
|
|
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)
|
|
|
|
redirURL := r.URL.Path
|
|
if mountPathed && len(ab.Config.Paths.Mount) != 0 {
|
|
redirURL = path.Join(ab.Config.Paths.Mount, redirURL)
|
|
}
|
|
vals.Set(FormValueRedirect, redirURL)
|
|
|
|
ro := RedirectOptions{
|
|
Code: http.StatusTemporaryRedirect,
|
|
Failure: "please re-login",
|
|
RedirectPath: path.Join(ab.Config.Paths.Mount, fmt.Sprintf("/login?%s", vals.Encode())),
|
|
}
|
|
|
|
if err := ab.Config.Core.Redirector.Redirect(w, r, ro); err != nil {
|
|
log.Errorf("failed to redirect user during authboss.Middleware redirect: %+v", err)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
if hasBit(reqs, RequireFullAuth) && !IsFullyAuthed(r) || hasBit(reqs, Require2FA) && !IsTwoFactored(r) {
|
|
fail(w, r)
|
|
return
|
|
}
|
|
|
|
if _, err := ab.LoadCurrentUser(&r); err == ErrUserNotFound {
|
|
fail(w, r)
|
|
return
|
|
} else if err != nil {
|
|
log.Errorf("error fetching current user: %+v", err)
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
} else {
|
|
next.ServeHTTP(w, r)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func hasBit(reqs, req MWRequirements) bool {
|
|
return reqs&req == req
|
|
}
|