-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Simplify the flow for SSO UIA #8881
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Simplify logic for handling user-interactive-auth via single-sign-on servers. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -183,6 +183,24 @@ async def handle_saml_response(self, request: SynapseRequest) -> None: | |
saml2_auth.in_response_to, None | ||
) | ||
|
||
# first check if we're doing a UIA | ||
if current_session and current_session.ui_auth_session_id: | ||
try: | ||
remote_user_id = self._remote_id_from_saml_response(saml2_auth, None) | ||
except MappingException as e: | ||
logger.exception("Failed to extract remote user id from SAML response") | ||
self._sso_handler.render_error(request, "mapping_error", str(e)) | ||
return | ||
|
||
return await self._sso_handler.complete_sso_ui_auth_request( | ||
self._auth_provider_id, | ||
remote_user_id, | ||
current_session.ui_auth_session_id, | ||
request, | ||
) | ||
|
||
# otherwise, we're handling a login request. | ||
|
||
# Ensure that the attributes of the logged in user meet the required | ||
# attributes. | ||
for requirement in self._saml2_attribute_requirements: | ||
|
@@ -206,14 +224,7 @@ async def handle_saml_response(self, request: SynapseRequest) -> None: | |
self._sso_handler.render_error(request, "mapping_error", str(e)) | ||
return | ||
|
||
# Complete the interactive auth session or the login. | ||
if current_session and current_session.ui_auth_session_id: | ||
await self._auth_handler.complete_sso_ui_auth( | ||
user_id, current_session.ui_auth_session_id, request | ||
) | ||
|
||
else: | ||
await self._auth_handler.complete_sso_login(user_id, request, relay_state) | ||
await self._auth_handler.complete_sso_login(user_id, request, relay_state) | ||
|
||
async def _map_saml_response_to_user( | ||
self, | ||
|
@@ -239,16 +250,10 @@ async def _map_saml_response_to_user( | |
RedirectException: some mapping providers may raise this if they need | ||
to redirect to an interstitial page. | ||
""" | ||
|
||
remote_user_id = self._user_mapping_provider.get_remote_user_id( | ||
remote_user_id = self._remote_id_from_saml_response( | ||
saml2_auth, client_redirect_url | ||
) | ||
|
||
if not remote_user_id: | ||
raise MappingException( | ||
"Failed to extract remote user id from SAML response" | ||
) | ||
|
||
async def saml_response_to_remapped_user_attributes( | ||
failures: int, | ||
) -> UserAttributes: | ||
|
@@ -304,6 +309,35 @@ async def grandfather_existing_users() -> Optional[str]: | |
grandfather_existing_users, | ||
) | ||
|
||
def _remote_id_from_saml_response( | ||
self, | ||
saml2_auth: saml2.response.AuthnResponse, | ||
client_redirect_url: Optional[str], | ||
) -> str: | ||
"""Extract the unique remote id from a SAML2 AuthnResponse | ||
|
||
Args: | ||
saml2_auth: The parsed SAML2 response. | ||
client_redirect_url: The redirect URL passed in by the client. | ||
Returns: | ||
remote user id | ||
|
||
Raises: | ||
MappingException if there was an error extracting the user id | ||
""" | ||
# It's not obvious why we need to pass in the redirect URI to the mapping | ||
# provider, but we do :/ | ||
Comment on lines
+328
to
+329
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you see any use for it? I don't think any of the custom mapping providers I know of use it. I guess there isn't a huge harm in keeping it though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, I don't see any use for it, but changing the api for the mapping providers is annoying. |
||
remote_user_id = self._user_mapping_provider.get_remote_user_id( | ||
saml2_auth, client_redirect_url | ||
) | ||
|
||
if not remote_user_id: | ||
raise MappingException( | ||
"Failed to extract remote user id from SAML response" | ||
) | ||
|
||
return remote_user_id | ||
|
||
def expire_sessions(self): | ||
expire_before = self.clock.time_msec() - self._saml2_session_lifetime | ||
to_expire = set() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,8 +17,9 @@ | |
|
||
import attr | ||
|
||
from twisted.web.http import Request | ||
|
||
from synapse.api.errors import RedirectException | ||
from synapse.handlers._base import BaseHandler | ||
from synapse.http.server import respond_with_html | ||
from synapse.types import UserID, contains_invalid_mxid_characters | ||
|
||
|
@@ -42,14 +43,16 @@ class UserAttributes: | |
emails = attr.ib(type=List[str], default=attr.Factory(list)) | ||
|
||
|
||
class SsoHandler(BaseHandler): | ||
class SsoHandler: | ||
# The number of attempts to ask the mapping provider for when generating an MXID. | ||
_MAP_USERNAME_RETRIES = 1000 | ||
|
||
def __init__(self, hs: "HomeServer"): | ||
super().__init__(hs) | ||
self._store = hs.get_datastore() | ||
self._server_name = hs.hostname | ||
self._registration_handler = hs.get_registration_handler() | ||
self._error_template = hs.config.sso_error_template | ||
self._auth_handler = hs.get_auth_handler() | ||
|
||
def render_error( | ||
self, request, error: str, error_description: Optional[str] = None | ||
|
@@ -95,7 +98,7 @@ async def get_sso_user_by_remote_user_id( | |
) | ||
|
||
# Check if we already have a mapping for this user. | ||
previously_registered_user_id = await self.store.get_user_by_external_id( | ||
previously_registered_user_id = await self._store.get_user_by_external_id( | ||
auth_provider_id, remote_user_id, | ||
) | ||
|
||
|
@@ -181,7 +184,7 @@ async def get_mxid_from_sso( | |
previously_registered_user_id = await grandfather_existing_users() | ||
if previously_registered_user_id: | ||
# Future logins should also match this user ID. | ||
await self.store.record_user_external_id( | ||
await self._store.record_user_external_id( | ||
auth_provider_id, remote_user_id, previously_registered_user_id | ||
) | ||
return previously_registered_user_id | ||
|
@@ -214,8 +217,8 @@ async def get_mxid_from_sso( | |
) | ||
|
||
# Check if this mxid already exists | ||
user_id = UserID(attributes.localpart, self.server_name).to_string() | ||
if not await self.store.get_users_by_id_case_insensitive(user_id): | ||
user_id = UserID(attributes.localpart, self._server_name).to_string() | ||
if not await self._store.get_users_by_id_case_insensitive(user_id): | ||
# This mxid is free | ||
break | ||
else: | ||
|
@@ -238,7 +241,47 @@ async def get_mxid_from_sso( | |
user_agent_ips=[(user_agent, ip_address)], | ||
) | ||
|
||
await self.store.record_user_external_id( | ||
await self._store.record_user_external_id( | ||
auth_provider_id, remote_user_id, registered_user_id | ||
) | ||
return registered_user_id | ||
|
||
async def complete_sso_ui_auth_request( | ||
self, | ||
auth_provider_id: str, | ||
remote_user_id: str, | ||
ui_auth_session_id: str, | ||
request: Request, | ||
) -> None: | ||
""" | ||
Given an SSO ID, retrieve the user ID for it and complete UIA. | ||
|
||
Note that this requires that the user is mapped in the "user_external_ids" | ||
table. This will be the case if they have ever logged in via SAML or OIDC in | ||
recentish synapse versions, but may not be for older users. | ||
|
||
Args: | ||
auth_provider_id: A unique identifier for this SSO provider, e.g. | ||
"oidc" or "saml". | ||
remote_user_id: The unique identifier from the SSO provider. | ||
ui_auth_session_id: The ID of the user-interactive auth session. | ||
request: The request to complete. | ||
""" | ||
|
||
user_id = await self.get_sso_user_by_remote_user_id( | ||
auth_provider_id, remote_user_id, | ||
) | ||
|
||
if not user_id: | ||
logger.warning( | ||
"Remote user %s/%s has not previously logged in here: UIA will fail", | ||
auth_provider_id, | ||
remote_user_id, | ||
) | ||
# Let the UIA flow handle this the same as if they presented creds for a | ||
# different user. | ||
Comment on lines
+281
to
+282
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to end up storing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (To be clear -- I was just wondering how this worked and figured I'd save the trouble of anyone else looking at this PR.) |
||
user_id = "" | ||
|
||
await self._auth_handler.complete_sso_ui_auth( | ||
user_id, ui_auth_session_id, request | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this only be
MappingException
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, I don't think so.
We used to catch all exceptions (and turn them into
MappingExceptions
) at https://github.com/matrix-org/synapse/pull/8881/files#diff-b70d930a35a860a3c89284899c7c71ead788407bb579dca9d2d2d82dd4cf321dR870-R873, so I'm just copying that. (In this case we can log the exception immediately, rather than turning it into aMappingException
for a higher-level method to catch).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, makes sense. Just differs a bit from the SAML code is why I was asking. It seems fine. 👍