diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py
index 3d0c55daa0..679ab9f266 100644
--- a/synapse/rest/client/account.py
+++ b/synapse/rest/client/account.py
@@ -27,6 +27,7 @@ from synapse.api.constants import LoginType
from synapse.api.errors import (
Codes,
InteractiveAuthIncompleteError,
+ NotFoundError,
SynapseError,
ThreepidValidationError,
)
@@ -600,6 +601,9 @@ class ThreepidRestServlet(RestServlet):
# ThreePidBindRestServelet.PostBody with an `alias_generator` to handle
# `threePidCreds` versus `three_pid_creds`.
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
+ if self.hs.config.experimental.msc3861.enabled:
+ raise NotFoundError(errcode=Codes.UNRECOGNIZED)
+
if not self.hs.config.registration.enable_3pid_changes:
raise SynapseError(
400, "3PID changes are disabled on this server", Codes.FORBIDDEN
@@ -890,19 +894,21 @@ class AccountStatusRestServlet(RestServlet):
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
if hs.config.worker.worker_app is None:
- EmailPasswordRequestTokenRestServlet(hs).register(http_server)
- PasswordRestServlet(hs).register(http_server)
- DeactivateAccountRestServlet(hs).register(http_server)
- EmailThreepidRequestTokenRestServlet(hs).register(http_server)
- MsisdnThreepidRequestTokenRestServlet(hs).register(http_server)
- AddThreepidEmailSubmitTokenServlet(hs).register(http_server)
- AddThreepidMsisdnSubmitTokenServlet(hs).register(http_server)
+ if not hs.config.experimental.msc3861.enabled:
+ EmailPasswordRequestTokenRestServlet(hs).register(http_server)
+ DeactivateAccountRestServlet(hs).register(http_server)
+ PasswordRestServlet(hs).register(http_server)
+ EmailThreepidRequestTokenRestServlet(hs).register(http_server)
+ MsisdnThreepidRequestTokenRestServlet(hs).register(http_server)
+ AddThreepidEmailSubmitTokenServlet(hs).register(http_server)
+ AddThreepidMsisdnSubmitTokenServlet(hs).register(http_server)
ThreepidRestServlet(hs).register(http_server)
if hs.config.worker.worker_app is None:
- ThreepidAddRestServlet(hs).register(http_server)
ThreepidBindRestServlet(hs).register(http_server)
ThreepidUnbindRestServlet(hs).register(http_server)
- ThreepidDeleteRestServlet(hs).register(http_server)
+ if not hs.config.experimental.msc3861.enabled:
+ ThreepidAddRestServlet(hs).register(http_server)
+ ThreepidDeleteRestServlet(hs).register(http_server)
WhoamiRestServlet(hs).register(http_server)
if hs.config.worker.worker_app is None and hs.config.experimental.msc3720_enabled:
diff --git a/synapse/rest/client/capabilities.py b/synapse/rest/client/capabilities.py
index 0dbf8f6818..3154b9f77e 100644
--- a/synapse/rest/client/capabilities.py
+++ b/synapse/rest/client/capabilities.py
@@ -65,6 +65,9 @@ class CapabilitiesRestServlet(RestServlet):
"m.3pid_changes": {
"enabled": self.config.registration.enable_3pid_changes
},
+ "m.get_login_token": {
+ "enabled": self.config.auth.login_via_existing_enabled,
+ },
}
}
diff --git a/synapse/rest/client/devices.py b/synapse/rest/client/devices.py
index e97d0bf475..38dff9703f 100644
--- a/synapse/rest/client/devices.py
+++ b/synapse/rest/client/devices.py
@@ -19,7 +19,7 @@ from typing import TYPE_CHECKING, List, Optional, Tuple
from pydantic import Extra, StrictStr
from synapse.api import errors
-from synapse.api.errors import NotFoundError
+from synapse.api.errors import NotFoundError, UnrecognizedRequestError
from synapse.handlers.device import DeviceHandler
from synapse.http.server import HttpServer
from synapse.http.servlet import (
@@ -135,6 +135,7 @@ class DeviceRestServlet(RestServlet):
self.device_handler = handler
self.auth_handler = hs.get_auth_handler()
self._msc3852_enabled = hs.config.experimental.msc3852_enabled
+ self._msc3861_oauth_delegation_enabled = hs.config.experimental.msc3861.enabled
async def on_GET(
self, request: SynapseRequest, device_id: str
@@ -166,6 +167,9 @@ class DeviceRestServlet(RestServlet):
async def on_DELETE(
self, request: SynapseRequest, device_id: str
) -> Tuple[int, JsonDict]:
+ if self._msc3861_oauth_delegation_enabled:
+ raise UnrecognizedRequestError(code=404)
+
requester = await self.auth.get_user_by_req(request)
try:
@@ -344,7 +348,10 @@ class ClaimDehydratedDeviceServlet(RestServlet):
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
- if hs.config.worker.worker_app is None:
+ if (
+ hs.config.worker.worker_app is None
+ and not hs.config.experimental.msc3861.enabled
+ ):
DeleteDevicesRestServlet(hs).register(http_server)
DevicesRestServlet(hs).register(http_server)
if hs.config.worker.worker_app is None:
diff --git a/synapse/rest/client/keys.py b/synapse/rest/client/keys.py
index 413edd8a4d..70b8be1aa2 100644
--- a/synapse/rest/client/keys.py
+++ b/synapse/rest/client/keys.py
@@ -17,9 +17,10 @@
import logging
import re
from collections import Counter
+from http import HTTPStatus
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple
-from synapse.api.errors import InvalidAPICallError, SynapseError
+from synapse.api.errors import Codes, InvalidAPICallError, SynapseError
from synapse.http.server import HttpServer
from synapse.http.servlet import (
RestServlet,
@@ -375,9 +376,29 @@ class SigningKeyUploadServlet(RestServlet):
user_id = requester.user.to_string()
body = parse_json_object_from_request(request)
- if self.hs.config.experimental.msc3967_enabled:
- if await self.e2e_keys_handler.is_cross_signing_set_up_for_user(user_id):
- # If we already have a master key then cross signing is set up and we require UIA to reset
+ is_cross_signing_setup = (
+ await self.e2e_keys_handler.is_cross_signing_set_up_for_user(user_id)
+ )
+
+ # Before MSC3967 we required UIA both when setting up cross signing for the
+ # first time and when resetting the device signing key. With MSC3967 we only
+ # require UIA when resetting cross-signing, and not when setting up the first
+ # time. Because there is no UIA in MSC3861, for now we throw an error if the
+ # user tries to reset the device signing key when MSC3861 is enabled, but allow
+ # first-time setup.
+ if self.hs.config.experimental.msc3861.enabled:
+ # There is no way to reset the device signing key with MSC3861
+ if is_cross_signing_setup:
+ raise SynapseError(
+ HTTPStatus.NOT_IMPLEMENTED,
+ "Resetting cross signing keys is not yet supported with MSC3861",
+ Codes.UNRECOGNIZED,
+ )
+ # But first-time setup is fine
+
+ elif self.hs.config.experimental.msc3967_enabled:
+ # If we already have a master key then cross signing is set up and we require UIA to reset
+ if is_cross_signing_setup:
await self.auth_handler.validate_user_via_ui_auth(
requester,
request,
@@ -387,6 +408,7 @@ class SigningKeyUploadServlet(RestServlet):
can_skip_ui_auth=False,
)
# Otherwise we don't require UIA since we are setting up cross signing for first time
+
else:
# Previous behaviour is to always require UIA but allow it to be skipped
await self.auth_handler.validate_user_via_ui_auth(
diff --git a/synapse/rest/client/login.py b/synapse/rest/client/login.py
index 6ca61ffbd0..6493b00bb8 100644
--- a/synapse/rest/client/login.py
+++ b/synapse/rest/client/login.py
@@ -104,6 +104,9 @@ class LoginRestServlet(RestServlet):
and hs.config.experimental.msc3866.require_approval_for_new_accounts
)
+ # Whether get login token is enabled.
+ self._get_login_token_enabled = hs.config.auth.login_via_existing_enabled
+
self.auth = hs.get_auth()
self.clock = hs.get_clock()
@@ -142,6 +145,9 @@ class LoginRestServlet(RestServlet):
# to SSO.
flows.append({"type": LoginRestServlet.CAS_TYPE})
+ # The login token flow requires m.login.token to be advertised.
+ support_login_token_flow = self._get_login_token_enabled
+
if self.cas_enabled or self.saml2_enabled or self.oidc_enabled:
flows.append(
{
@@ -153,14 +159,23 @@ class LoginRestServlet(RestServlet):
}
)
- # While it's valid for us to advertise this login type generally,
- # synapse currently only gives out these tokens as part of the
- # SSO login flow.
- # Generally we don't want to advertise login flows that clients
- # don't know how to implement, since they (currently) will always
- # fall back to the fallback API if they don't understand one of the
- # login flow types returned.
- flows.append({"type": LoginRestServlet.TOKEN_TYPE})
+ # SSO requires a login token to be generated, so we need to advertise that flow
+ support_login_token_flow = True
+
+ # While it's valid for us to advertise this login type generally,
+ # synapse currently only gives out these tokens as part of the
+ # SSO login flow or as part of login via an existing session.
+ #
+ # Generally we don't want to advertise login flows that clients
+ # don't know how to implement, since they (currently) will always
+ # fall back to the fallback API if they don't understand one of the
+ # login flow types returned.
+ if support_login_token_flow:
+ tokenTypeFlow: Dict[str, Any] = {"type": LoginRestServlet.TOKEN_TYPE}
+ # If the login token flow is enabled advertise the get_login_token flag.
+ if self._get_login_token_enabled:
+ tokenTypeFlow["get_login_token"] = True
+ flows.append(tokenTypeFlow)
flows.extend({"type": t} for t in self.auth_handler.get_supported_login_types())
@@ -633,6 +648,9 @@ class CasTicketServlet(RestServlet):
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
+ if hs.config.experimental.msc3861.enabled:
+ return
+
LoginRestServlet(hs).register(http_server)
if (
hs.config.worker.worker_app is None
diff --git a/synapse/rest/client/login_token_request.py b/synapse/rest/client/login_token_request.py
index 43ea21d5e6..b1629f94a5 100644
--- a/synapse/rest/client/login_token_request.py
+++ b/synapse/rest/client/login_token_request.py
@@ -15,6 +15,7 @@
import logging
from typing import TYPE_CHECKING, Tuple
+from synapse.api.ratelimiting import Ratelimiter
from synapse.http.server import HttpServer
from synapse.http.servlet import RestServlet, parse_json_object_from_request
from synapse.http.site import SynapseRequest
@@ -33,7 +34,7 @@ class LoginTokenRequestServlet(RestServlet):
Request:
- POST /login/token HTTP/1.1
+ POST /login/get_token HTTP/1.1
Content-Type: application/json
{}
@@ -43,30 +44,45 @@ class LoginTokenRequestServlet(RestServlet):
HTTP/1.1 200 OK
{
"login_token": "ABDEFGH",
- "expires_in": 3600,
+ "expires_in_ms": 3600000,
}
"""
- PATTERNS = client_patterns(
- "/org.matrix.msc3882/login/token$", releases=[], v1=False, unstable=True
- )
+ PATTERNS = [
+ *client_patterns(
+ "/login/get_token$", releases=["v1"], v1=False, unstable=False
+ ),
+ # TODO: this is no longer needed once unstable MSC3882 does not need to be supported:
+ *client_patterns(
+ "/org.matrix.msc3882/login/token$", releases=[], v1=False, unstable=True
+ ),
+ ]
def __init__(self, hs: "HomeServer"):
super().__init__()
self.auth = hs.get_auth()
- self.store = hs.get_datastores().main
- self.clock = hs.get_clock()
- self.server_name = hs.config.server.server_name
+ self._main_store = hs.get_datastores().main
self.auth_handler = hs.get_auth_handler()
- self.token_timeout = hs.config.experimental.msc3882_token_timeout
- self.ui_auth = hs.config.experimental.msc3882_ui_auth
+ self.token_timeout = hs.config.auth.login_via_existing_token_timeout
+ self._require_ui_auth = hs.config.auth.login_via_existing_require_ui_auth
+
+ # Ratelimit aggressively to a maxmimum of 1 request per minute.
+ #
+ # This endpoint can be used to spawn additional sessions and could be
+ # abused by a malicious client to create many sessions.
+ self._ratelimiter = Ratelimiter(
+ store=self._main_store,
+ clock=hs.get_clock(),
+ rate_hz=1 / 60,
+ burst_count=1,
+ )
@interactive_auth_handler
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
requester = await self.auth.get_user_by_req(request)
body = parse_json_object_from_request(request)
- if self.ui_auth:
+ if self._require_ui_auth:
await self.auth_handler.validate_user_via_ui_auth(
requester,
request,
@@ -75,9 +91,12 @@ class LoginTokenRequestServlet(RestServlet):
can_skip_ui_auth=False, # Don't allow skipping of UI auth
)
+ # Ensure that this endpoint isn't being used too often. (Ensure this is
+ # done *after* UI auth.)
+ await self._ratelimiter.ratelimit(None, requester.user.to_string().lower())
+
login_token = await self.auth_handler.create_login_token_for_user_id(
user_id=requester.user.to_string(),
- auth_provider_id="org.matrix.msc3882.login_token_request",
duration_ms=self.token_timeout,
)
@@ -85,11 +104,13 @@ class LoginTokenRequestServlet(RestServlet):
200,
{
"login_token": login_token,
+ # TODO: this is no longer needed once unstable MSC3882 does not need to be supported:
"expires_in": self.token_timeout // 1000,
+ "expires_in_ms": self.token_timeout,
},
)
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
- if hs.config.experimental.msc3882_enabled:
+ if hs.config.auth.login_via_existing_enabled:
LoginTokenRequestServlet(hs).register(http_server)
diff --git a/synapse/rest/client/logout.py b/synapse/rest/client/logout.py
index 6d34625ad5..94ad90942f 100644
--- a/synapse/rest/client/logout.py
+++ b/synapse/rest/client/logout.py
@@ -80,5 +80,8 @@ class LogoutAllRestServlet(RestServlet):
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
+ if hs.config.experimental.msc3861.enabled:
+ return
+
LogoutRestServlet(hs).register(http_server)
LogoutAllRestServlet(hs).register(http_server)
diff --git a/synapse/rest/client/register.py b/synapse/rest/client/register.py
index 7f84a17e29..d59669f0b6 100644
--- a/synapse/rest/client/register.py
+++ b/synapse/rest/client/register.py
@@ -869,6 +869,74 @@ class RegisterRestServlet(RestServlet):
return 200, result
+class RegisterAppServiceOnlyRestServlet(RestServlet):
+ """An alternative registration API endpoint that only allows ASes to register
+
+ This replaces the regular /register endpoint if MSC3861. There are two notable
+ differences with the regular /register endpoint:
+ - It only allows the `m.login.application_service` login type
+ - It does not create a device or access token for the just-registered user
+
+ Note that the exact behaviour of this endpoint is not yet finalised. It should be
+ just good enough to make most ASes work.
+ """
+
+ PATTERNS = client_patterns("/register$")
+ CATEGORY = "Registration/login requests"
+
+ def __init__(self, hs: "HomeServer"):
+ super().__init__()
+
+ self.auth = hs.get_auth()
+ self.registration_handler = hs.get_registration_handler()
+ self.ratelimiter = hs.get_registration_ratelimiter()
+
+ @interactive_auth_handler
+ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
+ body = parse_json_object_from_request(request)
+
+ client_addr = request.getClientAddress().host
+
+ await self.ratelimiter.ratelimit(None, client_addr, update=False)
+
+ kind = parse_string(request, "kind", default="user")
+
+ if kind == "guest":
+ raise SynapseError(403, "Guest access is disabled")
+ elif kind != "user":
+ raise UnrecognizedRequestError(
+ f"Do not understand membership kind: {kind}",
+ )
+
+ # Pull out the provided username and do basic sanity checks early since
+ # the auth layer will store these in sessions.
+ desired_username = body.get("username")
+ if not isinstance(desired_username, str) or len(desired_username) > 512:
+ raise SynapseError(400, "Invalid username")
+
+ # Allow only ASes to use this API.
+ if body.get("type") != APP_SERVICE_REGISTRATION_TYPE:
+ raise SynapseError(403, "Non-application service registration type")
+
+ if not self.auth.has_access_token(request):
+ raise SynapseError(
+ 400,
+ "Appservice token must be provided when using a type of m.login.application_service",
+ )
+
+ # XXX we should check that desired_username is valid. Currently
+ # we give appservices carte blanche for any insanity in mxids,
+ # because the IRC bridges rely on being able to register stupid
+ # IDs.
+
+ as_token = self.auth.get_access_token_from_request(request)
+
+ user_id = await self.registration_handler.appservice_register(
+ desired_username, as_token
+ )
+ return 200, {"user_id": user_id}
+
+
def _calculate_registration_flows(
config: HomeServerConfig, auth_handler: AuthHandler
) -> List[List[str]]:
@@ -955,6 +1023,10 @@ def _calculate_registration_flows(
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
+ if hs.config.experimental.msc3861.enabled:
+ RegisterAppServiceOnlyRestServlet(hs).register(http_server)
+ return
+
if hs.config.worker.worker_app is None:
EmailRegisterRequestTokenRestServlet(hs).register(http_server)
MsisdnRegisterRequestTokenRestServlet(hs).register(http_server)
diff --git a/synapse/rest/client/versions.py b/synapse/rest/client/versions.py
index 32df054f56..547bf34df1 100644
--- a/synapse/rest/client/versions.py
+++ b/synapse/rest/client/versions.py
@@ -113,8 +113,8 @@ class VersionsRestServlet(RestServlet):
"fi.mau.msc2815": self.config.experimental.msc2815_enabled,
# Adds a ping endpoint for appservices to check HS->AS connection
"fi.mau.msc2659.stable": True, # TODO: remove when "v1.7" is added above
- # Adds support for login token requests as per MSC3882
- "org.matrix.msc3882": self.config.experimental.msc3882_enabled,
+ # TODO: this is no longer needed once unstable MSC3882 does not need to be supported:
+ "org.matrix.msc3882": self.config.auth.login_via_existing_enabled,
# Adds support for remotely enabling/disabling pushers, as per MSC3881
"org.matrix.msc3881": self.config.experimental.msc3881_enabled,
# Adds support for filtering /messages by event relation.
|