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

Commit

Permalink
Honour AS ratelimit settings for /login requests (#8920)
Browse files Browse the repository at this point in the history
Fixes #8846.
  • Loading branch information
erikjohnston authored Dec 11, 2020
1 parent 3af0672 commit a8eceb0
Show file tree
Hide file tree
Showing 4 changed files with 27 additions and 10 deletions.
1 change: 1 addition & 0 deletions changelog.d/8920.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix login API to not ratelimit application services that have ratelimiting disabled.
4 changes: 3 additions & 1 deletion synapse/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
MissingClientTokenError,
)
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS
from synapse.appservice import ApplicationService
from synapse.events import EventBase
from synapse.http.site import SynapseRequest
from synapse.logging import opentracing as opentracing
from synapse.storage.databases.main.registration import TokenLookupResult
from synapse.types import StateMap, UserID
Expand Down Expand Up @@ -474,7 +476,7 @@ def _verify_expiry(self, caveat):
now = self.hs.get_clock().time_msec()
return now < expiry

def get_appservice_by_req(self, request):
def get_appservice_by_req(self, request: SynapseRequest) -> ApplicationService:
token = self.get_access_token_from_request(request)
service = self.store.get_app_service_by_token(token)
if not service:
Expand Down
7 changes: 4 additions & 3 deletions synapse/handlers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from typing import (
TYPE_CHECKING,
Any,
Awaitable,
Callable,
Dict,
Iterable,
Expand Down Expand Up @@ -861,7 +862,7 @@ def get_supported_login_types(self) -> Iterable[str]:

async def validate_login(
self, login_submission: Dict[str, Any], ratelimit: bool = False,
) -> Tuple[str, Optional[Callable[[Dict[str, str]], None]]]:
) -> Tuple[str, Optional[Callable[[Dict[str, str]], Awaitable[None]]]]:
"""Authenticates the user for the /login API
Also used by the user-interactive auth flow to validate auth types which don't
Expand Down Expand Up @@ -1004,7 +1005,7 @@ async def validate_login(

async def _validate_userid_login(
self, username: str, login_submission: Dict[str, Any],
) -> Tuple[str, Optional[Callable[[Dict[str, str]], None]]]:
) -> Tuple[str, Optional[Callable[[Dict[str, str]], Awaitable[None]]]]:
"""Helper for validate_login
Handles login, once we've mapped 3pids onto userids
Expand Down Expand Up @@ -1082,7 +1083,7 @@ async def _validate_userid_login(

async def check_password_provider_3pid(
self, medium: str, address: str, password: str
) -> Tuple[Optional[str], Optional[Callable[[Dict[str, str]], None]]]:
) -> Tuple[Optional[str], Optional[Callable[[Dict[str, str]], Awaitable[None]]]]:
"""Check if a password provider is able to validate a thirdparty login
Args:
Expand Down
25 changes: 19 additions & 6 deletions synapse/rest/client/v1/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# limitations under the License.

import logging
from typing import Awaitable, Callable, Dict, Optional
from typing import TYPE_CHECKING, Awaitable, Callable, Dict, Optional

from synapse.api.errors import Codes, LoginError, SynapseError
from synapse.api.ratelimiting import Ratelimiter
Expand All @@ -30,6 +30,9 @@
from synapse.rest.well_known import WellKnownBuilder
from synapse.types import JsonDict, UserID

if TYPE_CHECKING:
from synapse.server import HomeServer

logger = logging.getLogger(__name__)


Expand All @@ -42,7 +45,7 @@ class LoginRestServlet(RestServlet):
JWT_TYPE_DEPRECATED = "m.login.jwt"
APPSERVICE_TYPE = "uk.half-shot.msc2778.login.application_service"

def __init__(self, hs):
def __init__(self, hs: "HomeServer"):
super().__init__()
self.hs = hs

Expand Down Expand Up @@ -105,22 +108,27 @@ def on_GET(self, request: SynapseRequest):
return 200, {"flows": flows}

async def on_POST(self, request: SynapseRequest):
self._address_ratelimiter.ratelimit(request.getClientIP())

login_submission = parse_json_object_from_request(request)

try:
if login_submission["type"] == LoginRestServlet.APPSERVICE_TYPE:
appservice = self.auth.get_appservice_by_req(request)

if appservice.is_rate_limited():
self._address_ratelimiter.ratelimit(request.getClientIP())

result = await self._do_appservice_login(login_submission, appservice)
elif self.jwt_enabled and (
login_submission["type"] == LoginRestServlet.JWT_TYPE
or login_submission["type"] == LoginRestServlet.JWT_TYPE_DEPRECATED
):
self._address_ratelimiter.ratelimit(request.getClientIP())
result = await self._do_jwt_login(login_submission)
elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE:
self._address_ratelimiter.ratelimit(request.getClientIP())
result = await self._do_token_login(login_submission)
else:
self._address_ratelimiter.ratelimit(request.getClientIP())
result = await self._do_other_login(login_submission)
except KeyError:
raise SynapseError(400, "Missing JSON keys.")
Expand Down Expand Up @@ -159,7 +167,9 @@ async def _do_appservice_login(
if not appservice.is_interested_in_user(qualified_user_id):
raise LoginError(403, "Invalid access_token", errcode=Codes.FORBIDDEN)

return await self._complete_login(qualified_user_id, login_submission)
return await self._complete_login(
qualified_user_id, login_submission, ratelimit=appservice.is_rate_limited()
)

async def _do_other_login(self, login_submission: JsonDict) -> Dict[str, str]:
"""Handle non-token/saml/jwt logins
Expand Down Expand Up @@ -194,6 +204,7 @@ async def _complete_login(
login_submission: JsonDict,
callback: Optional[Callable[[Dict[str, str]], Awaitable[None]]] = None,
create_non_existent_users: bool = False,
ratelimit: bool = True,
) -> Dict[str, str]:
"""Called when we've successfully authed the user and now need to
actually login them in (e.g. create devices). This gets called on
Expand All @@ -208,6 +219,7 @@ async def _complete_login(
callback: Callback function to run after login.
create_non_existent_users: Whether to create the user if they don't
exist. Defaults to False.
ratelimit: Whether to ratelimit the login request.
Returns:
result: Dictionary of account information after successful login.
Expand All @@ -216,7 +228,8 @@ async def _complete_login(
# 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())
if ratelimit:
self._account_ratelimiter.ratelimit(user_id.lower())

if create_non_existent_users:
canonical_uid = await self.auth_handler.check_user_exists(user_id)
Expand Down

0 comments on commit a8eceb0

Please sign in to comment.