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 }