From 127c4aa7c8c172eebc72224761af2626f93a43b9 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 10 Mar 2020 12:17:52 -0400 Subject: [PATCH 01/10] Support SSO in the user interactive authentication workflow. --- changelog.d/7102.feature | 1 + synapse/api/constants.py | 1 + synapse/handlers/auth.py | 44 +++++++++++++++++++++++ synapse/handlers/saml_handler.py | 52 ++++++++++++++++++++++------ synapse/rest/client/v2_alpha/auth.py | 37 ++++++++++++++++++++ 5 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 changelog.d/7102.feature diff --git a/changelog.d/7102.feature b/changelog.d/7102.feature new file mode 100644 index 000000000000..01057aa396ba --- /dev/null +++ b/changelog.d/7102.feature @@ -0,0 +1 @@ +Support SSO in the user interactive authentication workflow. diff --git a/synapse/api/constants.py b/synapse/api/constants.py index cc8577552b16..fda2c2e5bbf8 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -61,6 +61,7 @@ class LoginType(object): MSISDN = "m.login.msisdn" RECAPTCHA = "m.login.recaptcha" TERMS = "m.login.terms" + SSO = "org.matrix.login.sso" DUMMY = "m.login.dummy" # Only for C/S API v1 diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 2ce1425dfaec..10ed771cc6ee 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -91,6 +91,7 @@ def __init__(self, hs): self.hs = hs # FIXME better possibility to access registrationHandler later? self.macaroon_gen = hs.get_macaroon_generator() self._password_enabled = hs.config.password_enabled + self._saml2_enabled = hs.config.saml2_enabled # we keep this as a list despite the O(N^2) implication so that we can # keep PASSWORD first and avoid confusing clients which pick the first @@ -105,6 +106,8 @@ def __init__(self, hs): for t in provider.get_supported_login_types().keys(): if t not in login_types: login_types.append(t) + if self._saml2_enabled: + login_types.append(LoginType.SSO) self._supported_login_types = login_types # Ratelimiter for failed auth during UIA. Uses same ratelimit config @@ -991,6 +994,47 @@ def _do_validate_hash(): else: return defer.succeed(False) + def complete_sso_ui_auth( + self, + registered_user_id: str, + session_id: str, + request: SynapseRequest, + requester: Requester, + ): + """Having figured out a mxid for this user, complete the HTTP request + + Args: + registered_user_id: The registered user ID to complete SSO login for. + request: The request to complete. + client_redirect_url: The URL to which to redirect the user at the end of the + process. + """ + # If the user ID of the SAML session does not match the user from the + # request, something went wrong. + if registered_user_id != requester.user.to_string(): + raise SynapseError(403, "SAML user does not match requester.") + + # Mark the stage of the authentication as successful. + sess = self._get_session_info(session_id) + if "creds" not in sess: + sess["creds"] = {} + creds = sess["creds"] + + creds[LoginType.SSO] = True + self._save_session(sess) + + # TODO Import this seems wrong. + from synapse.rest.client.v2_alpha.auth import SUCCESS_TEMPLATE + + # Render the HTML and return. + html_bytes = SUCCESS_TEMPLATE.encode("utf8") + request.setResponseCode(200) + request.setHeader(b"Content-Type", b"text/html; charset=utf-8") + request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),)) + + request.write(html_bytes) + finish_request(request) + def complete_sso_login( self, registered_user_id: str, diff --git a/synapse/handlers/saml_handler.py b/synapse/handlers/saml_handler.py index dc04b53f43ca..223e6eecf7a4 100644 --- a/synapse/handlers/saml_handler.py +++ b/synapse/handlers/saml_handler.py @@ -14,7 +14,7 @@ # limitations under the License. import logging import re -from typing import Tuple +from typing import Optional, Tuple import attr import saml2 @@ -44,11 +44,15 @@ class Saml2SessionData: # time the session was created, in milliseconds creation_time = attr.ib() + # The user interactive authentication session ID associated with this SAML + # session (or None if this SAML session is for an initial login). + ui_auth_session_id = attr.ib(type=Optional[str], default=None) class SamlHandler: def __init__(self, hs): self._saml_client = Saml2Client(hs.config.saml2_sp_config) + self._auth = hs.get_auth() self._auth_handler = hs.get_auth_handler() self._registration_handler = hs.get_registration_handler() @@ -77,12 +81,14 @@ def __init__(self, hs): self._error_html_content = hs.config.saml2_error_html_content - def handle_redirect_request(self, client_redirect_url): + def handle_redirect_request(self, client_redirect_url, ui_auth_session_id=None): """Handle an incoming request to /login/sso/redirect Args: client_redirect_url (bytes): the URL that we should redirect the client to when everything is done + ui_auth_session_id (Optional[str]): The session ID of the ongoing UI Auth (or + None if this is a login). Returns: bytes: URL to redirect to @@ -92,7 +98,9 @@ def handle_redirect_request(self, client_redirect_url): ) now = self._clock.time_msec() - self._outstanding_requests_dict[reqid] = Saml2SessionData(creation_time=now) + self._outstanding_requests_dict[reqid] = Saml2SessionData( + creation_time=now, ui_auth_session_id=ui_auth_session_id, + ) for key, value in info["headers"]: if key == "Location": @@ -119,7 +127,9 @@ async def handle_saml_response(self, request): self.expire_sessions() try: - user_id = await self._map_saml_response_to_user(resp_bytes, relay_state) + user_id, current_session = await self._map_saml_response_to_user( + resp_bytes, relay_state + ) except RedirectException: # Raise the exception as per the wishes of the SAML module response raise @@ -137,9 +147,29 @@ async def handle_saml_response(self, request): finish_request(request) return - self._auth_handler.complete_sso_login(user_id, request, relay_state) + # Complete the interactive auth session or the login. + if current_session and current_session.ui_auth_session_id: + requester = await self._auth.get_user_by_req(request) + self._auth_handler.complete_sso_ui_auth( + user_id, current_session.ui_auth_session_id, request, requester + ) + + else: + self._auth_handler.complete_sso_login(user_id, request, relay_state) + + async def _map_saml_response_to_user( + self, resp_bytes: str, client_redirect_url: str + ) -> Tuple[str, Optional[Saml2SessionData]]: + """ + Given a sample response, retrieve the cached session and user for it. - async def _map_saml_response_to_user(self, resp_bytes, client_redirect_url): + Args: + resp_bytes: The SAML response. + client_redirect_url: The redirect URL passed in by the client. + + Returns: + Tuple of the user ID and SAML session associated with this response. + """ try: saml2_auth = self._saml_client.parse_authn_request_response( resp_bytes, @@ -167,7 +197,9 @@ async def _map_saml_response_to_user(self, resp_bytes, client_redirect_url): logger.info("SAML2 mapped attributes: %s", saml2_auth.ava) - self._outstanding_requests_dict.pop(saml2_auth.in_response_to, None) + current_session = self._outstanding_requests_dict.pop( + saml2_auth.in_response_to, None + ) remote_user_id = self._user_mapping_provider.get_remote_user_id( saml2_auth, client_redirect_url @@ -188,7 +220,7 @@ async def _map_saml_response_to_user(self, resp_bytes, client_redirect_url): ) if registered_user_id is not None: logger.info("Found existing mapping %s", registered_user_id) - return registered_user_id + return registered_user_id, current_session # backwards-compatibility hack: see if there is an existing user with a # suitable mapping from the uid @@ -213,7 +245,7 @@ async def _map_saml_response_to_user(self, resp_bytes, client_redirect_url): await self._datastore.record_user_external_id( self._auth_provider_id, remote_user_id, registered_user_id ) - return registered_user_id + return registered_user_id, current_session # Map saml response to user attributes using the configured mapping provider for i in range(1000): @@ -260,7 +292,7 @@ async def _map_saml_response_to_user(self, resp_bytes, client_redirect_url): await self._datastore.record_user_external_id( self._auth_provider_id, remote_user_id, registered_user_id ) - return registered_user_id + return registered_user_id, current_session def expire_sessions(self): expire_before = self._clock.time_msec() - self._saml2_session_lifetime diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py index 85cf5a14c647..1e38989dad05 100644 --- a/synapse/rest/client/v2_alpha/auth.py +++ b/synapse/rest/client/v2_alpha/auth.py @@ -113,6 +113,24 @@ """ +SAML2_TEMPLATE = """ + + +Authentication + + +
+

+ A client is trying to remove a device/add an email address/take over + your account. To confirm this action, + re-authenticate with single sign-on. + If you did not expect this, your account may be compromised! +

+
+ + +""" + class AuthRestServlet(RestServlet): """ @@ -130,6 +148,11 @@ def __init__(self, hs): self.auth_handler = hs.get_auth_handler() self.registration_handler = hs.get_registration_handler() + # SSO configuration. + self._saml_enabled = hs.config.saml2_enabled + if self._saml_enabled: + self._saml_handler = hs.get_saml_handler() + def on_GET(self, request, stagetype): session = parse_string(request, "session") if not session: @@ -150,6 +173,17 @@ def on_GET(self, request, stagetype): "myurl": "%s/r0/auth/%s/fallback/web" % (CLIENT_API_PREFIX, LoginType.TERMS), } + + elif stagetype == LoginType.SSO and self._saml_enabled: + # Display a confirmation page which prompts the user to + # re-authenticate with their SSO provider. + client_redirect_url = "" + value = self._saml_handler.handle_redirect_request( + client_redirect_url, session + ) + html = SAML2_TEMPLATE % { + "myurl": value, + } else: raise SynapseError(404, "Unknown auth stage type") @@ -210,6 +244,9 @@ async def on_POST(self, request, stagetype): "myurl": "%s/r0/auth/%s/fallback/web" % (CLIENT_API_PREFIX, LoginType.TERMS), } + elif stagetype == LoginType.SSO: + # The SSO fallback workflow should not post here, + raise SynapseError(404, "Fallback SSO auth does not support POST requests.") else: raise SynapseError(404, "Unknown auth stage type") From 44b6cd8c06c2dd0bf1c8c3121bf64173fa42d6d6 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 19 Mar 2020 13:22:51 -0400 Subject: [PATCH 02/10] Render the SSO redirect page from Jinja. --- synapse/handlers/auth.py | 22 ++++++++++++++++++- synapse/res/templates/sso_auth_confirm.html | 15 +++++++++++++ synapse/rest/client/v2_alpha/auth.py | 24 ++------------------- 3 files changed, 38 insertions(+), 23 deletions(-) create mode 100644 synapse/res/templates/sso_auth_confirm.html diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 10ed771cc6ee..733688e55bbf 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -116,10 +116,21 @@ def __init__(self, hs): self._clock = self.hs.get_clock() - # Load the SSO redirect confirmation page HTML template + # Load the SSO HTML templates. + + # The following template is shown to the user before redirecting them to + # the SSO login page. It notifies the user they are about to be + # redirected and that the Synapse server will be gaining access to their + # SSO account. self._sso_redirect_confirm_template = load_jinja2_templates( hs.config.sso_redirect_confirm_template_dir, ["sso_redirect_confirm.html"], )[0] + # The following template is shown during user interactive authentication + # in the fallback auth scenario. It notifies the user that they are + # authenticating for an operation to occur on their account. + self._sso_auth_confirm_template = load_jinja2_templates( + hs.config.sso_redirect_confirm_template_dir, ["sso_auth_confirm.html"], + )[0] self._server_name = hs.config.server_name @@ -994,6 +1005,15 @@ def _do_validate_hash(): else: return defer.succeed(False) + def start_sso_ui_auth(self, redirect_url: str) -> str: + """ + Get the HTML for the SSO redirect confirmation page. + + :param redirect_url: The URL to redirect to the SSO provider. + :return: The HTML to render. + """ + return self._sso_auth_confirm_template.render(redirect_url=redirect_url,) + def complete_sso_ui_auth( self, registered_user_id: str, diff --git a/synapse/res/templates/sso_auth_confirm.html b/synapse/res/templates/sso_auth_confirm.html new file mode 100644 index 000000000000..7f73b6be6fbe --- /dev/null +++ b/synapse/res/templates/sso_auth_confirm.html @@ -0,0 +1,15 @@ + + + Authentication + + +
+

+ A client is trying to remove a device/add an email address/take over + your account. To confirm this action, + re-authenticate with single sign-on. + If you did not expect this, your account may be compromised! +

+
+ + diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py index 1e38989dad05..27a0f7de5324 100644 --- a/synapse/rest/client/v2_alpha/auth.py +++ b/synapse/rest/client/v2_alpha/auth.py @@ -113,24 +113,6 @@ """ -SAML2_TEMPLATE = """ - - -Authentication - - -
-

- A client is trying to remove a device/add an email address/take over - your account. To confirm this action, - re-authenticate with single sign-on. - If you did not expect this, your account may be compromised! -

-
- - -""" - class AuthRestServlet(RestServlet): """ @@ -178,12 +160,10 @@ def on_GET(self, request, stagetype): # Display a confirmation page which prompts the user to # re-authenticate with their SSO provider. client_redirect_url = "" - value = self._saml_handler.handle_redirect_request( + sso_redirect_url = self._saml_handler.handle_redirect_request( client_redirect_url, session ) - html = SAML2_TEMPLATE % { - "myurl": value, - } + html = self.auth_handler.start_sso_ui_auth(sso_redirect_url) else: raise SynapseError(404, "Unknown auth stage type") From b0f3bda4400312d5379a7d22952e2b7fc60f2d9f Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 19 Mar 2020 16:10:54 -0400 Subject: [PATCH 03/10] Fix import cycle. --- synapse/handlers/auth.py | 28 +++++++++++++++++++++++++--- synapse/rest/client/v2_alpha/auth.py | 25 +------------------------ 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 733688e55bbf..d5ef011fcc86 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -53,6 +53,31 @@ logger = logging.getLogger(__name__) +SUCCESS_TEMPLATE = """ + + +Success! + + + + + +
+

Thank you

+

You may now close this window and return to the application

+
+ + +""" + + class AuthHandler(BaseHandler): SESSION_EXPIRE_MS = 48 * 60 * 60 * 1000 @@ -1043,9 +1068,6 @@ def complete_sso_ui_auth( creds[LoginType.SSO] = True self._save_session(sess) - # TODO Import this seems wrong. - from synapse.rest.client.v2_alpha.auth import SUCCESS_TEMPLATE - # Render the HTML and return. html_bytes = SUCCESS_TEMPLATE.encode("utf8") request.setResponseCode(200) diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py index 27a0f7de5324..d22a304cda7e 100644 --- a/synapse/rest/client/v2_alpha/auth.py +++ b/synapse/rest/client/v2_alpha/auth.py @@ -18,6 +18,7 @@ from synapse.api.constants import LoginType from synapse.api.errors import SynapseError from synapse.api.urls import CLIENT_API_PREFIX +from synapse.handlers.auth import SUCCESS_TEMPLATE from synapse.http.server import finish_request from synapse.http.servlet import RestServlet, parse_string @@ -89,30 +90,6 @@ """ -SUCCESS_TEMPLATE = """ - - -Success! - - - - - -
-

Thank you

-

You may now close this window and return to the application

-
- - -""" - class AuthRestServlet(RestServlet): """ From 311949f300d1823b5623e54d75ae40986d90e124 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 27 Mar 2020 07:54:55 -0400 Subject: [PATCH 04/10] Add a note to CHANGES.md. --- CHANGES.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index f794c585b72b..b997af1630b8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,11 @@ +Next version +============ + +* A new template (`sso_auth_confirm.html`) was added to Synapse. If your Synapse + is configured to use SSO and a custom `sso_redirect_confirm_template_dir` + configuration then this template will need to be duplicated into that + directory. + Synapse 1.12.0 (2020-03-23) =========================== From 1e310f2da2a57c4ddacb49104759cf1a7561f1f0 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 27 Mar 2020 09:33:46 -0400 Subject: [PATCH 05/10] Separate supported login types for login and UI Auth. --- synapse/handlers/auth.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index d5ef011fcc86..fc2b34380ad9 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -131,9 +131,14 @@ def __init__(self, hs): for t in provider.get_supported_login_types().keys(): if t not in login_types: login_types.append(t) - if self._saml2_enabled: - login_types.append(LoginType.SSO) self._supported_login_types = login_types + # Login types and UI Auth types have a heavy overlap, but are not + # necessarily identical. Login types have SSO (and other login types) + # added in the rest layer, see synapse.rest.client.v1.login.LoginRestServerlet.on_GET. + ui_auth_types = login_types.copy() + if self._saml2_enabled: + ui_auth_types.append(LoginType.SSO) + self._supported_ui_auth_types = ui_auth_types # Ratelimiter for failed auth during UIA. Uses same ratelimit config # as per `rc_login.failed_attempts`. @@ -214,7 +219,7 @@ def validate_user_via_ui_auth( ) # build a list of supported flows - flows = [[login_type] for login_type in self._supported_login_types] + flows = [[login_type] for login_type in self._supported_ui_auth_types] try: result, params, _ = yield self.check_auth( @@ -232,7 +237,7 @@ def validate_user_via_ui_auth( raise # find the completed login type - for login_type in self._supported_login_types: + for login_type in self._supported_ui_auth_types: if login_type not in result: continue From 266e7d40e80e9c5a68cf2f3879138d38fbc27159 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 27 Mar 2020 14:33:52 -0400 Subject: [PATCH 06/10] Save the SSO user ID for later comparison to the auth user ID. --- synapse/handlers/auth.py | 15 ++++----------- synapse/handlers/saml_handler.py | 3 +-- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index fc2b34380ad9..17901a45c3c4 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -1045,11 +1045,7 @@ def start_sso_ui_auth(self, redirect_url: str) -> str: return self._sso_auth_confirm_template.render(redirect_url=redirect_url,) def complete_sso_ui_auth( - self, - registered_user_id: str, - session_id: str, - request: SynapseRequest, - requester: Requester, + self, registered_user_id: str, session_id: str, request: SynapseRequest, ): """Having figured out a mxid for this user, complete the HTTP request @@ -1059,18 +1055,15 @@ def complete_sso_ui_auth( client_redirect_url: The URL to which to redirect the user at the end of the process. """ - # If the user ID of the SAML session does not match the user from the - # request, something went wrong. - if registered_user_id != requester.user.to_string(): - raise SynapseError(403, "SAML user does not match requester.") - # Mark the stage of the authentication as successful. sess = self._get_session_info(session_id) if "creds" not in sess: sess["creds"] = {} creds = sess["creds"] - creds[LoginType.SSO] = True + # Save the user who authenticated with SSO, this will be used to ensure + # that the account be modified is also the person who logged in. + creds[LoginType.SSO] = registered_user_id self._save_session(sess) # Render the HTML and return. diff --git a/synapse/handlers/saml_handler.py b/synapse/handlers/saml_handler.py index 223e6eecf7a4..4741c82f6156 100644 --- a/synapse/handlers/saml_handler.py +++ b/synapse/handlers/saml_handler.py @@ -149,9 +149,8 @@ async def handle_saml_response(self, request): # Complete the interactive auth session or the login. if current_session and current_session.ui_auth_session_id: - requester = await self._auth.get_user_by_req(request) self._auth_handler.complete_sso_ui_auth( - user_id, current_session.ui_auth_session_id, request, requester + user_id, current_session.ui_auth_session_id, request ) else: From d1df905f85548409ff5c535261776645b482ee80 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 31 Mar 2020 09:37:07 -0400 Subject: [PATCH 07/10] Clarify comment for redirect_confirm_template. Co-Authored-By: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- synapse/handlers/auth.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 17901a45c3c4..a4bbad7e761c 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -148,10 +148,10 @@ def __init__(self, hs): # Load the SSO HTML templates. - # The following template is shown to the user before redirecting them to - # the SSO login page. It notifies the user they are about to be - # redirected and that the Synapse server will be gaining access to their - # SSO account. + # The following template is shown to the user during a client login via SSO, + # after the SSO completes and before redirecting them back to their client. + # It notifies the user they are about to give access to their matrix account + # to the client. self._sso_redirect_confirm_template = load_jinja2_templates( hs.config.sso_redirect_confirm_template_dir, ["sso_redirect_confirm.html"], )[0] From 018cd02b247d9a8e15e08165e1026e81f093050a Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 31 Mar 2020 10:37:28 -0400 Subject: [PATCH 08/10] Fix docstring formatting. --- synapse/handlers/auth.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index a4bbad7e761c..370e8bc9b8de 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -1039,8 +1039,11 @@ def start_sso_ui_auth(self, redirect_url: str) -> str: """ Get the HTML for the SSO redirect confirmation page. - :param redirect_url: The URL to redirect to the SSO provider. - :return: The HTML to render. + Args: + redirect_url: The URL to redirect to the SSO provider. + + Returns: + The HTML to render. """ return self._sso_auth_confirm_template.render(redirect_url=redirect_url,) From dc5ef6c9fd927a12f380a0250d73c0ddf9e925aa Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 31 Mar 2020 10:54:29 -0400 Subject: [PATCH 09/10] Add a human-readable description to the fallback template. --- synapse/handlers/auth.py | 27 ++++++++++++++++++--- synapse/res/templates/sso_auth_confirm.html | 3 +-- synapse/rest/client/v2_alpha/account.py | 19 ++++++++++++--- synapse/rest/client/v2_alpha/auth.py | 2 +- synapse/rest/client/v2_alpha/devices.py | 12 +++++++-- synapse/rest/client/v2_alpha/keys.py | 6 ++++- synapse/rest/client/v2_alpha/register.py | 1 + 7 files changed, 58 insertions(+), 12 deletions(-) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 370e8bc9b8de..7c09d15a7245 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -174,6 +174,7 @@ def validate_user_via_ui_auth( request: SynapseRequest, request_body: Dict[str, Any], clientip: str, + description: str, ): """ Checks that the user is who they claim to be, via a UI auth. @@ -191,6 +192,9 @@ def validate_user_via_ui_auth( clientip: The IP address of the client. + description: A human readable string to be displayed to the user that + describes the operation happening on their account. + Returns: defer.Deferred[dict]: the parameters for this request (which may have been given only in a previous call). @@ -223,7 +227,7 @@ def validate_user_via_ui_auth( try: result, params, _ = yield self.check_auth( - flows, request, request_body, clientip + flows, request, request_body, clientip, description ) except LoginError: # Update the ratelimite to say we failed (`can_do_action` doesn't raise). @@ -268,6 +272,7 @@ def check_auth( request: SynapseRequest, clientdict: Dict[str, Any], clientip: str, + description: str, ): """ Takes a dictionary sent by the client in the login / registration @@ -294,6 +299,9 @@ def check_auth( clientip: The IP address of the client. + description: A human readable string to be displayed to the user that + describes the operation happening on their account. + Returns: defer.Deferred[dict, dict, str]: a deferred tuple of (creds, params, session_id). @@ -343,12 +351,18 @@ def check_auth( comparator = (request.uri, request.method, clientdict) if "ui_auth" not in session: session["ui_auth"] = comparator + self._save_session(session) elif session["ui_auth"] != comparator: raise SynapseError( 403, "Requested operation has changed during the UI authentication session.", ) + # Add a human readable description to the session. + if "description" not in session: + session["description"] = description + self._save_session(session) + if not authdict: raise InteractiveAuthIncompleteError( self._auth_dict_for_flows(flows, session) @@ -1035,17 +1049,24 @@ def _do_validate_hash(): else: return defer.succeed(False) - def start_sso_ui_auth(self, redirect_url: str) -> str: + def start_sso_ui_auth(self, redirect_url: str, session_id: str) -> str: """ Get the HTML for the SSO redirect confirmation page. Args: redirect_url: The URL to redirect to the SSO provider. + session_id: The user interactive authentication session ID. Returns: The HTML to render. """ - return self._sso_auth_confirm_template.render(redirect_url=redirect_url,) + session = self._get_session_info(session_id) + # Get the human readable operation of what is occurring, falling back to + # a generic message if it isn't available for some reason. + description = session.get("description", "modify your account") + return self._sso_auth_confirm_template.render( + description=description, redirect_url=redirect_url, + ) def complete_sso_ui_auth( self, registered_user_id: str, session_id: str, request: SynapseRequest, diff --git a/synapse/res/templates/sso_auth_confirm.html b/synapse/res/templates/sso_auth_confirm.html index 7f73b6be6fbe..0d9de9d46528 100644 --- a/synapse/res/templates/sso_auth_confirm.html +++ b/synapse/res/templates/sso_auth_confirm.html @@ -5,8 +5,7 @@

- A client is trying to remove a device/add an email address/take over - your account. To confirm this action, + A client is trying to {{ description | e }}. To confirm this action, re-authenticate with single sign-on. If you did not expect this, your account may be compromised!

diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index f80b5e40ea83..31435b1e1c1c 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -234,7 +234,11 @@ async def on_POST(self, request): if self.auth.has_access_token(request): requester = await self.auth.get_user_by_req(request) params = await self.auth_handler.validate_user_via_ui_auth( - requester, request, body, self.hs.get_ip_from_request(request), + requester, + request, + body, + self.hs.get_ip_from_request(request), + "modify your account password", ) user_id = requester.user.to_string() else: @@ -244,6 +248,7 @@ async def on_POST(self, request): request, body, self.hs.get_ip_from_request(request), + "modify your account password", ) if LoginType.EMAIL_IDENTITY in result: @@ -311,7 +316,11 @@ async def on_POST(self, request): return 200, {} await self.auth_handler.validate_user_via_ui_auth( - requester, request, body, self.hs.get_ip_from_request(request), + requester, + request, + body, + self.hs.get_ip_from_request(request), + "deactivate your account", ) result = await self._deactivate_account_handler.deactivate_account( requester.user.to_string(), erase, id_server=body.get("id_server") @@ -669,7 +678,11 @@ async def on_POST(self, request): assert_valid_client_secret(client_secret) await self.auth_handler.validate_user_via_ui_auth( - requester, request, body, self.hs.get_ip_from_request(request), + requester, + request, + body, + self.hs.get_ip_from_request(request), + "add a third-party identifier to your account", ) validation_session = await self.identity_handler.validate_threepid_session( diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py index d22a304cda7e..1787562b9080 100644 --- a/synapse/rest/client/v2_alpha/auth.py +++ b/synapse/rest/client/v2_alpha/auth.py @@ -140,7 +140,7 @@ def on_GET(self, request, stagetype): sso_redirect_url = self._saml_handler.handle_redirect_request( client_redirect_url, session ) - html = self.auth_handler.start_sso_ui_auth(sso_redirect_url) + html = self.auth_handler.start_sso_ui_auth(sso_redirect_url, session) else: raise SynapseError(404, "Unknown auth stage type") diff --git a/synapse/rest/client/v2_alpha/devices.py b/synapse/rest/client/v2_alpha/devices.py index 119d9790524a..c0714fcfb105 100644 --- a/synapse/rest/client/v2_alpha/devices.py +++ b/synapse/rest/client/v2_alpha/devices.py @@ -81,7 +81,11 @@ async def on_POST(self, request): assert_params_in_dict(body, ["devices"]) await self.auth_handler.validate_user_via_ui_auth( - requester, request, body, self.hs.get_ip_from_request(request), + requester, + request, + body, + self.hs.get_ip_from_request(request), + "remove device(s) from your account", ) await self.device_handler.delete_devices( @@ -127,7 +131,11 @@ async def on_DELETE(self, request, device_id): raise await self.auth_handler.validate_user_via_ui_auth( - requester, request, body, self.hs.get_ip_from_request(request), + requester, + request, + body, + self.hs.get_ip_from_request(request), + "remove a device from your account", ) await self.device_handler.delete_device(requester.user.to_string(), device_id) diff --git a/synapse/rest/client/v2_alpha/keys.py b/synapse/rest/client/v2_alpha/keys.py index 5eb7ef35a4ba..8f41a3edbfcb 100644 --- a/synapse/rest/client/v2_alpha/keys.py +++ b/synapse/rest/client/v2_alpha/keys.py @@ -263,7 +263,11 @@ async def on_POST(self, request): body = parse_json_object_from_request(request) await self.auth_handler.validate_user_via_ui_auth( - requester, request, body, self.hs.get_ip_from_request(request), + requester, + request, + body, + self.hs.get_ip_from_request(request), + "add a device signing key to your account", ) result = await self.e2e_keys_handler.upload_signing_keys_for_user(user_id, body) diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index 66fc8ec17931..12f9cb5678b3 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -505,6 +505,7 @@ async def on_POST(self, request): request, body, self.hs.get_ip_from_request(request), + "log into your account", ) # Check that we're not trying to register a denied 3pid. From d7c2cad21d0d59cde50e23d638a3fc5e73175277 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 1 Apr 2020 08:26:09 -0400 Subject: [PATCH 10/10] Clarify human readable string for registration. Co-Authored-By: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- synapse/rest/client/v2_alpha/register.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index 12f9cb5678b3..431ecf4f84e9 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -505,7 +505,7 @@ async def on_POST(self, request): request, body, self.hs.get_ip_from_request(request), - "log into your account", + "register a new account", ) # Check that we're not trying to register a denied 3pid.