From 3aaecb4f355bfcf1437bdcb72d5ef5b4ad0188ba Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Fri, 10 Oct 2025 17:48:49 +0100 Subject: [PATCH] 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. --- synapse/handlers/auth.py | 31 +++++++++++++++++++++---------- synapse/rest/client/auth.py | 9 +++++++-- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 2d1990cce5..e38d0924e6 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -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 diff --git a/synapse/rest/client/auth.py b/synapse/rest/client/auth.py index 600bb51a7e..2a42520907 100644 --- a/synapse/rest/client/auth.py +++ b/synapse/rest/client/auth.py @@ -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(