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  }