diff --git a/synapse/config/ratelimiting.py b/synapse/config/ratelimiting.py
index 649f018356..7e6cc5d0ea 100644
--- a/synapse/config/ratelimiting.py
+++ b/synapse/config/ratelimiting.py
@@ -32,6 +32,9 @@ class RatelimitConfig(Config):
rc_login_config = config.get("rc_login", {})
self.rc_login_address = RateLimitConfig(rc_login_config.get("address", {}))
self.rc_login_account = RateLimitConfig(rc_login_config.get("account", {}))
+ self.rc_login_failed_attempts = RateLimitConfig(
+ rc_login_config.get("failed_attempts", {}),
+ )
self.federation_rc_window_size = config["federation_rc_window_size"]
self.federation_rc_sleep_limit = config["federation_rc_sleep_limit"]
@@ -64,6 +67,9 @@ class RatelimitConfig(Config):
# address.
# - one for login that ratelimits login requests based on the account the
# client is attempting to log into.
+ # - one for login that ratelimits login requests based on the account the
+ # client is attempting to log into, based on the amount of failed login
+ # attempts for this account.
#
# The defaults are as shown below.
#
@@ -78,6 +84,9 @@ class RatelimitConfig(Config):
# account:
# per_second: 0.17
# burst_count: 3
+ # failed_attempts:
+ # per_second: 0.17
+ # burst_count: 3
# The federation window size in milliseconds
#
diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py
index 74f3790f25..caad9ae2dd 100644
--- a/synapse/handlers/auth.py
+++ b/synapse/handlers/auth.py
@@ -101,6 +101,7 @@ class AuthHandler(BaseHandler):
self._supported_login_types = login_types
self._account_ratelimiter = Ratelimiter()
+ self._failed_attempts_ratelimiter = Ratelimiter()
self._clock = self.hs.get_clock()
@@ -729,9 +730,16 @@ class AuthHandler(BaseHandler):
if not known_login_type:
raise SynapseError(400, "Unknown login type %s" % login_type)
- # unknown username or invalid password. We raise a 403 here, but note
- # that if we're doing user-interactive login, it turns all LoginErrors
- # into a 401 anyway.
+ # unknown username or invalid password.
+ self._failed_attempts_ratelimiter.ratelimit(
+ qualified_user_id.lower(), time_now_s=self._clock.time(),
+ rate_hz=self.hs.config.rc_login_failed_attempts.per_second,
+ burst_count=self.hs.config.rc_login_failed_attempts.burst_count,
+ update=True,
+ )
+
+ # We raise a 403 here, but note that if we're doing user-interactive
+ # login, it turns all LoginErrors into a 401 anyway.
raise LoginError(
403, "Invalid password",
errcode=Codes.FORBIDDEN
@@ -956,13 +964,23 @@ class AuthHandler(BaseHandler):
def ratelimit_login_per_account(self, user_id):
"""Checks whether the process must be stopped because of ratelimiting.
+ Checks against two ratelimiters: the generic one for login attempts per
+ account and the one specific to failed attempts.
+
Args:
user_id (unicode): complete @user:id
Raises:
- LimitExceededError if the ratelimiter's login requests count for this
- user is too high too proceed.
+ LimitExceededError if one of the ratelimiters' login requests count
+ for this user is too high too proceed.
"""
+ self._failed_attempts_ratelimiter.ratelimit(
+ user_id.lower(), time_now_s=self._clock.time(),
+ rate_hz=self.hs.config.rc_login_failed_attempts.per_second,
+ burst_count=self.hs.config.rc_login_failed_attempts.burst_count,
+ update=False,
+ )
+
self._account_ratelimiter.ratelimit(
user_id.lower(), time_now_s=self._clock.time(),
rate_hz=self.hs.config.rc_login_account.per_second,
|