github.com/volatiletech/authboss@v2.4.1+incompatible/otp/twofactor/totp2fa/totp_test.go (about) 1 package totp2fa 2 3 import ( 4 "context" 5 "net/http" 6 "net/http/httptest" 7 "testing" 8 "time" 9 10 "golang.org/x/crypto/bcrypt" 11 12 "github.com/volatiletech/authboss/otp/twofactor" 13 14 "github.com/pquerna/otp/totp" 15 "github.com/volatiletech/authboss" 16 "github.com/volatiletech/authboss/mocks" 17 ) 18 19 func TestTOTPSetup(t *testing.T) { 20 t.Parallel() 21 22 ab := authboss.New() 23 router := &mocks.Router{} 24 renderer := &mocks.Renderer{} 25 errHandler := &mocks.ErrorHandler{} 26 27 ab.Config.Core.Router = router 28 ab.Config.Core.ViewRenderer = renderer 29 ab.Config.Core.ErrorHandler = errHandler 30 31 totpNew := &TOTP{Authboss: ab} 32 if err := totpNew.Setup(); err != nil { 33 t.Fatal(err) 34 } 35 36 gets := []string{"/2fa/totp/setup", "/2fa/totp/qr", "/2fa/totp/confirm", "/2fa/totp/remove", "/2fa/totp/validate"} 37 posts := []string{"/2fa/totp/setup", "/2fa/totp/confirm", "/2fa/totp/remove", "/2fa/totp/validate"} 38 if err := router.HasGets(gets...); err != nil { 39 t.Error(err) 40 } 41 if err := router.HasPosts(posts...); err != nil { 42 t.Error(err) 43 } 44 } 45 46 type testHarness struct { 47 totp *TOTP 48 ab *authboss.Authboss 49 50 bodyReader *mocks.BodyReader 51 responder *mocks.Responder 52 redirector *mocks.Redirector 53 session *mocks.ClientStateRW 54 storer *mocks.ServerStorer 55 } 56 57 func testSetup() *testHarness { 58 harness := &testHarness{} 59 60 harness.ab = authboss.New() 61 harness.bodyReader = &mocks.BodyReader{} 62 harness.redirector = &mocks.Redirector{} 63 harness.responder = &mocks.Responder{} 64 harness.session = mocks.NewClientRW() 65 harness.storer = mocks.NewServerStorer() 66 67 harness.ab.Config.Paths.AuthLoginOK = "/login/ok" 68 harness.ab.Config.Modules.TOTP2FAIssuer = "TOTPTest" 69 70 harness.ab.Config.Core.BodyReader = harness.bodyReader 71 harness.ab.Config.Core.Logger = mocks.Logger{} 72 harness.ab.Config.Core.Responder = harness.responder 73 harness.ab.Config.Core.Redirector = harness.redirector 74 harness.ab.Config.Storage.SessionState = harness.session 75 harness.ab.Config.Storage.Server = harness.storer 76 77 harness.totp = &TOTP{harness.ab} 78 79 return harness 80 } 81 82 func (h *testHarness) loadClientState(w http.ResponseWriter, r **http.Request) { 83 req, err := h.ab.LoadClientState(w, *r) 84 if err != nil { 85 panic(err) 86 } 87 88 *r = req 89 } 90 91 func (h *testHarness) putUserInCtx(u *mocks.User, r **http.Request) { 92 req := (*r).WithContext(context.WithValue((*r).Context(), authboss.CTXKeyUser, u)) 93 *r = req 94 } 95 96 func (h *testHarness) newHTTP(method string, bodyArgs ...string) (*http.Request, *authboss.ClientStateResponseWriter, *httptest.ResponseRecorder) { 97 r := mocks.Request(method, bodyArgs...) 98 wr := httptest.NewRecorder() 99 w := h.ab.NewResponse(wr) 100 101 return r, w, wr 102 } 103 104 func (h *testHarness) setSession(key, value string) { 105 h.session.ClientValues[key] = value 106 } 107 108 func TestHijackAuth(t *testing.T) { 109 t.Parallel() 110 111 t.Run("Handled", func(t *testing.T) { 112 harness := testSetup() 113 114 handled, err := harness.totp.HijackAuth(nil, nil, true) 115 if handled { 116 t.Error("should not be handled") 117 } 118 if err != nil { 119 t.Error(err) 120 } 121 }) 122 123 t.Run("UserNoTOTP", func(t *testing.T) { 124 harness := testSetup() 125 126 r, w, _ := harness.newHTTP("POST") 127 r.URL.RawQuery = "test=query" 128 129 user := &mocks.User{Email: "test@test.com"} 130 harness.putUserInCtx(user, &r) 131 132 harness.loadClientState(w, &r) 133 handled, err := harness.totp.HijackAuth(w, r, false) 134 if handled { 135 t.Error("should not be handled") 136 } 137 if err != nil { 138 t.Error(err) 139 } 140 }) 141 142 t.Run("Ok", func(t *testing.T) { 143 harness := testSetup() 144 145 handled, err := harness.totp.HijackAuth(nil, nil, true) 146 if handled { 147 t.Error("should not be handled") 148 } 149 if err != nil { 150 t.Error(err) 151 } 152 153 r, w, _ := harness.newHTTP("POST") 154 r.URL.RawQuery = "test=query" 155 156 user := &mocks.User{Email: "test@test.com", TOTPSecretKey: "secret"} 157 harness.putUserInCtx(user, &r) 158 harness.loadClientState(w, &r) 159 160 handled, err = harness.totp.HijackAuth(w, r, false) 161 if !handled { 162 t.Error("should be handled") 163 } 164 if err != nil { 165 t.Error(err) 166 } 167 168 opts := harness.redirector.Options 169 if opts.Code != http.StatusTemporaryRedirect { 170 t.Error("status wrong:", opts.Code) 171 } 172 173 if opts.RedirectPath != "/auth/2fa/totp/validate?test=query" { 174 t.Error("redir path wrong:", opts.RedirectPath) 175 } 176 }) 177 } 178 179 func TestGetSetup(t *testing.T) { 180 t.Parallel() 181 h := testSetup() 182 183 r, w, _ := h.newHTTP("GET") 184 185 h.setSession(SessionTOTPSecret, "secret") 186 h.loadClientState(w, &r) 187 188 var err error 189 if err = h.totp.GetSetup(w, r); err != nil { 190 t.Error(err) 191 } 192 193 // Flush ClientState 194 w.WriteHeader(http.StatusOK) 195 196 if h.session.ClientValues[SessionTOTPSecret] != "" { 197 t.Error("session totp secret should be cleared") 198 } 199 200 if h.responder.Page != PageTOTPSetup { 201 t.Error("page wrong:", h.responder.Page) 202 } 203 } 204 205 func TestPostSetup(t *testing.T) { 206 t.Parallel() 207 h := testSetup() 208 209 r, w, _ := h.newHTTP("GET") 210 user := &mocks.User{Email: "test@test.com"} 211 h.putUserInCtx(user, &r) 212 213 var err error 214 if err = h.totp.PostSetup(w, r); err != nil { 215 t.Error(err) 216 } 217 218 // Flush ClientState 219 w.WriteHeader(http.StatusOK) 220 221 opts := h.redirector.Options 222 if opts.Code != http.StatusTemporaryRedirect { 223 t.Error("status wrong:", opts.Code) 224 } 225 226 if opts.RedirectPath != "/auth/2fa/totp/confirm" { 227 t.Error("redir path wrong:", opts.RedirectPath) 228 } 229 230 if len(h.session.ClientValues[SessionTOTPSecret]) == 0 { 231 t.Error("no secret in the session") 232 } 233 } 234 235 func TestGetQRCode(t *testing.T) { 236 t.Parallel() 237 h := testSetup() 238 239 r, w, wr := h.newHTTP("GET") 240 241 user := &mocks.User{Email: "test@test.com"} 242 h.putUserInCtx(user, &r) 243 244 if err := h.totp.GetQRCode(w, r); err == nil { 245 t.Error("should fail because there is no totp secret") 246 } 247 248 secret := makeSecretKey(h, user.Email) 249 h.setSession(SessionTOTPSecret, secret) 250 h.loadClientState(w, &r) 251 252 if err := h.totp.GetQRCode(w, r); err != nil { 253 t.Error(err) 254 } 255 256 if got := wr.Header().Get("Content-Type"); got != "image/png" { 257 t.Error("content type wrong:", got) 258 } 259 if wr.Body.Len() == 0 { 260 t.Error("body should have been sizable") 261 } 262 } 263 264 func TestGetConfirm(t *testing.T) { 265 t.Parallel() 266 h := testSetup() 267 268 r, w, _ := h.newHTTP("GET") 269 270 if err := h.totp.GetConfirm(w, r); err == nil { 271 t.Error("should fail because there is no totp secret") 272 } 273 274 secret := "secret" 275 h.setSession(SessionTOTPSecret, secret) 276 h.loadClientState(w, &r) 277 278 if err := h.totp.GetConfirm(w, r); err != nil { 279 t.Error(err) 280 } 281 282 if h.responder.Page != PageTOTPConfirm { 283 t.Error("page wrong:", h.responder.Page) 284 } 285 if got := h.responder.Data[DataTOTPSecret]; got != secret { 286 t.Error("data wrong:", got) 287 } 288 } 289 290 func TestPostConfirm(t *testing.T) { 291 t.Parallel() 292 h := testSetup() 293 294 r, w, _ := h.newHTTP("POST") 295 296 if err := h.totp.PostConfirm(w, r); err == nil { 297 t.Error("should fail because there is no totp secret") 298 } 299 300 user := &mocks.User{Email: "test@test.com"} 301 h.storer.Users[user.Email] = user 302 303 secret := makeSecretKey(h, user.Email) 304 h.setSession(SessionTOTPSecret, secret) 305 h.setSession(authboss.SessionKey, user.Email) 306 h.loadClientState(w, &r) 307 308 code, err := totp.GenerateCode(secret, time.Now()) 309 if err != nil { 310 t.Fatal(err) 311 } 312 h.bodyReader.Return = &mocks.Values{Code: code} 313 314 if err = h.totp.PostConfirm(w, r); err != nil { 315 t.Error(err) 316 } 317 318 // Flush client state 319 w.WriteHeader(http.StatusOK) 320 321 if len(user.TOTPSecretKey) == 0 { 322 t.Error("totp secret key unset") 323 } 324 if len(user.RecoveryCodes) == 0 { 325 t.Error("user recovery codes unset") 326 } 327 if _, ok := h.session.ClientValues[SessionTOTPSecret]; ok { 328 t.Error("session totp secret not deleted") 329 } 330 331 if h.responder.Page != PageTOTPConfirmSuccess { 332 t.Error("page wrong:", h.responder.Page) 333 } 334 if got := h.responder.Data[twofactor.DataRecoveryCodes].([]string); len(got) == 0 { 335 t.Error("data wrong:", got) 336 } 337 } 338 339 func TestGetRemove(t *testing.T) { 340 t.Parallel() 341 h := testSetup() 342 343 r, w, _ := h.newHTTP("GET") 344 345 if err := h.totp.GetRemove(w, r); err != nil { 346 t.Error(err) 347 } 348 349 if h.responder.Page != PageTOTPRemove { 350 t.Error("page wrong:", h.responder.Page) 351 } 352 } 353 354 func TestPostRemove(t *testing.T) { 355 t.Parallel() 356 357 setupMore := func(h *testHarness) *mocks.User { 358 user := &mocks.User{Email: "test@test.com"} 359 h.storer.Users[user.Email] = user 360 h.setSession(authboss.SessionKey, user.Email) 361 362 return user 363 } 364 365 t.Run("NoTOTPActivated", func(t *testing.T) { 366 h := testSetup() 367 368 r, w, _ := h.newHTTP("POST") 369 setupMore(h) 370 h.loadClientState(w, &r) 371 372 // No session 373 if err := h.totp.PostRemove(w, r); err != nil { 374 t.Fatal(err) 375 } 376 377 if h.responder.Page != PageTOTPRemove { 378 t.Error("page wrong:", h.responder.Page) 379 } 380 if got := h.responder.Data[authboss.DataErr]; got != "totp 2fa not active" { 381 t.Error("data wrong:", got) 382 } 383 }) 384 385 t.Run("WrongCode", func(t *testing.T) { 386 h := testSetup() 387 388 r, w, _ := h.newHTTP("POST") 389 390 user := setupMore(h) 391 secret := makeSecretKey(h, user.Email) 392 user.TOTPSecretKey = secret 393 h.bodyReader.Return = mocks.Values{Code: "wrong"} 394 395 h.loadClientState(w, &r) 396 397 if err := h.totp.PostRemove(w, r); err != nil { 398 t.Error(err) 399 } 400 401 if h.responder.Page != PageTOTPRemove { 402 t.Error("page wrong:", h.responder.Page) 403 } 404 if got := h.responder.Data[authboss.DataValidation].(map[string][]string); got[FormValueCode][0] != "2fa code was invalid" { 405 t.Error("data wrong:", got) 406 } 407 }) 408 409 t.Run("OkCode", func(t *testing.T) { 410 h := testSetup() 411 412 r, w, _ := h.newHTTP("POST") 413 414 user := setupMore(h) 415 secret := makeSecretKey(h, user.Email) 416 user.TOTPSecretKey = secret 417 h.setSession(authboss.Session2FA, "totp") 418 h.loadClientState(w, &r) 419 420 code, err := totp.GenerateCode(secret, time.Now()) 421 if err != nil { 422 t.Fatal(err) 423 } 424 h.bodyReader.Return = mocks.Values{Code: code} 425 426 if err := h.totp.PostRemove(w, r); err != nil { 427 t.Error(err) 428 } 429 430 if h.responder.Page != PageTOTPRemoveSuccess { 431 t.Error("page wrong:", h.responder.Page) 432 } 433 434 // Flush client state 435 w.WriteHeader(http.StatusOK) 436 437 if _, ok := h.session.ClientValues[authboss.Session2FA]; ok { 438 t.Error("session 2fa should be cleared") 439 } 440 }) 441 } 442 443 func TestGetValidate(t *testing.T) { 444 t.Parallel() 445 h := testSetup() 446 447 r, w, _ := h.newHTTP("GET") 448 449 if err := h.totp.GetValidate(w, r); err != nil { 450 t.Error(err) 451 } 452 453 if h.responder.Page != PageTOTPValidate { 454 t.Error("page wrong:", h.responder.Page) 455 } 456 } 457 458 func TestPostValidate(t *testing.T) { 459 t.Parallel() 460 461 setupMore := func(h *testHarness) *mocks.User { 462 user := &mocks.User{Email: "test@test.com"} 463 h.storer.Users[user.Email] = user 464 h.setSession(authboss.SessionKey, user.Email) 465 h.session.ClientValues[authboss.SessionKey] = user.Email 466 467 return user 468 } 469 470 t.Run("NoTOTPActivated", func(t *testing.T) { 471 h := testSetup() 472 473 r, w, _ := h.newHTTP("POST") 474 475 setupMore(h) 476 h.loadClientState(w, &r) 477 478 // No session 479 if err := h.totp.PostValidate(w, r); err != nil { 480 t.Fatal(err) 481 } 482 483 if h.responder.Page != PageTOTPValidate { 484 t.Error("page wrong:", h.responder.Page) 485 } 486 if got := h.responder.Data[authboss.DataErr]; got != "totp 2fa not active" { 487 t.Error("data wrong:", got) 488 } 489 }) 490 491 t.Run("WrongCode", func(t *testing.T) { 492 h := testSetup() 493 494 r, w, _ := h.newHTTP("POST") 495 h.loadClientState(w, &r) 496 497 user := setupMore(h) 498 secret := makeSecretKey(h, user.Email) 499 user.TOTPSecretKey = secret 500 h.bodyReader.Return = mocks.Values{Code: "wrong"} 501 502 if err := h.totp.PostValidate(w, r); err != nil { 503 t.Error(err) 504 } 505 506 if h.responder.Page != PageTOTPValidate { 507 t.Error("page wrong:", h.responder.Page) 508 } 509 if got := h.responder.Data[authboss.DataValidation].(map[string][]string); got[FormValueCode][0] != "2fa code was invalid" { 510 t.Error("data wrong:", got) 511 } 512 }) 513 514 t.Run("OkRecovery", func(t *testing.T) { 515 h := testSetup() 516 517 r, w, _ := h.newHTTP("POST") 518 user := setupMore(h) 519 secret := makeSecretKey(h, user.Email) 520 user.TOTPSecretKey = secret 521 522 // Create a single recovery code 523 codes, err := twofactor.GenerateRecoveryCodes() 524 if err != nil { 525 t.Fatal(err) 526 } 527 b, err := bcrypt.GenerateFromPassword([]byte(codes[0]), bcrypt.DefaultCost) 528 if err != nil { 529 t.Fatal(err) 530 } 531 user.RecoveryCodes = string(b) 532 533 // User inputs the only code he has 534 h.bodyReader.Return = mocks.Values{Recovery: codes[0]} 535 536 h.setSession(SessionTOTPPendingPID, user.Email) 537 h.setSession(SessionTOTPSecret, "secret") 538 h.setSession(authboss.SessionHalfAuthKey, "true") 539 h.loadClientState(w, &r) 540 541 if err := h.totp.PostValidate(w, r); err != nil { 542 t.Error(err) 543 } 544 545 // Flush client state 546 w.WriteHeader(http.StatusOK) 547 548 if pid := h.session.ClientValues[authboss.SessionKey]; pid != user.Email { 549 t.Error("session pid should be set:", pid) 550 } 551 if twofa := h.session.ClientValues[authboss.Session2FA]; twofa != "totp" { 552 t.Error("session 2fa should be totp:", twofa) 553 } 554 555 cleared := []string{SessionTOTPSecret, SessionTOTPPendingPID, authboss.SessionHalfAuthKey} 556 for _, c := range cleared { 557 if _, ok := h.session.ClientValues[c]; ok { 558 t.Error(c, "was not cleared") 559 } 560 } 561 562 opts := h.redirector.Options 563 if opts.Code != http.StatusTemporaryRedirect { 564 t.Error("status wrong:", opts.Code) 565 } 566 if !opts.FollowRedirParam { 567 t.Error("it should follow redirects") 568 } 569 if opts.RedirectPath != h.ab.Paths.AuthLoginOK { 570 t.Error("path wrong:", opts.RedirectPath) 571 } 572 }) 573 } 574 575 func makeSecretKey(h *testHarness, email string) string { 576 key, err := totp.Generate(totp.GenerateOpts{ 577 Issuer: h.totp.Modules.TOTP2FAIssuer, 578 AccountName: email, 579 }) 580 if err != nil { 581 panic(err) 582 } 583 584 return key.Secret() 585 }