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 }