github.com/volatiletech/authboss@v2.4.1+incompatible/otp/twofactor/totp2fa/totp_test.go (about)

     1  package totp2fa
     2  
     3  import (
     4  	"context"
     5  	"net/http"
     6  	"net/http/httptest"
     7  	"testing"
     8  	"time"
     9  
    10  	"golang.org/x/crypto/bcrypt"
    11  
    12  	"github.com/volatiletech/authboss/otp/twofactor"
    13  
    14  	"github.com/pquerna/otp/totp"
    15  	"github.com/volatiletech/authboss"
    16  	"github.com/volatiletech/authboss/mocks"
    17  )
    18  
    19  func TestTOTPSetup(t *testing.T) {
    20  	t.Parallel()
    21  
    22  	ab := authboss.New()
    23  	router := &mocks.Router{}
    24  	renderer := &mocks.Renderer{}
    25  	errHandler := &mocks.ErrorHandler{}
    26  
    27  	ab.Config.Core.Router = router
    28  	ab.Config.Core.ViewRenderer = renderer
    29  	ab.Config.Core.ErrorHandler = errHandler
    30  
    31  	totpNew := &TOTP{Authboss: ab}
    32  	if err := totpNew.Setup(); err != nil {
    33  		t.Fatal(err)
    34  	}
    35  
    36  	gets := []string{"/2fa/totp/setup", "/2fa/totp/qr", "/2fa/totp/confirm", "/2fa/totp/remove", "/2fa/totp/validate"}
    37  	posts := []string{"/2fa/totp/setup", "/2fa/totp/confirm", "/2fa/totp/remove", "/2fa/totp/validate"}
    38  	if err := router.HasGets(gets...); err != nil {
    39  		t.Error(err)
    40  	}
    41  	if err := router.HasPosts(posts...); err != nil {
    42  		t.Error(err)
    43  	}
    44  }
    45  
    46  type testHarness struct {
    47  	totp *TOTP
    48  	ab   *authboss.Authboss
    49  
    50  	bodyReader *mocks.BodyReader
    51  	responder  *mocks.Responder
    52  	redirector *mocks.Redirector
    53  	session    *mocks.ClientStateRW
    54  	storer     *mocks.ServerStorer
    55  }
    56  
    57  func testSetup() *testHarness {
    58  	harness := &testHarness{}
    59  
    60  	harness.ab = authboss.New()
    61  	harness.bodyReader = &mocks.BodyReader{}
    62  	harness.redirector = &mocks.Redirector{}
    63  	harness.responder = &mocks.Responder{}
    64  	harness.session = mocks.NewClientRW()
    65  	harness.storer = mocks.NewServerStorer()
    66  
    67  	harness.ab.Config.Paths.AuthLoginOK = "/login/ok"
    68  	harness.ab.Config.Modules.TOTP2FAIssuer = "TOTPTest"
    69  
    70  	harness.ab.Config.Core.BodyReader = harness.bodyReader
    71  	harness.ab.Config.Core.Logger = mocks.Logger{}
    72  	harness.ab.Config.Core.Responder = harness.responder
    73  	harness.ab.Config.Core.Redirector = harness.redirector
    74  	harness.ab.Config.Storage.SessionState = harness.session
    75  	harness.ab.Config.Storage.Server = harness.storer
    76  
    77  	harness.totp = &TOTP{harness.ab}
    78  
    79  	return harness
    80  }
    81  
    82  func (h *testHarness) loadClientState(w http.ResponseWriter, r **http.Request) {
    83  	req, err := h.ab.LoadClientState(w, *r)
    84  	if err != nil {
    85  		panic(err)
    86  	}
    87  
    88  	*r = req
    89  }
    90  
    91  func (h *testHarness) putUserInCtx(u *mocks.User, r **http.Request) {
    92  	req := (*r).WithContext(context.WithValue((*r).Context(), authboss.CTXKeyUser, u))
    93  	*r = req
    94  }
    95  
    96  func (h *testHarness) newHTTP(method string, bodyArgs ...string) (*http.Request, *authboss.ClientStateResponseWriter, *httptest.ResponseRecorder) {
    97  	r := mocks.Request(method, bodyArgs...)
    98  	wr := httptest.NewRecorder()
    99  	w := h.ab.NewResponse(wr)
   100  
   101  	return r, w, wr
   102  }
   103  
   104  func (h *testHarness) setSession(key, value string) {
   105  	h.session.ClientValues[key] = value
   106  }
   107  
   108  func TestHijackAuth(t *testing.T) {
   109  	t.Parallel()
   110  
   111  	t.Run("Handled", func(t *testing.T) {
   112  		harness := testSetup()
   113  
   114  		handled, err := harness.totp.HijackAuth(nil, nil, true)
   115  		if handled {
   116  			t.Error("should not be handled")
   117  		}
   118  		if err != nil {
   119  			t.Error(err)
   120  		}
   121  	})
   122  
   123  	t.Run("UserNoTOTP", func(t *testing.T) {
   124  		harness := testSetup()
   125  
   126  		r, w, _ := harness.newHTTP("POST")
   127  		r.URL.RawQuery = "test=query"
   128  
   129  		user := &mocks.User{Email: "test@test.com"}
   130  		harness.putUserInCtx(user, &r)
   131  
   132  		harness.loadClientState(w, &r)
   133  		handled, err := harness.totp.HijackAuth(w, r, false)
   134  		if handled {
   135  			t.Error("should not be handled")
   136  		}
   137  		if err != nil {
   138  			t.Error(err)
   139  		}
   140  	})
   141  
   142  	t.Run("Ok", func(t *testing.T) {
   143  		harness := testSetup()
   144  
   145  		handled, err := harness.totp.HijackAuth(nil, nil, true)
   146  		if handled {
   147  			t.Error("should not be handled")
   148  		}
   149  		if err != nil {
   150  			t.Error(err)
   151  		}
   152  
   153  		r, w, _ := harness.newHTTP("POST")
   154  		r.URL.RawQuery = "test=query"
   155  
   156  		user := &mocks.User{Email: "test@test.com", TOTPSecretKey: "secret"}
   157  		harness.putUserInCtx(user, &r)
   158  		harness.loadClientState(w, &r)
   159  
   160  		handled, err = harness.totp.HijackAuth(w, r, false)
   161  		if !handled {
   162  			t.Error("should be handled")
   163  		}
   164  		if err != nil {
   165  			t.Error(err)
   166  		}
   167  
   168  		opts := harness.redirector.Options
   169  		if opts.Code != http.StatusTemporaryRedirect {
   170  			t.Error("status wrong:", opts.Code)
   171  		}
   172  
   173  		if opts.RedirectPath != "/auth/2fa/totp/validate?test=query" {
   174  			t.Error("redir path wrong:", opts.RedirectPath)
   175  		}
   176  	})
   177  }
   178  
   179  func TestGetSetup(t *testing.T) {
   180  	t.Parallel()
   181  	h := testSetup()
   182  
   183  	r, w, _ := h.newHTTP("GET")
   184  
   185  	h.setSession(SessionTOTPSecret, "secret")
   186  	h.loadClientState(w, &r)
   187  
   188  	var err error
   189  	if err = h.totp.GetSetup(w, r); err != nil {
   190  		t.Error(err)
   191  	}
   192  
   193  	// Flush ClientState
   194  	w.WriteHeader(http.StatusOK)
   195  
   196  	if h.session.ClientValues[SessionTOTPSecret] != "" {
   197  		t.Error("session totp secret should be cleared")
   198  	}
   199  
   200  	if h.responder.Page != PageTOTPSetup {
   201  		t.Error("page wrong:", h.responder.Page)
   202  	}
   203  }
   204  
   205  func TestPostSetup(t *testing.T) {
   206  	t.Parallel()
   207  	h := testSetup()
   208  
   209  	r, w, _ := h.newHTTP("GET")
   210  	user := &mocks.User{Email: "test@test.com"}
   211  	h.putUserInCtx(user, &r)
   212  
   213  	var err error
   214  	if err = h.totp.PostSetup(w, r); err != nil {
   215  		t.Error(err)
   216  	}
   217  
   218  	// Flush ClientState
   219  	w.WriteHeader(http.StatusOK)
   220  
   221  	opts := h.redirector.Options
   222  	if opts.Code != http.StatusTemporaryRedirect {
   223  		t.Error("status wrong:", opts.Code)
   224  	}
   225  
   226  	if opts.RedirectPath != "/auth/2fa/totp/confirm" {
   227  		t.Error("redir path wrong:", opts.RedirectPath)
   228  	}
   229  
   230  	if len(h.session.ClientValues[SessionTOTPSecret]) == 0 {
   231  		t.Error("no secret in the session")
   232  	}
   233  }
   234  
   235  func TestGetQRCode(t *testing.T) {
   236  	t.Parallel()
   237  	h := testSetup()
   238  
   239  	r, w, wr := h.newHTTP("GET")
   240  
   241  	user := &mocks.User{Email: "test@test.com"}
   242  	h.putUserInCtx(user, &r)
   243  
   244  	if err := h.totp.GetQRCode(w, r); err == nil {
   245  		t.Error("should fail because there is no totp secret")
   246  	}
   247  
   248  	secret := makeSecretKey(h, user.Email)
   249  	h.setSession(SessionTOTPSecret, secret)
   250  	h.loadClientState(w, &r)
   251  
   252  	if err := h.totp.GetQRCode(w, r); err != nil {
   253  		t.Error(err)
   254  	}
   255  
   256  	if got := wr.Header().Get("Content-Type"); got != "image/png" {
   257  		t.Error("content type wrong:", got)
   258  	}
   259  	if wr.Body.Len() == 0 {
   260  		t.Error("body should have been sizable")
   261  	}
   262  }
   263  
   264  func TestGetConfirm(t *testing.T) {
   265  	t.Parallel()
   266  	h := testSetup()
   267  
   268  	r, w, _ := h.newHTTP("GET")
   269  
   270  	if err := h.totp.GetConfirm(w, r); err == nil {
   271  		t.Error("should fail because there is no totp secret")
   272  	}
   273  
   274  	secret := "secret"
   275  	h.setSession(SessionTOTPSecret, secret)
   276  	h.loadClientState(w, &r)
   277  
   278  	if err := h.totp.GetConfirm(w, r); err != nil {
   279  		t.Error(err)
   280  	}
   281  
   282  	if h.responder.Page != PageTOTPConfirm {
   283  		t.Error("page wrong:", h.responder.Page)
   284  	}
   285  	if got := h.responder.Data[DataTOTPSecret]; got != secret {
   286  		t.Error("data wrong:", got)
   287  	}
   288  }
   289  
   290  func TestPostConfirm(t *testing.T) {
   291  	t.Parallel()
   292  	h := testSetup()
   293  
   294  	r, w, _ := h.newHTTP("POST")
   295  
   296  	if err := h.totp.PostConfirm(w, r); err == nil {
   297  		t.Error("should fail because there is no totp secret")
   298  	}
   299  
   300  	user := &mocks.User{Email: "test@test.com"}
   301  	h.storer.Users[user.Email] = user
   302  
   303  	secret := makeSecretKey(h, user.Email)
   304  	h.setSession(SessionTOTPSecret, secret)
   305  	h.setSession(authboss.SessionKey, user.Email)
   306  	h.loadClientState(w, &r)
   307  
   308  	code, err := totp.GenerateCode(secret, time.Now())
   309  	if err != nil {
   310  		t.Fatal(err)
   311  	}
   312  	h.bodyReader.Return = &mocks.Values{Code: code}
   313  
   314  	if err = h.totp.PostConfirm(w, r); err != nil {
   315  		t.Error(err)
   316  	}
   317  
   318  	// Flush client state
   319  	w.WriteHeader(http.StatusOK)
   320  
   321  	if len(user.TOTPSecretKey) == 0 {
   322  		t.Error("totp secret key unset")
   323  	}
   324  	if len(user.RecoveryCodes) == 0 {
   325  		t.Error("user recovery codes unset")
   326  	}
   327  	if _, ok := h.session.ClientValues[SessionTOTPSecret]; ok {
   328  		t.Error("session totp secret not deleted")
   329  	}
   330  
   331  	if h.responder.Page != PageTOTPConfirmSuccess {
   332  		t.Error("page wrong:", h.responder.Page)
   333  	}
   334  	if got := h.responder.Data[twofactor.DataRecoveryCodes].([]string); len(got) == 0 {
   335  		t.Error("data wrong:", got)
   336  	}
   337  }
   338  
   339  func TestGetRemove(t *testing.T) {
   340  	t.Parallel()
   341  	h := testSetup()
   342  
   343  	r, w, _ := h.newHTTP("GET")
   344  
   345  	if err := h.totp.GetRemove(w, r); err != nil {
   346  		t.Error(err)
   347  	}
   348  
   349  	if h.responder.Page != PageTOTPRemove {
   350  		t.Error("page wrong:", h.responder.Page)
   351  	}
   352  }
   353  
   354  func TestPostRemove(t *testing.T) {
   355  	t.Parallel()
   356  
   357  	setupMore := func(h *testHarness) *mocks.User {
   358  		user := &mocks.User{Email: "test@test.com"}
   359  		h.storer.Users[user.Email] = user
   360  		h.setSession(authboss.SessionKey, user.Email)
   361  
   362  		return user
   363  	}
   364  
   365  	t.Run("NoTOTPActivated", func(t *testing.T) {
   366  		h := testSetup()
   367  
   368  		r, w, _ := h.newHTTP("POST")
   369  		setupMore(h)
   370  		h.loadClientState(w, &r)
   371  
   372  		// No session
   373  		if err := h.totp.PostRemove(w, r); err != nil {
   374  			t.Fatal(err)
   375  		}
   376  
   377  		if h.responder.Page != PageTOTPRemove {
   378  			t.Error("page wrong:", h.responder.Page)
   379  		}
   380  		if got := h.responder.Data[authboss.DataErr]; got != "totp 2fa not active" {
   381  			t.Error("data wrong:", got)
   382  		}
   383  	})
   384  
   385  	t.Run("WrongCode", func(t *testing.T) {
   386  		h := testSetup()
   387  
   388  		r, w, _ := h.newHTTP("POST")
   389  
   390  		user := setupMore(h)
   391  		secret := makeSecretKey(h, user.Email)
   392  		user.TOTPSecretKey = secret
   393  		h.bodyReader.Return = mocks.Values{Code: "wrong"}
   394  
   395  		h.loadClientState(w, &r)
   396  
   397  		if err := h.totp.PostRemove(w, r); err != nil {
   398  			t.Error(err)
   399  		}
   400  
   401  		if h.responder.Page != PageTOTPRemove {
   402  			t.Error("page wrong:", h.responder.Page)
   403  		}
   404  		if got := h.responder.Data[authboss.DataValidation].(map[string][]string); got[FormValueCode][0] != "2fa code was invalid" {
   405  			t.Error("data wrong:", got)
   406  		}
   407  	})
   408  
   409  	t.Run("OkCode", func(t *testing.T) {
   410  		h := testSetup()
   411  
   412  		r, w, _ := h.newHTTP("POST")
   413  
   414  		user := setupMore(h)
   415  		secret := makeSecretKey(h, user.Email)
   416  		user.TOTPSecretKey = secret
   417  		h.setSession(authboss.Session2FA, "totp")
   418  		h.loadClientState(w, &r)
   419  
   420  		code, err := totp.GenerateCode(secret, time.Now())
   421  		if err != nil {
   422  			t.Fatal(err)
   423  		}
   424  		h.bodyReader.Return = mocks.Values{Code: code}
   425  
   426  		if err := h.totp.PostRemove(w, r); err != nil {
   427  			t.Error(err)
   428  		}
   429  
   430  		if h.responder.Page != PageTOTPRemoveSuccess {
   431  			t.Error("page wrong:", h.responder.Page)
   432  		}
   433  
   434  		// Flush client state
   435  		w.WriteHeader(http.StatusOK)
   436  
   437  		if _, ok := h.session.ClientValues[authboss.Session2FA]; ok {
   438  			t.Error("session 2fa should be cleared")
   439  		}
   440  	})
   441  }
   442  
   443  func TestGetValidate(t *testing.T) {
   444  	t.Parallel()
   445  	h := testSetup()
   446  
   447  	r, w, _ := h.newHTTP("GET")
   448  
   449  	if err := h.totp.GetValidate(w, r); err != nil {
   450  		t.Error(err)
   451  	}
   452  
   453  	if h.responder.Page != PageTOTPValidate {
   454  		t.Error("page wrong:", h.responder.Page)
   455  	}
   456  }
   457  
   458  func TestPostValidate(t *testing.T) {
   459  	t.Parallel()
   460  
   461  	setupMore := func(h *testHarness) *mocks.User {
   462  		user := &mocks.User{Email: "test@test.com"}
   463  		h.storer.Users[user.Email] = user
   464  		h.setSession(authboss.SessionKey, user.Email)
   465  		h.session.ClientValues[authboss.SessionKey] = user.Email
   466  
   467  		return user
   468  	}
   469  
   470  	t.Run("NoTOTPActivated", func(t *testing.T) {
   471  		h := testSetup()
   472  
   473  		r, w, _ := h.newHTTP("POST")
   474  
   475  		setupMore(h)
   476  		h.loadClientState(w, &r)
   477  
   478  		// No session
   479  		if err := h.totp.PostValidate(w, r); err != nil {
   480  			t.Fatal(err)
   481  		}
   482  
   483  		if h.responder.Page != PageTOTPValidate {
   484  			t.Error("page wrong:", h.responder.Page)
   485  		}
   486  		if got := h.responder.Data[authboss.DataErr]; got != "totp 2fa not active" {
   487  			t.Error("data wrong:", got)
   488  		}
   489  	})
   490  
   491  	t.Run("WrongCode", func(t *testing.T) {
   492  		h := testSetup()
   493  
   494  		r, w, _ := h.newHTTP("POST")
   495  		h.loadClientState(w, &r)
   496  
   497  		user := setupMore(h)
   498  		secret := makeSecretKey(h, user.Email)
   499  		user.TOTPSecretKey = secret
   500  		h.bodyReader.Return = mocks.Values{Code: "wrong"}
   501  
   502  		if err := h.totp.PostValidate(w, r); err != nil {
   503  			t.Error(err)
   504  		}
   505  
   506  		if h.responder.Page != PageTOTPValidate {
   507  			t.Error("page wrong:", h.responder.Page)
   508  		}
   509  		if got := h.responder.Data[authboss.DataValidation].(map[string][]string); got[FormValueCode][0] != "2fa code was invalid" {
   510  			t.Error("data wrong:", got)
   511  		}
   512  	})
   513  
   514  	t.Run("OkRecovery", func(t *testing.T) {
   515  		h := testSetup()
   516  
   517  		r, w, _ := h.newHTTP("POST")
   518  		user := setupMore(h)
   519  		secret := makeSecretKey(h, user.Email)
   520  		user.TOTPSecretKey = secret
   521  
   522  		// Create a single recovery code
   523  		codes, err := twofactor.GenerateRecoveryCodes()
   524  		if err != nil {
   525  			t.Fatal(err)
   526  		}
   527  		b, err := bcrypt.GenerateFromPassword([]byte(codes[0]), bcrypt.DefaultCost)
   528  		if err != nil {
   529  			t.Fatal(err)
   530  		}
   531  		user.RecoveryCodes = string(b)
   532  
   533  		// User inputs the only code he has
   534  		h.bodyReader.Return = mocks.Values{Recovery: codes[0]}
   535  
   536  		h.setSession(SessionTOTPPendingPID, user.Email)
   537  		h.setSession(SessionTOTPSecret, "secret")
   538  		h.setSession(authboss.SessionHalfAuthKey, "true")
   539  		h.loadClientState(w, &r)
   540  
   541  		if err := h.totp.PostValidate(w, r); err != nil {
   542  			t.Error(err)
   543  		}
   544  
   545  		// Flush client state
   546  		w.WriteHeader(http.StatusOK)
   547  
   548  		if pid := h.session.ClientValues[authboss.SessionKey]; pid != user.Email {
   549  			t.Error("session pid should be set:", pid)
   550  		}
   551  		if twofa := h.session.ClientValues[authboss.Session2FA]; twofa != "totp" {
   552  			t.Error("session 2fa should be totp:", twofa)
   553  		}
   554  
   555  		cleared := []string{SessionTOTPSecret, SessionTOTPPendingPID, authboss.SessionHalfAuthKey}
   556  		for _, c := range cleared {
   557  			if _, ok := h.session.ClientValues[c]; ok {
   558  				t.Error(c, "was not cleared")
   559  			}
   560  		}
   561  
   562  		opts := h.redirector.Options
   563  		if opts.Code != http.StatusTemporaryRedirect {
   564  			t.Error("status wrong:", opts.Code)
   565  		}
   566  		if !opts.FollowRedirParam {
   567  			t.Error("it should follow redirects")
   568  		}
   569  		if opts.RedirectPath != h.ab.Paths.AuthLoginOK {
   570  			t.Error("path wrong:", opts.RedirectPath)
   571  		}
   572  	})
   573  }
   574  
   575  func makeSecretKey(h *testHarness, email string) string {
   576  	key, err := totp.Generate(totp.GenerateOpts{
   577  		Issuer:      h.totp.Modules.TOTP2FAIssuer,
   578  		AccountName: email,
   579  	})
   580  	if err != nil {
   581  		panic(err)
   582  	}
   583  
   584  	return key.Secret()
   585  }