You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

97 lines
3.2 KiB

  1. import time
  2. from unittest.mock import patch
  3. from hc.api.models import TokenBucket
  4. from hc.test import BaseTestCase
  5. class LoginTotpTestCase(BaseTestCase):
  6. def setUp(self):
  7. super().setUp()
  8. # This is the user we're trying to authenticate
  9. session = self.client.session
  10. session["2fa_user"] = [self.alice.id, self.alice.email, (time.time()) + 300]
  11. session.save()
  12. self.profile.totp = "0" * 32
  13. self.profile.save()
  14. self.url = "/accounts/login/two_factor/totp/"
  15. self.checks_url = f"/projects/{self.project.code}/checks/"
  16. def test_it_shows_form(self):
  17. r = self.client.get(self.url)
  18. self.assertContains(r, "Please enter the six-digit code")
  19. def test_it_requires_unauthenticated_user(self):
  20. self.client.login(username="[email protected]", password="password")
  21. r = self.client.get(self.url)
  22. self.assertEqual(r.status_code, 400)
  23. def test_it_requires_totp_secret(self):
  24. self.profile.totp = None
  25. self.profile.save()
  26. r = self.client.get(self.url)
  27. self.assertEqual(r.status_code, 400)
  28. def test_it_rejects_changed_email(self):
  29. session = self.client.session
  30. session["2fa_user"] = [self.alice.id, "[email protected]", int(time.time())]
  31. session.save()
  32. r = self.client.get(self.url)
  33. self.assertEqual(r.status_code, 400)
  34. def test_it_rejects_old_timestamp(self):
  35. session = self.client.session
  36. session["2fa_user"] = [self.alice.id, self.alice.email, int(time.time()) - 310]
  37. session.save()
  38. r = self.client.get(self.url)
  39. self.assertRedirects(r, "/accounts/login/")
  40. @patch("hc.accounts.views.pyotp.totp.TOTP")
  41. def test_it_logs_in(self, mock_TOTP):
  42. mock_TOTP.return_value.verify.return_value = True
  43. r = self.client.post(self.url, {"code": "000000"})
  44. self.assertRedirects(r, self.checks_url)
  45. self.assertNotIn("2fa_user_id", self.client.session)
  46. @patch("hc.accounts.views.pyotp.totp.TOTP")
  47. def test_it_redirects_after_login(self, mock_TOTP):
  48. mock_TOTP.return_value.verify.return_value = True
  49. url = self.url + "?next=" + self.channels_url
  50. r = self.client.post(url, {"code": "000000"})
  51. self.assertRedirects(r, self.channels_url)
  52. @patch("hc.accounts.views.pyotp.totp.TOTP")
  53. def test_it_handles_authentication_failure(self, mock_TOTP):
  54. mock_TOTP.return_value.verify.return_value = False
  55. r = self.client.post(self.url, {"code": "000000"})
  56. self.assertContains(r, "The code you entered was incorrect.")
  57. def test_it_uses_rate_limiting(self):
  58. obj = TokenBucket(value=f"totp-{self.alice.id}")
  59. obj.tokens = 0
  60. obj.save()
  61. r = self.client.post(self.url, {"code": "000000"})
  62. self.assertContains(r, "Too Many Requests")
  63. @patch("hc.accounts.views.pyotp.totp.TOTP")
  64. def test_it_rejects_used_code(self, mock_TOTP):
  65. mock_TOTP.return_value.verify.return_value = True
  66. obj = TokenBucket(value=f"totpc-{self.alice.id}-000000")
  67. obj.tokens = 0
  68. obj.save()
  69. r = self.client.post(self.url, {"code": "000000"})
  70. self.assertContains(r, "Too Many Requests")