diff --git a/hc/accounts/tests/test_login_totp.py b/hc/accounts/tests/test_login_totp.py index 8c70985e..ed0ed767 100644 --- a/hc/accounts/tests/test_login_totp.py +++ b/hc/accounts/tests/test_login_totp.py @@ -1,6 +1,7 @@ import time from unittest.mock import patch +from hc.api.models import TokenBucket from hc.test import BaseTestCase @@ -75,3 +76,11 @@ class LoginTotpTestCase(BaseTestCase): r = self.client.post(self.url, {"code": "000000"}) self.assertContains(r, "The code you entered was incorrect.") + + def test_it_uses_rate_limiting(self): + obj = TokenBucket(value=f"totp-{self.alice.id}") + obj.tokens = 0 + obj.save() + + r = self.client.post(self.url, {"code": "000000"}) + self.assertContains(r, "Too Many Requests") diff --git a/hc/accounts/views.py b/hc/accounts/views.py index 2a541a33..d173ce42 100644 --- a/hc/accounts/views.py +++ b/hc/accounts/views.py @@ -848,6 +848,9 @@ def login_totp(request): totp = pyotp.totp.TOTP(user.profile.totp) if request.method == "POST": + if not TokenBucket.authorize_totp(user): + return render(request, "try_later.html") + form = forms.TotpForm(totp, request.POST) if form.is_valid(): request.session.pop("2fa_user") diff --git a/hc/api/models.py b/hc/api/models.py index 6b8dddff..bfbf3b8f 100644 --- a/hc/api/models.py +++ b/hc/api/models.py @@ -983,3 +983,11 @@ class TokenBucket(models.Model): # 10 sudo attempts per day return TokenBucket.authorize(value, 10, 3600 * 24) + + @staticmethod + def authorize_totp(user): + value = "totp-%d" % user.id + + # 96 attempts per 24 hours + # (or, on average, one attempt per 15 minutes) + return TokenBucket.authorize(value, 96, 3600 * 24)