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

     1  package twofactor
     2  
     3  import (
     4  	"context"
     5  	"net/http"
     6  	"net/http/httptest"
     7  	"regexp"
     8  	"testing"
     9  
    10  	"github.com/volatiletech/authboss"
    11  	"github.com/volatiletech/authboss/mocks"
    12  )
    13  
    14  func TestSetupEmailVerify(t *testing.T) {
    15  	t.Parallel()
    16  
    17  	router := &mocks.Router{}
    18  	renderer := &mocks.Renderer{}
    19  	mailRenderer := &mocks.Renderer{}
    20  
    21  	ab := &authboss.Authboss{}
    22  	ab.Config.Core.Router = router
    23  	ab.Config.Core.ViewRenderer = renderer
    24  	ab.Config.Core.MailRenderer = mailRenderer
    25  	ab.Config.Core.ErrorHandler = &mocks.ErrorHandler{}
    26  
    27  	ab.Config.Modules.MailRouteMethod = http.MethodGet
    28  
    29  	if _, err := SetupEmailVerify(ab, "totp", "/2fa/totp/setup"); err != nil {
    30  		t.Error(err)
    31  	}
    32  
    33  	if err := router.HasGets("/2fa/totp/email/verify", "/2fa/totp/email/verify/end"); err != nil {
    34  		t.Error(err)
    35  	}
    36  	if err := router.HasPosts("/2fa/totp/email/verify"); err != nil {
    37  		t.Error(err)
    38  	}
    39  
    40  	if err := renderer.HasLoadedViews(PageVerify2FA); err != nil {
    41  		t.Error(err)
    42  	}
    43  
    44  	if err := mailRenderer.HasLoadedViews(EmailVerifyHTML, EmailVerifyTxt); err != nil {
    45  		t.Error(err)
    46  	}
    47  }
    48  
    49  type testEmailVerifyHarness struct {
    50  	emailverify EmailVerify
    51  	ab          *authboss.Authboss
    52  
    53  	bodyReader *mocks.BodyReader
    54  	mailer     *mocks.Emailer
    55  	responder  *mocks.Responder
    56  	renderer   *mocks.Renderer
    57  	redirector *mocks.Redirector
    58  	session    *mocks.ClientStateRW
    59  	storer     *mocks.ServerStorer
    60  }
    61  
    62  func testEmailVerifySetup() *testEmailVerifyHarness {
    63  	harness := &testEmailVerifyHarness{}
    64  
    65  	harness.ab = authboss.New()
    66  	harness.bodyReader = &mocks.BodyReader{}
    67  	harness.mailer = &mocks.Emailer{}
    68  	harness.redirector = &mocks.Redirector{}
    69  	harness.renderer = &mocks.Renderer{}
    70  	harness.responder = &mocks.Responder{}
    71  	harness.session = mocks.NewClientRW()
    72  	harness.storer = mocks.NewServerStorer()
    73  
    74  	harness.ab.Config.Core.BodyReader = harness.bodyReader
    75  	harness.ab.Config.Core.Logger = mocks.Logger{}
    76  	harness.ab.Config.Core.Responder = harness.responder
    77  	harness.ab.Config.Core.Redirector = harness.redirector
    78  	harness.ab.Config.Core.Mailer = harness.mailer
    79  	harness.ab.Config.Core.MailRenderer = harness.renderer
    80  	harness.ab.Config.Storage.SessionState = harness.session
    81  	harness.ab.Config.Storage.Server = harness.storer
    82  
    83  	harness.ab.Config.Modules.TwoFactorEmailAuthRequired = true
    84  	harness.ab.Config.Modules.MailNoGoroutine = true
    85  
    86  	harness.emailverify = EmailVerify{
    87  		Authboss:          harness.ab,
    88  		TwofactorKind:     "totp",
    89  		TwofactorSetupURL: "/2fa/totp/setup",
    90  	}
    91  
    92  	return harness
    93  }
    94  
    95  func (h *testEmailVerifyHarness) loadClientState(w http.ResponseWriter, r **http.Request) {
    96  	req, err := h.ab.LoadClientState(w, *r)
    97  	if err != nil {
    98  		panic(err)
    99  	}
   100  
   101  	*r = req
   102  }
   103  
   104  func (h *testEmailVerifyHarness) putUserInCtx(u *mocks.User, r **http.Request) {
   105  	req := (*r).WithContext(context.WithValue((*r).Context(), authboss.CTXKeyUser, u))
   106  	*r = req
   107  }
   108  
   109  func TestEmailVerifyGetStart(t *testing.T) {
   110  	t.Parallel()
   111  
   112  	h := testEmailVerifySetup()
   113  
   114  	rec := httptest.NewRecorder()
   115  	r := mocks.Request("GET")
   116  	w := h.ab.NewResponse(rec)
   117  
   118  	u := &mocks.User{Email: "test@test.com"}
   119  	h.putUserInCtx(u, &r)
   120  	h.loadClientState(w, &r)
   121  
   122  	if err := h.emailverify.GetStart(w, r); err != nil {
   123  		t.Fatal(err)
   124  	}
   125  
   126  	if got := h.responder.Data["email"]; got != "test@test.com" {
   127  		t.Error("email was wrong:", got)
   128  	}
   129  
   130  	if got := h.responder.Page; got != PageVerify2FA {
   131  		t.Error("page was wrong:", got)
   132  	}
   133  }
   134  
   135  func TestEmailVerifyPostStart(t *testing.T) {
   136  	t.Parallel()
   137  	h := testEmailVerifySetup()
   138  
   139  	rec := httptest.NewRecorder()
   140  	r := mocks.Request("POST")
   141  	w := h.ab.NewResponse(rec)
   142  
   143  	u := &mocks.User{Email: "test@test.com"}
   144  	h.putUserInCtx(u, &r)
   145  	h.loadClientState(w, &r)
   146  
   147  	if err := h.emailverify.PostStart(w, r); err != nil {
   148  		t.Fatal(err)
   149  	}
   150  
   151  	ro := h.redirector.Options
   152  	if ro.Code != http.StatusTemporaryRedirect {
   153  		t.Error("code wrong:", ro.Code)
   154  	}
   155  
   156  	if ro.Success != "An e-mail has been sent to confirm 2FA activation." {
   157  		t.Error("message was wrong:", ro.Success)
   158  	}
   159  
   160  	mail := h.mailer.Email
   161  	if mail.To[0] != "test@test.com" {
   162  		t.Error("email was sent to wrong person:", mail.To)
   163  	}
   164  
   165  	if mail.Subject != "Add 2FA to Account" {
   166  		t.Error("subject wrong:", mail.Subject)
   167  	}
   168  
   169  	urlRgx := regexp.MustCompile(`^http://localhost:8080/auth/2fa/totp/email/verify/end\?token=[\-_a-zA-Z0-9=%]+$`)
   170  
   171  	data := h.renderer.Data
   172  	if !urlRgx.MatchString(data[DataVerifyURL].(string)) {
   173  		t.Error("url is wrong:", data[DataVerifyURL])
   174  	}
   175  }
   176  
   177  func TestEmailVerifyEnd(t *testing.T) {
   178  	t.Parallel()
   179  
   180  	h := testEmailVerifySetup()
   181  
   182  	rec := httptest.NewRecorder()
   183  	r := mocks.Request("POST")
   184  	w := h.ab.NewResponse(rec)
   185  
   186  	h.bodyReader.Return = mocks.Values{Token: "abc"}
   187  
   188  	h.session.ClientValues[authboss.Session2FAAuthToken] = "abc"
   189  	h.loadClientState(w, &r)
   190  
   191  	if err := h.emailverify.End(w, r); err != nil {
   192  		t.Error(err)
   193  	}
   194  
   195  	ro := h.redirector.Options
   196  	if ro.Code != http.StatusTemporaryRedirect {
   197  		t.Error("code wrong:", ro.Code)
   198  	}
   199  
   200  	if ro.RedirectPath != "/2fa/totp/setup" {
   201  		t.Error("redir path wrong:", ro.RedirectPath)
   202  	}
   203  
   204  	// Flush session state
   205  	w.WriteHeader(http.StatusOK)
   206  
   207  	if h.session.ClientValues[authboss.Session2FAAuthed] != "true" {
   208  		t.Error("authed value not set")
   209  	}
   210  
   211  	if h.session.ClientValues[authboss.Session2FAAuthToken] != "" {
   212  		t.Error("auth token not removed")
   213  	}
   214  }
   215  
   216  func TestEmailVerifyEndFail(t *testing.T) {
   217  	t.Parallel()
   218  
   219  	h := testEmailVerifySetup()
   220  
   221  	rec := httptest.NewRecorder()
   222  	r := mocks.Request("POST")
   223  	w := h.ab.NewResponse(rec)
   224  
   225  	h.bodyReader.Return = mocks.Values{Token: "abc"}
   226  
   227  	h.session.ClientValues[authboss.Session2FAAuthToken] = "notabc"
   228  	h.loadClientState(w, &r)
   229  
   230  	if err := h.emailverify.End(w, r); err != nil {
   231  		t.Error(err)
   232  	}
   233  
   234  	ro := h.redirector.Options
   235  	if ro.Code != http.StatusTemporaryRedirect {
   236  		t.Error("code wrong:", ro.Code)
   237  	}
   238  
   239  	if ro.RedirectPath != "/" {
   240  		t.Error("redir path wrong:", ro.RedirectPath)
   241  	}
   242  
   243  	if ro.Failure != "invalid 2fa e-mail verification token" {
   244  		t.Error("did not get correct failure")
   245  	}
   246  
   247  	if h.session.ClientValues[authboss.Session2FAAuthed] != "" {
   248  		t.Error("should not be authed")
   249  	}
   250  }
   251  
   252  func TestEmailVerifyWrap(t *testing.T) {
   253  	t.Parallel()
   254  
   255  	t.Run("NotRequired", func(t *testing.T) {
   256  		h := testEmailVerifySetup()
   257  
   258  		rec := httptest.NewRecorder()
   259  		r := mocks.Request("POST")
   260  		w := h.ab.NewResponse(rec)
   261  
   262  		h.ab.Config.Modules.TwoFactorEmailAuthRequired = false
   263  
   264  		called := false
   265  		server := h.emailverify.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   266  			called = true
   267  		}))
   268  
   269  		server.ServeHTTP(w, r)
   270  		if !called {
   271  			t.Error("should have called the handler")
   272  		}
   273  	})
   274  	t.Run("Success", func(t *testing.T) {
   275  		h := testEmailVerifySetup()
   276  
   277  		rec := httptest.NewRecorder()
   278  		r := mocks.Request("POST")
   279  		w := h.ab.NewResponse(rec)
   280  
   281  		h.session.ClientValues[authboss.Session2FAAuthed] = "true"
   282  		h.loadClientState(w, &r)
   283  
   284  		called := false
   285  		server := h.emailverify.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   286  			called = true
   287  		}))
   288  
   289  		server.ServeHTTP(w, r)
   290  		if !called {
   291  			t.Error("should have called the handler")
   292  		}
   293  	})
   294  	t.Run("Fail", func(t *testing.T) {
   295  		h := testEmailVerifySetup()
   296  
   297  		rec := httptest.NewRecorder()
   298  		r := mocks.Request("POST")
   299  		w := h.ab.NewResponse(rec)
   300  
   301  		called := false
   302  		server := h.emailverify.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   303  			called = true
   304  		}))
   305  
   306  		server.ServeHTTP(w, r)
   307  		if called {
   308  			t.Error("should not have called the handler")
   309  		}
   310  
   311  		ro := h.redirector.Options
   312  		if ro.Code != http.StatusTemporaryRedirect {
   313  			t.Error("code wrong:", ro.Code)
   314  		}
   315  
   316  		if ro.RedirectPath != "/auth/2fa/totp/email/verify" {
   317  			t.Error("redir path wrong:", ro.RedirectPath)
   318  		}
   319  
   320  		if ro.Failure != "You must first authorize adding 2fa by e-mail." {
   321  			t.Error("did not get correct failure")
   322  		}
   323  	})
   324  }