github.com/volatiletech/authboss@v2.4.1+incompatible/recover/recover_test.go (about) 1 package recover 2 3 import ( 4 "bytes" 5 "crypto/sha512" 6 "encoding/base64" 7 "errors" 8 "net/http" 9 "net/http/httptest" 10 "strings" 11 "testing" 12 "time" 13 14 "github.com/volatiletech/authboss" 15 "github.com/volatiletech/authboss/mocks" 16 ) 17 18 const ( 19 testSelector = `rnaGE8TDilrINHPxq/2xNU1FUTzsUSX8FvN5YzooyyWKk88fw1DjjbKBRGFtGew9OeZ+xeCC4mslfvQQMYspIg==` 20 testVerifier = `W1Mz30QhavVM4d8jKaFtxGBfb4GX+fOn7V0Pc1WeftgtyOtY5OX7sY9gIeY5CIY4n8LvfWy14W7/6rs2KO9pgA==` 21 testToken = `w5OZ51E61Q6wsJOVr9o7KmyepP7Od5VBHQ1ADDUBkiGGMjKfnMFPjtvNpLjLKJqffw72KWZzNLj0Cs8wqywdEQ==` 22 ) 23 24 func TestInit(t *testing.T) { 25 t.Parallel() 26 27 ab := authboss.New() 28 29 router := &mocks.Router{} 30 renderer := &mocks.Renderer{} 31 mailRenderer := &mocks.Renderer{} 32 errHandler := &mocks.ErrorHandler{} 33 ab.Config.Core.Router = router 34 ab.Config.Core.ViewRenderer = renderer 35 ab.Config.Core.MailRenderer = mailRenderer 36 ab.Config.Core.ErrorHandler = errHandler 37 38 r := &Recover{} 39 if err := r.Init(ab); err != nil { 40 t.Fatal(err) 41 } 42 43 if err := renderer.HasLoadedViews(PageRecoverStart, PageRecoverEnd); err != nil { 44 t.Error(err) 45 } 46 if err := mailRenderer.HasLoadedViews(EmailRecoverHTML, EmailRecoverTxt); err != nil { 47 t.Error(err) 48 } 49 50 if err := router.HasGets("/recover", "/recover/end"); err != nil { 51 t.Error(err) 52 } 53 if err := router.HasPosts("/recover", "/recover/end"); err != nil { 54 t.Error(err) 55 } 56 } 57 58 type testHarness struct { 59 recover *Recover 60 ab *authboss.Authboss 61 62 bodyReader *mocks.BodyReader 63 mailer *mocks.Emailer 64 redirector *mocks.Redirector 65 renderer *mocks.Renderer 66 responder *mocks.Responder 67 session *mocks.ClientStateRW 68 storer *mocks.ServerStorer 69 } 70 71 func testSetup() *testHarness { 72 harness := &testHarness{} 73 74 harness.ab = authboss.New() 75 harness.bodyReader = &mocks.BodyReader{} 76 harness.mailer = &mocks.Emailer{} 77 harness.redirector = &mocks.Redirector{} 78 harness.renderer = &mocks.Renderer{} 79 harness.responder = &mocks.Responder{} 80 harness.session = mocks.NewClientRW() 81 harness.storer = mocks.NewServerStorer() 82 83 harness.ab.Paths.RecoverOK = "/recover/ok" 84 harness.ab.Modules.MailNoGoroutine = true 85 86 harness.ab.Config.Core.BodyReader = harness.bodyReader 87 harness.ab.Config.Core.Logger = mocks.Logger{} 88 harness.ab.Config.Core.Mailer = harness.mailer 89 harness.ab.Config.Core.Redirector = harness.redirector 90 harness.ab.Config.Core.MailRenderer = harness.renderer 91 harness.ab.Config.Core.Responder = harness.responder 92 harness.ab.Config.Storage.SessionState = harness.session 93 harness.ab.Config.Storage.Server = harness.storer 94 95 harness.recover = &Recover{harness.ab} 96 97 return harness 98 } 99 100 func TestStartGet(t *testing.T) { 101 t.Parallel() 102 103 h := testSetup() 104 105 r := mocks.Request("GET") 106 w := httptest.NewRecorder() 107 108 if err := h.recover.StartGet(w, r); err != nil { 109 t.Error(err) 110 } 111 112 if w.Code != http.StatusOK { 113 t.Error("code was wrong:", w.Code) 114 } 115 if h.responder.Page != PageRecoverStart { 116 t.Error("page was wrong:", h.responder.Page) 117 } 118 if h.responder.Data != nil { 119 t.Error("expected no data:", h.responder.Data) 120 } 121 } 122 123 func TestStartPostSuccess(t *testing.T) { 124 t.Parallel() 125 126 h := testSetup() 127 128 h.bodyReader.Return = &mocks.Values{ 129 PID: "test@test.com", 130 } 131 h.storer.Users["test@test.com"] = &mocks.User{ 132 Email: "test@test.com", 133 Password: "i can't recall, doesn't seem like something bcrypted though", 134 } 135 136 r := mocks.Request("GET") 137 w := httptest.NewRecorder() 138 139 if err := h.recover.StartPost(w, r); err != nil { 140 t.Error(err) 141 } 142 143 if w.Code != http.StatusTemporaryRedirect { 144 t.Error("code was wrong:", w.Code) 145 } 146 if h.redirector.Options.RedirectPath != h.ab.Config.Paths.RecoverOK { 147 t.Error("page was wrong:", h.responder.Page) 148 } 149 if len(h.redirector.Options.Success) == 0 { 150 t.Error("expected a nice success message") 151 } 152 153 if h.mailer.Email.To[0] != "test@test.com" { 154 t.Error("e-mail to address is wrong:", h.mailer.Email.To) 155 } 156 if !strings.HasSuffix(h.mailer.Email.Subject, "Password Reset") { 157 t.Error("e-mail subject line is wrong:", h.mailer.Email.Subject) 158 } 159 if len(h.renderer.Data[DataRecoverURL].(string)) == 0 { 160 t.Errorf("the renderer's url in data was missing: %#v", h.renderer.Data) 161 } 162 } 163 164 func TestStartPostFailure(t *testing.T) { 165 t.Parallel() 166 167 h := testSetup() 168 169 h.bodyReader.Return = &mocks.Values{ 170 PID: "test@test.com", 171 } 172 173 r := mocks.Request("GET") 174 w := httptest.NewRecorder() 175 176 if err := h.recover.StartPost(w, r); err != nil { 177 t.Error(err) 178 } 179 180 if w.Code != http.StatusTemporaryRedirect { 181 t.Error("code was wrong:", w.Code) 182 } 183 if h.redirector.Options.RedirectPath != h.ab.Config.Paths.RecoverOK { 184 t.Error("page was wrong:", h.responder.Page) 185 } 186 if len(h.redirector.Options.Success) == 0 { 187 t.Error("expected a nice success message") 188 } 189 190 if len(h.mailer.Email.To) != 0 { 191 t.Error("should not have sent an e-mail out!") 192 } 193 } 194 195 func TestEndGet(t *testing.T) { 196 t.Parallel() 197 198 h := testSetup() 199 200 h.bodyReader.Return = &mocks.Values{ 201 Token: "abcd", 202 } 203 204 r := mocks.Request("GET") 205 w := httptest.NewRecorder() 206 207 if err := h.recover.EndGet(w, r); err != nil { 208 t.Error(err) 209 } 210 211 if w.Code != http.StatusOK { 212 t.Error("code was wrong:", w.Code) 213 } 214 if h.responder.Page != PageRecoverEnd { 215 t.Error("page was wrong:", h.responder.Page) 216 } 217 if h.responder.Data[DataRecoverToken].(string) != "abcd" { 218 t.Errorf("recovery token is wrong: %#v", h.responder.Data) 219 } 220 } 221 222 func TestEndPostSuccess(t *testing.T) { 223 t.Parallel() 224 225 h := testSetup() 226 227 h.bodyReader.Return = &mocks.Values{ 228 Token: testToken, 229 } 230 h.storer.Users["test@test.com"] = &mocks.User{ 231 Email: "test@test.com", 232 Password: "to-overwrite", 233 RecoverSelector: testSelector, 234 RecoverVerifier: testVerifier, 235 RecoverTokenExpiry: time.Now().UTC().AddDate(0, 0, 1), 236 } 237 238 r := mocks.Request("POST") 239 w := httptest.NewRecorder() 240 241 if err := h.recover.EndPost(w, r); err != nil { 242 t.Error(err) 243 } 244 245 if w.Code != http.StatusTemporaryRedirect { 246 t.Error("code was wrong:", w.Code) 247 } 248 if p := h.redirector.Options.RedirectPath; p != h.ab.Paths.RecoverOK { 249 t.Error("path was wrong:", p) 250 } 251 if len(h.session.ClientValues[authboss.SessionKey]) != 0 { 252 t.Error("should not have logged in the user") 253 } 254 if !strings.Contains(h.redirector.Options.Success, "updated password") { 255 t.Error("should talk about recovering the password") 256 } 257 if strings.Contains(h.redirector.Options.Success, "logged in") { 258 t.Error("should not talk about logging in") 259 } 260 } 261 262 func TestEndPostSuccessLogin(t *testing.T) { 263 t.Parallel() 264 265 h := testSetup() 266 267 h.ab.Config.Modules.RecoverLoginAfterRecovery = true 268 h.bodyReader.Return = &mocks.Values{ 269 Token: testToken, 270 } 271 h.storer.Users["test@test.com"] = &mocks.User{ 272 Email: "test@test.com", 273 Password: "to-overwrite", 274 RecoverSelector: testSelector, 275 RecoverVerifier: testVerifier, 276 RecoverTokenExpiry: time.Now().UTC().AddDate(0, 0, 1), 277 } 278 279 r := mocks.Request("POST") 280 w := httptest.NewRecorder() 281 282 if err := h.recover.EndPost(h.ab.NewResponse(w), r); err != nil { 283 t.Error(err) 284 } 285 286 if w.Code != http.StatusTemporaryRedirect { 287 t.Error("code was wrong:", w.Code) 288 } 289 if p := h.redirector.Options.RedirectPath; p != h.ab.Paths.RecoverOK { 290 t.Error("path was wrong:", p) 291 } 292 if len(h.session.ClientValues[authboss.SessionKey]) == 0 { 293 t.Error("it should have logged in the user") 294 } 295 if !strings.Contains(h.redirector.Options.Success, "logged in") { 296 t.Error("should talk about logging in") 297 } 298 } 299 300 func TestEndPostValidationFailure(t *testing.T) { 301 t.Parallel() 302 303 h := testSetup() 304 305 h.bodyReader.Return = &mocks.Values{ 306 Errors: []error{errors.New("password is not sufficiently complex")}, 307 } 308 h.storer.Users["test@test.com"] = &mocks.User{ 309 Email: "test@test.com", 310 Password: "to-overwrite", 311 RecoverSelector: testSelector, 312 RecoverVerifier: testVerifier, 313 RecoverTokenExpiry: time.Now().UTC().AddDate(0, 0, 1), 314 } 315 316 r := mocks.Request("POST") 317 w := httptest.NewRecorder() 318 319 if err := h.recover.EndPost(w, r); err != nil { 320 t.Error(err) 321 } 322 323 if w.Code != http.StatusOK { 324 t.Error("code was wrong:", w.Code) 325 } 326 if h.responder.Page != PageRecoverEnd { 327 t.Error("rendered the wrong page") 328 } 329 if m, ok := h.responder.Data[authboss.DataValidation].(map[string][]string); !ok { 330 t.Error("expected validation errors") 331 } else if m[""][0] != "password is not sufficiently complex" { 332 t.Error("error message data was not correct:", m[""]) 333 } 334 if len(h.session.ClientValues[authboss.SessionKey]) != 0 { 335 t.Error("should not have logged in the user") 336 } 337 } 338 339 func TestEndPostInvalidBase64(t *testing.T) { 340 t.Parallel() 341 342 h := testSetup() 343 344 h.bodyReader.Return = &mocks.Values{ 345 Token: "a", 346 } 347 348 r := mocks.Request("GET") 349 w := httptest.NewRecorder() 350 351 if err := h.recover.EndPost(w, r); err != nil { 352 t.Error(err) 353 } 354 355 invalidCheck(t, h, w) 356 } 357 358 func TestEndPostExpiredToken(t *testing.T) { 359 t.Parallel() 360 361 h := testSetup() 362 363 h.bodyReader.Return = &mocks.Values{ 364 Token: testToken, 365 } 366 h.storer.Users["test@test.com"] = &mocks.User{ 367 Email: "test@test.com", 368 Password: "to-overwrite", 369 RecoverSelector: testSelector, 370 RecoverVerifier: testVerifier, 371 RecoverTokenExpiry: time.Now().UTC().AddDate(0, 0, -1), 372 } 373 374 r := mocks.Request("GET") 375 w := httptest.NewRecorder() 376 377 if err := h.recover.EndPost(w, r); err != nil { 378 t.Error(err) 379 } 380 381 invalidCheck(t, h, w) 382 } 383 384 func TestEndPostUserNotExist(t *testing.T) { 385 t.Parallel() 386 387 h := testSetup() 388 389 h.bodyReader.Return = &mocks.Values{ 390 Token: testToken, 391 } 392 393 r := mocks.Request("GET") 394 w := httptest.NewRecorder() 395 396 if err := h.recover.EndPost(w, r); err != nil { 397 t.Error(err) 398 } 399 400 invalidCheck(t, h, w) 401 } 402 403 func TestMailURL(t *testing.T) { 404 t.Parallel() 405 406 h := testSetup() 407 h.ab.Config.Paths.RootURL = "https://api.test.com:6343" 408 h.ab.Config.Paths.Mount = "/v1/auth" 409 410 want := "https://api.test.com:6343/v1/auth/recover/end?token=abc" 411 if got := h.recover.mailURL("abc"); got != want { 412 t.Error("want:", want, "got:", got) 413 } 414 415 h.ab.Config.Mail.RootURL = "https://test.com:3333/testauth" 416 417 want = "https://test.com:3333/testauth/recover/end?token=abc" 418 if got := h.recover.mailURL("abc"); got != want { 419 t.Error("want:", want, "got:", got) 420 } 421 } 422 423 func invalidCheck(t *testing.T, h *testHarness, w *httptest.ResponseRecorder) { 424 t.Helper() 425 426 if w.Code != http.StatusOK { 427 t.Error("code was wrong:", w.Code) 428 } 429 if h.responder.Page != PageRecoverEnd { 430 t.Error("page was wrong:", h.responder.Page) 431 } 432 if h.responder.Data[authboss.DataValidation].(map[string][]string)[""][0] != "recovery token is invalid" { 433 t.Error("expected a vague error to mislead") 434 } 435 } 436 437 func TestGenerateRecoverCreds(t *testing.T) { 438 t.Parallel() 439 440 selector, verifier, token, err := GenerateRecoverCreds() 441 if err != nil { 442 t.Error(err) 443 } 444 445 if verifier == selector { 446 t.Error("the verifier and selector should be different") 447 } 448 449 // base64 length: n = 64; 4*(64/3) = 85.3; round to nearest 4: 88 450 if len(verifier) != 88 { 451 t.Errorf("verifier length was wrong (%d): %s", len(verifier), verifier) 452 } 453 454 // base64 length: n = 64; 4*(64/3) = 85.3; round to nearest 4: 88 455 if len(selector) != 88 { 456 t.Errorf("selector length was wrong (%d): %s", len(selector), selector) 457 } 458 459 // base64 length: n = 64; 4*(64/3) = 85.33; round to nearest 4: 88 460 if len(token) != 88 { 461 t.Errorf("token length was wrong (%d): %s", len(token), token) 462 } 463 464 rawToken, err := base64.URLEncoding.DecodeString(token) 465 if err != nil { 466 t.Error(err) 467 } 468 469 rawSelector, err := base64.StdEncoding.DecodeString(selector) 470 if err != nil { 471 t.Error(err) 472 } 473 rawVerifier, err := base64.StdEncoding.DecodeString(verifier) 474 if err != nil { 475 t.Error(err) 476 } 477 478 checkSelector := sha512.Sum512(rawToken[:recoverTokenSplit]) 479 if 0 != bytes.Compare(checkSelector[:], rawSelector) { 480 t.Error("expected selector to match") 481 } 482 checkVerifier := sha512.Sum512(rawToken[recoverTokenSplit:]) 483 if 0 != bytes.Compare(checkVerifier[:], rawVerifier) { 484 t.Error("expected verifier to match") 485 } 486 }