diff --git a/hc/accounts/tests/test_login.py b/hc/accounts/tests/test_login.py index fc966999..b7c27a35 100644 --- a/hc/accounts/tests/test_login.py +++ b/hc/accounts/tests/test_login.py @@ -11,6 +11,16 @@ class LoginTestCase(BaseTestCase): super().setUp() self.checks_url = f"/projects/{self.project.code}/checks/" + def test_it_shows_form(self): + r = self.client.get("/accounts/login/") + self.assertContains(r, "Email Me a Link") + + def test_it_redirects_authenticated_get(self): + self.client.login(username="alice@example.org", password="password") + + r = self.client.get("/accounts/login/") + self.assertRedirects(r, self.checks_url) + def test_it_sends_link(self): form = {"identity": "alice@example.org"} diff --git a/hc/accounts/views.py b/hc/accounts/views.py index e9a9b77d..7a7fba64 100644 --- a/hc/accounts/views.py +++ b/hc/accounts/views.py @@ -164,6 +164,9 @@ def login(request): response.set_cookie("auto-login", "1", max_age=300, httponly=True) return response + if request.user.is_authenticated: + return _redirect_after_login(request) + bad_link = request.session.pop("bad_link", None) ctx = { "page": "login", @@ -868,11 +871,15 @@ def login_totp(request): totp = pyotp.totp.TOTP(user.profile.totp) if request.method == "POST": + # To guard against brute-forcing TOTP codes, we allow + # 96 attempts per user per 24h. if not TokenBucket.authorize_totp_attempt(user): return render(request, "try_later.html") form = forms.TotpForm(totp, request.POST) if form.is_valid(): + # We blacklist an used TOTP code for 90 seconds, + # so an attacker cannot reuse a stolen code. if not TokenBucket.authorize_totp_code(user, form.cleaned_data["code"]): return render(request, "try_later.html")