diff --git a/synapse/api/auth/base.py b/synapse/api/auth/base.py
index 240f2b90de..9321d6f186 100644
--- a/synapse/api/auth/base.py
+++ b/synapse/api/auth/base.py
@@ -14,6 +14,8 @@
import logging
from typing import TYPE_CHECKING, Optional, Tuple
+from netaddr import IPAddress
+
from twisted.web.server import Request
from synapse import event_auth
@@ -26,7 +28,8 @@ from synapse.api.errors import (
)
from synapse.appservice import ApplicationService
from synapse.logging.opentracing import trace
-from synapse.types import Requester
+from synapse.types import Requester, create_requester
+from synapse.util.cancellation import cancellable
if TYPE_CHECKING:
from synapse.server import HomeServer
@@ -271,3 +274,78 @@ class BaseAuth:
raise MissingClientTokenError()
return query_params[0].decode("ascii")
+
+ @cancellable
+ async def get_appservice_user(
+ self, request: Request, access_token: str
+ ) -> Optional[Requester]:
+ """
+ Given a request, reads the request parameters to determine:
+ - whether it's an application service that's making this request
+ - what user the application service should be treated as controlling
+ (the user_id URI parameter allows an application service to masquerade
+ any applicable user in its namespace)
+ - what device the application service should be treated as controlling
+ (the device_id[^1] URI parameter allows an application service to masquerade
+ as any device that exists for the relevant user)
+
+ [^1] Unstable and provided by MSC3202.
+ Must use `org.matrix.msc3202.device_id` in place of `device_id` for now.
+
+ Returns:
+ the application service `Requester` of that request
+
+ Postconditions:
+ - The `app_service` field in the returned `Requester` is set
+ - The `user_id` field in the returned `Requester` is either the application
+ service sender or the controlled user set by the `user_id` URI parameter
+ - The returned application service is permitted to control the returned user ID.
+ - The returned device ID, if present, has been checked to be a valid device ID
+ for the returned user ID.
+ """
+ DEVICE_ID_ARG_NAME = b"org.matrix.msc3202.device_id"
+
+ app_service = self.store.get_app_service_by_token(access_token)
+ if app_service is None:
+ return None
+
+ if app_service.ip_range_whitelist:
+ ip_address = IPAddress(request.getClientAddress().host)
+ if ip_address not in app_service.ip_range_whitelist:
+ return None
+
+ # This will always be set by the time Twisted calls us.
+ assert request.args is not None
+
+ if b"user_id" in request.args:
+ effective_user_id = request.args[b"user_id"][0].decode("utf8")
+ await self.validate_appservice_can_control_user_id(
+ app_service, effective_user_id
+ )
+ else:
+ effective_user_id = app_service.sender
+
+ effective_device_id: Optional[str] = None
+
+ if (
+ self.hs.config.experimental.msc3202_device_masquerading_enabled
+ and DEVICE_ID_ARG_NAME in request.args
+ ):
+ effective_device_id = request.args[DEVICE_ID_ARG_NAME][0].decode("utf8")
+ # We only just set this so it can't be None!
+ assert effective_device_id is not None
+ device_opt = await self.store.get_device(
+ effective_user_id, effective_device_id
+ )
+ if device_opt is None:
+ # For now, use 400 M_EXCLUSIVE if the device doesn't exist.
+ # This is an open thread of discussion on MSC3202 as of 2021-12-09.
+ raise AuthError(
+ 400,
+ f"Application service trying to use a device that doesn't exist ('{effective_device_id}' for {effective_user_id})",
+ Codes.EXCLUSIVE,
+ )
+
+ return create_requester(
+ effective_user_id, app_service=app_service, device_id=effective_device_id
+ )
diff --git a/synapse/api/auth/internal.py b/synapse/api/auth/internal.py
index 813d537e53..e2ae198b19 100644
--- a/synapse/api/auth/internal.py
+++ b/synapse/api/auth/internal.py
@@ -12,12 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
-from typing import TYPE_CHECKING, Optional
+from typing import TYPE_CHECKING
import pymacaroons
-from netaddr import IPAddress
-
-from twisted.web.server import Request
from synapse.api.errors import (
AuthError,
@@ -122,7 +119,7 @@ class InternalAuth(BaseAuth):
access_token = self.get_access_token_from_request(request)
# First check if it could be a request from an appservice
- requester = await self._get_appservice_user(request)
+ requester = await self.get_appservice_user(request, access_token)
if not requester:
# If not, it should be from a regular user
requester = await self.get_user_by_access_token(
@@ -189,81 +186,6 @@ class InternalAuth(BaseAuth):
except KeyError:
raise MissingClientTokenError()
- @cancellable
- async def _get_appservice_user(self, request: Request) -> Optional[Requester]:
- """
- Given a request, reads the request parameters to determine:
- - whether it's an application service that's making this request
- - what user the application service should be treated as controlling
- (the user_id URI parameter allows an application service to masquerade
- any applicable user in its namespace)
- - what device the application service should be treated as controlling
- (the device_id[^1] URI parameter allows an application service to masquerade
- as any device that exists for the relevant user)
-
- [^1] Unstable and provided by MSC3202.
- Must use `org.matrix.msc3202.device_id` in place of `device_id` for now.
-
- Returns:
- the application service `Requester` of that request
-
- Postconditions:
- - The `app_service` field in the returned `Requester` is set
- - The `user_id` field in the returned `Requester` is either the application
- service sender or the controlled user set by the `user_id` URI parameter
- - The returned application service is permitted to control the returned user ID.
- - The returned device ID, if present, has been checked to be a valid device ID
- for the returned user ID.
- """
- DEVICE_ID_ARG_NAME = b"org.matrix.msc3202.device_id"
-
- app_service = self.store.get_app_service_by_token(
- self.get_access_token_from_request(request)
- )
- if app_service is None:
- return None
-
- if app_service.ip_range_whitelist:
- ip_address = IPAddress(request.getClientAddress().host)
- if ip_address not in app_service.ip_range_whitelist:
- return None
-
- # This will always be set by the time Twisted calls us.
- assert request.args is not None
-
- if b"user_id" in request.args:
- effective_user_id = request.args[b"user_id"][0].decode("utf8")
- await self.validate_appservice_can_control_user_id(
- app_service, effective_user_id
- )
- else:
- effective_user_id = app_service.sender
-
- effective_device_id: Optional[str] = None
-
- if (
- self.hs.config.experimental.msc3202_device_masquerading_enabled
- and DEVICE_ID_ARG_NAME in request.args
- ):
- effective_device_id = request.args[DEVICE_ID_ARG_NAME][0].decode("utf8")
- # We only just set this so it can't be None!
- assert effective_device_id is not None
- device_opt = await self.store.get_device(
- effective_user_id, effective_device_id
- )
- if device_opt is None:
- # For now, use 400 M_EXCLUSIVE if the device doesn't exist.
- # This is an open thread of discussion on MSC3202 as of 2021-12-09.
- raise AuthError(
- 400,
- f"Application service trying to use a device that doesn't exist ('{effective_device_id}' for {effective_user_id})",
- Codes.EXCLUSIVE,
- )
-
- return create_requester(
- effective_user_id, app_service=app_service, device_id=effective_device_id
- )
-
async def get_user_by_access_token(
self,
token: str,
diff --git a/synapse/api/auth/msc3861_delegated.py b/synapse/api/auth/msc3861_delegated.py
index a84b7730b3..b84dce2563 100644
--- a/synapse/api/auth/msc3861_delegated.py
+++ b/synapse/api/auth/msc3861_delegated.py
@@ -162,14 +162,19 @@ class MSC3861DelegatedAuth(BaseAuth):
) -> Requester:
access_token = self.get_access_token_from_request(request)
- # TODO: we probably want to assert the allow_guest inside this call so that we don't provision the user if they don't have enough permission:
- requester = await self.get_user_by_access_token(access_token, allow_expired)
+ requester = await self.get_appservice_user(request, access_token)
+ if not requester:
+ # TODO: we probably want to assert the allow_guest inside this call
+ # so that we don't provision the user if they don't have enough permission:
+ requester = await self.get_user_by_access_token(access_token, allow_expired)
if not allow_guest and requester.is_guest:
raise OAuthInsufficientScopeError(
["urn:matrix:org.matrix.msc2967.client:api:*"]
)
+ request.requester = requester
+
return requester
async def get_user_by_access_token(
diff --git a/synapse/rest/client/register.py b/synapse/rest/client/register.py
index f8fb0e1dee..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]]:
@@ -956,6 +1024,7 @@ 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:
diff --git a/tests/handlers/test_oauth_delegation.py b/tests/handlers/test_oauth_delegation.py
index 081fef51ec..e53020a58a 100644
--- a/tests/handlers/test_oauth_delegation.py
+++ b/tests/handlers/test_oauth_delegation.py
@@ -527,8 +527,8 @@ class MSC3861OAuthDelegation(HomeserverTestCase):
self.expect_unrecognized(
"GET", "/_matrix/client/v1/register/m.login.registration_token/validity"
)
- self.expect_unrecognized("POST", "/_matrix/client/v3/register")
- self.expect_unrecognized("GET", "/_matrix/client/v3/register")
+ # This is still available for AS registrations
+ # self.expect_unrecognized("POST", "/_matrix/client/v3/register")
self.expect_unrecognized("GET", "/_matrix/client/v3/register/available")
self.expect_unrecognized(
"POST", "/_matrix/client/v3/register/email/requestToken"
|