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 }