Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Commit

Permalink
Only do rc_login ratelimiting on succesful login.
Browse files Browse the repository at this point in the history
We were doing this in a number of places which meant that some login
code paths incremented the counter multiple times.

It was also applying ratelimiting to UIA endpoints, which was probably
not intentional.

In particular, some custom auth modules were calling
`check_user_exists`, which incremented the counters, meaning that people
would fail to login sometimes.
  • Loading branch information
erikjohnston committed Nov 6, 2019
1 parent 4086002 commit 541f1b9
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 72 deletions.
55 changes: 1 addition & 54 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
SynapseError,
UserDeactivatedError,
)
from synapse.api.ratelimiting import Ratelimiter
from synapse.handlers.ui_auth import INTERACTIVE_AUTH_CHECKERS
from synapse.handlers.ui_auth.checkers import UserInteractiveAuthChecker
from synapse.logging.context import defer_to_thread
Expand Down Expand Up @@ -102,9 +101,6 @@ def __init__(self, hs):
login_types.append(t)
self._supported_login_types = login_types

self._account_ratelimiter = Ratelimiter()
self._failed_attempts_ratelimiter = Ratelimiter()

self._clock = self.hs.get_clock()

@defer.inlineCallbacks
Expand Down Expand Up @@ -501,11 +497,8 @@ def check_user_exists(self, user_id):
multiple matches
Raises:
LimitExceededError if the ratelimiter's login requests count for this
user is too high too proceed.
UserDeactivatedError if a user is found but is deactivated.
"""
self.ratelimit_login_per_account(user_id)
res = yield self._find_user_id_and_pwd_hash(user_id)
if res is not None:
return res[0]
Expand Down Expand Up @@ -572,17 +565,13 @@ def validate_login(self, username, login_submission):
StoreError if there was a problem accessing the database
SynapseError if there was a problem with the request
LoginError if there was an authentication problem.
LimitExceededError if the ratelimiter's login requests count for this
user is too high too proceed.
"""

if username.startswith("@"):
qualified_user_id = username
else:
qualified_user_id = UserID(username, self.hs.hostname).to_string()

self.ratelimit_login_per_account(qualified_user_id)

login_type = login_submission.get("type")
known_login_type = False

Expand Down Expand Up @@ -650,15 +639,6 @@ def validate_login(self, username, login_submission):
if not known_login_type:
raise SynapseError(400, "Unknown login type %s" % login_type)

# 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)
Expand Down Expand Up @@ -710,10 +690,6 @@ def _check_local_password(self, user_id, password):
Returns:
Deferred[unicode] the canonical_user_id, or Deferred[None] if
unknown user/bad password
Raises:
LimitExceededError if the ratelimiter's login requests count for this
user is too high too proceed.
"""
lookupres = yield self._find_user_id_and_pwd_hash(user_id)
if not lookupres:
Expand Down Expand Up @@ -742,7 +718,7 @@ def validate_short_term_login_token_and_get_user_id(self, login_token):
auth_api.validate_macaroon(macaroon, "login", user_id)
except Exception:
raise AuthError(403, "Invalid token", errcode=Codes.FORBIDDEN)
self.ratelimit_login_per_account(user_id)

yield self.auth.check_auth_blocking(user_id)
return user_id

Expand Down Expand Up @@ -912,35 +888,6 @@ def _do_validate_hash():
else:
return defer.succeed(False)

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 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,
burst_count=self.hs.config.rc_login_account.burst_count,
update=True,
)


@attr.s
class MacaroonGenerator(object):
Expand Down
111 changes: 93 additions & 18 deletions synapse/rest/client/v1/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,11 @@ def __init__(self, hs):
self.auth_handler = self.hs.get_auth_handler()
self.registration_handler = hs.get_registration_handler()
self.handlers = hs.get_handlers()
self._clock = hs.get_clock()
self._well_known_builder = WellKnownBuilder(hs)
self._address_ratelimiter = Ratelimiter()
self._account_ratelimiter = Ratelimiter()
self._failed_attempts_ratelimiter = Ratelimiter()

def on_GET(self, request):
flows = []
Expand Down Expand Up @@ -202,6 +205,16 @@ def _do_other_login(self, login_submission):
# (See add_threepid in synapse/handlers/auth.py)
address = address.lower()

# We also apply account rate limiting using the 3PID as a key, as
# otherwise using 3PID bypasses the ratelimiting based on user ID.
self._failed_attempts_ratelimiter.ratelimit(
(medium, address),
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,
)

# Check for login providers that support 3pid login types
(
canonical_user_id,
Expand All @@ -211,7 +224,8 @@ def _do_other_login(self, login_submission):
)
if canonical_user_id:
# Authentication through password provider and 3pid succeeded
result = yield self._register_device_with_callback(

result = yield self._complete_login(
canonical_user_id, login_submission, callback_3pid
)
return result
Expand All @@ -225,6 +239,21 @@ def _do_other_login(self, login_submission):
logger.warning(
"unknown 3pid identifier medium %s, address %r", medium, address
)
# We mark that we've failed to log in here, as
# `check_password_provider_3pid` might have returned `None` due
# to an incorrect password, rather than the account not
# existing.
#
# If it returned None but the 3PID was bound then we won't hit
# this code path, which is fine as then the per-user ratelimit
# will kick in below.
self._failed_attempts_ratelimiter.can_do_action(
(medium, address),
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,
)
raise LoginError(403, "", errcode=Codes.FORBIDDEN)

identifier = {"type": "m.id.user", "user": user_id}
Expand All @@ -236,29 +265,84 @@ def _do_other_login(self, login_submission):
if "user" not in identifier:
raise SynapseError(400, "User identifier is missing 'user' key")

canonical_user_id, callback = yield self.auth_handler.validate_login(
identifier["user"], login_submission
if identifier["user"].startswith("@"):
qualified_user_id = identifier["user"]
else:
qualified_user_id = UserID(identifier["user"], self.hs.hostname).to_string()

# Check if we've hit the failed ratelimit (but don't update it)
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=False,
)

result = yield self._register_device_with_callback(
try:
canonical_user_id, callback = yield self.auth_handler.validate_login(
identifier["user"], login_submission
)
except LoginError:
# The user has failed to log in, so we need to update the rate
# limiter. Using `can_do_action` avoids us raising a ratelimit
# exception and masking the LoginError. The actual ratelimiting
# should have happened above.
self._failed_attempts_ratelimiter.can_do_action(
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,
)
raise

result = yield self._complete_login(
canonical_user_id, login_submission, callback
)
return result

@defer.inlineCallbacks
def _register_device_with_callback(self, user_id, login_submission, callback=None):
""" Registers a device with a given user_id. Optionally run a callback
function after registration has completed.
def _complete_login(
self, user_id, login_submission, callback=None, create_non_existant_users=False
):
"""Called when we've successfully authed the user and now need to
actually login them in (e.g. create devices). This gets called on
all succesful logins.
Applies the ratelimiting for succesful login attempts against an
account.
Args:
user_id (str): ID of the user to register.
login_submission (dict): Dictionary of login information.
callback (func|None): Callback function to run after registration.
create_non_existant_users (bool): Whether to create the user if
they don't exist. Defaults to False.
Returns:
result (Dict[str,str]): Dictionary of account information after
successful registration.
"""

# Before we actually log them in we check if they've already logged in
# too often. This happens here rather than before as we don't
# necessarily know the user before now.
self._account_ratelimiter.ratelimit(
user_id.lower(),
time_now_s=self._clock.time(),
rate_hz=self.hs.config.rc_login_account.per_second,
burst_count=self.hs.config.rc_login_account.burst_count,
update=True,
)

if create_non_existant_users:
user_id = yield self.auth_handler.check_user_exists(user_id)
if not user_id:
user_id = yield self.registration_handler.register_user(
localpart=UserID.from_string(user_id).localpart
)

device_id = login_submission.get("device_id")
initial_display_name = login_submission.get("initial_device_display_name")
device_id, access_token = yield self.registration_handler.register_device(
Expand All @@ -285,7 +369,7 @@ def do_token_login(self, login_submission):
token
)

result = yield self._register_device_with_callback(user_id, login_submission)
result = yield self._complete_login(user_id, login_submission)
return result

@defer.inlineCallbacks
Expand Down Expand Up @@ -313,16 +397,7 @@ def do_jwt_login(self, login_submission):
raise LoginError(401, "Invalid JWT", errcode=Codes.UNAUTHORIZED)

user_id = UserID(user, self.hs.hostname).to_string()

registered_user_id = yield self.auth_handler.check_user_exists(user_id)
if not registered_user_id:
registered_user_id = yield self.registration_handler.register_user(
localpart=user
)

result = yield self._register_device_with_callback(
registered_user_id, login_submission
)
result = yield self._complete_login(user_id, login_submission)
return result


Expand Down

0 comments on commit 541f1b9

Please sign in to comment.