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

     1  package otp
     2  
     3  import (
     4  	"crypto/sha512"
     5  	"encoding/base64"
     6  	"net/http"
     7  	"net/http/httptest"
     8  	"testing"
     9  
    10  	"github.com/volatiletech/authboss"
    11  	"github.com/volatiletech/authboss/mocks"
    12  )
    13  
    14  type testUser struct {
    15  	PID  string
    16  	OTPs string
    17  }
    18  
    19  func (t *testUser) GetPID() string      { return t.PID }
    20  func (t *testUser) PutPID(pid string)   { t.PID = pid }
    21  func (t *testUser) GetOTPs() string     { return t.OTPs }
    22  func (t *testUser) PutOTPs(otps string) { t.OTPs = otps }
    23  
    24  func TestMustBeOTPable(t *testing.T) {
    25  	t.Parallel()
    26  
    27  	var user authboss.User = &testUser{}
    28  	_ = MustBeOTPable(user)
    29  }
    30  
    31  func TestInit(t *testing.T) {
    32  	t.Parallel()
    33  
    34  	ab := authboss.New()
    35  	router := &mocks.Router{}
    36  	renderer := &mocks.Renderer{}
    37  	errHandler := &mocks.ErrorHandler{}
    38  
    39  	ab.Config.Core.Router = router
    40  	ab.Config.Core.ViewRenderer = renderer
    41  	ab.Config.Core.ErrorHandler = errHandler
    42  
    43  	o := &OTP{}
    44  	if err := o.Init(ab); err != nil {
    45  		t.Fatal(err)
    46  	}
    47  
    48  	routes := []string{"/otp/login", "/otp/add", "/otp/clear"}
    49  	if err := router.HasGets(routes...); err != nil {
    50  		t.Error(err)
    51  	}
    52  	if err := router.HasPosts(routes...); err != nil {
    53  		t.Error(err)
    54  	}
    55  }
    56  
    57  func TestLoginGet(t *testing.T) {
    58  	t.Parallel()
    59  
    60  	ab := authboss.New()
    61  	responder := &mocks.Responder{}
    62  	ab.Config.Core.Responder = responder
    63  
    64  	a := &OTP{ab}
    65  
    66  	r := mocks.Request("POST")
    67  	r.URL.RawQuery = "redir=/redirectpage"
    68  	a.LoginGet(nil, r)
    69  
    70  	if responder.Page != PageLogin {
    71  		t.Error("wanted login page, got:", responder.Page)
    72  	}
    73  
    74  	if responder.Status != http.StatusOK {
    75  		t.Error("wanted ok status, got:", responder.Status)
    76  	}
    77  
    78  	if got := responder.Data[authboss.FormValueRedirect]; got != "/redirectpage" {
    79  		t.Error("redirect page was wrong:", got)
    80  	}
    81  }
    82  
    83  type testHarness struct {
    84  	otp *OTP
    85  	ab  *authboss.Authboss
    86  
    87  	bodyReader *mocks.BodyReader
    88  	responder  *mocks.Responder
    89  	redirector *mocks.Redirector
    90  	session    *mocks.ClientStateRW
    91  	storer     *mocks.ServerStorer
    92  }
    93  
    94  func testSetup() *testHarness {
    95  	harness := &testHarness{}
    96  
    97  	harness.ab = authboss.New()
    98  	harness.bodyReader = &mocks.BodyReader{}
    99  	harness.redirector = &mocks.Redirector{}
   100  	harness.responder = &mocks.Responder{}
   101  	harness.session = mocks.NewClientRW()
   102  	harness.storer = mocks.NewServerStorer()
   103  
   104  	harness.ab.Config.Paths.AuthLoginOK = "/login/ok"
   105  
   106  	harness.ab.Config.Core.BodyReader = harness.bodyReader
   107  	harness.ab.Config.Core.Logger = mocks.Logger{}
   108  	harness.ab.Config.Core.Responder = harness.responder
   109  	harness.ab.Config.Core.Redirector = harness.redirector
   110  	harness.ab.Config.Storage.SessionState = harness.session
   111  	harness.ab.Config.Storage.Server = harness.storer
   112  
   113  	harness.otp = &OTP{harness.ab}
   114  
   115  	return harness
   116  }
   117  
   118  func TestLoginPostSuccess(t *testing.T) {
   119  	t.Parallel()
   120  
   121  	setupMore := func(h *testHarness) *testHarness {
   122  		h.bodyReader.Return = mocks.Values{
   123  			PID:      "test@test.com",
   124  			Password: "3cc94671-958a912d-bd5a3ba7-3326a380",
   125  		}
   126  		h.storer.Users["test@test.com"] = &mocks.User{
   127  			Email: "test@test.com",
   128  			// 3cc94671-958a912d-bd5a3ba7-3326a380
   129  			OTPs: "2aID,2aIDHxmTIy1W7Uyz9c+iqhOJSE0a2Yna3zTRTs2q/X7Bv3xdVjExoztBEG4sQ2Nn3jcaPxdIuhslvSsjaYK5uA==",
   130  		}
   131  		h.session.ClientValues[authboss.SessionHalfAuthKey] = "true"
   132  
   133  		return h
   134  	}
   135  
   136  	t.Run("normal", func(t *testing.T) {
   137  		t.Parallel()
   138  		h := setupMore(testSetup())
   139  
   140  		var beforeCalled, afterCalled bool
   141  		var beforeHasValues, afterHasValues bool
   142  		h.ab.Events.Before(authboss.EventAuth, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
   143  			beforeCalled = true
   144  			beforeHasValues = r.Context().Value(authboss.CTXKeyValues) != nil
   145  			return false, nil
   146  		})
   147  		h.ab.Events.After(authboss.EventAuth, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
   148  			afterCalled = true
   149  			afterHasValues = r.Context().Value(authboss.CTXKeyValues) != nil
   150  			return false, nil
   151  		})
   152  
   153  		r := mocks.Request("POST")
   154  		resp := httptest.NewRecorder()
   155  		w := h.ab.NewResponse(resp)
   156  
   157  		if err := h.otp.LoginPost(w, r); err != nil {
   158  			t.Error(err)
   159  		}
   160  
   161  		if resp.Code != http.StatusTemporaryRedirect {
   162  			t.Error("code was wrong:", resp.Code)
   163  		}
   164  		if h.redirector.Options.RedirectPath != "/login/ok" {
   165  			t.Error("redirect path was wrong:", h.redirector.Options.RedirectPath)
   166  		}
   167  
   168  		if _, ok := h.session.ClientValues[authboss.SessionHalfAuthKey]; ok {
   169  			t.Error("half auth should have been deleted")
   170  		}
   171  		if pid := h.session.ClientValues[authboss.SessionKey]; pid != "test@test.com" {
   172  			t.Error("pid was wrong:", pid)
   173  		}
   174  
   175  		// Remaining length of the chunk of base64 is 4 characters
   176  		if len(h.storer.Users["test@test.com"].OTPs) != 4 {
   177  			t.Error("the user should have used one of his OTPs")
   178  		}
   179  
   180  		if !beforeCalled {
   181  			t.Error("before should have been called")
   182  		}
   183  		if !afterCalled {
   184  			t.Error("after should have been called")
   185  		}
   186  		if !beforeHasValues {
   187  			t.Error("before callback should have access to values")
   188  		}
   189  		if !afterHasValues {
   190  			t.Error("after callback should have access to values")
   191  		}
   192  	})
   193  
   194  	t.Run("handledBefore", func(t *testing.T) {
   195  		t.Parallel()
   196  		h := setupMore(testSetup())
   197  
   198  		var beforeCalled bool
   199  		h.ab.Events.Before(authboss.EventAuth, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
   200  			w.WriteHeader(http.StatusTeapot)
   201  			beforeCalled = true
   202  			return true, nil
   203  		})
   204  
   205  		r := mocks.Request("POST")
   206  		resp := httptest.NewRecorder()
   207  		w := h.ab.NewResponse(resp)
   208  
   209  		if err := h.otp.LoginPost(w, r); err != nil {
   210  			t.Error(err)
   211  		}
   212  
   213  		if h.responder.Status != 0 {
   214  			t.Error("a status should never have been sent back")
   215  		}
   216  		if _, ok := h.session.ClientValues[authboss.SessionKey]; ok {
   217  			t.Error("session key should not have been set")
   218  		}
   219  
   220  		if !beforeCalled {
   221  			t.Error("before should have been called")
   222  		}
   223  		if resp.Code != http.StatusTeapot {
   224  			t.Error("should have left the response alone once teapot was sent")
   225  		}
   226  	})
   227  
   228  	t.Run("handledAfter", func(t *testing.T) {
   229  		t.Parallel()
   230  		h := setupMore(testSetup())
   231  
   232  		var afterCalled bool
   233  		h.ab.Events.After(authboss.EventAuth, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
   234  			w.WriteHeader(http.StatusTeapot)
   235  			afterCalled = true
   236  			return true, nil
   237  		})
   238  
   239  		r := mocks.Request("POST")
   240  		resp := httptest.NewRecorder()
   241  		w := h.ab.NewResponse(resp)
   242  
   243  		if err := h.otp.LoginPost(w, r); err != nil {
   244  			t.Error(err)
   245  		}
   246  
   247  		if h.responder.Status != 0 {
   248  			t.Error("a status should never have been sent back")
   249  		}
   250  		if _, ok := h.session.ClientValues[authboss.SessionKey]; !ok {
   251  			t.Error("session key should have been set")
   252  		}
   253  
   254  		if !afterCalled {
   255  			t.Error("after should have been called")
   256  		}
   257  		if resp.Code != http.StatusTeapot {
   258  			t.Error("should have left the response alone once teapot was sent")
   259  		}
   260  	})
   261  }
   262  
   263  func TestLoginPostBadPassword(t *testing.T) {
   264  	t.Parallel()
   265  
   266  	setupMore := func(h *testHarness) *testHarness {
   267  		h.bodyReader.Return = mocks.Values{
   268  			PID:      "test@test.com",
   269  			Password: "nope",
   270  		}
   271  		h.storer.Users["test@test.com"] = &mocks.User{
   272  			Email:    "test@test.com",
   273  			Password: "", // hello world
   274  		}
   275  
   276  		return h
   277  	}
   278  
   279  	t.Run("normal", func(t *testing.T) {
   280  		t.Parallel()
   281  		h := setupMore(testSetup())
   282  
   283  		r := mocks.Request("POST")
   284  		resp := httptest.NewRecorder()
   285  		w := h.ab.NewResponse(resp)
   286  
   287  		var afterCalled bool
   288  		h.ab.Events.After(authboss.EventAuthFail, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
   289  			afterCalled = true
   290  			return false, nil
   291  		})
   292  
   293  		if err := h.otp.LoginPost(w, r); err != nil {
   294  			t.Error(err)
   295  		}
   296  
   297  		if resp.Code != 200 {
   298  			t.Error("wanted a 200:", resp.Code)
   299  		}
   300  
   301  		if h.responder.Data[authboss.DataErr] != "Invalid Credentials" {
   302  			t.Error("wrong error:", h.responder.Data)
   303  		}
   304  
   305  		if _, ok := h.session.ClientValues[authboss.SessionKey]; ok {
   306  			t.Error("user should not be logged in")
   307  		}
   308  
   309  		if !afterCalled {
   310  			t.Error("after should have been called")
   311  		}
   312  	})
   313  
   314  	t.Run("handledAfter", func(t *testing.T) {
   315  		t.Parallel()
   316  		h := setupMore(testSetup())
   317  
   318  		r := mocks.Request("POST")
   319  		resp := httptest.NewRecorder()
   320  		w := h.ab.NewResponse(resp)
   321  
   322  		var afterCalled bool
   323  		h.ab.Events.After(authboss.EventAuthFail, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
   324  			w.WriteHeader(http.StatusTeapot)
   325  			afterCalled = true
   326  			return true, nil
   327  		})
   328  
   329  		if err := h.otp.LoginPost(w, r); err != nil {
   330  			t.Error(err)
   331  		}
   332  
   333  		if h.responder.Status != 0 {
   334  			t.Error("responder should not have been called to give a status")
   335  		}
   336  		if _, ok := h.session.ClientValues[authboss.SessionKey]; ok {
   337  			t.Error("user should not be logged in")
   338  		}
   339  
   340  		if !afterCalled {
   341  			t.Error("after should have been called")
   342  		}
   343  		if resp.Code != http.StatusTeapot {
   344  			t.Error("should have left the response alone once teapot was sent")
   345  		}
   346  	})
   347  }
   348  
   349  func TestAuthPostUserNotFound(t *testing.T) {
   350  	t.Parallel()
   351  
   352  	harness := testSetup()
   353  	harness.bodyReader.Return = mocks.Values{
   354  		PID:      "test@test.com",
   355  		Password: "world hello",
   356  	}
   357  
   358  	r := mocks.Request("POST")
   359  	resp := httptest.NewRecorder()
   360  	w := harness.ab.NewResponse(resp)
   361  
   362  	// This event is really the only thing that separates
   363  	// "user not found" from "bad password"
   364  	var afterCalled bool
   365  	harness.ab.Events.After(authboss.EventAuthFail, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) {
   366  		afterCalled = true
   367  		return false, nil
   368  	})
   369  
   370  	if err := harness.otp.LoginPost(w, r); err != nil {
   371  		t.Error(err)
   372  	}
   373  
   374  	if resp.Code != 200 {
   375  		t.Error("wanted a 200:", resp.Code)
   376  	}
   377  
   378  	if harness.responder.Data[authboss.DataErr] != "Invalid Credentials" {
   379  		t.Error("wrong error:", harness.responder.Data)
   380  	}
   381  
   382  	if _, ok := harness.session.ClientValues[authboss.SessionKey]; ok {
   383  		t.Error("user should not be logged in")
   384  	}
   385  
   386  	if afterCalled {
   387  		t.Error("after should not have been called")
   388  	}
   389  }
   390  
   391  func TestAddGet(t *testing.T) {
   392  	t.Parallel()
   393  
   394  	h := testSetup()
   395  	h.storer.Users["test@test.com"] = &mocks.User{
   396  		Email: "test@test.com",
   397  		// 3cc94671-958a912d-bd5a3ba7-3326a380
   398  		OTPs: "2aID,2aIDHxmTIy1W7Uyz9c+iqhOJSE0a2Yna3zTRTs2q/X7Bv3xdVjExoztBEG4sQ2Nn3jcaPxdIuhslvSsjaYK5uA==",
   399  	}
   400  	h.session.ClientValues[authboss.SessionKey] = "test@test.com"
   401  
   402  	r := mocks.Request("POST")
   403  	w := h.ab.NewResponse(httptest.NewRecorder())
   404  
   405  	var err error
   406  	r, err = h.ab.LoadClientState(w, r)
   407  	if err != nil {
   408  		t.Fatal(err)
   409  	}
   410  
   411  	if err := h.otp.AddGet(w, r); err != nil {
   412  		t.Fatal(err)
   413  	}
   414  
   415  	if h.responder.Page != PageAdd {
   416  		t.Error("wanted add page, got:", h.responder.Page)
   417  	}
   418  
   419  	if h.responder.Status != http.StatusOK {
   420  		t.Error("wanted ok status, got:", h.responder.Status)
   421  	}
   422  
   423  	if ln := h.responder.Data[DataNumberOTPs]; ln != "2" {
   424  		t.Error("want two otps:", ln)
   425  	}
   426  }
   427  
   428  func TestAddPost(t *testing.T) {
   429  	t.Parallel()
   430  
   431  	h := testSetup()
   432  	uname := "test@test.com"
   433  	h.storer.Users[uname] = &mocks.User{
   434  		Email: uname,
   435  		// 3cc94671-958a912d-bd5a3ba7-3326a380
   436  		OTPs: "2aID,2aIDHxmTIy1W7Uyz9c+iqhOJSE0a2Yna3zTRTs2q/X7Bv3xdVjExoztBEG4sQ2Nn3jcaPxdIuhslvSsjaYK5uA==",
   437  	}
   438  	h.session.ClientValues[authboss.SessionKey] = uname
   439  
   440  	r := mocks.Request("POST")
   441  	w := h.ab.NewResponse(httptest.NewRecorder())
   442  
   443  	var err error
   444  	r, err = h.ab.LoadClientState(w, r)
   445  	if err != nil {
   446  		t.Fatal(err)
   447  	}
   448  
   449  	if err := h.otp.AddPost(w, r); err != nil {
   450  		t.Fatal(err)
   451  	}
   452  
   453  	if h.responder.Page != PageAdd {
   454  		t.Error("wanted add page, got:", h.responder.Page)
   455  	}
   456  
   457  	if h.responder.Status != http.StatusOK {
   458  		t.Error("wanted ok status, got:", h.responder.Status)
   459  	}
   460  
   461  	sum := sha512.Sum512([]byte(h.responder.Data[DataOTP].(string)))
   462  	encoded := base64.StdEncoding.EncodeToString(sum[:])
   463  
   464  	otps := splitOTPs(h.storer.Users[uname].OTPs)
   465  	if len(otps) != 3 || encoded != otps[2] {
   466  		t.Error("expected one new otp to be appended to the end")
   467  	}
   468  }
   469  
   470  func TestAddPostTooMany(t *testing.T) {
   471  	t.Parallel()
   472  
   473  	h := testSetup()
   474  	uname := "test@test.com"
   475  	h.storer.Users[uname] = &mocks.User{
   476  		Email: uname,
   477  		OTPs:  "2aID,2aID,2aID,2aID,2aID",
   478  	}
   479  	h.session.ClientValues[authboss.SessionKey] = uname
   480  
   481  	r := mocks.Request("POST")
   482  	w := h.ab.NewResponse(httptest.NewRecorder())
   483  
   484  	var err error
   485  	r, err = h.ab.LoadClientState(w, r)
   486  	if err != nil {
   487  		t.Fatal(err)
   488  	}
   489  
   490  	if err := h.otp.AddPost(w, r); err != nil {
   491  		t.Fatal(err)
   492  	}
   493  
   494  	if h.responder.Page != PageAdd {
   495  		t.Error("wanted add page, got:", h.responder.Page)
   496  	}
   497  	if h.responder.Status != http.StatusOK {
   498  		t.Error("wanted ok status, got:", h.responder.Status)
   499  	}
   500  	if len(h.responder.Data[authboss.DataValidation].(string)) == 0 {
   501  		t.Error("there should have been a validation error")
   502  	}
   503  
   504  	otps := splitOTPs(h.storer.Users[uname].OTPs)
   505  	if len(otps) != maxOTPs {
   506  		t.Error("expected the number of OTPs to be equal to the maximum")
   507  	}
   508  }
   509  
   510  func TestAddGetUserNotFound(t *testing.T) {
   511  	t.Parallel()
   512  
   513  	h := testSetup()
   514  
   515  	r := mocks.Request("GET")
   516  	w := h.ab.NewResponse(httptest.NewRecorder())
   517  
   518  	if err := h.otp.AddGet(w, r); err != authboss.ErrUserNotFound {
   519  		t.Error("it should have failed with user not found")
   520  	}
   521  }
   522  
   523  func TestAddPostUserNotFound(t *testing.T) {
   524  	t.Parallel()
   525  
   526  	h := testSetup()
   527  
   528  	r := mocks.Request("POST")
   529  	w := h.ab.NewResponse(httptest.NewRecorder())
   530  
   531  	if err := h.otp.AddPost(w, r); err != authboss.ErrUserNotFound {
   532  		t.Error("it should have failed with user not found")
   533  	}
   534  }
   535  
   536  func TestClearGet(t *testing.T) {
   537  	t.Parallel()
   538  
   539  	h := testSetup()
   540  
   541  	h.storer.Users["test@test.com"] = &mocks.User{
   542  		Email: "test@test.com",
   543  		// 3cc94671-958a912d-bd5a3ba7-3326a380
   544  		OTPs: "2aID,2aIDHxmTIy1W7Uyz9c+iqhOJSE0a2Yna3zTRTs2q/X7Bv3xdVjExoztBEG4sQ2Nn3jcaPxdIuhslvSsjaYK5uA==",
   545  	}
   546  	h.session.ClientValues[authboss.SessionKey] = "test@test.com"
   547  
   548  	r := mocks.Request("POST")
   549  	w := h.ab.NewResponse(httptest.NewRecorder())
   550  
   551  	var err error
   552  	r, err = h.ab.LoadClientState(w, r)
   553  	if err != nil {
   554  		t.Fatal(err)
   555  	}
   556  
   557  	if err := h.otp.ClearGet(w, r); err != nil {
   558  		t.Fatal(err)
   559  	}
   560  
   561  	if h.responder.Page != PageClear {
   562  		t.Error("wanted clear page, got:", h.responder.Page)
   563  	}
   564  
   565  	if h.responder.Status != http.StatusOK {
   566  		t.Error("wanted ok status, got:", h.responder.Status)
   567  	}
   568  
   569  	if ln := h.responder.Data[DataNumberOTPs]; ln != "2" {
   570  		t.Error("want two otps:", ln)
   571  	}
   572  }
   573  
   574  func TestClearPost(t *testing.T) {
   575  	t.Parallel()
   576  
   577  	h := testSetup()
   578  	uname := "test@test.com"
   579  	h.storer.Users[uname] = &mocks.User{
   580  		Email: uname,
   581  		// 3cc94671-958a912d-bd5a3ba7-3326a380
   582  		OTPs: "2aID,2aIDHxmTIy1W7Uyz9c+iqhOJSE0a2Yna3zTRTs2q/X7Bv3xdVjExoztBEG4sQ2Nn3jcaPxdIuhslvSsjaYK5uA==",
   583  	}
   584  	h.session.ClientValues[authboss.SessionKey] = uname
   585  
   586  	r := mocks.Request("POST")
   587  	w := h.ab.NewResponse(httptest.NewRecorder())
   588  
   589  	var err error
   590  	r, err = h.ab.LoadClientState(w, r)
   591  	if err != nil {
   592  		t.Fatal(err)
   593  	}
   594  
   595  	if err := h.otp.ClearPost(w, r); err != nil {
   596  		t.Fatal(err)
   597  	}
   598  
   599  	if h.responder.Page != PageAdd {
   600  		t.Error("wanted add page, got:", h.responder.Page)
   601  	}
   602  
   603  	if h.responder.Status != http.StatusOK {
   604  		t.Error("wanted ok status, got:", h.responder.Status)
   605  	}
   606  
   607  	otps := splitOTPs(h.storer.Users[uname].OTPs)
   608  	if len(otps) != 0 {
   609  		t.Error("expected all otps to be gone")
   610  	}
   611  }
   612  
   613  func TestClearGetUserNotFound(t *testing.T) {
   614  	t.Parallel()
   615  
   616  	h := testSetup()
   617  
   618  	r := mocks.Request("GET")
   619  	w := h.ab.NewResponse(httptest.NewRecorder())
   620  
   621  	if err := h.otp.ClearGet(w, r); err != authboss.ErrUserNotFound {
   622  		t.Error("it should have failed with user not found")
   623  	}
   624  }
   625  
   626  func TestClearPostUserNotFound(t *testing.T) {
   627  	t.Parallel()
   628  
   629  	h := testSetup()
   630  
   631  	r := mocks.Request("POST")
   632  	w := h.ab.NewResponse(httptest.NewRecorder())
   633  
   634  	if err := h.otp.AddPost(w, r); err != authboss.ErrUserNotFound {
   635  		t.Error("it should have failed with user not found")
   636  	}
   637  }