From 4dcf113bffc3de557d86fcb90bd47b12a5f287e6 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 7 Jan 2026 12:52:21 +0000 Subject: [PATCH] Support for stable `m.oauth` UIA stage for MSC4312 (#19273) --- changelog.d/19273.feature | 1 + synapse/rest/client/auth.py | 6 +++++- synapse/rest/client/keys.py | 10 ++++++++++ tests/rest/client/test_keys.py | 30 +++++++++++++++++++++++++++++- 4 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 changelog.d/19273.feature diff --git a/changelog.d/19273.feature b/changelog.d/19273.feature new file mode 100644 index 0000000000..ef849b3c37 --- /dev/null +++ b/changelog.d/19273.feature @@ -0,0 +1 @@ +Stabilise support for [MSC4312](https://github.com/matrix-org/matrix-spec-proposals/pull/4312)'s `m.oauth` User-Interactive Auth stage for resetting cross-signing identity with the OAuth 2.0 API. The old, unstable name (`org.matrix.cross_signing_reset`) is now deprecated and will be removed in a future release. \ No newline at end of file diff --git a/synapse/rest/client/auth.py b/synapse/rest/client/auth.py index 600bb51a7e..f325499044 100644 --- a/synapse/rest/client/auth.py +++ b/synapse/rest/client/auth.py @@ -67,7 +67,11 @@ class AuthRestServlet(RestServlet): if not session: raise SynapseError(400, "No session supplied") - if stagetype == "org.matrix.cross_signing_reset": + # We support the unstable (`org.matrix.cross_signing_reset`) name from MSC4312 until + # enough clients have adopted the stable name (`m.oauth`). + # Note: `org.matrix.cross_signing_reset` *is* the stable name of the *action* in the + # authorization server metadata. The unstable status only applies to the UIA stage name. + if stagetype == "m.oauth" or stagetype == "org.matrix.cross_signing_reset": if self.hs.config.mas.enabled: assert isinstance(self.auth, MasDelegatedAuth) diff --git a/synapse/rest/client/keys.py b/synapse/rest/client/keys.py index 5f488674b4..502c5d495a 100644 --- a/synapse/rest/client/keys.py +++ b/synapse/rest/client/keys.py @@ -560,9 +560,14 @@ class SigningKeyUploadServlet(RestServlet): { "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, }, @@ -594,9 +599,14 @@ class SigningKeyUploadServlet(RestServlet): { "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, }, diff --git a/tests/rest/client/test_keys.py b/tests/rest/client/test_keys.py index 817edfb8d3..9c83a284d7 100644 --- a/tests/rest/client/test_keys.py +++ b/tests/rest/client/test_keys.py @@ -353,6 +353,7 @@ 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( @@ -362,7 +363,7 @@ class SigningKeyUploadServletTestCase(unittest.HomeserverTestCase): "msc3861": { "enabled": True, "issuer": "https://issuer", - "account_management_url": "https://my-account.issuer", + "account_management_url": ACCOUNT_MANAGEMENT_URL, "client_id": "id", "client_auth_method": "client_secret_post", "client_secret": "secret", @@ -457,6 +458,33 @@ class SigningKeyUploadServletTestCase(unittest.HomeserverTestCase): }, ) 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(