github.com/volatiletech/authboss@v2.4.1+incompatible/otp/twofactor/totp2fa/totp.go (about) 1 // Package totp2fa implements two factor auth using time-based 2 // one time passwords. 3 package totp2fa 4 5 import ( 6 "bytes" 7 "context" 8 "fmt" 9 "image/png" 10 "io" 11 "net/http" 12 "net/url" 13 "path" 14 15 "github.com/pkg/errors" 16 "github.com/pquerna/otp" 17 "github.com/pquerna/otp/totp" 18 "github.com/volatiletech/authboss" 19 "github.com/volatiletech/authboss/otp/twofactor" 20 ) 21 22 const ( 23 otpKeyFormat = "otpauth://totp/%s:%s?issuer=%s&secret=%s" 24 ) 25 26 // Session keys 27 const ( 28 SessionTOTPSecret = "totp_secret" 29 SessionTOTPPendingPID = "totp_pending" 30 ) 31 32 // Pages 33 const ( 34 PageTOTPConfirm = "totp2fa_confirm" 35 PageTOTPConfirmSuccess = "totp2fa_confirm_success" 36 PageTOTPRemove = "totp2fa_remove" 37 PageTOTPRemoveSuccess = "totp2fa_remove_success" 38 PageTOTPSetup = "totp2fa_setup" 39 PageTOTPValidate = "totp2fa_validate" 40 ) 41 42 // Form value constants 43 const ( 44 FormValueCode = "code" 45 ) 46 47 // Data constants 48 const ( 49 DataTOTPSecret = SessionTOTPSecret 50 ) 51 52 var ( 53 errNoTOTPEnabled = errors.New("user does not have totp 2fa enabled") 54 ) 55 56 // User for TOTP 57 type User interface { 58 twofactor.User 59 60 GetTOTPSecretKey() string 61 PutTOTPSecretKey(string) 62 } 63 64 // TOTP implements time based one time passwords 65 type TOTP struct { 66 *authboss.Authboss 67 } 68 69 // Setup the module 70 func (t *TOTP) Setup() error { 71 var unauthedResponse authboss.MWRespondOnFailure 72 if t.Config.Modules.ResponseOnUnauthed != 0 { 73 unauthedResponse = t.Config.Modules.ResponseOnUnauthed 74 } else if t.Config.Modules.RoutesRedirectOnUnauthed { 75 unauthedResponse = authboss.RespondRedirect 76 } 77 abmw := authboss.MountedMiddleware2(t.Authboss, true, authboss.RequireFullAuth, unauthedResponse) 78 79 var middleware, verified func(func(w http.ResponseWriter, r *http.Request) error) http.Handler 80 middleware = func(handler func(http.ResponseWriter, *http.Request) error) http.Handler { 81 return abmw(t.Core.ErrorHandler.Wrap(handler)) 82 } 83 84 if t.Authboss.Config.Modules.TwoFactorEmailAuthRequired { 85 setupPath := path.Join(t.Authboss.Paths.Mount, "/2fa/totp/setup") 86 emailVerify, err := twofactor.SetupEmailVerify(t.Authboss, "totp", setupPath) 87 if err != nil { 88 return err 89 } 90 verified = func(handler func(http.ResponseWriter, *http.Request) error) http.Handler { 91 return abmw(emailVerify.Wrap(t.Core.ErrorHandler.Wrap(handler))) 92 } 93 } else { 94 verified = middleware 95 } 96 97 t.Authboss.Core.Router.Get("/2fa/totp/setup", verified(t.GetSetup)) 98 t.Authboss.Core.Router.Post("/2fa/totp/setup", verified(t.PostSetup)) 99 100 t.Authboss.Core.Router.Get("/2fa/totp/qr", verified(t.GetQRCode)) 101 102 t.Authboss.Core.Router.Get("/2fa/totp/confirm", verified(t.GetConfirm)) 103 t.Authboss.Core.Router.Post("/2fa/totp/confirm", verified(t.PostConfirm)) 104 105 t.Authboss.Core.Router.Get("/2fa/totp/remove", middleware(t.GetRemove)) 106 t.Authboss.Core.Router.Post("/2fa/totp/remove", middleware(t.PostRemove)) 107 108 t.Authboss.Core.Router.Get("/2fa/totp/validate", t.Core.ErrorHandler.Wrap(t.GetValidate)) 109 t.Authboss.Core.Router.Post("/2fa/totp/validate", t.Core.ErrorHandler.Wrap(t.PostValidate)) 110 111 t.Authboss.Events.Before(authboss.EventAuthHijack, t.HijackAuth) 112 113 return t.Authboss.Core.ViewRenderer.Load( 114 PageTOTPSetup, 115 PageTOTPValidate, 116 PageTOTPConfirm, 117 PageTOTPConfirmSuccess, 118 PageTOTPRemove, 119 PageTOTPRemoveSuccess, 120 ) 121 } 122 123 // HijackAuth stores the user's pid in a special temporary session variable 124 // and redirects them to the validation endpoint. 125 func (t *TOTP) HijackAuth(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) { 126 if handled { 127 return false, nil 128 } 129 130 user := r.Context().Value(authboss.CTXKeyUser).(User) 131 132 if len(user.GetTOTPSecretKey()) == 0 { 133 return false, nil 134 } 135 136 authboss.PutSession(w, SessionTOTPPendingPID, user.GetPID()) 137 138 var query string 139 if len(r.URL.RawQuery) != 0 { 140 query = "?" + r.URL.RawQuery 141 } 142 ro := authboss.RedirectOptions{ 143 Code: http.StatusTemporaryRedirect, 144 RedirectPath: t.Paths.Mount + "/2fa/totp/validate" + query, 145 } 146 return true, t.Authboss.Config.Core.Redirector.Redirect(w, r, ro) 147 } 148 149 // GetSetup shows a screen allows a user to opt in to setting up totp 2fa 150 func (t *TOTP) GetSetup(w http.ResponseWriter, r *http.Request) error { 151 authboss.DelSession(w, SessionTOTPSecret) 152 return t.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPSetup, nil) 153 } 154 155 // PostSetup prepares adds a key to the user's session 156 func (t *TOTP) PostSetup(w http.ResponseWriter, r *http.Request) error { 157 abUser, err := t.CurrentUser(r) 158 if err != nil { 159 return err 160 } 161 162 user := abUser.(User) 163 164 key, err := totp.Generate(totp.GenerateOpts{ 165 Issuer: t.Authboss.Config.Modules.TOTP2FAIssuer, 166 AccountName: user.GetEmail(), 167 }) 168 169 if err != nil { 170 return errors.Wrap(err, "failed to create a totp key") 171 } 172 173 secret := key.Secret() 174 authboss.PutSession(w, SessionTOTPSecret, secret) 175 176 ro := authboss.RedirectOptions{ 177 Code: http.StatusTemporaryRedirect, 178 RedirectPath: t.Paths.Mount + "/2fa/totp/confirm", 179 } 180 return t.Core.Redirector.Redirect(w, r, ro) 181 } 182 183 // GetQRCode responds with a QR code image 184 func (t *TOTP) GetQRCode(w http.ResponseWriter, r *http.Request) error { 185 abUser, err := t.CurrentUser(r) 186 if err != nil { 187 return err 188 } 189 user := abUser.(User) 190 191 totpSecret, ok := authboss.GetSession(r, SessionTOTPSecret) 192 193 var key *otp.Key 194 if !ok || len(totpSecret) == 0 { 195 totpSecret = user.GetTOTPSecretKey() 196 } 197 198 if len(totpSecret) == 0 { 199 return errors.New("no totp secret found") 200 } 201 202 key, err = otp.NewKeyFromURL( 203 fmt.Sprintf(otpKeyFormat, 204 url.PathEscape(t.Authboss.Config.Modules.TOTP2FAIssuer), 205 url.PathEscape(user.GetEmail()), 206 url.QueryEscape(t.Authboss.Config.Modules.TOTP2FAIssuer), 207 url.QueryEscape(totpSecret), 208 )) 209 210 if err != nil { 211 return errors.Wrap(err, "failed to reconstruct key from session key: %s") 212 } 213 214 image, err := key.Image(200, 200) 215 if err != nil { 216 return errors.Wrap(err, "failed to create totp qr code") 217 } 218 219 buf := &bytes.Buffer{} 220 if err = png.Encode(buf, image); err != nil { 221 return errors.Wrap(err, "failed to encode qr code to png") 222 } 223 224 w.Header().Set("Content-Type", "image/png") 225 w.WriteHeader(http.StatusOK) 226 _, err = io.Copy(w, buf) 227 return err 228 } 229 230 // GetConfirm requests a user to enter their totp code 231 func (t *TOTP) GetConfirm(w http.ResponseWriter, r *http.Request) error { 232 totpSecret, ok := authboss.GetSession(r, SessionTOTPSecret) 233 if !ok { 234 return errors.New("request failed, no totp secret present in session") 235 } 236 237 data := authboss.HTMLData{DataTOTPSecret: totpSecret} 238 return t.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPConfirm, data) 239 } 240 241 // PostConfirm finally activates totp if the code matches 242 func (t *TOTP) PostConfirm(w http.ResponseWriter, r *http.Request) error { 243 abUser, err := t.CurrentUser(r) 244 if err != nil { 245 return err 246 } 247 user := abUser.(User) 248 249 totpSecret, ok := authboss.GetSession(r, SessionTOTPSecret) 250 if !ok { 251 return errors.New("request failed, no totp secret present in session") 252 } 253 254 validator, err := t.Authboss.Config.Core.BodyReader.Read(PageTOTPConfirm, r) 255 if err != nil { 256 return err 257 } 258 259 totpCodeValues := MustHaveTOTPCodeValues(validator) 260 inputCode := totpCodeValues.GetCode() 261 262 ok = totp.Validate(inputCode, totpSecret) 263 if !ok { 264 data := authboss.HTMLData{ 265 authboss.DataValidation: map[string][]string{FormValueCode: {"2fa code was invalid"}}, 266 DataTOTPSecret: totpSecret, 267 } 268 return t.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPConfirm, data) 269 } 270 271 codes, err := twofactor.GenerateRecoveryCodes() 272 if err != nil { 273 return err 274 } 275 276 crypted, err := twofactor.BCryptRecoveryCodes(codes) 277 if err != nil { 278 return err 279 } 280 281 // Save the user which activates 2fa 282 user.PutTOTPSecretKey(totpSecret) 283 user.PutRecoveryCodes(twofactor.EncodeRecoveryCodes(crypted)) 284 if err = t.Authboss.Config.Storage.Server.Save(r.Context(), user); err != nil { 285 return err 286 } 287 288 authboss.DelSession(w, SessionTOTPSecret) 289 290 logger := t.RequestLogger(r) 291 logger.Infof("user %s enabled totp 2fa", user.GetPID()) 292 293 data := authboss.HTMLData{twofactor.DataRecoveryCodes: codes} 294 return t.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPConfirmSuccess, data) 295 } 296 297 // GetRemove starts removal 298 func (t *TOTP) GetRemove(w http.ResponseWriter, r *http.Request) error { 299 return t.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPRemove, nil) 300 } 301 302 // PostRemove removes totp 303 func (t *TOTP) PostRemove(w http.ResponseWriter, r *http.Request) error { 304 user, ok, err := t.validate(r) 305 switch { 306 case err == errNoTOTPEnabled: 307 data := authboss.HTMLData{authboss.DataErr: "totp 2fa not active"} 308 return t.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPRemove, data) 309 case err != nil: 310 return err 311 case !ok: 312 data := authboss.HTMLData{ 313 authboss.DataValidation: map[string][]string{FormValueCode: {"2fa code was invalid"}}, 314 } 315 return t.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPRemove, data) 316 } 317 318 authboss.DelSession(w, authboss.Session2FA) 319 user.PutTOTPSecretKey("") 320 if err = t.Authboss.Config.Storage.Server.Save(r.Context(), user); err != nil { 321 return err 322 } 323 324 logger := t.RequestLogger(r) 325 logger.Infof("user %s disabled totp 2fa", user.GetPID()) 326 327 return t.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPRemoveSuccess, nil) 328 } 329 330 // GetValidate shows a page to enter a code into 331 func (t *TOTP) GetValidate(w http.ResponseWriter, r *http.Request) error { 332 return t.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPValidate, nil) 333 } 334 335 // PostValidate redirects on success 336 func (t *TOTP) PostValidate(w http.ResponseWriter, r *http.Request) error { 337 logger := t.RequestLogger(r) 338 339 user, ok, err := t.validate(r) 340 switch { 341 case err == errNoTOTPEnabled: 342 logger.Infof("user %s totp failure (not enabled)", user.GetPID()) 343 data := authboss.HTMLData{authboss.DataErr: "totp 2fa not active"} 344 return t.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPValidate, data) 345 case err != nil: 346 return err 347 case !ok: 348 r = r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyUser, user)) 349 handled, err := t.Authboss.Events.FireAfter(authboss.EventAuthFail, w, r) 350 if err != nil { 351 return err 352 } else if handled { 353 return nil 354 } 355 356 logger.Infof("user %s totp 2fa failure (wrong code)", user.GetPID()) 357 data := authboss.HTMLData{ 358 authboss.DataValidation: map[string][]string{FormValueCode: {"2fa code was invalid"}}, 359 } 360 return t.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageTOTPValidate, data) 361 } 362 363 authboss.PutSession(w, authboss.SessionKey, user.GetPID()) 364 authboss.PutSession(w, authboss.Session2FA, "totp") 365 366 authboss.DelSession(w, authboss.SessionHalfAuthKey) 367 authboss.DelSession(w, SessionTOTPPendingPID) 368 authboss.DelSession(w, SessionTOTPSecret) 369 370 logger.Infof("user %s totp 2fa success", user.GetPID()) 371 372 r = r.WithContext(context.WithValue(r.Context(), authboss.CTXKeyUser, user)) 373 handled, err := t.Authboss.Events.FireAfter(authboss.EventAuth, w, r) 374 if err != nil { 375 return err 376 } else if handled { 377 return nil 378 } 379 380 ro := authboss.RedirectOptions{ 381 Code: http.StatusTemporaryRedirect, 382 Success: "Successfully Authenticated", 383 RedirectPath: t.Authboss.Config.Paths.AuthLoginOK, 384 FollowRedirParam: true, 385 } 386 return t.Authboss.Core.Redirector.Redirect(w, r, ro) 387 } 388 389 func (t *TOTP) validate(r *http.Request) (User, bool, error) { 390 logger := t.RequestLogger(r) 391 392 // Look up CurrentUser first, otherwise session persistence can allow 393 // a previous login attempt's user to be recalled here by a logged in 394 // user for 2fa removal and verification. 395 abUser, err := t.CurrentUser(r) 396 if err == authboss.ErrUserNotFound { 397 pid, ok := authboss.GetSession(r, SessionTOTPPendingPID) 398 if ok && len(pid) != 0 { 399 abUser, err = t.Authboss.Config.Storage.Server.Load(r.Context(), pid) 400 } 401 } 402 if err != nil { 403 return nil, false, err 404 } 405 406 user := abUser.(User) 407 408 secret := user.GetTOTPSecretKey() 409 if len(secret) == 0 { 410 return user, false, errNoTOTPEnabled 411 } 412 413 validator, err := t.Authboss.Config.Core.BodyReader.Read(PageTOTPValidate, r) 414 if err != nil { 415 return nil, false, err 416 } 417 418 totpCodeValues := MustHaveTOTPCodeValues(validator) 419 420 if recoveryCode := totpCodeValues.GetRecoveryCode(); len(recoveryCode) != 0 { 421 var ok bool 422 recoveryCodes := twofactor.DecodeRecoveryCodes(user.GetRecoveryCodes()) 423 recoveryCodes, ok = twofactor.UseRecoveryCode(recoveryCodes, recoveryCode) 424 425 if ok { 426 logger.Infof("user %s used recovery code instead of sms2fa", user.GetPID()) 427 user.PutRecoveryCodes(twofactor.EncodeRecoveryCodes(recoveryCodes)) 428 if err := t.Authboss.Config.Storage.Server.Save(r.Context(), user); err != nil { 429 return nil, false, err 430 } 431 } 432 433 return user, ok, nil 434 } 435 436 input := totpCodeValues.GetCode() 437 438 return user, totp.Validate(input, secret), nil 439 }