From 5df6d1be652b46fc6201c9eb2541bf1828f83274 Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Thu, 2 Jul 2026 12:25:11 +0100 Subject: [PATCH] Remove legacy MSC3861 auth delegation support (#19895) Modern 'MAS integration' (as we call it now) is, of course, preserved. Fixes: #19549 --------- Signed-off-by: Olivier 'reivilibre --- changelog.d/19895.removal | 1 + docs/admin_api/account_validity.md | 2 +- docs/admin_api/register_api.md | 3 +- docs/admin_api/user_admin_api.md | 10 +- docs/upgrade.md | 15 +- .../admin_api/registration_tokens.md | 2 +- docs/workers.md | 8 - synapse/api/auth/msc3861_delegated.py | 618 ----------------- synapse/config/auth.py | 8 +- synapse/config/experimental.py | 346 +--------- synapse/config/mas.py | 7 - synapse/config/registration.py | 10 +- synapse/handlers/auth.py | 7 +- synapse/module_api/__init__.py | 4 +- synapse/rest/admin/__init__.py | 25 +- synapse/rest/admin/users.py | 4 +- synapse/rest/client/account.py | 4 +- synapse/rest/client/auth.py | 14 - synapse/rest/client/auth_metadata.py | 23 +- synapse/rest/client/devices.py | 8 +- synapse/rest/client/keys.py | 43 +- synapse/rest/client/login.py | 2 +- synapse/rest/client/logout.py | 2 +- synapse/rest/client/register.py | 4 +- synapse/rest/synapse/client/__init__.py | 5 - synapse/rest/synapse/client/jwks.py | 77 --- synapse/rest/synapse/mas/_base.py | 13 +- synapse/rest/well_known.py | 16 - synapse/server.py | 4 - synapse/storage/databases/main/appservice.py | 2 +- tests/config/test_load.py | 42 -- tests/config/test_oauth_delegation.py | 228 ------- tests/handlers/test_oauth_delegation.py | 629 +----------------- tests/rest/admin/test_jwks.py | 106 --- tests/rest/client/test_auth_metadata.py | 91 +-- tests/rest/client/test_keys.py | 194 +----- tests/rest/client/test_rendezvous.py | 78 +-- tests/rest/synapse/mas/_base.py | 13 +- tests/rest/test_well_known.py | 17 +- 39 files changed, 116 insertions(+), 2569 deletions(-) create mode 100644 changelog.d/19895.removal delete mode 100644 synapse/api/auth/msc3861_delegated.py delete mode 100644 synapse/rest/synapse/client/jwks.py delete mode 100644 tests/rest/admin/test_jwks.py diff --git a/changelog.d/19895.removal b/changelog.d/19895.removal new file mode 100644 index 0000000000..1b39ffb139 --- /dev/null +++ b/changelog.d/19895.removal @@ -0,0 +1 @@ +Remove support for experimental [MSC3861](https://github.com/matrix-org/matrix-spec-proposals/pull/3861) auth delegation, in favour of the stable Matrix Authentication Service integration support. See [the upgrade notes](https://element-hq.github.io/synapse/v1.157/upgrade.html#upgrading-to-v11570). diff --git a/docs/admin_api/account_validity.md b/docs/admin_api/account_validity.md index dfa69e515b..fbe1f985b8 100644 --- a/docs/admin_api/account_validity.md +++ b/docs/admin_api/account_validity.md @@ -1,6 +1,6 @@ # Account validity API -**Note:** This API is disabled when MSC3861 is enabled. [See #15582](https://github.com/matrix-org/synapse/pull/15582) +**Note:** This API is disabled when Matrix Authentication Service integration is enabled. [See #15582](https://github.com/matrix-org/synapse/pull/15582) This API allows a server administrator to manage the validity of an account. To use it, you must enable the account validity feature (under diff --git a/docs/admin_api/register_api.md b/docs/admin_api/register_api.md index e9a235ada5..ffe7d9fa17 100644 --- a/docs/admin_api/register_api.md +++ b/docs/admin_api/register_api.md @@ -1,6 +1,7 @@ # Shared-Secret Registration -**Note:** This API is disabled when MSC3861 is enabled. [See #15582](https://github.com/matrix-org/synapse/pull/15582) +**Note:** This API is disabled when Matrix Authentication Service integration is enabled. +Use the [MAS Admin API](https://element-hq.github.io/matrix-authentication-service/topics/admin-api.html) or [the MAS CLI](https://element-hq.github.io/matrix-authentication-service/reference/cli/manage.html#manage-register-user) instead. This API allows for the creation of users in an administrative and non-interactive way. This is generally used for bootstrapping a Synapse diff --git a/docs/admin_api/user_admin_api.md b/docs/admin_api/user_admin_api.md index 5d5b23cdc4..44d0985697 100644 --- a/docs/admin_api/user_admin_api.md +++ b/docs/admin_api/user_admin_api.md @@ -227,7 +227,7 @@ The following parameters should be set in the URL: - `name` - Is optional and filters to only return users with user ID localparts **or** displaynames that contain this value. - `guests` - string representing a bool - Is optional and if `false` will **exclude** guest users. - Defaults to `true` to include guest users. This parameter is not supported when MSC3861 is enabled. [See #15582](https://github.com/matrix-org/synapse/pull/15582) + Defaults to `true` to include guest users. This parameter is not supported when Matrix Authentication Service integration is enabled. [See #15582](https://github.com/matrix-org/synapse/pull/15582) - `admins` - Optional flag to filter admins. If `true`, only admins are queried. If `false`, admins are excluded from the query. When the flag is absent (the default), **both** admins and non-admins are included in the search results. - `deactivated` - string representing a bool - Is optional and if `true` will **include** deactivated users. @@ -444,7 +444,7 @@ To unsuspend a user, use the same endpoint with a body of: ## Reset password -**Note:** This API is disabled when MSC3861 is enabled. [See #15582](https://github.com/matrix-org/synapse/pull/15582) +**Note:** This API is disabled when Matrix Authentication Service integration is enabled. Use the [MAS Admin API](https://element-hq.github.io/matrix-authentication-service/topics/admin-api.html) or [the MAS CLI](https://element-hq.github.io/matrix-authentication-service/reference/cli/manage.html#manage-set-password) instead. An easy way to make use of this functionality is provided by [Element Admin](https://element.io/en/server-suite/admin) as part of [ESS](https://element.io/en/server-suite) Community and Pro. Changes the password of another user. This will automatically log the user out of all their devices. @@ -469,7 +469,7 @@ The parameter `logout_devices` is optional and defaults to `true`. ## Get whether a user is a server administrator or not -**Note:** This API is disabled when MSC3861 is enabled. [See #15582](https://github.com/matrix-org/synapse/pull/15582) +**Note:** This API is disabled when Matrix Authentication Service integration is enabled. Use the [MAS Admin API](https://element-hq.github.io/matrix-authentication-service/topics/admin-api.html) instead. An easy way to make use of this functionality is provided by [Element Admin](https://element.io/en/server-suite/admin) as part of [ESS](https://element.io/en/server-suite) Community and Pro. The api is: @@ -488,7 +488,7 @@ A response body like the following is returned: ## Change whether a user is a server administrator or not -**Note:** This API is disabled when MSC3861 is enabled. [See #15582](https://github.com/matrix-org/synapse/pull/15582) +**Note:** This API is disabled when Matrix Authentication Service integration is enabled. Use the [MAS Admin API](https://element-hq.github.io/matrix-authentication-service/topics/admin-api.html) or [the MAS CLI](https://element-hq.github.io/matrix-authentication-service/reference/cli/manage.html#manage-promote-admin) instead. An easy way to make use of this functionality is provided by [Element Admin](https://element.io/en/server-suite/admin) as part of [ESS](https://element.io/en/server-suite) Community and Pro. Note that you cannot demote yourself. @@ -910,7 +910,7 @@ delete largest/smallest or newest/oldest files first. ## Login as a user -**Note:** This API is disabled when MSC3861 is enabled. [See #15582](https://github.com/matrix-org/synapse/pull/15582) +**Note:** This API is disabled when Matrix Authentication Service integration is enabled. Use [Personal sessions](https://element-hq.github.io/matrix-authentication-service/topics/authorization.html#personal-sessions-personal-access-tokens) through the [MAS Admin API](https://element-hq.github.io/matrix-authentication-service/topics/admin-api.html) instead. An easy way to make use of this functionality is provided by [Element Admin](https://element.io/en/server-suite/admin) as part of [ESS](https://element.io/en/server-suite) Community and Pro. Get an access token that can be used to authenticate as that user. Useful for when admins wish to do actions on behalf of a user. diff --git a/docs/upgrade.md b/docs/upgrade.md index 44ab342d5a..372e9d147b 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -117,6 +117,18 @@ each upgrade are complete before moving on to the next upgrade, to avoid stacking them up. You can monitor the currently running background updates with [the Admin API](usage/administration/admin_api/background_updates.html#status). +# Upgrading to v1.157.0 + +## MSC3861 Auth Delegation must be migrated to stable Matrix Authentication Service integration + +Support for the deprecated MSC3861 Auth Delegation (`experimental_features.msc3861`) +has been dropped in this version, in favour of the stable Matrix Authentication Service +integration. + +See [the previous upgrade notes](#stable-integration-with-matrix-authentication-service) +and the [`matrix_authentication_service` section in the Configuration Manual](usage/configuration/config_documentation.md#matrix_authentication_service) +for more information. + # Upgrading to v1.152.0 ## Workers which quarantine media must be stream writers @@ -327,7 +339,8 @@ using these metrics. Support for [Matrix Authentication Service (MAS)](https://github.com/element-hq/matrix-authentication-service) is now stable, with a simplified configuration. This stable integration requires MAS 0.20.0 or later. -The existing `experimental_features.msc3861` configuration option is now deprecated and will be removed in Synapse v1.137.0. +The existing `experimental_features.msc3861` configuration option is now deprecated and will be removed in Synapse v1.157.0. +(*Note*: this previously read v1.137.0 but the removal date was missed.) Synapse deployments already using MAS should now use the new configuration options: diff --git a/docs/usage/administration/admin_api/registration_tokens.md b/docs/usage/administration/admin_api/registration_tokens.md index ba95bcf038..c6cc433e35 100644 --- a/docs/usage/administration/admin_api/registration_tokens.md +++ b/docs/usage/administration/admin_api/registration_tokens.md @@ -1,6 +1,6 @@ # Registration Tokens -**Note:** This API is disabled when MSC3861 is enabled. [See #15582](https://github.com/matrix-org/synapse/pull/15582) +**Note:** This API is disabled when Matrix Authentication Service integration is enabled. Use the [MAS Admin API](https://element-hq.github.io/matrix-authentication-service/topics/admin-api.html) or [the MAS CLI](https://element-hq.github.io/matrix-authentication-service/reference/cli/manage.html#manage-issue-user-registration-token) instead. An easy way to make use of this functionality is provided by [Element Admin](https://element.io/en/server-suite/admin) as part of [ESS](https://element.io/en/server-suite) Community and Pro. This API allows you to manage tokens which can be used to authenticate registration requests, as proposed in diff --git a/docs/workers.md b/docs/workers.md index d987ff8980..92b606607c 100644 --- a/docs/workers.md +++ b/docs/workers.md @@ -338,14 +338,6 @@ For multiple workers not handling the SSO endpoints properly, see [#7530](https://github.com/matrix-org/synapse/issues/7530) and [#9427](https://github.com/matrix-org/synapse/issues/9427). -Additionally, when MSC3861 is enabled (`experimental_features.msc3861.enabled` -set to `true`), the following endpoints can be handled by the worker: - - ^/_synapse/admin/v2/users/[^/]+$ - ^/_synapse/admin/v1/username_available$ - ^/_synapse/admin/v1/users/[^/]+/_allow_cross_signing_replacement_without_uia$ - ^/_synapse/admin/v1/users/[^/]+/devices$ - Note that a [HTTP listener](usage/configuration/config_documentation.md#listeners) with `client` and `federation` `resources` must be configured in the [`worker_listeners`](usage/configuration/config_documentation.md#worker_listeners) diff --git a/synapse/api/auth/msc3861_delegated.py b/synapse/api/auth/msc3861_delegated.py deleted file mode 100644 index 3b37f39875..0000000000 --- a/synapse/api/auth/msc3861_delegated.py +++ /dev/null @@ -1,618 +0,0 @@ -# -# This file is licensed under the Affero General Public License (AGPL) version 3. -# -# Copyright 2023 The Matrix.org Foundation. -# Copyright (C) 2023 New Vector, Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# See the GNU Affero General Public License for more details: -# . -# -# Originally licensed under the Apache License, Version 2.0: -# . -# -# [This file includes modifications made by New Vector Limited] -# -# -import logging -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Callable -from urllib.parse import urlencode - -from authlib.oauth2 import ClientAuth -from authlib.oauth2.auth import encode_client_secret_basic, encode_client_secret_post -from authlib.oauth2.rfc7523 import ClientSecretJWT, PrivateKeyJWT, private_key_jwt_sign -from authlib.oauth2.rfc7662 import IntrospectionToken -from authlib.oidc.discovery import OpenIDProviderMetadata, get_well_known_url - -from synapse.api.auth.base import BaseAuth -from synapse.api.errors import ( - AuthError, - HttpResponseException, - InvalidClientTokenError, - SynapseError, - UnrecognizedRequestError, -) -from synapse.http.site import SynapseRequest -from synapse.logging.opentracing import ( - active_span, - force_tracing, - inject_request_headers, - start_active_span, -) -from synapse.metrics import SERVER_NAME_LABEL -from synapse.synapse_rust.http_client import HttpClient -from synapse.types import Requester, UserID, create_requester -from synapse.util.caches.cached_call import RetryOnExceptionCachedCall -from synapse.util.caches.response_cache import ResponseCache, ResponseCacheContext -from synapse.util.duration import Duration -from synapse.util.json import json_decoder - -from . import introspection_response_timer - -if TYPE_CHECKING: - from synapse.rest.admin.experimental_features import ExperimentalFeature - from synapse.server import HomeServer - -logger = logging.getLogger(__name__) - -# Scope as defined by MSC2967 -# https://github.com/matrix-org/matrix-spec-proposals/pull/2967 -UNSTABLE_SCOPE_MATRIX_API = "urn:matrix:org.matrix.msc2967.client:api:*" -UNSTABLE_SCOPE_MATRIX_DEVICE_PREFIX = "urn:matrix:org.matrix.msc2967.client:device:" -STABLE_SCOPE_MATRIX_API = "urn:matrix:client:api:*" -STABLE_SCOPE_MATRIX_DEVICE_PREFIX = "urn:matrix:client:device:" - -# Scope which allows access to the Synapse admin API -SCOPE_SYNAPSE_ADMIN = "urn:synapse:admin:*" - - -def scope_to_list(scope: str) -> list[str]: - """Convert a scope string to a list of scope tokens""" - return scope.strip().split(" ") - - -@dataclass -class IntrospectionResult: - _inner: IntrospectionToken - - # when we retrieved this token, - # in milliseconds since the Unix epoch - retrieved_at_ms: int - - def is_active(self, now_ms: int) -> bool: - if not self._inner.get("active"): - return False - - expires_in = self._inner.get("expires_in") - if expires_in is None: - return True - if not isinstance(expires_in, int): - raise InvalidClientTokenError("token `expires_in` is not an int") - - absolute_expiry_ms = expires_in * 1000 + self.retrieved_at_ms - return now_ms < absolute_expiry_ms - - def get_scope_list(self) -> list[str]: - value = self._inner.get("scope") - if not isinstance(value, str): - return [] - return scope_to_list(value) - - def get_sub(self) -> str | None: - value = self._inner.get("sub") - if not isinstance(value, str): - return None - return value - - def get_username(self) -> str | None: - value = self._inner.get("username") - if not isinstance(value, str): - return None - return value - - def get_name(self) -> str | None: - value = self._inner.get("name") - if not isinstance(value, str): - return None - return value - - def get_device_id(self) -> str | None: - value = self._inner.get("device_id") - if value is not None and not isinstance(value, str): - raise AuthError( - 500, - "Invalid device ID in introspection result", - ) - return value - - -class PrivateKeyJWTWithKid(PrivateKeyJWT): # type: ignore[misc] - """An implementation of the private_key_jwt client auth method that includes a kid header. - - This is needed because some providers (Keycloak) require the kid header to figure - out which key to use to verify the signature. - """ - - def sign(self, auth: Any, token_endpoint: str) -> bytes: - return private_key_jwt_sign( - auth.client_secret, - client_id=auth.client_id, - token_endpoint=token_endpoint, - claims=self.claims, - header={"kid": auth.client_secret["kid"]}, - ) - - -class MSC3861DelegatedAuth(BaseAuth): - AUTH_METHODS = { - "client_secret_post": encode_client_secret_post, - "client_secret_basic": encode_client_secret_basic, - "client_secret_jwt": ClientSecretJWT(), - "private_key_jwt": PrivateKeyJWTWithKid(), - } - - EXTERNAL_ID_PROVIDER = "oauth-delegated" - - def __init__(self, hs: "HomeServer"): - super().__init__(hs) - - self._config = hs.config.experimental.msc3861 - auth_method = MSC3861DelegatedAuth.AUTH_METHODS.get( - self._config.client_auth_method.value, None - ) - # Those assertions are already checked when parsing the config - assert self._config.enabled, "OAuth delegation is not enabled" - assert self._config.issuer, "No issuer provided" - assert self._config.client_id, "No client_id provided" - assert auth_method is not None, "Invalid client_auth_method provided" - - self.server_name = hs.hostname - self._clock = hs.get_clock() - self._http_client = hs.get_proxied_http_client() - self._hostname = hs.hostname - self._admin_token: Callable[[], str | None] = self._config.admin_token - self._force_tracing_for_users = hs.config.tracing.force_tracing_for_users - - self._rust_http_client = HttpClient( - reactor=hs.get_reactor(), - user_agent=self._http_client.user_agent.decode("utf8"), - ) - - # # Token Introspection Cache - # This remembers what users/devices are represented by which access tokens, - # in order to reduce overall system load: - # - on Synapse (as requests are relatively expensive) - # - on the network - # - on MAS - # - # Since there is no invalidation mechanism currently, - # the entries expire after 2 minutes. - # This does mean tokens can be treated as valid by Synapse - # for longer than reality. - # - # Ideally, tokens should logically be invalidated in the following circumstances: - # - If a session logout happens. - # In this case, MAS will delete the device within Synapse - # anyway and this is good enough as an invalidation. - # - If the client refreshes their token in MAS. - # In this case, the device still exists and it's not the end of the world for - # the old access token to continue working for a short time. - self._introspection_cache: ResponseCache[str] = ResponseCache( - clock=self._clock, - name="token_introspection", - server_name=self.server_name, - timeout=Duration(minutes=2), - # don't log because the keys are access tokens - enable_logging=False, - ) - - self._issuer_metadata = RetryOnExceptionCachedCall[OpenIDProviderMetadata]( - self._load_metadata - ) - - if isinstance(auth_method, PrivateKeyJWTWithKid): - # Use the JWK as the client secret when using the private_key_jwt method - assert self._config.jwk, "No JWK provided" - self._client_auth = ClientAuth( - self._config.client_id, self._config.jwk, auth_method - ) - else: - # Else use the client secret - client_secret = self._config.client_secret() - assert client_secret, "No client_secret provided" - self._client_auth = ClientAuth( - self._config.client_id, client_secret, auth_method - ) - - async def _load_metadata(self) -> OpenIDProviderMetadata: - if self._config.issuer_metadata is not None: - return OpenIDProviderMetadata(**self._config.issuer_metadata) - url = get_well_known_url(self._config.issuer, external=True) - response = await self._http_client.get_json(url) - metadata = OpenIDProviderMetadata(**response) - # metadata.validate_introspection_endpoint() - return metadata - - async def issuer(self) -> str: - """ - Get the configured issuer - - This will use the issuer value set in the metadata, - falling back to the one set in the config if not set in the metadata - """ - metadata = await self._issuer_metadata.get() - return metadata.issuer or self._config.issuer - - async def account_management_url(self) -> str | None: - """ - Get the configured account management URL - - This will discover the account management URL from the issuer if it's not set in the config - """ - if self._config.account_management_url is not None: - return self._config.account_management_url - - try: - metadata = await self._issuer_metadata.get() - return metadata.get("account_management_uri", None) - # We don't want to raise here if we can't load the metadata - except Exception: - logger.warning("Failed to load metadata:", exc_info=True) - return None - - async def auth_metadata(self) -> dict[str, Any]: - """ - Returns the auth metadata dict - """ - return await self._issuer_metadata.get() - - async def _introspection_endpoint(self) -> str: - """ - Returns the introspection endpoint of the issuer - - It uses the config option if set, otherwise it will use OIDC discovery to get it - """ - if self._config.introspection_endpoint is not None: - return self._config.introspection_endpoint - - metadata = await self._issuer_metadata.get() - return metadata.get("introspection_endpoint") - - async def _introspect_token( - self, token: str, cache_context: ResponseCacheContext[str] - ) -> IntrospectionResult: - """ - Send a token to the introspection endpoint and returns the introspection response - - Parameters: - token: The token to introspect - - Raises: - HttpResponseException: If the introspection endpoint returns a non-2xx response - ValueError: If the introspection endpoint returns an invalid JSON response - JSONDecodeError: If the introspection endpoint returns a non-JSON response - Exception: If the HTTP request fails - - Returns: - The introspection response - """ - # By default, we shouldn't cache the result unless we know it's valid - cache_context.should_cache = False - introspection_endpoint = await self._introspection_endpoint() - raw_headers: dict[str, str] = { - "Content-Type": "application/x-www-form-urlencoded", - "Accept": "application/json", - # Tell MAS that we support reading the device ID as an explicit - # value, not encoded in the scope. This is supported by MAS 0.15+ - "X-MAS-Supports-Device-Id": "1", - } - - args = {"token": token, "token_type_hint": "access_token"} - body = urlencode(args, True) - - # Fill the body/headers with credentials - uri, raw_headers, body = self._client_auth.prepare( - method="POST", uri=introspection_endpoint, headers=raw_headers, body=body - ) - - # Do the actual request - - logger.debug("Fetching token from MAS") - start_time = self._clock.time() - try: - with start_active_span("mas-introspect-token"): - inject_request_headers(raw_headers) - resp_body = await self._rust_http_client.post( - url=uri, - response_limit=1 * 1024 * 1024, - headers=raw_headers, - request_body=body, - ) - except HttpResponseException as e: - end_time = self._clock.time() - introspection_response_timer.labels( - code=e.code, **{SERVER_NAME_LABEL: self.server_name} - ).observe(end_time - start_time) - raise - except Exception: - end_time = self._clock.time() - introspection_response_timer.labels( - code="ERR", **{SERVER_NAME_LABEL: self.server_name} - ).observe(end_time - start_time) - raise - - logger.debug("Fetched token from MAS") - - end_time = self._clock.time() - introspection_response_timer.labels( - code=200, **{SERVER_NAME_LABEL: self.server_name} - ).observe(end_time - start_time) - - resp = json_decoder.decode(resp_body.decode("utf-8")) - - if not isinstance(resp, dict): - raise ValueError( - "The introspection endpoint returned an invalid JSON response." - ) - - # We had a valid response, so we can cache it - cache_context.should_cache = True - return IntrospectionResult( - IntrospectionToken(**resp), retrieved_at_ms=self._clock.time_msec() - ) - - async def is_server_admin(self, requester: Requester) -> bool: - return "urn:synapse:admin:*" in requester.scope - - def _is_access_token_the_admin_token(self, token: str) -> bool: - admin_token = self._admin_token() - if admin_token is None: - return False - return token == admin_token - - async def get_user_by_req( - self, - request: SynapseRequest, - allow_guest: bool = False, - allow_expired: bool = False, - allow_locked: bool = False, - ) -> Requester: - """Get a registered user's ID. - - Args: - request: An HTTP request with an access_token query parameter. - allow_guest: If False, will raise an AuthError if the user making the - request is a guest. - allow_expired: If True, allow the request through even if the account - is expired, or session token lifetime has ended. Note that - /login will deliver access tokens regardless of expiration. - - Returns: - Resolves to the requester - Raises: - InvalidClientCredentialsError if no user by that token exists or the token - is invalid. - AuthError if access is denied for the user in the access token - """ - parent_span = active_span() - with start_active_span("get_user_by_req"): - requester = await self._wrapped_get_user_by_req( - request, allow_guest, allow_expired, allow_locked - ) - - if parent_span: - if requester.authenticated_entity in self._force_tracing_for_users: - # request tracing is enabled for this user, so we need to force it - # tracing on for the parent span (which will be the servlet span). - # - # It's too late for the get_user_by_req span to inherit the setting, - # so we also force it on for that. - force_tracing() - force_tracing(parent_span) - parent_span.set_tag( - "authenticated_entity", requester.authenticated_entity - ) - parent_span.set_tag("user_id", requester.user.to_string()) - if requester.device_id is not None: - parent_span.set_tag("device_id", requester.device_id) - if requester.app_service_id is not None: - parent_span.set_tag("appservice_id", requester.app_service_id) - return requester - - async def _wrapped_get_user_by_req( - self, - request: SynapseRequest, - allow_guest: bool = False, - allow_expired: bool = False, - allow_locked: bool = False, - ) -> Requester: - access_token = self.get_access_token_from_request(request) - - 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) - - # Do not record requests from MAS using the virtual `__oidc_admin` user. - if not self._is_access_token_the_admin_token(access_token): - await self._record_request(request, requester) - - request.requester = requester - - return requester - - async def get_user_by_req_experimental_feature( - self, - request: SynapseRequest, - feature: "ExperimentalFeature", - allow_guest: bool = False, - allow_expired: bool = False, - allow_locked: bool = False, - ) -> Requester: - try: - requester = await self.get_user_by_req( - request, - allow_guest=allow_guest, - allow_expired=allow_expired, - allow_locked=allow_locked, - ) - if await self.store.is_feature_enabled(requester.user.to_string(), feature): - return requester - - raise UnrecognizedRequestError(code=404) - except (AuthError, InvalidClientTokenError): - if feature.is_globally_enabled(self.hs.config): - # If its globally enabled then return the auth error - raise - - raise UnrecognizedRequestError(code=404) - - def is_request_using_the_admin_token(self, request: SynapseRequest) -> bool: - """ - Check if the request is using the admin token. - - Args: - request: The request to check. - - Returns: - True if the request is using the admin token, False otherwise. - """ - access_token = self.get_access_token_from_request(request) - return self._is_access_token_the_admin_token(access_token) - - async def get_user_by_access_token( - self, - token: str, - allow_expired: bool = False, - ) -> Requester: - if self._is_access_token_the_admin_token(token): - # XXX: This is a temporary solution so that the admin API can be called by - # the OIDC provider. This will be removed once we have OIDC client - # credentials grant support in matrix-authentication-service. - logger.info("Admin token used") - # XXX: that user doesn't exist and won't be provisioned. - # This is mostly fine for admin calls, but we should also think about doing - # requesters without a user_id. - admin_user = UserID("__oidc_admin", self._hostname) - return create_requester( - user_id=admin_user, - scope=["urn:synapse:admin:*"], - ) - - try: - introspection_result = await self._introspection_cache.wrap( - token, self._introspect_token, token, cache_context=True - ) - except Exception: - logger.exception("Failed to introspect token") - raise SynapseError(503, "Unable to introspect the access token") - - logger.debug("Introspection result: %r", introspection_result) - - # TODO: introspection verification should be more extensive, especially: - # - verify the audience - if not introspection_result.is_active(self._clock.time_msec()): - raise InvalidClientTokenError("Token is not active") - - # Let's look at the scope - scope: list[str] = introspection_result.get_scope_list() - - # Determine type of user based on presence of particular scopes - has_user_scope = ( - UNSTABLE_SCOPE_MATRIX_API in scope or STABLE_SCOPE_MATRIX_API in scope - ) - - if not has_user_scope: - raise InvalidClientTokenError("No scope in token granting user rights") - - # Match via the sub claim - sub = introspection_result.get_sub() - if sub is None: - raise InvalidClientTokenError( - "Invalid sub claim in the introspection result" - ) - - user_id_str = await self.store.get_user_by_external_id( - MSC3861DelegatedAuth.EXTERNAL_ID_PROVIDER, sub - ) - if user_id_str is None: - # If we could not find a user via the external_id, it either does not exist, - # or the external_id was never recorded - - username = introspection_result.get_username() - if username is None: - raise AuthError( - 500, - "Invalid username claim in the introspection result", - ) - user_id = UserID(username, self._hostname) - - # Try to find a user from the username claim - user_info = await self.store.get_user_by_id(user_id=user_id.to_string()) - if user_info is None: - raise AuthError( - 500, - "User not found", - ) - - # And record the sub as external_id - await self.store.record_user_external_id( - MSC3861DelegatedAuth.EXTERNAL_ID_PROVIDER, sub, user_id.to_string() - ) - else: - user_id = UserID.from_string(user_id_str) - - # MAS 0.15+ will give us the device ID as an explicit value for compatibility sessions - # If present, we get it from here, if not we get it in thee scope - device_id = introspection_result.get_device_id() - if device_id is None: - # Find device_ids in scope - # We only allow a single device_id in the scope, so we find them all in the - # scope list, and raise if there are more than one. The OIDC server should be - # the one enforcing valid scopes, so we raise a 500 if we find an invalid scope. - device_ids: set[str] = set() - for tok in scope: - if tok.startswith(UNSTABLE_SCOPE_MATRIX_DEVICE_PREFIX): - device_ids.add(tok[len(UNSTABLE_SCOPE_MATRIX_DEVICE_PREFIX) :]) - elif tok.startswith(STABLE_SCOPE_MATRIX_DEVICE_PREFIX): - device_ids.add(tok[len(STABLE_SCOPE_MATRIX_DEVICE_PREFIX) :]) - - if len(device_ids) > 1: - raise AuthError( - 500, - "Multiple device IDs in scope", - ) - - device_id = next(iter(device_ids), None) - - if device_id is not None: - # Sanity check the device_id - if len(device_id) > 255 or len(device_id) < 1: - raise AuthError( - 500, - "Invalid device ID in introspection result", - ) - - # Make sure the device exists - await self.store.get_device( - user_id=user_id.to_string(), device_id=device_id - ) - - # TODO: there is a few things missing in the requester here, which still need - # to be figured out, like: - # - impersonation, with the `authenticated_entity`, which is used for - # rate-limiting, MAU limits, etc. - # - shadow-banning, with the `shadow_banned` flag - # - a proper solution for appservices, which still needs to be figured out in - # the context of MSC3861 - return create_requester( - user_id=user_id, - device_id=device_id, - scope=scope, - ) diff --git a/synapse/config/auth.py b/synapse/config/auth.py index 31b332dc09..35b730aed3 100644 --- a/synapse/config/auth.py +++ b/synapse/config/auth.py @@ -36,11 +36,9 @@ class AuthConfig(Config): if password_config is None: password_config = {} - auth_delegated = (config.get("experimental_features") or {}).get( - "msc3861", {} - ).get("enabled", False) or ( - config.get("matrix_authentication_service") or {} - ).get("enabled", False) + auth_delegated = (config.get("matrix_authentication_service") or {}).get( + "enabled", False + ) # The default value of password_config.enabled is True, unless auth is delegated passwords_enabled = password_config.get("enabled", not auth_delegated) diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py index 2d74e9b886..f99f7b139e 100644 --- a/synapse/config/experimental.py +++ b/synapse/config/experimental.py @@ -20,30 +20,16 @@ # # -import enum from functools import cache -from typing import TYPE_CHECKING, Any, Optional +from typing import Any import attr -import attr.validators from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions from synapse.config import ConfigError -from synapse.config._base import Config, RootConfig, read_file +from synapse.config._base import Config, read_file from synapse.types import JsonDict, StrSequence -# Determine whether authlib is installed. -try: - import authlib # noqa: F401 - - HAS_AUTHLIB = True -except ImportError: - HAS_AUTHLIB = False - -if TYPE_CHECKING: - # Only import this if we're type checking, as it might not be installed at runtime. - from authlib.jose.rfc7517 import JsonWebKey - @cache def read_secret_from_file_once(file_path: Any, config_path: StrSequence) -> str: @@ -51,308 +37,6 @@ def read_secret_from_file_once(file_path: Any, config_path: StrSequence) -> str: return read_file(file_path, config_path).strip() -class ClientAuthMethod(enum.Enum): - """List of supported client auth methods.""" - - CLIENT_SECRET_POST = "client_secret_post" - CLIENT_SECRET_BASIC = "client_secret_basic" - CLIENT_SECRET_JWT = "client_secret_jwt" - PRIVATE_KEY_JWT = "private_key_jwt" - - -def _parse_jwks(jwks: JsonDict | None) -> Optional["JsonWebKey"]: - """A helper function to parse a JWK dict into a JsonWebKey.""" - - if jwks is None: - return None - - from authlib.jose.rfc7517 import JsonWebKey - - return JsonWebKey.import_key(jwks) - - -def _check_client_secret( - instance: "MSC3861", _attribute: attr.Attribute, _value: str | None -) -> None: - if instance._client_secret and instance._client_secret_path: - raise ConfigError( - ( - "You have configured both " - "`experimental_features.msc3861.client_secret` and " - "`experimental_features.msc3861.client_secret_path`. " - "These are mutually incompatible." - ), - ("experimental", "msc3861", "client_secret"), - ) - # Check client secret can be retrieved - instance.client_secret() - - -def _check_admin_token( - instance: "MSC3861", _attribute: attr.Attribute, _value: str | None -) -> None: - if instance._admin_token and instance._admin_token_path: - raise ConfigError( - ( - "You have configured both " - "`experimental_features.msc3861.admin_token` and " - "`experimental_features.msc3861.admin_token_path`. " - "These are mutually incompatible." - ), - ("experimental", "msc3861", "admin_token"), - ) - # Check client secret can be retrieved - instance.admin_token() - - -@attr.s(slots=True, frozen=True) -class MSC3861: - """Configuration for MSC3861: Matrix architecture change to delegate authentication via OIDC""" - - enabled: bool = attr.ib(default=False, validator=attr.validators.instance_of(bool)) - """Whether to enable MSC3861 auth delegation.""" - - @enabled.validator - def _check_enabled(self, attribute: attr.Attribute, value: bool) -> None: - # Only allow enabling MSC3861 if authlib is installed - if value and not HAS_AUTHLIB: - raise ConfigError( - "MSC3861 is enabled but authlib is not installed. " - "Please install authlib to use MSC3861.", - ("experimental", "msc3861", "enabled"), - ) - - issuer: str = attr.ib(default="", validator=attr.validators.instance_of(str)) - """The URL of the OIDC Provider.""" - - issuer_metadata: JsonDict | None = attr.ib(default=None) - """The issuer metadata to use, otherwise discovered from /.well-known/openid-configuration as per MSC2965.""" - - client_id: str = attr.ib( - default="", - validator=attr.validators.instance_of(str), - ) - """The client ID to use when calling the introspection endpoint.""" - - client_auth_method: ClientAuthMethod = attr.ib( - default=ClientAuthMethod.CLIENT_SECRET_POST, converter=ClientAuthMethod - ) - """The auth method used when calling the introspection endpoint.""" - - _client_secret: str | None = attr.ib( - default=None, - validator=[ - attr.validators.optional(attr.validators.instance_of(str)), - _check_client_secret, - ], - ) - """ - The client secret to use when calling the introspection endpoint, - when using any of the client_secret_* client auth methods. - """ - - _client_secret_path: str | None = attr.ib( - default=None, - validator=[ - attr.validators.optional(attr.validators.instance_of(str)), - _check_client_secret, - ], - ) - """ - Alternative to `client_secret`: allows the secret to be specified in an - external file. - """ - - jwk: Optional["JsonWebKey"] = attr.ib(default=None, converter=_parse_jwks) - """ - The JWKS to use when calling the introspection endpoint, - when using the private_key_jwt client auth method. - """ - - @client_auth_method.validator - def _check_client_auth_method( - self, attribute: attr.Attribute, value: ClientAuthMethod - ) -> None: - # Check that the right client credentials are provided for the client auth method. - if not self.enabled: - return - - if value == ClientAuthMethod.PRIVATE_KEY_JWT and self.jwk is None: - raise ConfigError( - "A JWKS must be provided when using the private_key_jwt client auth method", - ("experimental", "msc3861", "client_auth_method"), - ) - - if ( - value - in ( - ClientAuthMethod.CLIENT_SECRET_POST, - ClientAuthMethod.CLIENT_SECRET_BASIC, - ClientAuthMethod.CLIENT_SECRET_JWT, - ) - and self.client_secret() is None - ): - raise ConfigError( - f"A client secret must be provided when using the {value} client auth method", - ("experimental", "msc3861", "client_auth_method"), - ) - - introspection_endpoint: str | None = attr.ib( - default=None, - validator=attr.validators.optional(attr.validators.instance_of(str)), - ) - """The URL of the introspection endpoint used to validate access tokens.""" - - account_management_url: str | None = attr.ib( - default=None, - validator=attr.validators.optional(attr.validators.instance_of(str)), - ) - """The URL of the My Account page on the OIDC Provider as per MSC2965.""" - - _admin_token: str | None = attr.ib( - default=None, - validator=[ - attr.validators.optional(attr.validators.instance_of(str)), - _check_admin_token, - ], - ) - """ - A token that should be considered as an admin token. - This is used by the OIDC provider, to make admin calls to Synapse. - """ - - _admin_token_path: str | None = attr.ib( - default=None, - validator=[ - attr.validators.optional(attr.validators.instance_of(str)), - _check_admin_token, - ], - ) - """ - Alternative to `admin_token`: allows the secret to be specified in an - external file. - """ - - def client_secret(self) -> str | None: - """Returns the secret given via `client_secret` or `client_secret_path`.""" - if self._client_secret_path: - return read_secret_from_file_once( - self._client_secret_path, - ("experimental_features", "msc3861", "client_secret_path"), - ) - return self._client_secret - - def admin_token(self) -> str | None: - """Returns the admin token given via `admin_token` or `admin_token_path`.""" - if self._admin_token_path: - return read_secret_from_file_once( - self._admin_token_path, - ("experimental_features", "msc3861", "admin_token_path"), - ) - return self._admin_token - - def check_config_conflicts( - self, root: RootConfig, allow_secrets_in_config: bool - ) -> None: - """Checks for any configuration conflicts with other parts of Synapse. - - Raises: - ConfigError: If there are any configuration conflicts. - """ - - if not self.enabled: - return - - if self._client_secret and not allow_secrets_in_config: - raise ConfigError( - "Config options that expect an in-line secret as value are disabled", - ("experimental", "msc3861", "client_secret"), - ) - - if self.jwk and not allow_secrets_in_config: - raise ConfigError( - "Config options that expect an in-line secret as value are disabled", - ("experimental", "msc3861", "jwk"), - ) - - if self._admin_token and not allow_secrets_in_config: - raise ConfigError( - "Config options that expect an in-line secret as value are disabled", - ("experimental", "msc3861", "admin_token"), - ) - - if ( - root.auth.password_enabled_for_reauth - or root.auth.password_enabled_for_login - ): - raise ConfigError( - "Password auth cannot be enabled when OAuth delegation is enabled", - ("password_config", "enabled"), - ) - - if root.registration.enable_registration: - raise ConfigError( - "Registration cannot be enabled when OAuth delegation is enabled", - ("enable_registration",), - ) - - # We only need to test the user consent version, as if it must be set if the user_consent section was present in the config - if root.consent.user_consent_version is not None: - raise ConfigError( - "User consent cannot be enabled when OAuth delegation is enabled", - ("user_consent",), - ) - - if ( - root.oidc.oidc_enabled - or root.saml2.saml2_enabled - or root.cas.cas_enabled - or root.jwt.jwt_enabled - ): - raise ConfigError("SSO cannot be enabled when OAuth delegation is enabled") - - if bool(root.authproviders.password_providers): - raise ConfigError( - "Password auth providers cannot be enabled when OAuth delegation is enabled" - ) - - if root.captcha.enable_registration_captcha: - raise ConfigError( - "CAPTCHA cannot be enabled when OAuth delegation is enabled", - ("captcha", "enable_registration_captcha"), - ) - - if root.auth.login_via_existing_enabled: - raise ConfigError( - "Login via existing session cannot be enabled when OAuth delegation is enabled", - ("login_via_existing_session", "enabled"), - ) - - if root.registration.refresh_token_lifetime: - raise ConfigError( - "refresh_token_lifetime cannot be set when OAuth delegation is enabled", - ("refresh_token_lifetime",), - ) - - if root.registration.nonrefreshable_access_token_lifetime: - raise ConfigError( - "nonrefreshable_access_token_lifetime cannot be set when OAuth delegation is enabled", - ("nonrefreshable_access_token_lifetime",), - ) - - if root.registration.session_lifetime: - raise ConfigError( - "session_lifetime cannot be set when OAuth delegation is enabled", - ("session_lifetime",), - ) - - if root.registration.enable_3pid_changes: - raise ConfigError( - "enable_3pid_changes cannot be enabled when OAuth delegation is enabled", - ("enable_3pid_changes",), - ) - - @attr.s(auto_attribs=True, frozen=True, slots=True) class MSC3866Config: """Configuration for MSC3866 (mandating approval for new users)""" @@ -490,18 +174,14 @@ class ExperimentalConfig(Config): # MSC3391: Removing account data. self.msc3391_enabled = experimental.get("msc3391_enabled", False) - # MSC3861: Matrix architecture change to delegate authentication via OIDC - try: - self.msc3861 = MSC3861(**experimental.get("msc3861", {})) - except ValueError as exc: + # MSC3861 was replaced by the stable Matrix Authentication Service integration. + msc3861_config = experimental.get("msc3861", {}) + if msc3861_config: # non-empty dict raise ConfigError( - "Invalid MSC3861 configuration", ("experimental", "msc3861") - ) from exc - - # Check that none of the other config options conflict with MSC3861 when enabled - self.msc3861.check_config_conflicts( - self.root, allow_secrets_in_config=allow_secrets_in_config - ) + "experimental_features.msc3861 was removed. " + "Use the matrix_authentication_service configuration instead.", + ("experimental", "msc3861"), + ) self.msc4028_push_encrypted_events = experimental.get( "msc4028_push_encrypted_events", False @@ -523,15 +203,15 @@ class ExperimentalConfig(Config): # See https://github.com/element-hq/synapse/issues/19524 self.msc4370_enabled = experimental.get("msc4370_enabled", False) - auth_delegated = self.msc3861.enabled or ( - config.get("matrix_authentication_service") or {} - ).get("enabled", False) + auth_delegated = (config.get("matrix_authentication_service") or {}).get( + "enabled", False + ) if ( self.msc4108_enabled or self.msc4108_delegation_endpoint is not None ) and not auth_delegated: raise ConfigError( - "MSC4108 requires MSC3861 or matrix_authentication_service to be enabled", + "MSC4108 requires matrix_authentication_service to be enabled", ("experimental", "msc4108_delegation_endpoint"), ) diff --git a/synapse/config/mas.py b/synapse/config/mas.py index 6973e9ae58..a26ad1a13f 100644 --- a/synapse/config/mas.py +++ b/synapse/config/mas.py @@ -102,13 +102,6 @@ class MasConfig(Config): if not self.enabled: return - if root.experimental.msc3861.enabled: - raise ConfigError( - "Experimental MSC3861 was replaced by Matrix Authentication Service." - "Please disable MSC3861 or disable Matrix Authentication Service.", - ("experimental", "msc3861"), - ) - if ( root.auth.password_enabled_for_reauth or root.auth.password_enabled_for_login diff --git a/synapse/config/registration.py b/synapse/config/registration.py index 7f7a224e02..d020fbf14e 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -148,13 +148,11 @@ class RegistrationConfig(Config): self.enable_set_displayname = config.get("enable_set_displayname", True) self.enable_set_avatar_url = config.get("enable_set_avatar_url", True) - auth_delegated = (config.get("experimental_features") or {}).get( - "msc3861", {} - ).get("enabled", False) or ( - config.get("matrix_authentication_service") or {} - ).get("enabled", False) + auth_delegated = (config.get("matrix_authentication_service") or {}).get( + "enabled", False + ) - # The default value of enable_3pid_changes is True, unless msc3861 is enabled. + # The default value of enable_3pid_changes is True, unless auth is delegated. self.enable_3pid_changes = config.get("enable_3pid_changes", not auth_delegated) self.disable_msisdn_registration = config.get( diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 1c8e108ea8..fc4373e0a5 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -277,9 +277,7 @@ class AuthHandler: # response. self._extra_attributes: dict[str, SsoLoginExtraAttributes] = {} - self._auth_delegation_enabled = ( - hs.config.mas.enabled or hs.config.experimental.msc3861.enabled - ) + self._auth_delegation_enabled = hs.config.mas.enabled async def validate_user_via_ui_auth( self, @@ -332,7 +330,8 @@ class AuthHandler: """ if self._auth_delegation_enabled: raise SynapseError( - HTTPStatus.INTERNAL_SERVER_ERROR, "UIA shouldn't be used with MSC3861" + HTTPStatus.INTERNAL_SERVER_ERROR, + "UIA shouldn't be used when auth is delegated", ) if not requester.access_token_id: diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index 3341e49b85..1131253028 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -354,9 +354,7 @@ class ModuleApi: self._device_handler = hs.get_device_handler() self.custom_template_dir = hs.config.server.custom_template_directory self._callbacks = hs.get_module_api_callbacks() - self._auth_delegation_enabled = ( - hs.config.mas.enabled or hs.config.experimental.msc3861.enabled - ) + self._auth_delegation_enabled = hs.config.mas.enabled self._event_serializer = hs.get_event_client_serializer() try: diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py index 0774b6ed40..2e106826ff 100644 --- a/synapse/rest/admin/__init__.py +++ b/synapse/rest/admin/__init__.py @@ -281,17 +281,11 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: # Admin servlets below may not work on workers. if hs.config.worker.worker_app is not None: - # Some admin servlets can be mounted on workers when MSC3861 is enabled. - # Note that this is only for MSC3861 mode, as modern MAS using the - # matrix_authentication_service integration uses the dedicated MAS API. - if hs.config.experimental.msc3861.enabled: - register_servlets_for_msc3861_delegation(hs, http_server) - else: - UserRestServletV2Get(hs).register(http_server) + UserRestServletV2Get(hs).register(http_server) return - auth_delegated = hs.config.mas.enabled or hs.config.experimental.msc3861.enabled + auth_delegated = hs.config.mas.enabled register_servlets_for_client_rest_resource(hs, http_server) BlockRoomRestServlet(hs).register(http_server) @@ -362,7 +356,7 @@ def register_servlets_for_client_rest_resource( hs: "HomeServer", http_server: HttpServer ) -> None: """Register only the servlets which need to be exposed on /_matrix/client/xxx""" - auth_delegated = hs.config.mas.enabled or hs.config.experimental.msc3861.enabled + auth_delegated = hs.config.mas.enabled WhoisRestServlet(hs).register(http_server) PurgeHistoryStatusRestServlet(hs).register(http_server) @@ -386,16 +380,3 @@ def register_servlets_for_client_rest_resource( # don't add more things here: new servlets should only be exposed on # /_synapse/admin so should not go here. Instead register them in register_servlets. - - -def register_servlets_for_msc3861_delegation( - hs: "HomeServer", http_server: HttpServer -) -> None: - """Register servlets needed by MAS when MSC3861 is enabled""" - assert hs.config.experimental.msc3861.enabled - - UserRestServletV2(hs).register(http_server) - UsernameAvailableRestServlet(hs).register(http_server) - UserReplaceMasterCrossSigningKeyRestServlet(hs).register(http_server) - DeviceRestServlet(hs).register(http_server) - DevicesRestServlet(hs).register(http_server) diff --git a/synapse/rest/admin/users.py b/synapse/rest/admin/users.py index de4474cdc0..1bd48e18cc 100644 --- a/synapse/rest/admin/users.py +++ b/synapse/rest/admin/users.py @@ -109,9 +109,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._auth_delegation_enabled = ( - hs.config.mas.enabled or hs.config.experimental.msc3861.enabled - ) + self._auth_delegation_enabled = hs.config.mas.enabled async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: await assert_requester_is_admin(self.auth, request) diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py index d1e404f0dc..3b01e40121 100644 --- a/synapse/rest/client/account.py +++ b/synapse/rest/client/account.py @@ -619,7 +619,7 @@ 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.mas.enabled or self.hs.config.experimental.msc3861.enabled: + if self.hs.config.mas.enabled: raise NotFoundError(errcode=Codes.UNRECOGNIZED) if not self.hs.config.registration.enable_3pid_changes: @@ -911,7 +911,7 @@ class AccountStatusRestServlet(RestServlet): def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: - auth_delegated = hs.config.mas.enabled or hs.config.experimental.msc3861.enabled + auth_delegated = hs.config.mas.enabled ThreepidRestServlet(hs).register(http_server) WhoamiRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/auth.py b/synapse/rest/client/auth.py index 566c9c98c5..b1775346f6 100644 --- a/synapse/rest/client/auth.py +++ b/synapse/rest/client/auth.py @@ -97,20 +97,6 @@ class AuthRestServlet(RestServlet): url.encode(), ) - elif self.hs.config.experimental.msc3861.enabled: - # If MSC3861 is enabled, we can assume self._auth is an instance of MSC3861DelegatedAuth - # We import lazily here because of the authlib requirement - from synapse.api.auth.msc3861_delegated import MSC3861DelegatedAuth - - assert isinstance(self.auth, MSC3861DelegatedAuth) - - base = await self.auth.account_management_url() - if base is not None: - url = f"{base}?action=org.matrix.cross_signing_reset" - else: - url = await self.auth.issuer() - return respond_with_redirect(request, url.encode()) - if stagetype == LoginType.RECAPTCHA: html = self.recaptcha_template.render( session=session, diff --git a/synapse/rest/client/auth_metadata.py b/synapse/rest/client/auth_metadata.py index 062b8ed13e..42decfdd6a 100644 --- a/synapse/rest/client/auth_metadata.py +++ b/synapse/rest/client/auth_metadata.py @@ -13,7 +13,6 @@ # limitations under the License. import logging import typing -from typing import cast from synapse.api.auth.mas import MasDelegatedAuth from synapse.api.errors import Codes, SynapseError @@ -51,7 +50,7 @@ class AuthIssuerServlet(RestServlet): async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: # This endpoint is unauthenticated and the response only depends on # the metadata we get from Matrix Authentication Service. Internally, - # MasDelegatedAuth/MSC3861DelegatedAuth.issuer() are already caching the + # MasDelegatedAuth.issuer() is already caching the # response in memory anyway. Ideally we would follow any Cache-Control directive # given by MAS, but this is fine for now. # @@ -72,14 +71,6 @@ class AuthIssuerServlet(RestServlet): assert isinstance(self._auth, MasDelegatedAuth) return 200, {"issuer": await self._auth.issuer()} - elif self._config.experimental.msc3861.enabled: - # If MSC3861 is enabled, we can assume self._auth is an instance of MSC3861DelegatedAuth - # We import lazily here because of the authlib requirement - from synapse.api.auth.msc3861_delegated import MSC3861DelegatedAuth - - assert isinstance(self._auth, MSC3861DelegatedAuth) - return 200, {"issuer": await self._auth.issuer()} - else: # Wouldn't expect this to be reached: the servelet shouldn't have been # registered. Still, fail gracefully if we are registered for some reason. @@ -115,7 +106,7 @@ class AuthMetadataServlet(RestServlet): async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]: # This endpoint is unauthenticated and the response only depends on # the metadata we get from Matrix Authentication Service. Internally, - # MasDelegatedAuth/MSC3861DelegatedAuth.issuer() are already caching the + # MasDelegatedAuth.issuer() is already caching the # response in memory anyway. Ideally we would follow any Cache-Control directive # given by MAS, but this is fine for now. # @@ -136,14 +127,6 @@ class AuthMetadataServlet(RestServlet): assert isinstance(self._auth, MasDelegatedAuth) return 200, await self._auth.auth_metadata() - elif self._config.experimental.msc3861.enabled: - # If MSC3861 is enabled, we can assume self._auth is an instance of MSC3861DelegatedAuth - # We import lazily here because of the authlib requirement - from synapse.api.auth.msc3861_delegated import MSC3861DelegatedAuth - - auth = cast(MSC3861DelegatedAuth, self._auth) - return 200, await auth.auth_metadata() - else: # Wouldn't expect this to be reached: the servlet shouldn't have been # registered. Still, fail gracefully if we are registered for some reason. @@ -155,6 +138,6 @@ class AuthMetadataServlet(RestServlet): def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: - if hs.config.mas.enabled or hs.config.experimental.msc3861.enabled: + if hs.config.mas.enabled: AuthIssuerServlet(hs).register(http_server) AuthMetadataServlet(hs).register(http_server) diff --git a/synapse/rest/client/devices.py b/synapse/rest/client/devices.py index 76043e6120..aefcfe3e4d 100644 --- a/synapse/rest/client/devices.py +++ b/synapse/rest/client/devices.py @@ -137,9 +137,7 @@ class DeviceRestServlet(RestServlet): handler = hs.get_device_handler() self.device_handler = handler self.auth_handler = hs.get_auth_handler() - self._auth_delegation_enabled = ( - hs.config.mas.enabled or hs.config.experimental.msc3861.enabled - ) + self._auth_delegation_enabled = hs.config.mas.enabled async def on_GET( self, request: SynapseRequest, device_id: str @@ -180,7 +178,7 @@ class DeviceRestServlet(RestServlet): if requester.app_service_id: # MSC4190 allows appservices to delete devices through this endpoint without UIA - # It's also allowed with MSC3861 enabled + # It's also allowed when auth is delegated pass else: @@ -465,7 +463,7 @@ class DehydratedDeviceV2Servlet(RestServlet): def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: - auth_delegated = hs.config.mas.enabled or hs.config.experimental.msc3861.enabled + auth_delegated = hs.config.mas.enabled if not auth_delegated: DeleteDevicesRestServlet(hs).register(http_server) DevicesRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/keys.py b/synapse/rest/client/keys.py index 463c87d92b..7ffde7ac04 100644 --- a/synapse/rest/client/keys.py +++ b/synapse/rest/client/keys.py @@ -536,7 +536,7 @@ class SigningKeyUploadServlet(RestServlet): # If yes, then we need to authenticate the change. # MSC4190 can skip UIA for replacing cross-signing keys as well. if is_cross_signing_setup and not requester.app_service_id: - # With MSC3861, UIA is not possible. Instead, the auth service has to + # With auth delegation, UIA is not possible. Instead, the auth service has to # explicitly mark the master key as replaceable. if self.hs.config.mas.enabled: if not master_key_updatable_without_uia: @@ -569,47 +569,8 @@ class SigningKeyUploadServlet(RestServlet): }, ) - elif self.hs.config.experimental.msc3861.enabled: - if not master_key_updatable_without_uia: - # If MSC3861 is enabled, we can assume self.auth is an instance of MSC3861DelegatedAuth - # We import lazily here because of the authlib requirement - from synapse.api.auth.msc3861_delegated import MSC3861DelegatedAuth - - assert isinstance(self.auth, MSC3861DelegatedAuth) - - uri = await self.auth.account_management_url() - if uri is not None: - url = f"{uri}?action=org.matrix.cross_signing_reset" - else: - url = await self.auth.issuer() - - # We use a dummy session ID as this isn't really a UIA flow, but we - # reuse the same API shape for better client compatibility. - raise InteractiveAuthIncompleteError( - "dummy", - { - "session": "dummy", - "flows": [ - {"stages": ["m.oauth"]}, - # The unstable name from MSC4312 should be supported until enough clients have adopted the stable (`m.oauth`) name: - {"stages": ["org.matrix.cross_signing_reset"]}, - ], - "params": { - "m.oauth": { - "url": url, - }, - "org.matrix.cross_signing_reset": { - "url": url, - }, - }, - "msg": "To reset your end-to-end encryption cross-signing " - f"identity, you first need to approve it at {url} and " - "then try again.", - }, - ) - else: - # Without MSC3861, we require UIA. + # Without auth delegation, we require UIA. await self.auth_handler.validate_user_via_ui_auth( requester, request, diff --git a/synapse/rest/client/login.py b/synapse/rest/client/login.py index cfdc97b664..aaf26bac6f 100644 --- a/synapse/rest/client/login.py +++ b/synapse/rest/client/login.py @@ -733,7 +733,7 @@ class CasTicketServlet(RestServlet): def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: - if hs.config.mas.enabled or hs.config.experimental.msc3861.enabled: + if hs.config.mas.enabled: return LoginRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/logout.py b/synapse/rest/client/logout.py index d804552a4a..57e8022568 100644 --- a/synapse/rest/client/logout.py +++ b/synapse/rest/client/logout.py @@ -86,7 +86,7 @@ class LogoutAllRestServlet(RestServlet): def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: - if hs.config.mas.enabled or hs.config.experimental.msc3861.enabled: + if hs.config.mas.enabled: return LogoutRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/register.py b/synapse/rest/client/register.py index 73832ba7a8..ae81e80654 100644 --- a/synapse/rest/client/register.py +++ b/synapse/rest/client/register.py @@ -907,7 +907,7 @@ class RegisterRestServlet(RestServlet): 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 + This replaces the regular /register endpoint if auth is delegated to MAS. 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 @@ -1068,7 +1068,7 @@ def _calculate_registration_flows( def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: - if hs.config.mas.enabled or hs.config.experimental.msc3861.enabled: + if hs.config.mas.enabled: RegisterAppServiceOnlyRestServlet(hs).register(http_server) return diff --git a/synapse/rest/synapse/client/__init__.py b/synapse/rest/synapse/client/__init__.py index 665ce77dd7..e04b84ecac 100644 --- a/synapse/rest/synapse/client/__init__.py +++ b/synapse/rest/synapse/client/__init__.py @@ -58,11 +58,6 @@ def build_synapse_client_resource_tree(hs: "HomeServer") -> Mapping[str, Resourc if hs.config.mas.enabled: resources["/_synapse/mas"] = MasResource(hs) - elif hs.config.experimental.msc3861.enabled: - from synapse.rest.synapse.client.jwks import JwksResource - - resources["/_synapse/jwks"] = JwksResource(hs) - resources["/_synapse/mas"] = MasResource(hs) # provider-specific SSO bits. Only load these if they are enabled, since they # rely on optional dependencies. diff --git a/synapse/rest/synapse/client/jwks.py b/synapse/rest/synapse/client/jwks.py deleted file mode 100644 index 15ff6f47c1..0000000000 --- a/synapse/rest/synapse/client/jwks.py +++ /dev/null @@ -1,77 +0,0 @@ -# -# This file is licensed under the Affero General Public License (AGPL) version 3. -# -# Copyright 2022 The Matrix.org Foundation C.I.C. -# Copyright (C) 2023 New Vector, Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# See the GNU Affero General Public License for more details: -# . -# -# Originally licensed under the Apache License, Version 2.0: -# . -# -# [This file includes modifications made by New Vector Limited] -# -# -import logging -from typing import TYPE_CHECKING - -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__(clock=hs.get_clock(), 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/synapse/mas/_base.py b/synapse/rest/synapse/mas/_base.py index 7346198b75..71457a22b5 100644 --- a/synapse/rest/synapse/mas/_base.py +++ b/synapse/rest/synapse/mas/_base.py @@ -29,18 +29,9 @@ if TYPE_CHECKING: class MasBaseResource(DirectServeJsonResource): def __init__(self, hs: "HomeServer"): auth = hs.get_auth() - if hs.config.mas.enabled: - assert isinstance(auth, MasDelegatedAuth) + assert isinstance(auth, MasDelegatedAuth) - self._is_request_from_mas = auth.is_request_using_the_shared_secret - else: - # Importing this module requires authlib, which is an optional - # dependency but required if msc3861 is enabled - from synapse.api.auth.msc3861_delegated import MSC3861DelegatedAuth - - assert isinstance(auth, MSC3861DelegatedAuth) - - self._is_request_from_mas = auth.is_request_using_the_admin_token + self._is_request_from_mas = auth.is_request_using_the_shared_secret DirectServeJsonResource.__init__(self, extract_context=True) self.store = cast("GenericWorkerStore", hs.get_datastores().main) diff --git a/synapse/rest/well_known.py b/synapse/rest/well_known.py index 801d474ecc..3193761ad0 100644 --- a/synapse/rest/well_known.py +++ b/synapse/rest/well_known.py @@ -61,22 +61,6 @@ class WellKnownBuilder: "account": await self._auth.account_management_url(), } - elif self._config.experimental.msc3861.enabled: - # If MSC3861 is enabled, we can assume self._auth is an instance of MSC3861DelegatedAuth - # We import lazily here because of the authlib requirement - from synapse.api.auth.msc3861_delegated import MSC3861DelegatedAuth - - assert isinstance(self._auth, MSC3861DelegatedAuth) - - result["org.matrix.msc2965.authentication"] = { - "issuer": await self._auth.issuer(), - } - account_management_url = await self._auth.account_management_url() - if account_management_url is not None: - result["org.matrix.msc2965.authentication"]["account"] = ( - account_management_url - ) - if self._config.server.extra_well_known_client_content: for ( key, diff --git a/synapse/server.py b/synapse/server.py index 8bf19f11b5..bd4337c6e6 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -741,10 +741,6 @@ class HomeServer(metaclass=abc.ABCMeta): def get_auth(self) -> Auth: if self.config.mas.enabled: return MasDelegatedAuth(self) - if self.config.experimental.msc3861.enabled: - from synapse.api.auth.msc3861_delegated import MSC3861DelegatedAuth - - return MSC3861DelegatedAuth(self) return InternalAuth(self) @cache_in_self diff --git a/synapse/storage/databases/main/appservice.py b/synapse/storage/databases/main/appservice.py index 6c2bf90b37..1e2db20b44 100644 --- a/synapse/storage/databases/main/appservice.py +++ b/synapse/storage/databases/main/appservice.py @@ -84,7 +84,7 @@ class ApplicationServiceWorkerStore(RoomMemberWorkerStore): ) self.exclusive_user_regex = _make_exclusive_regex(self.services_cache) # When OAuth is enabled, force all appservices to enable MSC4190 too. - if hs.config.mas.enabled or hs.config.experimental.msc3861.enabled: + if hs.config.mas.enabled: for appservice in self.services_cache: appservice.msc4190_device_management = True diff --git a/tests/config/test_load.py b/tests/config/test_load.py index c1b787346e..8d94390acf 100644 --- a/tests/config/test_load.py +++ b/tests/config/test_load.py @@ -149,8 +149,6 @@ class ConfigLoadingFileTestCase(ConfigFileTestCase): "recaptcha_public_key_path: /does/not/exist", "form_secret_path: /does/not/exist", "worker_replication_secret_path: /does/not/exist", - "experimental_features:\n msc3861:\n client_secret_path: /does/not/exist", - "experimental_features:\n msc3861:\n admin_token_path: /does/not/exist", *["redis:\n enabled: true\n password_path: /does/not/exist"] * (hiredis is not None), ] @@ -192,14 +190,6 @@ class ConfigLoadingFileTestCase(ConfigFileTestCase): "worker_replication_secret_path: {}", lambda c: c.worker.worker_replication_secret.encode("utf-8"), ), - ( - "experimental_features:\n msc3861:\n client_secret_path: {}", - lambda c: c.experimental.msc3861.client_secret().encode("utf-8"), - ), - ( - "experimental_features:\n msc3861:\n admin_token_path: {}", - lambda c: c.experimental.msc3861.admin_token().encode("utf-8"), - ), *[ ( "redis:\n enabled: true\n password_path: {}", @@ -232,29 +222,6 @@ class ConfigLoadingFileTestCase(ConfigFileTestCase): "recaptcha_public_key: ¬53C237", "form_secret: 53C237", "worker_replication_secret: 53C237", - *[ - "experimental_features:\n" - " msc3861:\n" - " enabled: true\n" - " client_secret: 53C237" - ] - * (authlib is not None), - *[ - "experimental_features:\n" - " msc3861:\n" - " enabled: true\n" - " client_auth_method: private_key_jwt\n" - ' jwk: {{"mock": "mock"}}' - ] - * (authlib is not None), - *[ - "experimental_features:\n" - " msc3861:\n" - " enabled: true\n" - " admin_token: 53C237\n" - " client_secret_path: {secret_file}" - ] - * (authlib is not None), *["redis:\n enabled: true\n password: 53C237"] * (hiredis is not None), ] ) @@ -304,15 +271,6 @@ class ConfigLoadingFileTestCase(ConfigFileTestCase): f"recaptcha_public_key_path: {secret_file.name}", f"form_secret_path: {secret_file.name}", f"worker_replication_secret_path: {secret_file.name}", - *[ - "experimental_features:\n" - " msc3861:\n" - " enabled: true\n" - f" admin_token_path: {secret_file.name}\n" - f" client_secret_path: {secret_file.name}\n" - # f" jwk_path: {secret_file.name}" - ] - * (authlib is not None), *[f"redis:\n enabled: true\n password_path: {secret_file.name}"] * (hiredis is not None), ] diff --git a/tests/config/test_oauth_delegation.py b/tests/config/test_oauth_delegation.py index e0698764a1..17fb3a3410 100644 --- a/tests/config/test_oauth_delegation.py +++ b/tests/config/test_oauth_delegation.py @@ -54,234 +54,6 @@ class CustomAuthModule: ) -@skip_unless(HAS_AUTHLIB, "requires authlib") -class MSC3861OAuthDelegation(TestCase): - """Test that the Homeserver fails to initialize if the config is invalid.""" - - def setUp(self) -> None: - self.config_dict: JsonDict = { - **default_config(server_name="test"), - "public_baseurl": BASE_URL, - "enable_registration": False, - "experimental_features": { - "msc3861": { - "enabled": True, - "issuer": ISSUER, - "client_id": CLIENT_ID, - "client_auth_method": "client_secret_post", - "client_secret": CLIENT_SECRET, - } - }, - } - - def parse_config(self) -> HomeServerConfig: - config = HomeServerConfig() - config.parse_config_dict(self.config_dict, "", "") - return config - - def test_client_secret_post_works(self) -> None: - self.config_dict["experimental_features"]["msc3861"].update( - client_auth_method="client_secret_post", - client_secret=CLIENT_SECRET, - ) - - self.parse_config() - - def test_client_secret_post_requires_client_secret(self) -> None: - self.config_dict["experimental_features"]["msc3861"].update( - client_auth_method="client_secret_post", - client_secret=None, - ) - - with self.assertRaises(ConfigError): - self.parse_config() - - def test_client_secret_basic_works(self) -> None: - self.config_dict["experimental_features"]["msc3861"].update( - client_auth_method="client_secret_basic", - client_secret=CLIENT_SECRET, - ) - - self.parse_config() - - def test_client_secret_basic_requires_client_secret(self) -> None: - self.config_dict["experimental_features"]["msc3861"].update( - client_auth_method="client_secret_basic", - client_secret=None, - ) - - with self.assertRaises(ConfigError): - self.parse_config() - - def test_client_secret_jwt_works(self) -> None: - self.config_dict["experimental_features"]["msc3861"].update( - client_auth_method="client_secret_jwt", - client_secret=CLIENT_SECRET, - ) - - self.parse_config() - - def test_client_secret_jwt_requires_client_secret(self) -> None: - self.config_dict["experimental_features"]["msc3861"].update( - client_auth_method="client_secret_jwt", - client_secret=None, - ) - - with self.assertRaises(ConfigError): - self.parse_config() - - def test_invalid_client_auth_method(self) -> None: - self.config_dict["experimental_features"]["msc3861"].update( - client_auth_method="invalid", - ) - - with self.assertRaises(ConfigError): - self.parse_config() - - def test_private_key_jwt_requires_jwk(self) -> None: - self.config_dict["experimental_features"]["msc3861"].update( - client_auth_method="private_key_jwt", - ) - - with self.assertRaises(ConfigError): - self.parse_config() - - def test_private_key_jwt_works(self) -> None: - self.config_dict["experimental_features"]["msc3861"].update( - client_auth_method="private_key_jwt", - jwk={ - "p": "-frVdP_tZ-J_nIR6HNMDq1N7aunwm51nAqNnhqIyuA8ikx7LlQED1tt2LD3YEvYyW8nxE2V95HlCRZXQPMiRJBFOsbmYkzl2t-MpavTaObB_fct_JqcRtdXddg4-_ihdjRDwUOreq_dpWh6MIKsC3UyekfkHmeEJg5YpOTL15j8", - "kty": "RSA", - "q": "oFw-Enr_YozQB1ab-kawn4jY3yHi8B1nSmYT0s8oTCflrmps5BFJfCkHL5ij3iY15z0o2m0N-jjB1oSJ98O4RayEEYNQlHnTNTl0kRIWzpoqblHUIxVcahIpP_xTovBJzwi8XXoLGqHOOMA-r40LSyVgP2Ut8D9qBwV6_UfT0LU", - "d": "WFkDPYo4b4LIS64D_QtQfGGuAObPvc3HFfp9VZXyq3SJR58XZRHE0jqtlEMNHhOTgbMYS3w8nxPQ_qVzY-5hs4fIanwvB64mAoOGl0qMHO65DTD_WsGFwzYClJPBVniavkLE2Hmpu8IGe6lGliN8vREC6_4t69liY-XcN_ECboVtC2behKkLOEASOIMuS7YcKAhTJFJwkl1dqDlliEn5A4u4xy7nuWQz3juB1OFdKlwGA5dfhDNglhoLIwNnkLsUPPFO-WB5ZNEW35xxHOToxj4bShvDuanVA6mJPtTKjz0XibjB36bj_nF_j7EtbE2PdGJ2KevAVgElR4lqS4ISgQ", - "e": "AQAB", - "kid": "test", - "qi": "cPfNk8l8W5exVNNea4d7QZZ8Qr8LgHghypYAxz8PQh1fNa8Ya1SNUDVzC2iHHhszxxA0vB9C7jGze8dBrvnzWYF1XvQcqNIVVgHhD57R1Nm3dj2NoHIKe0Cu4bCUtP8xnZQUN4KX7y4IIcgRcBWG1hT6DEYZ4BxqicnBXXNXAUI", - "dp": "dKlMHvslV1sMBQaKWpNb3gPq0B13TZhqr3-E2_8sPlvJ3fD8P4CmwwnOn50JDuhY3h9jY5L06sBwXjspYISVv8hX-ndMLkEeF3lrJeA5S70D8rgakfZcPIkffm3tlf1Ok3v5OzoxSv3-67Df4osMniyYwDUBCB5Oq1tTx77xpU8", - "dq": "S4ooU1xNYYcjl9FcuJEEMqKsRrAXzzSKq6laPTwIp5dDwt2vXeAm1a4eDHXC-6rUSZGt5PbqVqzV4s-cjnJMI8YYkIdjNg4NSE1Ac_YpeDl3M3Colb5CQlU7yUB7xY2bt0NOOFp9UJZYJrOo09mFMGjy5eorsbitoZEbVqS3SuE", - "n": "nJbYKqFwnURKimaviyDFrNLD3gaKR1JW343Qem25VeZxoMq1665RHVoO8n1oBm4ClZdjIiZiVdpyqzD5-Ow12YQgQEf1ZHP3CCcOQQhU57Rh5XvScTe5IxYVkEW32IW2mp_CJ6WfjYpfeL4azarVk8H3Vr59d1rSrKTVVinVdZer9YLQyC_rWAQNtHafPBMrf6RYiNGV9EiYn72wFIXlLlBYQ9Fx7bfe1PaL6qrQSsZP3_rSpuvVdLh1lqGeCLR0pyclA9uo5m2tMyCXuuGQLbA_QJm5xEc7zd-WFdux2eXF045oxnSZ_kgQt-pdN7AxGWOVvwoTf9am6mSkEdv6iw", - }, - ) - self.parse_config() - - def test_registration_cannot_be_enabled(self) -> None: - self.config_dict["enable_registration"] = True - with self.assertRaises(ConfigError): - self.parse_config() - - def test_user_consent_cannot_be_enabled(self) -> None: - tmpdir = self.mktemp() - os.mkdir(tmpdir) - self.config_dict["user_consent"] = { - "require_at_registration": True, - "version": "1", - "template_dir": tmpdir, - "server_notice_content": { - "msgtype": "m.text", - "body": "foo", - }, - } - with self.assertRaises(ConfigError): - self.parse_config() - - def test_password_config_cannot_be_enabled(self) -> None: - self.config_dict["password_config"] = {"enabled": True} - with self.assertRaises(ConfigError): - self.parse_config() - - def test_oidc_sso_cannot_be_enabled(self) -> None: - self.config_dict["oidc_providers"] = [ - { - "idp_id": "microsoft", - "idp_name": "Microsoft", - "issuer": "https://login.microsoftonline.com//v2.0", - "client_id": "", - "client_secret": "", - "scopes": ["openid", "profile"], - "authorization_endpoint": "https://login.microsoftonline.com//oauth2/v2.0/authorize", - "token_endpoint": "https://login.microsoftonline.com//oauth2/v2.0/token", - "userinfo_endpoint": "https://graph.microsoft.com/oidc/userinfo", - } - ] - - with self.assertRaises(ConfigError): - self.parse_config() - - def test_cas_sso_cannot_be_enabled(self) -> None: - self.config_dict["cas_config"] = { - "enabled": True, - "server_url": "https://cas-server.com", - "displayname_attribute": "name", - "required_attributes": {"userGroup": "staff", "department": "None"}, - } - - with self.assertRaises(ConfigError): - self.parse_config() - - def test_auth_providers_cannot_be_enabled(self) -> None: - self.config_dict["modules"] = [ - { - "module": f"{__name__}.{CustomAuthModule.__qualname__}", - "config": {}, - } - ] - - # This requires actually setting up an HS, as the module will be run on setup, - # which should raise as the module tries to register an auth provider - config = self.parse_config() - reactor, clock = get_clock() - with self.assertRaises(ConfigError): - setup_test_homeserver( - cleanup_func=self.addCleanup, - config=config, - reactor=reactor, - clock=clock, - ) - - def test_jwt_auth_cannot_be_enabled(self) -> None: - self.config_dict["jwt_config"] = { - "enabled": True, - "secret": "my-secret-token", - "algorithm": "HS256", - } - - with self.assertRaises(ConfigError): - self.parse_config() - - def test_login_via_existing_session_cannot_be_enabled(self) -> None: - self.config_dict["login_via_existing_session"] = {"enabled": True} - with self.assertRaises(ConfigError): - self.parse_config() - - def test_captcha_cannot_be_enabled(self) -> None: - self.config_dict.update( - enable_registration_captcha=True, - recaptcha_public_key="test", - recaptcha_private_key="test", - ) - with self.assertRaises(ConfigError): - self.parse_config() - - def test_refreshable_tokens_cannot_be_enabled(self) -> None: - self.config_dict.update( - refresh_token_lifetime="24h", - refreshable_access_token_lifetime="10m", - nonrefreshable_access_token_lifetime="24h", - ) - with self.assertRaises(ConfigError): - self.parse_config() - - def test_session_lifetime_cannot_be_set(self) -> None: - self.config_dict["session_lifetime"] = "24h" - with self.assertRaises(ConfigError): - self.parse_config() - - def test_enable_3pid_changes_cannot_be_enabled(self) -> None: - self.config_dict["enable_3pid_changes"] = True - with self.assertRaises(ConfigError): - self.parse_config() - - class MasAuthDelegation(TestCase): """Test that the Homeserver fails to initialize if the config is invalid.""" diff --git a/tests/handlers/test_oauth_delegation.py b/tests/handlers/test_oauth_delegation.py index c88f2c2d15..6af2c068a4 100644 --- a/tests/handlers/test_oauth_delegation.py +++ b/tests/handlers/test_oauth_delegation.py @@ -22,20 +22,12 @@ import json import threading import time -from http import HTTPStatus from http.server import BaseHTTPRequestHandler, HTTPServer -from io import BytesIO from typing import Any, ClassVar, Coroutine, Generator, TypeVar, Union -from unittest.mock import ANY, AsyncMock, Mock +from unittest.mock import AsyncMock, Mock from urllib.parse import parse_qs from parameterized.parameterized import parameterized_class -from signedjson.key import ( - encode_verify_key_base64, - generate_signing_key, - get_verify_key, -) -from signedjson.sign import sign_json from twisted.internet.defer import Deferred, ensureDeferred from twisted.internet.testing import MemoryReactor @@ -44,21 +36,17 @@ from synapse.api.auth.mas import MasDelegatedAuth from synapse.api.errors import ( AuthError, Codes, - HttpResponseException, InvalidClientTokenError, SynapseError, ) from synapse.appservice import ApplicationService -from synapse.http.site import SynapseRequest from synapse.rest import admin from synapse.rest.client import account, devices, keys, login, logout, register from synapse.server import HomeServer from synapse.types import JsonDict, UserID, create_requester from synapse.util.clock import Clock -from tests.server import FakeChannel -from tests.test_utils import get_awaitable_result -from tests.unittest import HomeserverTestCase, override_config, skip_unless +from tests.unittest import HomeserverTestCase, skip_unless from tests.utils import HAS_AUTHLIB, checked_cast, mock_getRawHeaders # These are a few constants that are used as config parameters in the tests. @@ -107,599 +95,6 @@ async def get_json(url: str) -> JsonDict: @skip_unless(HAS_AUTHLIB, "requires authlib") -@parameterized_class( - ("device_scope_prefix", "api_scope"), - [ - ("urn:matrix:client:device:", "urn:matrix:client:api:*"), - ( - "urn:matrix:org.matrix.msc2967.client:device:", - "urn:matrix:org.matrix.msc2967.client:api:*", - ), - ], -) -class MSC3861OAuthDelegation(HomeserverTestCase): - device_scope_prefix: ClassVar[str] - api_scope: ClassVar[str] - - @property - def device_scope(self) -> str: - return self.device_scope_prefix + DEVICE - - servlets = [ - account.register_servlets, - keys.register_servlets, - ] - - def default_config(self) -> dict[str, Any]: - config = super().default_config() - config["public_baseurl"] = BASE_URL - config["disable_registration"] = True - config["experimental_features"] = { - "msc3861": { - "enabled": True, - "issuer": ISSUER, - "client_id": CLIENT_ID, - "client_auth_method": "client_secret_post", - "client_secret": CLIENT_SECRET, - "admin_token": "admin_token_value", - } - } - return config - - def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer: - self.http_client = Mock(spec=["get_json"]) - self.http_client.get_json.side_effect = get_json - self.http_client.user_agent = b"Synapse Test" - - hs = self.setup_test_homeserver(proxied_http_client=self.http_client) - - # Import this here so that we've checked that authlib is available. - from synapse.api.auth.msc3861_delegated import MSC3861DelegatedAuth - - self.auth = checked_cast(MSC3861DelegatedAuth, hs.get_auth()) - - self._rust_client = Mock(spec=["post"]) - self.auth._rust_http_client = self._rust_client - - return hs - - def prepare( - self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer - ) -> None: - # Provision the user and the device we use in the tests. - store = homeserver.get_datastores().main - self.get_success(store.register_user(USER_ID)) - self.get_success( - store.store_device(USER_ID, DEVICE, initial_device_display_name=None) - ) - - def _set_introspection_returnvalue(self, response_value: Any) -> AsyncMock: - self._rust_client.post = mock = AsyncMock( - return_value=json.dumps(response_value).encode("utf-8") - ) - return mock - - def _assertParams(self) -> None: - """Assert that the request parameters are correct.""" - params = parse_qs(self._rust_client.post.call_args[1]["request_body"]) - self.assertEqual(params["token"], ["mockAccessToken"]) - self.assertEqual(params["client_id"], [CLIENT_ID]) - self.assertEqual(params["client_secret"], [CLIENT_SECRET]) - - def test_inactive_token(self) -> None: - """The handler should return a 403 where the token is inactive.""" - - self._set_introspection_returnvalue({"active": False}) - request = Mock(args={}) - request.args[b"access_token"] = [b"mockAccessToken"] - request.requestHeaders.getRawHeaders = mock_getRawHeaders() - self.get_failure(self.auth.get_user_by_req(request), InvalidClientTokenError) - self.http_client.get_json.assert_called_once_with(WELL_KNOWN) - self._rust_client.post.assert_called_once_with( - url=INTROSPECTION_ENDPOINT, - response_limit=ANY, - request_body=ANY, - headers=ANY, - ) - self._assertParams() - - def test_active_no_scope(self) -> None: - """The handler should return a 403 where no scope is given.""" - - self._set_introspection_returnvalue({"active": True}) - request = Mock(args={}) - request.args[b"access_token"] = [b"mockAccessToken"] - request.requestHeaders.getRawHeaders = mock_getRawHeaders() - self.get_failure(self.auth.get_user_by_req(request), InvalidClientTokenError) - self.http_client.get_json.assert_called_once_with(WELL_KNOWN) - self._rust_client.post.assert_called_once_with( - url=INTROSPECTION_ENDPOINT, - response_limit=ANY, - request_body=ANY, - headers=ANY, - ) - self._assertParams() - - def test_active_user_no_subject(self) -> None: - """The handler should return a 500 when no subject is present.""" - - self._set_introspection_returnvalue( - {"active": True, "scope": " ".join([self.api_scope])}, - ) - - request = Mock(args={}) - request.args[b"access_token"] = [b"mockAccessToken"] - request.requestHeaders.getRawHeaders = mock_getRawHeaders() - self.get_failure(self.auth.get_user_by_req(request), InvalidClientTokenError) - self.http_client.get_json.assert_called_once_with(WELL_KNOWN) - self._rust_client.post.assert_called_once_with( - url=INTROSPECTION_ENDPOINT, - response_limit=ANY, - request_body=ANY, - headers=ANY, - ) - self._assertParams() - - def test_active_no_user_scope(self) -> None: - """The handler should return a 500 when no subject is present.""" - - self._set_introspection_returnvalue( - { - "active": True, - "sub": SUBJECT, - "scope": " ".join([self.device_scope]), - } - ) - request = Mock(args={}) - request.args[b"access_token"] = [b"mockAccessToken"] - request.requestHeaders.getRawHeaders = mock_getRawHeaders() - self.get_failure(self.auth.get_user_by_req(request), InvalidClientTokenError) - self.http_client.get_json.assert_called_once_with(WELL_KNOWN) - self._rust_client.post.assert_called_once_with( - url=INTROSPECTION_ENDPOINT, - response_limit=ANY, - request_body=ANY, - headers=ANY, - ) - self._assertParams() - - def test_active_admin_not_user(self) -> None: - """The handler should raise when the scope has admin right but not user.""" - - self._set_introspection_returnvalue( - { - "active": True, - "sub": SUBJECT, - "scope": " ".join([SYNAPSE_ADMIN_SCOPE]), - "username": USERNAME, - } - ) - request = Mock(args={}) - request.args[b"access_token"] = [b"mockAccessToken"] - request.requestHeaders.getRawHeaders = mock_getRawHeaders() - self.get_failure(self.auth.get_user_by_req(request), InvalidClientTokenError) - self.http_client.get_json.assert_called_once_with(WELL_KNOWN) - self._rust_client.post.assert_called_once_with( - url=INTROSPECTION_ENDPOINT, - response_limit=ANY, - request_body=ANY, - headers=ANY, - ) - self._assertParams() - - def test_active_admin(self) -> None: - """The handler should return a requester with admin rights.""" - - self._set_introspection_returnvalue( - { - "active": True, - "sub": SUBJECT, - "scope": " ".join([SYNAPSE_ADMIN_SCOPE, self.api_scope]), - "username": USERNAME, - } - ) - request = Mock(args={}) - request.args[b"access_token"] = [b"mockAccessToken"] - request.requestHeaders.getRawHeaders = mock_getRawHeaders() - requester = self.get_success(self.auth.get_user_by_req(request)) - self.http_client.get_json.assert_called_once_with(WELL_KNOWN) - self._rust_client.post.assert_called_once_with( - url=INTROSPECTION_ENDPOINT, - response_limit=ANY, - request_body=ANY, - headers=ANY, - ) - self._assertParams() - self.assertEqual(requester.user.to_string(), "@%s:%s" % (USERNAME, SERVER_NAME)) - self.assertEqual(requester.is_guest, False) - self.assertEqual(requester.device_id, None) - self.assertEqual( - get_awaitable_result(self.auth.is_server_admin(requester)), True - ) - - def test_active_admin_highest_privilege(self) -> None: - """The handler should resolve to the most permissive scope.""" - - self._set_introspection_returnvalue( - { - "active": True, - "sub": SUBJECT, - "scope": " ".join([SYNAPSE_ADMIN_SCOPE, self.api_scope]), - "username": USERNAME, - } - ) - request = Mock(args={}) - request.args[b"access_token"] = [b"mockAccessToken"] - request.requestHeaders.getRawHeaders = mock_getRawHeaders() - requester = self.get_success(self.auth.get_user_by_req(request)) - self.http_client.get_json.assert_called_once_with(WELL_KNOWN) - self._rust_client.post.assert_called_once_with( - url=INTROSPECTION_ENDPOINT, - response_limit=ANY, - request_body=ANY, - headers=ANY, - ) - self._assertParams() - self.assertEqual(requester.user.to_string(), "@%s:%s" % (USERNAME, SERVER_NAME)) - self.assertEqual(requester.is_guest, False) - self.assertEqual(requester.device_id, None) - self.assertEqual( - get_awaitable_result(self.auth.is_server_admin(requester)), True - ) - - def test_active_user(self) -> None: - """The handler should return a requester with normal user rights.""" - - self._set_introspection_returnvalue( - { - "active": True, - "sub": SUBJECT, - "scope": " ".join([self.api_scope]), - "username": USERNAME, - } - ) - request = Mock(args={}) - request.args[b"access_token"] = [b"mockAccessToken"] - request.requestHeaders.getRawHeaders = mock_getRawHeaders() - requester = self.get_success(self.auth.get_user_by_req(request)) - self.http_client.get_json.assert_called_once_with(WELL_KNOWN) - self._rust_client.post.assert_called_once_with( - url=INTROSPECTION_ENDPOINT, - response_limit=ANY, - request_body=ANY, - headers=ANY, - ) - self._assertParams() - self.assertEqual(requester.user.to_string(), "@%s:%s" % (USERNAME, SERVER_NAME)) - self.assertEqual(requester.is_guest, False) - self.assertEqual(requester.device_id, None) - self.assertEqual( - get_awaitable_result(self.auth.is_server_admin(requester)), False - ) - - def test_active_user_with_device(self) -> None: - """The handler should return a requester with normal user rights and a device ID.""" - - self._set_introspection_returnvalue( - { - "active": True, - "sub": SUBJECT, - "scope": " ".join([self.api_scope, self.device_scope]), - "username": USERNAME, - } - ) - request = Mock(args={}) - request.args[b"access_token"] = [b"mockAccessToken"] - request.requestHeaders.getRawHeaders = mock_getRawHeaders() - requester = self.get_success(self.auth.get_user_by_req(request)) - self.http_client.get_json.assert_called_once_with(WELL_KNOWN) - self._rust_client.post.assert_called_once_with( - url=INTROSPECTION_ENDPOINT, - response_limit=ANY, - request_body=ANY, - headers=ANY, - ) - self._assertParams() - self.assertEqual(requester.user.to_string(), "@%s:%s" % (USERNAME, SERVER_NAME)) - self.assertEqual(requester.is_guest, False) - self.assertEqual( - get_awaitable_result(self.auth.is_server_admin(requester)), False - ) - self.assertEqual(requester.device_id, DEVICE) - - def test_active_user_with_device_explicit_device_id(self) -> None: - """The handler should return a requester with normal user rights and a device ID, given explicitly, as supported by MAS 0.15+""" - - self._set_introspection_returnvalue( - { - "active": True, - "sub": SUBJECT, - "scope": " ".join([self.api_scope]), - "device_id": DEVICE, - "username": USERNAME, - } - ) - request = Mock(args={}) - request.args[b"access_token"] = [b"mockAccessToken"] - request.requestHeaders.getRawHeaders = mock_getRawHeaders() - requester = self.get_success(self.auth.get_user_by_req(request)) - self.http_client.get_json.assert_called_once_with(WELL_KNOWN) - self._rust_client.post.assert_called_once_with( - url=INTROSPECTION_ENDPOINT, - response_limit=ANY, - request_body=ANY, - headers=ANY, - ) - # It should have called with the 'X-MAS-Supports-Device-Id: 1' header - self.assertEqual( - self._rust_client.post.call_args[1]["headers"].get( - "X-MAS-Supports-Device-Id", - ), - "1", - ) - self._assertParams() - self.assertEqual(requester.user.to_string(), "@%s:%s" % (USERNAME, SERVER_NAME)) - self.assertEqual(requester.is_guest, False) - self.assertEqual( - get_awaitable_result(self.auth.is_server_admin(requester)), False - ) - self.assertEqual(requester.device_id, DEVICE) - - def test_multiple_devices(self) -> None: - """The handler should raise an error if multiple devices are found in the scope.""" - - self._set_introspection_returnvalue( - { - "active": True, - "sub": SUBJECT, - "scope": " ".join( - [ - self.api_scope, - f"{self.device_scope_prefix}AABBCC", - f"{self.device_scope_prefix}DDEEFF", - ] - ), - "username": USERNAME, - } - ) - request = Mock(args={}) - request.args[b"access_token"] = [b"mockAccessToken"] - request.requestHeaders.getRawHeaders = mock_getRawHeaders() - self.get_failure(self.auth.get_user_by_req(request), AuthError) - - def test_unavailable_introspection_endpoint(self) -> None: - """The handler should return an internal server error.""" - request = Mock(args={}) - request.args[b"access_token"] = [b"mockAccessToken"] - request.requestHeaders.getRawHeaders = mock_getRawHeaders() - - # The introspection endpoint is returning an error. - self._rust_client.post = AsyncMock( - side_effect=HttpResponseException( - code=500, msg="Internal Server Error", response=b"{}" - ) - ) - error = self.get_failure(self.auth.get_user_by_req(request), SynapseError) - self.assertEqual(error.value.code, 503) - - # The introspection endpoint request fails. - self._rust_client.post = AsyncMock(side_effect=Exception()) - error = self.get_failure(self.auth.get_user_by_req(request), SynapseError) - self.assertEqual(error.value.code, 503) - - # The introspection endpoint does not return a JSON object. - self._set_introspection_returnvalue(["this is an array", "not an object"]) - - error = self.get_failure(self.auth.get_user_by_req(request), SynapseError) - self.assertEqual(error.value.code, 503) - - # The introspection endpoint does not return valid JSON. - self._set_introspection_returnvalue("this is not valid JSON") - - error = self.get_failure(self.auth.get_user_by_req(request), SynapseError) - self.assertEqual(error.value.code, 503) - - def test_cached_expired_introspection(self) -> None: - """The handler should raise an error if the introspection response gives - an expiry time, the introspection response is cached and then the entry is - re-requested after it has expired.""" - - introspection_mock = self._set_introspection_returnvalue( - { - "active": True, - "sub": SUBJECT, - "scope": " ".join( - [ - self.api_scope, - f"{self.device_scope_prefix}AABBCC", - ] - ), - "username": USERNAME, - "expires_in": 60, - } - ) - - request = Mock(args={}) - request.args[b"access_token"] = [b"mockAccessToken"] - request.requestHeaders.getRawHeaders = mock_getRawHeaders() - - # The first CS-API request causes a successful introspection - self.get_success(self.auth.get_user_by_req(request)) - self.assertEqual(introspection_mock.call_count, 1) - - # Sleep for 60 seconds so the token expires. - self.reactor.advance(60.0) - - # Now the CS-API request fails because the token expired - self.get_failure(self.auth.get_user_by_req(request), InvalidClientTokenError) - # Ensure another introspection request was not sent - self.assertEqual(introspection_mock.call_count, 1) - - def make_device_keys(self, user_id: str, device_id: str) -> JsonDict: - # We only generate a master key to simplify the test. - master_signing_key = generate_signing_key(device_id) - master_verify_key = encode_verify_key_base64(get_verify_key(master_signing_key)) - - return { - "master_key": sign_json( - { - "user_id": user_id, - "usage": ["master"], - "keys": {"ed25519:" + master_verify_key: master_verify_key}, - }, - user_id, - master_signing_key, - ), - } - - def test_cross_signing(self) -> None: - """Try uploading device keys with OAuth delegation enabled.""" - - self._set_introspection_returnvalue( - { - "active": True, - "sub": SUBJECT, - "scope": " ".join([self.api_scope, self.device_scope]), - "username": USERNAME, - } - ) - keys_upload_body = self.make_device_keys(USER_ID, DEVICE) - channel = self.make_request( - "POST", - "/_matrix/client/v3/keys/device_signing/upload", - keys_upload_body, - access_token="mockAccessToken", - ) - - self.assertEqual(channel.code, 200, channel.json_body) - - # Try uploading *different* keys; it should cause a 501 error. - keys_upload_body = self.make_device_keys(USER_ID, DEVICE) - channel = self.make_request( - "POST", - "/_matrix/client/v3/keys/device_signing/upload", - keys_upload_body, - access_token="mockAccessToken", - ) - - self.assertEqual(channel.code, HTTPStatus.UNAUTHORIZED, channel.json_body) - - def test_admin_token(self) -> None: - """The handler should return a requester with admin rights when admin_token is used.""" - self._set_introspection_returnvalue({"active": False}) - - request = Mock(args={}) - request.args[b"access_token"] = [b"admin_token_value"] - request.requestHeaders.getRawHeaders = mock_getRawHeaders() - requester = self.get_success(self.auth.get_user_by_req(request)) - self.assertEqual( - requester.user.to_string(), - OIDC_ADMIN_USERID, - ) - self.assertEqual(requester.is_guest, False) - self.assertEqual(requester.device_id, None) - self.assertEqual( - get_awaitable_result(self.auth.is_server_admin(requester)), True - ) - - # There should be no call to the introspection endpoint - self._rust_client.post.assert_not_called() - - @override_config({"mau_stats_only": True}) - def test_request_tracking(self) -> None: - """Using an access token should update the client_ips and MAU tables.""" - # To start, there are no MAU users. - store = self.hs.get_datastores().main - mau = self.get_success(store.get_monthly_active_count()) - self.assertEqual(mau, 0) - - known_token = "token-token-GOOD-:)" - - async def mock_http_client_request( - url: str, request_body: str, **kwargs: Any - ) -> bytes: - """Mocked auth provider response.""" - token = parse_qs(request_body)["token"][0] - if token == known_token: - return json.dumps( - { - "active": True, - "scope": self.api_scope, - "sub": SUBJECT, - "username": USERNAME, - }, - ).encode("utf-8") - - return json.dumps({"active": False}).encode("utf-8") - - self._rust_client.post = mock_http_client_request - - EXAMPLE_IPV4_ADDR = "123.123.123.123" - EXAMPLE_USER_AGENT = "httprettygood" - - # First test a known access token - channel = FakeChannel(self.site, self.reactor) - # type-ignore: FakeChannel is a mock of an HTTPChannel, not a proper HTTPChannel - req = SynapseRequest(channel, self.site, self.hs.hostname) # type: ignore[arg-type] - req.client.host = EXAMPLE_IPV4_ADDR - req.requestHeaders.addRawHeader("Authorization", f"Bearer {known_token}") - req.requestHeaders.addRawHeader("User-Agent", EXAMPLE_USER_AGENT) - req.content = BytesIO(b"") - req.requestReceived( - b"GET", - b"/_matrix/client/v3/account/whoami", - b"1.1", - ) - channel.await_result() - self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body) - self.assertEqual(channel.json_body["user_id"], USER_ID, channel.json_body) - - # Expect to see one MAU entry, from the first request - mau = self.get_success(store.get_monthly_active_count()) - self.assertEqual(mau, 1) - - conn_infos = self.get_success( - store.get_user_ip_and_agents(UserID.from_string(USER_ID)) - ) - self.assertEqual(len(conn_infos), 1, conn_infos) - conn_info = conn_infos[0] - self.assertEqual(conn_info["access_token"], known_token) - self.assertEqual(conn_info["ip"], EXAMPLE_IPV4_ADDR) - self.assertEqual(conn_info["user_agent"], EXAMPLE_USER_AGENT) - - # Now test MAS making a request using the special __oidc_admin token - MAS_IPV4_ADDR = "127.0.0.1" - MAS_USER_AGENT = "masmasmas" - - channel = FakeChannel(self.site, self.reactor) - req = SynapseRequest(channel, self.site, self.hs.hostname) # type: ignore[arg-type] - req.client.host = MAS_IPV4_ADDR - req.requestHeaders.addRawHeader( - "Authorization", f"Bearer {self.auth._admin_token()}" - ) - req.requestHeaders.addRawHeader("User-Agent", MAS_USER_AGENT) - req.content = BytesIO(b"") - req.requestReceived( - b"GET", - b"/_matrix/client/v3/account/whoami", - b"1.1", - ) - channel.await_result() - self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body) - self.assertEqual( - channel.json_body["user_id"], OIDC_ADMIN_USERID, channel.json_body - ) - - # Still expect to see one MAU entry, from the first request - mau = self.get_success(store.get_monthly_active_count()) - self.assertEqual(mau, 1) - - conn_infos = self.get_success( - store.get_user_ip_and_agents(UserID.from_string(OIDC_ADMIN_USERID)) - ) - self.assertEqual(conn_infos, []) - - class FakeMasHandler(BaseHTTPRequestHandler): server: "FakeMasServer" @@ -1095,25 +490,7 @@ class MasAuthDelegationWithSubpath(MasAuthDelegation): }, }, ), - ] - # Run the tests with experimental delegation only if authlib is available - + [ - ( - { - "experimental_features": { - "msc3861": { - "enabled": True, - "issuer": ISSUER, - "client_id": CLIENT_ID, - "client_auth_method": "client_secret_post", - "client_secret": CLIENT_SECRET, - "admin_token": "admin_token_value", - } - } - }, - ), - ] - * HAS_AUTHLIB, + ], ) class DisabledEndpointsTestCase(HomeserverTestCase): servlets = [ diff --git a/tests/rest/admin/test_jwks.py b/tests/rest/admin/test_jwks.py deleted file mode 100644 index ee5588951b..0000000000 --- a/tests/rest/admin/test_jwks.py +++ /dev/null @@ -1,106 +0,0 @@ -# -# This file is licensed under the Affero General Public License (AGPL) version 3. -# -# Copyright 2023 The Matrix.org Foundation C.I.C. -# Copyright (C) 2023 New Vector, Ltd -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# See the GNU Affero General Public License for more details: -# . -# -# Originally licensed under the Apache License, Version 2.0: -# . -# -# [This file includes modifications made by New Vector Limited] -# -# - - -from twisted.web.resource import Resource - -from synapse.rest.synapse.client import build_synapse_client_resource_tree - -from tests.unittest import HomeserverTestCase, override_config, skip_unless -from tests.utils import HAS_AUTHLIB - - -@skip_unless(HAS_AUTHLIB, "requires authlib") -class JWKSTestCase(HomeserverTestCase): - """Test /_synapse/jwks JWKS data.""" - - def create_resource_dict(self) -> dict[str, Resource]: - d = super().create_resource_dict() - d.update(build_synapse_client_resource_tree(self.hs)) - return d - - def test_empty_jwks(self) -> None: - """Test that the JWKS endpoint is not present by default.""" - channel = self.make_request("GET", "/_synapse/jwks") - self.assertEqual(404, channel.code, channel.result) - - @override_config( - { - "disable_registration": True, - "experimental_features": { - "msc3861": { - "enabled": True, - "issuer": "https://issuer/", - "client_id": "test-client-id", - "client_auth_method": "client_secret_post", - "client_secret": "secret", - }, - }, - } - ) - def test_empty_jwks_for_msc3861_client_secret_post(self) -> None: - """Test that the JWKS endpoint is empty when plain auth is used.""" - channel = self.make_request("GET", "/_synapse/jwks") - self.assertEqual(200, channel.code, channel.result) - self.assertEqual({"keys": []}, channel.json_body) - - @override_config( - { - "disable_registration": True, - "experimental_features": { - "msc3861": { - "enabled": True, - "issuer": "https://issuer/", - "client_id": "test-client-id", - "client_auth_method": "private_key_jwt", - "jwk": { - "p": "-frVdP_tZ-J_nIR6HNMDq1N7aunwm51nAqNnhqIyuA8ikx7LlQED1tt2LD3YEvYyW8nxE2V95HlCRZXQPMiRJBFOsbmYkzl2t-MpavTaObB_fct_JqcRtdXddg4-_ihdjRDwUOreq_dpWh6MIKsC3UyekfkHmeEJg5YpOTL15j8", - "kty": "RSA", - "q": "oFw-Enr_YozQB1ab-kawn4jY3yHi8B1nSmYT0s8oTCflrmps5BFJfCkHL5ij3iY15z0o2m0N-jjB1oSJ98O4RayEEYNQlHnTNTl0kRIWzpoqblHUIxVcahIpP_xTovBJzwi8XXoLGqHOOMA-r40LSyVgP2Ut8D9qBwV6_UfT0LU", - "d": "WFkDPYo4b4LIS64D_QtQfGGuAObPvc3HFfp9VZXyq3SJR58XZRHE0jqtlEMNHhOTgbMYS3w8nxPQ_qVzY-5hs4fIanwvB64mAoOGl0qMHO65DTD_WsGFwzYClJPBVniavkLE2Hmpu8IGe6lGliN8vREC6_4t69liY-XcN_ECboVtC2behKkLOEASOIMuS7YcKAhTJFJwkl1dqDlliEn5A4u4xy7nuWQz3juB1OFdKlwGA5dfhDNglhoLIwNnkLsUPPFO-WB5ZNEW35xxHOToxj4bShvDuanVA6mJPtTKjz0XibjB36bj_nF_j7EtbE2PdGJ2KevAVgElR4lqS4ISgQ", - "e": "AQAB", - "kid": "test", - "qi": "cPfNk8l8W5exVNNea4d7QZZ8Qr8LgHghypYAxz8PQh1fNa8Ya1SNUDVzC2iHHhszxxA0vB9C7jGze8dBrvnzWYF1XvQcqNIVVgHhD57R1Nm3dj2NoHIKe0Cu4bCUtP8xnZQUN4KX7y4IIcgRcBWG1hT6DEYZ4BxqicnBXXNXAUI", - "dp": "dKlMHvslV1sMBQaKWpNb3gPq0B13TZhqr3-E2_8sPlvJ3fD8P4CmwwnOn50JDuhY3h9jY5L06sBwXjspYISVv8hX-ndMLkEeF3lrJeA5S70D8rgakfZcPIkffm3tlf1Ok3v5OzoxSv3-67Df4osMniyYwDUBCB5Oq1tTx77xpU8", - "dq": "S4ooU1xNYYcjl9FcuJEEMqKsRrAXzzSKq6laPTwIp5dDwt2vXeAm1a4eDHXC-6rUSZGt5PbqVqzV4s-cjnJMI8YYkIdjNg4NSE1Ac_YpeDl3M3Colb5CQlU7yUB7xY2bt0NOOFp9UJZYJrOo09mFMGjy5eorsbitoZEbVqS3SuE", - "n": "nJbYKqFwnURKimaviyDFrNLD3gaKR1JW343Qem25VeZxoMq1665RHVoO8n1oBm4ClZdjIiZiVdpyqzD5-Ow12YQgQEf1ZHP3CCcOQQhU57Rh5XvScTe5IxYVkEW32IW2mp_CJ6WfjYpfeL4azarVk8H3Vr59d1rSrKTVVinVdZer9YLQyC_rWAQNtHafPBMrf6RYiNGV9EiYn72wFIXlLlBYQ9Fx7bfe1PaL6qrQSsZP3_rSpuvVdLh1lqGeCLR0pyclA9uo5m2tMyCXuuGQLbA_QJm5xEc7zd-WFdux2eXF045oxnSZ_kgQt-pdN7AxGWOVvwoTf9am6mSkEdv6iw", - }, - }, - }, - } - ) - def test_key_returned_for_msc3861_client_secret_post(self) -> None: - """Test that the JWKS includes public part of JWK for private_key_jwt auth is used.""" - channel = self.make_request("GET", "/_synapse/jwks") - self.assertEqual(200, channel.code, channel.result) - self.assertEqual( - { - "keys": [ - { - "kty": "RSA", - "e": "AQAB", - "kid": "test", - "n": "nJbYKqFwnURKimaviyDFrNLD3gaKR1JW343Qem25VeZxoMq1665RHVoO8n1oBm4ClZdjIiZiVdpyqzD5-Ow12YQgQEf1ZHP3CCcOQQhU57Rh5XvScTe5IxYVkEW32IW2mp_CJ6WfjYpfeL4azarVk8H3Vr59d1rSrKTVVinVdZer9YLQyC_rWAQNtHafPBMrf6RYiNGV9EiYn72wFIXlLlBYQ9Fx7bfe1PaL6qrQSsZP3_rSpuvVdLh1lqGeCLR0pyclA9uo5m2tMyCXuuGQLbA_QJm5xEc7zd-WFdux2eXF045oxnSZ_kgQt-pdN7AxGWOVvwoTf9am6mSkEdv6iw", - } - ] - }, - channel.json_body, - ) diff --git a/tests/rest/client/test_auth_metadata.py b/tests/rest/client/test_auth_metadata.py index c13d410636..e9f8597c5a 100644 --- a/tests/rest/client/test_auth_metadata.py +++ b/tests/rest/client/test_auth_metadata.py @@ -19,16 +19,12 @@ # from http import HTTPStatus from typing import ClassVar -from unittest.mock import AsyncMock from parameterized import parameterized_class from synapse.rest.client import auth_metadata -from tests.unittest import HomeserverTestCase, override_config, skip_unless -from tests.utils import HAS_AUTHLIB - -ISSUER = "https://account.example.com/" +from tests.unittest import HomeserverTestCase class AuthIssuerTestCase(HomeserverTestCase): @@ -36,7 +32,7 @@ class AuthIssuerTestCase(HomeserverTestCase): auth_metadata.register_servlets, ] - def test_returns_404_when_msc3861_disabled(self) -> None: + def test_returns_404_when_mas_disabled(self) -> None: # Make an unauthenticated request for the discovery info. channel = self.make_request( "GET", @@ -44,49 +40,6 @@ class AuthIssuerTestCase(HomeserverTestCase): ) self.assertEqual(channel.code, HTTPStatus.NOT_FOUND) - @skip_unless(HAS_AUTHLIB, "requires authlib") - @override_config( - { - "disable_registration": True, - "experimental_features": { - "msc3861": { - "enabled": True, - "issuer": ISSUER, - "client_id": "David Lister", - "client_auth_method": "client_secret_post", - "client_secret": "Who shot Mister Burns?", - } - }, - } - ) - def test_returns_issuer_when_oidc_enabled(self) -> None: - # Patch the HTTP client to return the issuer metadata - req_mock = AsyncMock(return_value={"issuer": ISSUER}) - self.hs.get_proxied_http_client().get_json = req_mock # type: ignore[method-assign] - - channel = self.make_request( - "GET", - "/_matrix/client/unstable/org.matrix.msc2965/auth_issuer", - ) - - self.assertEqual(channel.code, HTTPStatus.OK) - self.assertEqual(channel.json_body, {"issuer": ISSUER}) - - req_mock.assert_called_with( - "https://account.example.com/.well-known/openid-configuration" - ) - req_mock.reset_mock() - - # Second call it should use the cached value - channel = self.make_request( - "GET", - "/_matrix/client/unstable/org.matrix.msc2965/auth_issuer", - ) - - self.assertEqual(channel.code, HTTPStatus.OK) - self.assertEqual(channel.json_body, {"issuer": ISSUER}) - req_mock.assert_not_called() - @parameterized_class( ("endpoint",), @@ -101,45 +54,7 @@ class AuthMetadataTestCase(HomeserverTestCase): auth_metadata.register_servlets, ] - def test_returns_404_when_msc3861_disabled(self) -> None: + def test_returns_404_when_mas_disabled(self) -> None: # Make an unauthenticated request for the discovery info. channel = self.make_request("GET", self.endpoint) self.assertEqual(channel.code, HTTPStatus.NOT_FOUND) - - @skip_unless(HAS_AUTHLIB, "requires authlib") - @override_config( - { - "disable_registration": True, - "experimental_features": { - "msc3861": { - "enabled": True, - "issuer": ISSUER, - "client_id": "David Lister", - "client_auth_method": "client_secret_post", - "client_secret": "Who shot Mister Burns?", - } - }, - } - ) - def test_returns_issuer_when_oidc_enabled(self) -> None: - # Patch the HTTP client to return the issuer metadata - req_mock = AsyncMock( - return_value={ - "issuer": ISSUER, - "authorization_endpoint": "https://example.com/auth", - "token_endpoint": "https://example.com/token", - } - ) - self.hs.get_proxied_http_client().get_json = req_mock # type: ignore[method-assign] - - channel = self.make_request("GET", self.endpoint) - - self.assertEqual(channel.code, HTTPStatus.OK) - self.assertEqual( - channel.json_body, - { - "issuer": ISSUER, - "authorization_endpoint": "https://example.com/auth", - "token_endpoint": "https://example.com/token", - }, - ) diff --git a/tests/rest/client/test_keys.py b/tests/rest/client/test_keys.py index fc21d83ef2..4f3d6a365f 100644 --- a/tests/rest/client/test_keys.py +++ b/tests/rest/client/test_keys.py @@ -18,9 +18,7 @@ # [This file includes modifications made by New Vector Limited] # # -import urllib.parse from http import HTTPStatus -from unittest.mock import patch from signedjson.key import ( encode_verify_key_base64, @@ -32,12 +30,10 @@ from signedjson.sign import sign_json from synapse.api.errors import Codes from synapse.rest import admin from synapse.rest.client import keys, login -from synapse.types import JsonDict, Requester, create_requester +from synapse.types import JsonDict from tests import unittest from tests.http.server._base import make_request_with_cancellation_test -from tests.unittest import override_config -from tests.utils import HAS_AUTHLIB class KeyUploadTestCase(unittest.HomeserverTestCase): @@ -355,191 +351,3 @@ class SigningKeyUploadServletTestCase(unittest.HomeserverTestCase): OIDC_ADMIN_TOKEN = "_oidc_admin_token" ACCOUNT_MANAGEMENT_URL = "https://my-account.issuer" - - @unittest.skip_unless(HAS_AUTHLIB, "requires authlib") - @override_config( - { - "enable_registration": False, - "experimental_features": { - "msc3861": { - "enabled": True, - "issuer": "https://issuer", - "account_management_url": ACCOUNT_MANAGEMENT_URL, - "client_id": "id", - "client_auth_method": "client_secret_post", - "client_secret": "secret", - "admin_token": OIDC_ADMIN_TOKEN, - }, - }, - } - ) - def test_master_cross_signing_key_replacement_msc3861(self) -> None: - # Provision a user like MAS would, cribbing from - # https://github.com/matrix-org/matrix-authentication-service/blob/08d46a79a4adb22819ac9d55e15f8375dfe2c5c7/crates/matrix-synapse/src/lib.rs#L224-L229 - alice = "@alice:test" - channel = self.make_request( - "PUT", - f"/_synapse/admin/v2/users/{urllib.parse.quote(alice)}", - access_token=self.OIDC_ADMIN_TOKEN, - content={}, - ) - self.assertEqual(channel.code, HTTPStatus.CREATED, channel.json_body) - - # Provision a device like MAS would, cribbing from - # https://github.com/matrix-org/matrix-authentication-service/blob/08d46a79a4adb22819ac9d55e15f8375dfe2c5c7/crates/matrix-synapse/src/lib.rs#L260-L262 - alice_device = "alice_device" - channel = self.make_request( - "POST", - f"/_synapse/admin/v2/users/{urllib.parse.quote(alice)}/devices", - access_token=self.OIDC_ADMIN_TOKEN, - content={"device_id": alice_device}, - ) - self.assertEqual(channel.code, HTTPStatus.CREATED, channel.json_body) - - # Prepare a mock MAS access token. - alice_token = "alice_token_1234_oidcwhatyoudidthere" - - async def mocked_get_user_by_access_token( - token: str, allow_expired: bool = False - ) -> Requester: - self.assertEqual(token, alice_token) - return create_requester( - user_id=alice, - device_id=alice_device, - scope=[], - is_guest=False, - ) - - patch_get_user_by_access_token = patch.object( - self.hs.get_auth(), - "get_user_by_access_token", - wraps=mocked_get_user_by_access_token, - ) - - # Copied from E2eKeysHandlerTestCase - master_pubkey = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk" - master_pubkey2 = "fHZ3NPiKxoLQm5OoZbKa99SYxprOjNs4TwJUKP+twCM" - master_pubkey3 = "85T7JXPFBAySB/jwby4S3lBPTqY3+Zg53nYuGmu1ggY" - - master_key: JsonDict = { - "user_id": alice, - "usage": ["master"], - "keys": {"ed25519:" + master_pubkey: master_pubkey}, - } - master_key2: JsonDict = { - "user_id": alice, - "usage": ["master"], - "keys": {"ed25519:" + master_pubkey2: master_pubkey2}, - } - master_key3: JsonDict = { - "user_id": alice, - "usage": ["master"], - "keys": {"ed25519:" + master_pubkey3: master_pubkey3}, - } - - with patch_get_user_by_access_token: - # Upload an initial cross-signing key. - channel = self.make_request( - "POST", - "/_matrix/client/v3/keys/device_signing/upload", - access_token=alice_token, - content={ - "master_key": master_key, - }, - ) - self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body) - - # Should not be able to upload another master key. - channel = self.make_request( - "POST", - "/_matrix/client/v3/keys/device_signing/upload", - access_token=alice_token, - content={ - "master_key": master_key2, - }, - ) - self.assertEqual(channel.code, HTTPStatus.UNAUTHORIZED, channel.json_body) - # Ensure that the response contains the expected UIA flows from https://spec.matrix.org/v1.17/client-server-api/#oauth-authentication - self.assertIn( - {"stages": ["m.oauth"]}, - channel.json_body["flows"], - "m.oauth flow not found", - ) - self.assertSubstring( - self.ACCOUNT_MANAGEMENT_URL, - channel.json_body["params"]["m.oauth"]["url"], - "m.oauth url does not match account management URL", - ) - self.assertSubstring( - "action=org.matrix.cross_signing_reset", - channel.json_body["params"]["m.oauth"]["url"], - "m.oauth url does not include expected action", - ) - # Unstable version of the flow - self.assertIn( - {"stages": ["org.matrix.cross_signing_reset"]}, - channel.json_body["flows"], - "unstable org.matrix.cross_signing_reset flow not found", - ) - self.assertEqual( - channel.json_body["params"]["org.matrix.cross_signing_reset"]["url"], - channel.json_body["params"]["m.oauth"]["url"], - "unstable org.matrix.cross_signing_reset url does not match m.oauth url", - ) - - # Pretend that MAS did UIA and allowed us to replace the master key. - channel = self.make_request( - "POST", - f"/_synapse/admin/v1/users/{urllib.parse.quote(alice)}/_allow_cross_signing_replacement_without_uia", - access_token=self.OIDC_ADMIN_TOKEN, - ) - self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body) - - with patch_get_user_by_access_token: - # Should now be able to upload master key2. - channel = self.make_request( - "POST", - "/_matrix/client/v3/keys/device_signing/upload", - access_token=alice_token, - content={ - "master_key": master_key2, - }, - ) - self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body) - - # Even though we're still in the grace period, we shouldn't be able to - # upload master key 3 immediately after uploading key 2. - channel = self.make_request( - "POST", - "/_matrix/client/v3/keys/device_signing/upload", - access_token=alice_token, - content={ - "master_key": master_key3, - }, - ) - self.assertEqual(channel.code, HTTPStatus.UNAUTHORIZED, channel.json_body) - - # Pretend that MAS did UIA and allowed us to replace the master key. - channel = self.make_request( - "POST", - f"/_synapse/admin/v1/users/{urllib.parse.quote(alice)}/_allow_cross_signing_replacement_without_uia", - access_token=self.OIDC_ADMIN_TOKEN, - ) - self.assertEqual(HTTPStatus.OK, channel.code, msg=channel.json_body) - timestamp_ms = channel.json_body["updatable_without_uia_before_ms"] - - # Advance to 1 second after the replacement period ends. - self.reactor.advance(timestamp_ms - self.clock.time_msec() + 1000) - - with patch_get_user_by_access_token: - # We should not be able to upload master key3 because the replacement has - # expired. - channel = self.make_request( - "POST", - "/_matrix/client/v3/keys/device_signing/upload", - access_token=alice_token, - content={ - "master_key": master_key3, - }, - ) - self.assertEqual(channel.code, HTTPStatus.UNAUTHORIZED, channel.json_body) diff --git a/tests/rest/client/test_rendezvous.py b/tests/rest/client/test_rendezvous.py index dc4f833fa2..c2bd7d1573 100644 --- a/tests/rest/client/test_rendezvous.py +++ b/tests/rest/client/test_rendezvous.py @@ -61,14 +61,11 @@ class RendezvousServletTestCase(unittest.HomeserverTestCase): "disable_registration": True, "experimental_features": { "msc4108_delegation_endpoint": "https://asd", - "msc3861": { - "enabled": True, - "issuer": "https://issuer", - "client_id": "client_id", - "client_auth_method": "client_secret_post", - "client_secret": "client_secret", - "admin_token": "admin_token_value", - }, + }, + "matrix_authentication_service": { + "enabled": True, + "endpoint": "http://localhost:8080/", + "secret": "secret", }, } ) @@ -83,14 +80,11 @@ class RendezvousServletTestCase(unittest.HomeserverTestCase): "disable_registration": True, "experimental_features": { "msc4108_enabled": True, - "msc3861": { - "enabled": True, - "issuer": "https://issuer", - "client_id": "client_id", - "client_auth_method": "client_secret_post", - "client_secret": "client_secret", - "admin_token": "admin_token_value", - }, + }, + "matrix_authentication_service": { + "enabled": True, + "endpoint": "http://localhost:8080/", + "secret": "secret", }, } ) @@ -227,14 +221,11 @@ class RendezvousServletTestCase(unittest.HomeserverTestCase): "disable_registration": True, "experimental_features": { "msc4108_enabled": True, - "msc3861": { - "enabled": True, - "issuer": "https://issuer", - "client_id": "client_id", - "client_auth_method": "client_secret_post", - "client_secret": "client_secret", - "admin_token": "admin_token_value", - }, + }, + "matrix_authentication_service": { + "enabled": True, + "endpoint": "http://localhost:8080/", + "secret": "secret", }, } ) @@ -279,14 +270,11 @@ class RendezvousServletTestCase(unittest.HomeserverTestCase): "disable_registration": True, "experimental_features": { "msc4108_enabled": True, - "msc3861": { - "enabled": True, - "issuer": "https://issuer", - "client_id": "client_id", - "client_auth_method": "client_secret_post", - "client_secret": "client_secret", - "admin_token": "admin_token_value", - }, + }, + "matrix_authentication_service": { + "enabled": True, + "endpoint": "http://localhost:8080/", + "secret": "secret", }, } ) @@ -351,14 +339,11 @@ class RendezvousServletTestCase(unittest.HomeserverTestCase): "disable_registration": True, "experimental_features": { "msc4108_enabled": True, - "msc3861": { - "enabled": True, - "issuer": "https://issuer", - "client_id": "client_id", - "client_auth_method": "client_secret_post", - "client_secret": "client_secret", - "admin_token": "admin_token_value", - }, + }, + "matrix_authentication_service": { + "enabled": True, + "endpoint": "http://localhost:8080/", + "secret": "secret", }, } ) @@ -415,14 +400,11 @@ class RendezvousServletTestCase(unittest.HomeserverTestCase): "disable_registration": True, "experimental_features": { "msc4108_enabled": True, - "msc3861": { - "enabled": True, - "issuer": "https://issuer", - "client_id": "client_id", - "client_auth_method": "client_secret_post", - "client_secret": "client_secret", - "admin_token": "admin_token_value", - }, + }, + "matrix_authentication_service": { + "enabled": True, + "endpoint": "http://localhost:8080/", + "secret": "secret", }, } ) diff --git a/tests/rest/synapse/mas/_base.py b/tests/rest/synapse/mas/_base.py index 19d33807a6..fb2419162e 100644 --- a/tests/rest/synapse/mas/_base.py +++ b/tests/rest/synapse/mas/_base.py @@ -25,15 +25,10 @@ class BaseTestCase(unittest.HomeserverTestCase): def default_config(self) -> JsonDict: config = super().default_config() config["enable_registration"] = False - config["experimental_features"] = { - "msc3861": { - "enabled": True, - "issuer": "https://example.com", - "client_id": "dummy", - "client_auth_method": "client_secret_basic", - "client_secret": "dummy", - "admin_token": self.SHARED_SECRET, - } + config["matrix_authentication_service"] = { + "enabled": True, + "endpoint": "http://localhost:8080/", + "secret": self.SHARED_SECRET, } return config diff --git a/tests/rest/test_well_known.py b/tests/rest/test_well_known.py index c73717f014..3accad842c 100644 --- a/tests/rest/test_well_known.py +++ b/tests/rest/test_well_known.py @@ -17,6 +17,7 @@ # [This file includes modifications made by New Vector Limited] # # + from unittest.mock import AsyncMock from twisted.web.resource import Resource @@ -24,7 +25,6 @@ from twisted.web.resource import Resource from synapse.rest.well_known import well_known_resource from tests import unittest -from tests.utils import HAS_AUTHLIB class WellKnownTests(unittest.HomeserverTestCase): @@ -106,23 +106,18 @@ class WellKnownTests(unittest.HomeserverTestCase): ) self.assertEqual(channel.code, 404) - @unittest.skip_unless(HAS_AUTHLIB, "requires authlib") @unittest.override_config( { "public_baseurl": "https://homeserver", # this is only required so that client well known is served - "experimental_features": { - "msc3861": { - "enabled": True, - "issuer": "https://issuer", - "client_id": "id", - "client_auth_method": "client_secret_post", - "client_secret": "secret", - }, + "matrix_authentication_service": { + "enabled": True, + "endpoint": "https://issuer", + "secret": "secret", }, "disable_registration": True, } ) - def test_client_well_known_msc3861_oauth_delegation(self) -> None: + def test_client_well_known_oauth_delegation(self) -> None: # Patch the HTTP client to return the issuer metadata req_mock = AsyncMock( return_value={