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

     1  package twofactor
     2  
     3  import (
     4  	"net/http/httptest"
     5  	"regexp"
     6  	"strings"
     7  	"testing"
     8  
     9  	"golang.org/x/crypto/bcrypt"
    10  
    11  	"github.com/volatiletech/authboss"
    12  	"github.com/volatiletech/authboss/mocks"
    13  )
    14  
    15  func TestSetup(t *testing.T) {
    16  	t.Parallel()
    17  
    18  	router := &mocks.Router{}
    19  	renderer := &mocks.Renderer{}
    20  
    21  	ab := &authboss.Authboss{}
    22  	ab.Config.Core.Router = router
    23  	ab.Config.Core.ViewRenderer = renderer
    24  	ab.Config.Core.ErrorHandler = &mocks.ErrorHandler{}
    25  
    26  	recovery := &Recovery{Authboss: ab}
    27  	if err := recovery.Setup(); err != nil {
    28  		t.Error(err)
    29  	}
    30  
    31  	if err := router.HasGets("/2fa/recovery/regen"); err != nil {
    32  		t.Error(err)
    33  	}
    34  	if err := router.HasPosts("/2fa/recovery/regen"); err != nil {
    35  		t.Error(err)
    36  	}
    37  
    38  	if err := renderer.HasLoadedViews(PageRecovery2FA); err != nil {
    39  		t.Error(err)
    40  	}
    41  }
    42  
    43  type testHarness struct {
    44  	recovery *Recovery
    45  	ab       *authboss.Authboss
    46  
    47  	bodyReader *mocks.BodyReader
    48  	responder  *mocks.Responder
    49  	redirector *mocks.Redirector
    50  	session    *mocks.ClientStateRW
    51  	storer     *mocks.ServerStorer
    52  }
    53  
    54  func testSetup() *testHarness {
    55  	harness := &testHarness{}
    56  
    57  	harness.ab = authboss.New()
    58  	harness.bodyReader = &mocks.BodyReader{}
    59  	harness.redirector = &mocks.Redirector{}
    60  	harness.responder = &mocks.Responder{}
    61  	harness.session = mocks.NewClientRW()
    62  	harness.storer = mocks.NewServerStorer()
    63  
    64  	harness.ab.Config.Core.BodyReader = harness.bodyReader
    65  	harness.ab.Config.Core.Logger = mocks.Logger{}
    66  	harness.ab.Config.Core.Responder = harness.responder
    67  	harness.ab.Config.Core.Redirector = harness.redirector
    68  	harness.ab.Config.Storage.SessionState = harness.session
    69  	harness.ab.Config.Storage.Server = harness.storer
    70  
    71  	harness.recovery = &Recovery{harness.ab}
    72  
    73  	return harness
    74  }
    75  
    76  func TestGetRegen(t *testing.T) {
    77  	t.Parallel()
    78  
    79  	var err error
    80  	harness := testSetup()
    81  	user := &mocks.User{Email: "test@test.com", RecoveryCodes: "a,b"}
    82  	harness.storer.Users["test@test.com"] = user
    83  
    84  	rec := httptest.NewRecorder()
    85  	r := mocks.Request("GET")
    86  	w := harness.ab.NewResponse(rec)
    87  
    88  	harness.session.ClientValues[authboss.SessionKey] = "test@test.com"
    89  	r, err = harness.ab.LoadClientState(w, r)
    90  	if err != nil {
    91  		t.Error(err)
    92  	}
    93  
    94  	if err := harness.recovery.GetRegen(w, r); err != nil {
    95  		t.Error(err)
    96  	}
    97  
    98  	if harness.responder.Data[DataNumRecoveryCodes].(int) != 2 {
    99  		t.Error("want two recovery codes")
   100  	}
   101  }
   102  
   103  func TestPostRegen(t *testing.T) {
   104  	t.Parallel()
   105  
   106  	var err error
   107  	harness := testSetup()
   108  	user := &mocks.User{Email: "test@test.com", RecoveryCodes: "a,b"}
   109  	harness.storer.Users["test@test.com"] = user
   110  
   111  	rec := httptest.NewRecorder()
   112  	r := mocks.Request("POST")
   113  	w := harness.ab.NewResponse(rec)
   114  
   115  	harness.session.ClientValues[authboss.SessionKey] = "test@test.com"
   116  	r, err = harness.ab.LoadClientState(w, r)
   117  	if err != nil {
   118  		t.Error(err)
   119  	}
   120  
   121  	if err := harness.recovery.PostRegen(w, r); err != nil {
   122  		t.Error(err)
   123  	}
   124  
   125  	userStrs := DecodeRecoveryCodes(user.GetRecoveryCodes())
   126  	dataStrs := harness.responder.Data[DataRecoveryCodes].([]string)
   127  
   128  	if ulen, dlen := len(userStrs), len(dataStrs); ulen != dlen {
   129  		t.Errorf("userStrs: %d dataStrs: %d", ulen, dlen)
   130  	}
   131  
   132  	for i := range userStrs {
   133  		err := bcrypt.CompareHashAndPassword([]byte(userStrs[i]), []byte(dataStrs[i]))
   134  		if err != nil {
   135  			t.Error("password mismatch:", userStrs[i], dataStrs[i])
   136  		}
   137  	}
   138  }
   139  
   140  func TestGenerateRecoveryCodes(t *testing.T) {
   141  	t.Parallel()
   142  
   143  	codes, err := GenerateRecoveryCodes()
   144  	if err != nil {
   145  		t.Fatal(err)
   146  	}
   147  
   148  	if len(codes) != 10 {
   149  		t.Error("it should create 10 codes, got:", len(codes))
   150  	}
   151  
   152  	rgx := regexp.MustCompile(`^[0-9a-z]{5}-[0-9a-z]{5}$`)
   153  	for _, c := range codes {
   154  		if !rgx.MatchString(c) {
   155  			t.Errorf("code %s did not match regexp", c)
   156  		}
   157  	}
   158  }
   159  
   160  func TestHashRecoveryCodes(t *testing.T) {
   161  	t.Parallel()
   162  
   163  	codes, err := GenerateRecoveryCodes()
   164  	if err != nil {
   165  		t.Fatal(err)
   166  	}
   167  
   168  	if len(codes) != 10 {
   169  		t.Error("it should create 10 codes, got:", len(codes))
   170  	}
   171  
   172  	cryptedCodes, err := BCryptRecoveryCodes(codes)
   173  	if err != nil {
   174  		t.Fatal(err)
   175  	}
   176  
   177  	for _, c := range cryptedCodes {
   178  		if !strings.HasPrefix(c, "$2a$10$") {
   179  			t.Error("code did not look like bcrypt:", c)
   180  		}
   181  	}
   182  }
   183  
   184  func TestUseRecoveryCode(t *testing.T) {
   185  	t.Parallel()
   186  
   187  	codes, err := GenerateRecoveryCodes()
   188  	if err != nil {
   189  		t.Fatal(err)
   190  	}
   191  
   192  	if len(codes) != 10 {
   193  		t.Error("it should create 10 codes, got:", len(codes))
   194  	}
   195  
   196  	cryptedCodes, err := BCryptRecoveryCodes(codes)
   197  	if err != nil {
   198  		t.Fatal(err)
   199  	}
   200  
   201  	for _, c := range cryptedCodes {
   202  		if !strings.HasPrefix(c, "$2a$10$") {
   203  			t.Error("code did not look like bcrypt:", c)
   204  		}
   205  	}
   206  
   207  	remaining, ok := UseRecoveryCode(cryptedCodes, codes[4])
   208  	if !ok {
   209  		t.Error("should have used a code")
   210  	}
   211  
   212  	if want, got := len(cryptedCodes)-1, len(remaining); want != got {
   213  		t.Error("want:", want, "got:", got)
   214  	}
   215  
   216  	if cryptedCodes[4] == remaining[4] {
   217  		t.Error("it should have used number 4")
   218  	}
   219  
   220  	remaining, ok = UseRecoveryCode(remaining, codes[0])
   221  	if !ok {
   222  		t.Error("should have used a code")
   223  	}
   224  
   225  	if want, got := len(cryptedCodes)-2, len(remaining); want != got {
   226  		t.Error("want:", want, "got:", got)
   227  	}
   228  
   229  	if cryptedCodes[0] == remaining[0] {
   230  		t.Error("it should have used number 0")
   231  	}
   232  
   233  	remaining, ok = UseRecoveryCode(remaining, codes[len(codes)-1])
   234  	if !ok {
   235  		t.Error("should have used a code")
   236  	}
   237  
   238  	if want, got := len(cryptedCodes)-3, len(remaining); want != got {
   239  		t.Error("want:", want, "got:", got)
   240  	}
   241  
   242  	if cryptedCodes[len(cryptedCodes)-1] == remaining[len(remaining)-1] {
   243  		t.Error("it should have used number 0")
   244  	}
   245  }