1
0

Compare commits

...

3 Commits

Author SHA1 Message Date
Andrew Morgan
3aaecb4f35 Add an unstable, namespaced idp_id query parameter to fallback/web
This allows clients to specify the identity provider they'd like to log
in with for SSO when they have multiple upstream IdPs associated with
their account.

Previously, Synapse would just pick one arbitrarily. But this was
undesirable as you may want to use a different one at that point in
time. When logging in, the user is able to choose when IdP they use -
during UIA (which uses fallback auth mechanism) they should be able to
do the same.
2025-10-10 17:49:04 +01:00
Andrew Morgan
036fb87584 1.139.2 2025-10-07 16:30:03 +01:00
Andrew Morgan
5e3839e2af Update KeyUploadServlet to handle case where client sends device_keys: null (#19023) 2025-10-07 16:28:26 +01:00
7 changed files with 67 additions and 16 deletions

View File

@@ -1,3 +1,12 @@
# Synapse 1.139.2 (2025-10-07)
## Bugfixes
- Fix a bug introduced in 1.139.1 where a client could receive an Internal Server Error if they set `device_keys: null` in the request to [`POST /_matrix/client/v3/keys/upload`](https://spec.matrix.org/v1.16/client-server-api/#post_matrixclientv3keysupload). ([\#19023](https://github.com/element-hq/synapse/issues/19023))
# Synapse 1.139.1 (2025-10-07)
## Security Fixes

6
debian/changelog vendored
View File

@@ -1,3 +1,9 @@
matrix-synapse-py3 (1.139.2) stable; urgency=medium
* New Synapse release 1.139.2.
-- Synapse Packaging team <packages@matrix.org> Tue, 07 Oct 2025 16:29:47 +0100
matrix-synapse-py3 (1.139.1) stable; urgency=medium
* New Synapse release 1.139.1.

View File

@@ -101,7 +101,7 @@ module-name = "synapse.synapse_rust"
[tool.poetry]
name = "matrix-synapse"
version = "1.139.1"
version = "1.139.2"
description = "Homeserver for the Matrix decentralised comms protocol"
authors = ["Matrix.org Team and Contributors <packages@matrix.org>"]
license = "AGPL-3.0-or-later"

View File

@@ -1722,13 +1722,17 @@ class AuthHandler:
else:
return False
async def start_sso_ui_auth(self, request: SynapseRequest, session_id: str) -> str:
async def start_sso_ui_auth(
self, request: SynapseRequest, session_id: str, preferred_idp_id: Optional[str]
) -> str:
"""
Get the HTML for the SSO redirect confirmation page.
Args:
request: The incoming HTTP request
session_id: The user interactive authentication session ID.
preferred_idp_id: The ID of the identity provider to use. If `None` one will
be picked randomly from those the user has already signed in with.
Returns:
The HTML to render.
@@ -1752,15 +1756,22 @@ class AuthHandler:
# it not being offered.
raise SynapseError(400, "User has no SSO identities")
# for now, just pick one
idp_id, sso_auth_provider = next(iter(idps.items()))
if len(idps) > 0:
logger.warning(
"User %r has previously logged in with multiple SSO IdPs; arbitrarily "
"picking %r",
user_id_to_verify,
idp_id,
)
if preferred_idp_id is not None:
# Use the idp specified by the client.
sso_auth_provider = idps.get(preferred_idp_id)
if sso_auth_provider is None:
raise SynapseError(400, "Unknown IdP %s" % (preferred_idp_id,))
else:
idp_id, sso_auth_provider = next(iter(idps.items()))
if len(idps) > 0:
# We arbitrarily picked an IdP from multiple potential
# candidates. This may be undesirable for the user.
logger.warning(
"User %r has previously logged in with multiple SSO IdPs; arbitrarily "
"picking %r",
user_id_to_verify,
idp_id,
)
redirect_url = await sso_auth_provider.handle_redirect_request(
request, None, session_id

View File

@@ -20,7 +20,7 @@
#
import logging
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional
from twisted.web.server import Request
@@ -67,6 +67,11 @@ class AuthRestServlet(RestServlet):
if not session:
raise SynapseError(400, "No session supplied")
# Unstable query parameter which allows clients to specify the IDP
# they wish to use for SSO.
# XXX: This needs an MSC and an experimental flag.
idp_id: Optional[str] = parse_string(request, "io.element.idp_id")
if stagetype == "org.matrix.cross_signing_reset":
if self.hs.config.mas.enabled:
assert isinstance(self.auth, MasDelegatedAuth)
@@ -114,7 +119,7 @@ class AuthRestServlet(RestServlet):
elif stagetype == LoginType.SSO:
# Display a confirmation page which prompts the user to
# re-authenticate with their SSO provider.
html = await self.auth_handler.start_sso_ui_auth(request, session)
html = await self.auth_handler.start_sso_ui_auth(request, session, idp_id)
elif stagetype == LoginType.REGISTRATION_TOKEN:
html = self.registration_token_template.render(

View File

@@ -270,7 +270,7 @@ class KeyUploadServlet(RestServlet):
400, "To upload keys, you must pass device_id when authenticating"
)
if "device_keys" in body:
if "device_keys" in body and isinstance(body["device_keys"], dict):
# Validate the provided `user_id` and `device_id` fields in
# `device_keys` match that of the requesting user. We can't do
# this directly in the pydantic model as we don't have access
@@ -278,13 +278,13 @@ class KeyUploadServlet(RestServlet):
#
# TODO: We could use ValidationInfo when we switch to Pydantic v2.
# https://docs.pydantic.dev/latest/concepts/validators/#validation-info
if body["device_keys"]["user_id"] != user_id:
if body["device_keys"].get("user_id") != user_id:
raise SynapseError(
code=HTTPStatus.BAD_REQUEST,
errcode=Codes.BAD_JSON,
msg="Provided `user_id` in `device_keys` does not match that of the authenticated user",
)
if body["device_keys"]["device_id"] != device_id:
if body["device_keys"].get("device_id") != device_id:
raise SynapseError(
code=HTTPStatus.BAD_REQUEST,
errcode=Codes.BAD_JSON,

View File

@@ -160,6 +160,26 @@ class KeyUploadTestCase(unittest.HomeserverTestCase):
channel.result,
)
def test_upload_keys_succeeds_when_fields_are_explicitly_set_to_null(self) -> None:
"""
This is a regression test for https://github.com/element-hq/synapse/pull/19023.
"""
device_id = "DEVICE_ID"
self.register_user("alice", "wonderland")
alice_token = self.login("alice", "wonderland", device_id=device_id)
channel = self.make_request(
"POST",
"/_matrix/client/v3/keys/upload",
{
"device_keys": None,
"one_time_keys": None,
"fallback_keys": None,
},
alice_token,
)
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
class KeyQueryTestCase(unittest.HomeserverTestCase):
servlets = [