diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py
index c729364839..fe8177ed4d 100644
--- a/synapse/rest/admin/__init__.py
+++ b/synapse/rest/admin/__init__.py
@@ -257,9 +257,11 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
DeleteRoomStatusByRoomIdRestServlet(hs).register(http_server)
JoinRoomAliasServlet(hs).register(http_server)
VersionServlet(hs).register(http_server)
- UserAdminServlet(hs).register(http_server)
+ if not hs.config.experimental.msc3861.enabled:
+ UserAdminServlet(hs).register(http_server)
UserMembershipRestServlet(hs).register(http_server)
- UserTokenRestServlet(hs).register(http_server)
+ if not hs.config.experimental.msc3861.enabled:
+ UserTokenRestServlet(hs).register(http_server)
UserRestServletV2(hs).register(http_server)
UsersRestServletV2(hs).register(http_server)
UserMediaStatisticsRestServlet(hs).register(http_server)
@@ -274,9 +276,10 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
RoomEventContextServlet(hs).register(http_server)
RateLimitRestServlet(hs).register(http_server)
UsernameAvailableRestServlet(hs).register(http_server)
- ListRegistrationTokensRestServlet(hs).register(http_server)
- NewRegistrationTokenRestServlet(hs).register(http_server)
- RegistrationTokenRestServlet(hs).register(http_server)
+ if not hs.config.experimental.msc3861.enabled:
+ ListRegistrationTokensRestServlet(hs).register(http_server)
+ NewRegistrationTokenRestServlet(hs).register(http_server)
+ RegistrationTokenRestServlet(hs).register(http_server)
DestinationMembershipRestServlet(hs).register(http_server)
DestinationResetConnectionRestServlet(hs).register(http_server)
DestinationRestServlet(hs).register(http_server)
@@ -306,10 +309,12 @@ def register_servlets_for_client_rest_resource(
# The following resources can only be run on the main process.
if hs.config.worker.worker_app is None:
DeactivateAccountRestServlet(hs).register(http_server)
- ResetPasswordRestServlet(hs).register(http_server)
+ if not hs.config.experimental.msc3861.enabled:
+ ResetPasswordRestServlet(hs).register(http_server)
SearchUsersRestServlet(hs).register(http_server)
- UserRegisterServlet(hs).register(http_server)
- AccountValidityRenewServlet(hs).register(http_server)
+ if not hs.config.experimental.msc3861.enabled:
+ UserRegisterServlet(hs).register(http_server)
+ AccountValidityRenewServlet(hs).register(http_server)
# Load the media repo ones if we're using them. Otherwise load the servlets which
# don't need a media repo (typically readonly admin APIs).
diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py
index 932333ae57..407fe9c804 100644
--- a/synapse/rest/admin/users.py
+++ b/synapse/rest/admin/users.py
@@ -71,6 +71,7 @@ class UsersRestServletV2(RestServlet):
self.auth = hs.get_auth()
self.admin_handler = hs.get_admin_handler()
self._msc3866_enabled = hs.config.experimental.msc3866.enabled
+ self._msc3861_enabled = hs.config.experimental.msc3861.enabled
async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
await assert_requester_is_admin(self.auth, request)
@@ -94,7 +95,14 @@ class UsersRestServletV2(RestServlet):
user_id = parse_string(request, "user_id")
name = parse_string(request, "name")
+
guests = parse_boolean(request, "guests", default=True)
+ if self._msc3861_enabled and guests:
+ raise SynapseError(
+ HTTPStatus.BAD_REQUEST,
+ "The guests parameter is not supported when MSC3861 is enabled.",
+ errcode=Codes.INVALID_PARAM,
+ )
deactivated = parse_boolean(request, "deactivated", default=False)
# If support for MSC3866 is not enabled, apply no filtering based on the
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.
diff --git a/synapse/rest/synapse/client/__init__.py b/synapse/rest/synapse/client/__init__.py
index e55924f597..57335fb913 100644
--- a/synapse/rest/synapse/client/__init__.py
+++ b/synapse/rest/synapse/client/__init__.py
@@ -46,6 +46,12 @@ def build_synapse_client_resource_tree(hs: "HomeServer") -> Mapping[str, Resourc
"/_synapse/client/unsubscribe": UnsubscribeResource(hs),
}
+ # Expose the JWKS endpoint if OAuth2 delegation is enabled
+ if hs.config.experimental.msc3861.enabled:
+ from synapse.rest.synapse.client.jwks import JwksResource
+
+ resources["/_synapse/jwks"] = JwksResource(hs)
+
# provider-specific SSO bits. Only load these if they are enabled, since they
# rely on optional dependencies.
if hs.config.oidc.oidc_enabled:
diff --git a/synapse/rest/synapse/client/jwks.py b/synapse/rest/synapse/client/jwks.py
new file mode 100644
index 0000000000..7c0a1223fb
--- /dev/null
+++ b/synapse/rest/synapse/client/jwks.py
@@ -0,0 +1,70 @@
+# Copyright 2022 The Matrix.org Foundation C.I.C.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import logging
+from typing import TYPE_CHECKING, Tuple
+
+from synapse.http.server import DirectServeJsonResource
+from synapse.http.site import SynapseRequest
+from synapse.types import JsonDict
+
+if TYPE_CHECKING:
+ from synapse.server import HomeServer
+
+logger = logging.getLogger(__name__)
+
+
+class JwksResource(DirectServeJsonResource):
+ def __init__(self, hs: "HomeServer"):
+ super().__init__(extract_context=True)
+
+ # Parameters that are allowed to be exposed in the public key.
+ # This is done manually, because authlib's private to public key conversion
+ # is unreliable depending on the version. Instead, we just serialize the private
+ # key and only keep the public parameters.
+ # List from https://www.iana.org/assignments/jose/jose.xhtml#web-key-parameters
+ public_parameters = {
+ "kty",
+ "use",
+ "key_ops",
+ "alg",
+ "kid",
+ "x5u",
+ "x5c",
+ "x5t",
+ "x5t#S256",
+ "crv",
+ "x",
+ "y",
+ "n",
+ "e",
+ "ext",
+ }
+
+ key = hs.config.experimental.msc3861.jwk
+
+ if key is not None:
+ private_key = key.as_dict()
+ public_key = {
+ k: v for k, v in private_key.items() if k in public_parameters
+ }
+ keys = [public_key]
+ else:
+ keys = []
+
+ self.res = {
+ "keys": keys,
+ }
+
+ async def _async_render_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
+ return 200, self.res
diff --git a/synapse/rest/well_known.py b/synapse/rest/well_known.py
index e2174fdfea..b8b4b5379b 100644
--- a/synapse/rest/well_known.py
+++ b/synapse/rest/well_known.py
@@ -44,6 +44,16 @@ class WellKnownBuilder:
"base_url": self._config.registration.default_identity_server
}
+ # We use the MSC3861 values as they are used by multiple MSCs
+ if self._config.experimental.msc3861.enabled:
+ result["org.matrix.msc2965.authentication"] = {
+ "issuer": self._config.experimental.msc3861.issuer
+ }
+ if self._config.experimental.msc3861.account_management_url is not None:
+ result["org.matrix.msc2965.authentication"][
+ "account"
+ ] = self._config.experimental.msc3861.account_management_url
+
if self._config.server.extra_well_known_client_content:
for (
key,
|