github.com/volatiletech/authboss@v2.4.1+incompatible/authboss.go (about)

     1  /*
     2  Package authboss is a modular authentication system for the web. It tries to
     3  remove as much boilerplate and "hard things" as possible so that each time you
     4  start a new web project in Go, you can plug it in, configure and be off to the
     5  races without having to think about how to store passwords or remember tokens.
     6  */
     7  package authboss
     8  
     9  import (
    10  	"context"
    11  	"fmt"
    12  	"net/http"
    13  	"net/url"
    14  	"path"
    15  
    16  	"github.com/pkg/errors"
    17  	"golang.org/x/crypto/bcrypt"
    18  )
    19  
    20  // Authboss contains a configuration and other details for running.
    21  type Authboss struct {
    22  	Config
    23  	Events *Events
    24  
    25  	loadedModules map[string]Moduler
    26  }
    27  
    28  // New makes a new instance of authboss with a default
    29  // configuration.
    30  func New() *Authboss {
    31  	ab := &Authboss{}
    32  
    33  	ab.loadedModules = make(map[string]Moduler)
    34  	ab.Events = NewEvents()
    35  
    36  	ab.Config.Defaults()
    37  	return ab
    38  }
    39  
    40  // Init authboss, modules, renderers
    41  func (a *Authboss) Init(modulesToLoad ...string) error {
    42  	if len(modulesToLoad) == 0 {
    43  		modulesToLoad = RegisteredModules()
    44  	}
    45  
    46  	for _, name := range modulesToLoad {
    47  		if err := a.loadModule(name); err != nil {
    48  			return errors.Errorf("module %s failed to load: %+v", name, err)
    49  		}
    50  	}
    51  
    52  	return nil
    53  }
    54  
    55  // UpdatePassword updates the password field of a user using the same semantics
    56  // that register/auth do to create and verify passwords. It saves this using
    57  // the storer.
    58  //
    59  // In addition to that, it also invalidates any remember me tokens, if the
    60  // storer supports that kind of operation.
    61  //
    62  // If it's also desirable to log the user out, use:
    63  // authboss.DelKnown(Session|Cookie)
    64  func (a *Authboss) UpdatePassword(ctx context.Context, user AuthableUser, newPassword string) error {
    65  	pass, err := bcrypt.GenerateFromPassword([]byte(newPassword), a.Config.Modules.BCryptCost)
    66  	if err != nil {
    67  		return err
    68  	}
    69  
    70  	user.PutPassword(string(pass))
    71  
    72  	storer := a.Config.Storage.Server
    73  	if err := storer.Save(ctx, user); err != nil {
    74  		return err
    75  	}
    76  
    77  	rmStorer, ok := storer.(RememberingServerStorer)
    78  	if !ok {
    79  		return nil
    80  	}
    81  
    82  	return rmStorer.DelRememberTokens(ctx, user.GetPID())
    83  }
    84  
    85  // VerifyPassword uses authboss mechanisms to check that a password is correct.
    86  // Returns nil on success otherwise there will be an error. Simply a helper
    87  // to do the bcrypt comparison.
    88  func VerifyPassword(user AuthableUser, password string) error {
    89  	return bcrypt.CompareHashAndPassword([]byte(user.GetPassword()), []byte(password))
    90  }
    91  
    92  // MWRequirements are user requirements for authboss.Middleware
    93  // in order to access the routes in protects. Requirements is a bit-set integer
    94  // to be able to easily combine requirements like so:
    95  //
    96  //   authboss.RequireFullAuth | authboss.Require2FA
    97  type MWRequirements int
    98  
    99  // MWRespondOnFailure tells authboss.Middleware how to respond to
   100  // a failure to meet the requirements.
   101  type MWRespondOnFailure int
   102  
   103  // Middleware requirements
   104  const (
   105  	RequireNone MWRequirements = 0x00
   106  	// RequireFullAuth means half-authed users will also be rejected
   107  	RequireFullAuth MWRequirements = 0x01
   108  	// Require2FA means that users who have not authed with 2fa will
   109  	// be rejected.
   110  	Require2FA MWRequirements = 0x02
   111  )
   112  
   113  // Middleware response types
   114  const (
   115  	// RespondNotFound does not allow users who are not logged in to know a
   116  	// route exists by responding with a 404.
   117  	RespondNotFound MWRespondOnFailure = iota
   118  	// RespondRedirect redirects users to the login page
   119  	RespondRedirect
   120  	// RespondUnauthorized provides a 401, this allows users to know the page
   121  	// exists unlike the 404 option.
   122  	RespondUnauthorized
   123  )
   124  
   125  // Middleware is deprecated. See Middleware2.
   126  func Middleware(ab *Authboss, redirectToLogin bool, forceFullAuth bool, force2fa bool) func(http.Handler) http.Handler {
   127  	return MountedMiddleware(ab, false, redirectToLogin, forceFullAuth, force2fa)
   128  }
   129  
   130  // MountedMiddleware is deprecated. See MountedMiddleware2.
   131  func MountedMiddleware(ab *Authboss, mountPathed, redirectToLogin, forceFullAuth, force2fa bool) func(http.Handler) http.Handler {
   132  	var reqs MWRequirements
   133  	failResponse := RespondNotFound
   134  	if forceFullAuth {
   135  		reqs |= RequireFullAuth
   136  	}
   137  	if force2fa {
   138  		reqs |= Require2FA
   139  	}
   140  	if redirectToLogin {
   141  		failResponse = RespondRedirect
   142  	}
   143  	return MountedMiddleware2(ab, mountPathed, reqs, failResponse)
   144  }
   145  
   146  // Middleware2 prevents someone from accessing a route that should be
   147  // only allowed for users who are logged in.
   148  // It allows the user through if they are logged in (SessionKey is present in
   149  // the session).
   150  //
   151  // requirements are set by logical or'ing together requirements. eg:
   152  //
   153  //   authboss.RequireFullAuth | authboss.Require2FA
   154  //
   155  // failureResponse is how the middleware rejects the users that don't meet
   156  // the criteria. This should be chosen from the MWRespondOnFailure constants.
   157  func Middleware2(ab *Authboss, requirements MWRequirements, failureResponse MWRespondOnFailure) func(http.Handler) http.Handler {
   158  	return MountedMiddleware2(ab, false, requirements, failureResponse)
   159  }
   160  
   161  // MountedMiddleware2 hides an option from typical users in "mountPathed".
   162  // Normal routes should never need this only authboss routes (since they
   163  // are behind mountPath typically). This method is exported only for use
   164  // by Authboss modules, normal users should use Middleware instead.
   165  //
   166  // If mountPathed is true, then before redirecting to a URL it will add
   167  // the mountpath to the front of it.
   168  func MountedMiddleware2(ab *Authboss, mountPathed bool, reqs MWRequirements, failResponse MWRespondOnFailure) func(http.Handler) http.Handler {
   169  	return func(next http.Handler) http.Handler {
   170  		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   171  			log := ab.RequestLogger(r)
   172  
   173  			fail := func(w http.ResponseWriter, r *http.Request) {
   174  				switch failResponse {
   175  				case RespondNotFound:
   176  					log.Infof("not found for unauthorized user at: %s", r.URL.Path)
   177  					w.WriteHeader(http.StatusNotFound)
   178  				case RespondUnauthorized:
   179  					log.Infof("unauthorized for unauthorized user at: %s", r.URL.Path)
   180  					w.WriteHeader(http.StatusUnauthorized)
   181  				case RespondRedirect:
   182  					log.Infof("redirecting unauthorized user to login from: %s", r.URL.Path)
   183  					vals := make(url.Values)
   184  
   185  					redirURL := r.URL.Path
   186  					if mountPathed && len(ab.Config.Paths.Mount) != 0 {
   187  						redirURL = path.Join(ab.Config.Paths.Mount, redirURL)
   188  					}
   189  					vals.Set(FormValueRedirect, redirURL)
   190  
   191  					ro := RedirectOptions{
   192  						Code:         http.StatusTemporaryRedirect,
   193  						Failure:      "please re-login",
   194  						RedirectPath: path.Join(ab.Config.Paths.Mount, fmt.Sprintf("/login?%s", vals.Encode())),
   195  					}
   196  
   197  					if err := ab.Config.Core.Redirector.Redirect(w, r, ro); err != nil {
   198  						log.Errorf("failed to redirect user during authboss.Middleware redirect: %+v", err)
   199  					}
   200  					return
   201  				}
   202  			}
   203  
   204  			if hasBit(reqs, RequireFullAuth) && !IsFullyAuthed(r) || hasBit(reqs, Require2FA) && !IsTwoFactored(r) {
   205  				fail(w, r)
   206  				return
   207  			}
   208  
   209  			if _, err := ab.LoadCurrentUser(&r); err == ErrUserNotFound {
   210  				fail(w, r)
   211  				return
   212  			} else if err != nil {
   213  				log.Errorf("error fetching current user: %+v", err)
   214  				w.WriteHeader(http.StatusInternalServerError)
   215  				return
   216  			} else {
   217  				next.ServeHTTP(w, r)
   218  			}
   219  		})
   220  	}
   221  }
   222  
   223  func hasBit(reqs, req MWRequirements) bool {
   224  	return reqs&req == req
   225  }