From b4329db27aa91ee5e2ebc78fb824e7300a5f92d7 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 25 Mar 2020 09:03:51 -0400 Subject: [PATCH 1/9] Support CAS in UI Auth flows. --- synapse/handlers/auth.py | 4 +- synapse/handlers/cas_handler.py | 144 ++++++++++++++++----------- synapse/rest/client/v1/login.py | 6 +- synapse/rest/client/v2_alpha/auth.py | 47 ++++++++- 4 files changed, 136 insertions(+), 65 deletions(-) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 7c09d15a7245..892adb00b96a 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -116,7 +116,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 + self._sso_enabled = hs.config.saml2_enabled or hs.config.cas_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 @@ -136,7 +136,7 @@ def __init__(self, hs): # 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: + if self._sso_enabled: ui_auth_types.append(LoginType.SSO) self._supported_ui_auth_types = ui_auth_types diff --git a/synapse/handlers/cas_handler.py b/synapse/handlers/cas_handler.py index f8dc274b78be..af86baaa0795 100644 --- a/synapse/handlers/cas_handler.py +++ b/synapse/handlers/cas_handler.py @@ -15,7 +15,7 @@ import logging import xml.etree.ElementTree as ET -from typing import AnyStr, Dict, Optional, Tuple +from typing import Dict, Optional, Tuple from six.moves import urllib @@ -48,26 +48,53 @@ def __init__(self, hs): self._http_client = hs.get_proxied_http_client() - def _build_service_param(self, client_redirect_url: AnyStr) -> str: + def _build_service_param(self, service_redirect_endpoint: str, **kwargs) -> str: + """ + Generates a value to use as the "service" parameter when redirecting or + querying the CAS service. + + Args: + service_redirect_endpoint: The homeserver endpoint to redirect + the client to after successful SSO negotiation. + kwargs: Additional arguments to include in the final redirect URL. + + Returns: + The URL to use as a "service" parameter. + """ return "%s%s?%s" % ( self._cas_service_url, - "/_matrix/client/r0/login/cas/ticket", - urllib.parse.urlencode({"redirectUrl": client_redirect_url}), + service_redirect_endpoint, + urllib.parse.urlencode(kwargs), ) - async def _handle_cas_response( - self, request: SynapseRequest, cas_response_body: str, client_redirect_url: str - ) -> None: + async def _validate_ticket( + self, ticket: str, service_redirect_endpoint: str, client_redirect_url: str + ) -> Tuple[str, Optional[str]]: """ - Retrieves the user and display name from the CAS response and continues with the authentication. + Validate a CAS ticket with the server, parse the response, and return the user and display name. Args: - request: The original client request. - cas_response_body: The response from the CAS server. - client_redirect_url: The URl to redirect the client to when - everything is done. + ticket: The CAS ticket from the client. + service_redirect_endpoint: The homeserver endpoint that the client + accessed to validate the ticket. + client_redirect_url: The URL to redirect the client to after + validation is done. """ - user, attributes = self._parse_cas_response(cas_response_body) + uri = self._cas_server_url + "/proxyValidate" + args = { + "ticket": ticket, + "service": self._build_service_param( + service_redirect_endpoint, redirectUrl=client_redirect_url + ), + } + try: + body = await self._http_client.get_raw(uri, args) + except PartialDownloadError as pde: + # Twisted raises this error if the connection is closed, + # even if that's being used old-http style to signal end-of-data + body = pde.response + + user, attributes = self._parse_cas_response(body) displayname = attributes.pop(self._cas_displayname_attribute, None) for required_attribute, required_value in self._cas_required_attributes.items(): @@ -82,7 +109,7 @@ async def _handle_cas_response( if required_value != actual_value: raise LoginError(401, "Unauthorized", errcode=Codes.UNAUTHORIZED) - await self._on_successful_auth(user, request, client_redirect_url, displayname) + return user, displayname def _parse_cas_response( self, cas_response_body: str @@ -127,31 +154,46 @@ def _parse_cas_response( ) return user, attributes - async def _on_successful_auth( - self, - username: str, - request: SynapseRequest, - client_redirect_url: str, - user_display_name: Optional[str] = None, + def get_redirect_url(self, service_redirect_endpoint: str, **kwargs) -> str: + """ + Generates a URL to the CAS server where the client should be redirected. + + Args: + service_redirect_endpoint: The homeserver endpoint to redirect + the client to after successful SSO negotiation. + kwargs: Additional arguments to include in the final redirect URL. + + Returns: + The URL to redirect the client to. + """ + args = urllib.parse.urlencode( + {"service": self._build_service_param(service_redirect_endpoint, **kwargs)} + ) + + return "%s/login?%s" % (self._cas_server_url, args) + + async def handle_login_request( + self, request: SynapseRequest, client_redirect_url: str, ticket: str ) -> None: - """Called once the user has successfully authenticated with the SSO. + """ + Validates a CAS ticket sent by the client for login and authenticates the user with SSO. Registers the user if necessary, and then returns a redirect (with a login token) to the client. Args: - username: the remote user id. We'll map this onto - something sane for a MXID localpath. - request: the incoming request from the browser. We'll respond to it with a redirect. client_redirect_url: the redirect_url the client gave us when it first started the process. - user_display_name: if set, and we have to register a new user, - we will set their displayname to this. + ticket: The CAS ticket provided by the client. """ + username, user_display_name = await self._validate_ticket( + ticket, "/_matrix/client/r0/login/cas/ticket", client_redirect_url + ) + localpart = map_username_to_mxid_localpart(username) user_id = UserID(localpart, self._hostname).to_string() registered_user_id = await self._auth_handler.check_user_exists(user_id) @@ -164,41 +206,31 @@ async def _on_successful_auth( registered_user_id, request, client_redirect_url ) - def handle_redirect_request(self, client_redirect_url: bytes) -> bytes: + async def handle_ui_auth_response( + self, request: SynapseRequest, ticket: str, session_id: str + ) -> None: """ - Generates a URL to the CAS server where the client should be redirected. + Validates a CAS ticket sent by the client for user interactive authentication. - Args: - client_redirect_url: The final URL the client should go to after the - user has negotiated SSO. + If successful, this completes the SSO step of UI auth and returns a + an HTML page to the client. - Returns: - The URL to redirect to. - """ - args = urllib.parse.urlencode( - {"service": self._build_service_param(client_redirect_url)} - ) + Args: + request: the incoming request from the browser. - return ("%s/login?%s" % (self._cas_server_url, args)).encode("ascii") + ticket: The CAS ticket provided by the client. - async def handle_ticket_request( - self, request: SynapseRequest, client_redirect_url: str, ticket: str - ) -> None: + session_id: The UI Auth session ID. """ - Validates a CAS ticket sent by the client for login/registration. + client_redirect_url = "" + user, _ = await self._validate_ticket( + ticket, "/_matrix/client/r0/auth/cas/ticket", client_redirect_url + ) - On a successful request, writes a redirect to the request. - """ - uri = self._cas_server_url + "/proxyValidate" - args = { - "ticket": ticket, - "service": self._build_service_param(client_redirect_url), - } - try: - body = await self._http_client.get_raw(uri, args) - except PartialDownloadError as pde: - # Twisted raises this error if the connection is closed, - # even if that's being used old-http style to signal end-of-data - body = pde.response + localpart = map_username_to_mxid_localpart(user) + user_id = UserID(localpart, self._hostname).to_string() + registered_user_id = await self._auth_handler.check_user_exists(user_id) - await self._handle_cas_response(request, body, client_redirect_url) + self._auth_handler.complete_sso_ui_auth( + registered_user_id, session_id, request, + ) diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index 59593cbf6e48..8beb2f1ed29e 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -425,7 +425,9 @@ def __init__(self, hs): self._cas_handler = hs.get_cas_handler() def get_sso_url(self, client_redirect_url: bytes) -> bytes: - return self._cas_handler.handle_redirect_request(client_redirect_url) + return self._cas_handler.get_redirect_url( + "/_matrix/client/r0/login/cas/ticket", redirectUrl=client_redirect_url + ).encode("ascii") class CasTicketServlet(RestServlet): @@ -438,7 +440,7 @@ def __init__(self, hs): async def on_GET(self, request: SynapseRequest) -> None: client_redirect_url = parse_string(request, "redirectUrl", required=True) ticket = parse_string(request, "ticket", required=True) - await self._cas_handler.handle_ticket_request( + await self._cas_handler.handle_login_request( request, client_redirect_url, ticket ) diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py index 1787562b9080..1a41bb73e634 100644 --- a/synapse/rest/client/v2_alpha/auth.py +++ b/synapse/rest/client/v2_alpha/auth.py @@ -111,6 +111,11 @@ def __init__(self, hs): self._saml_enabled = hs.config.saml2_enabled if self._saml_enabled: self._saml_handler = hs.get_saml_handler() + self._cas_enabled = hs.config.cas_enabled + if self._cas_enabled: + self._cas_handler = hs.get_cas_handler() + self._cas_server_url = hs.config.cas_server_url + self._cas_service_url = hs.config.cas_service_url def on_GET(self, request, stagetype): session = parse_string(request, "session") @@ -133,14 +138,27 @@ def on_GET(self, request, stagetype): % (CLIENT_API_PREFIX, LoginType.TERMS), } - elif stagetype == LoginType.SSO and self._saml_enabled: + elif stagetype == LoginType.SSO: # Display a confirmation page which prompts the user to # re-authenticate with their SSO provider. - client_redirect_url = "" - sso_redirect_url = self._saml_handler.handle_redirect_request( - client_redirect_url, session - ) + if self._cas_enabled: + # Generate a request to CAS that redirects back to an endpoint + # to verify the successful authentication. + sso_redirect_url = self._cas_handler.get_redirect_url( + "/_matrix/client/r0/auth/cas/ticket", session=session, + ) + + elif self._saml_enabled: + client_redirect_url = "" + sso_redirect_url = self._saml_handler.handle_redirect_request( + client_redirect_url, session + ) + + else: + raise SynapseError(400, "Homeserver not configured for SSO.") + html = self.auth_handler.start_sso_ui_auth(sso_redirect_url, session) + else: raise SynapseError(404, "Unknown auth stage type") @@ -221,5 +239,24 @@ def on_OPTIONS(self, _): return 200, {} +class CasAuthTicketServlet(RestServlet): + PATTERNS = client_patterns(r"/auth/cas/ticket") + + def __init__(self, hs): + super(CasAuthTicketServlet, self).__init__() + self._cas_handler = hs.get_cas_handler() + + async def on_GET(self, request): + ticket = parse_string(request, "ticket", required=True) + # Pull the UI Auth session ID out. + session_id = parse_string(request, "session", required=True) + + return await self._cas_handler.handle_ui_auth_response( + request, ticket, session_id + ) + + def register_servlets(hs, http_server): AuthRestServlet(hs).register(http_server) + if hs.config.cas_enabled: + CasAuthTicketServlet(hs).register(http_server) From d6bbeb3890e32b54660bf2a97e128d893fe6666d Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 1 Apr 2020 11:10:41 -0400 Subject: [PATCH 2/9] Add a docstring to the servlet. --- synapse/rest/client/v2_alpha/auth.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py index 1a41bb73e634..fba00c5412f2 100644 --- a/synapse/rest/client/v2_alpha/auth.py +++ b/synapse/rest/client/v2_alpha/auth.py @@ -240,6 +240,17 @@ def on_OPTIONS(self, _): class CasAuthTicketServlet(RestServlet): + """ + Completes a user interactive authentication session when using CAS. + + It is called after the user has completed SSO with the CAS provider and + received a ticket in response. It does the following: + + * Retrieves the CAS ticket and the UI auth session from the request. + * Validates the CAS ticket. + * Marks the UI auth session as complete. + """ + PATTERNS = client_patterns(r"/auth/cas/ticket") def __init__(self, hs): From 49a79fa24fdd30c733b2da8584236b6258e23afa Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 1 Apr 2020 11:13:10 -0400 Subject: [PATCH 3/9] Update method names and docstrings for handling tickets. --- synapse/handlers/cas_handler.py | 6 +++--- synapse/rest/client/v1/login.py | 2 +- synapse/rest/client/v2_alpha/auth.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/synapse/handlers/cas_handler.py b/synapse/handlers/cas_handler.py index af86baaa0795..b3cb29f366c4 100644 --- a/synapse/handlers/cas_handler.py +++ b/synapse/handlers/cas_handler.py @@ -172,11 +172,11 @@ def get_redirect_url(self, service_redirect_endpoint: str, **kwargs) -> str: return "%s/login?%s" % (self._cas_server_url, args) - async def handle_login_request( + async def handle_ticket_for_login( self, request: SynapseRequest, client_redirect_url: str, ticket: str ) -> None: """ - Validates a CAS ticket sent by the client for login and authenticates the user with SSO. + Validates a CAS ticket sent by the client and completes the login process. Registers the user if necessary, and then returns a redirect (with a login token) to the client. @@ -206,7 +206,7 @@ async def handle_login_request( registered_user_id, request, client_redirect_url ) - async def handle_ui_auth_response( + async def handle_ticket_for_ui_auth( self, request: SynapseRequest, ticket: str, session_id: str ) -> None: """ diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index 8beb2f1ed29e..ffaff41c4127 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -440,7 +440,7 @@ def __init__(self, hs): async def on_GET(self, request: SynapseRequest) -> None: client_redirect_url = parse_string(request, "redirectUrl", required=True) ticket = parse_string(request, "ticket", required=True) - await self._cas_handler.handle_login_request( + await self._cas_handler.handle_ticket_for_login( request, client_redirect_url, ticket ) diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py index fba00c5412f2..6a9deef984df 100644 --- a/synapse/rest/client/v2_alpha/auth.py +++ b/synapse/rest/client/v2_alpha/auth.py @@ -262,7 +262,7 @@ async def on_GET(self, request): # Pull the UI Auth session ID out. session_id = parse_string(request, "session", required=True) - return await self._cas_handler.handle_ui_auth_response( + return await self._cas_handler.handle_ticket_for_ui_auth( request, ticket, session_id ) From 79f7a9a405bbe87a432ccfd0c8fab6f82c99d81f Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 1 Apr 2020 11:27:04 -0400 Subject: [PATCH 4/9] Avoid duplicate the URLs. --- synapse/handlers/cas_handler.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/synapse/handlers/cas_handler.py b/synapse/handlers/cas_handler.py index b3cb29f366c4..c9c85ad061fd 100644 --- a/synapse/handlers/cas_handler.py +++ b/synapse/handlers/cas_handler.py @@ -173,7 +173,7 @@ def get_redirect_url(self, service_redirect_endpoint: str, **kwargs) -> str: return "%s/login?%s" % (self._cas_server_url, args) async def handle_ticket_for_login( - self, request: SynapseRequest, client_redirect_url: str, ticket: str + self, request: SynapseRequest, client_redirect_url: str, ticket: str, ) -> None: """ Validates a CAS ticket sent by the client and completes the login process. @@ -191,7 +191,7 @@ async def handle_ticket_for_login( ticket: The CAS ticket provided by the client. """ username, user_display_name = await self._validate_ticket( - ticket, "/_matrix/client/r0/login/cas/ticket", client_redirect_url + ticket, request.path, client_redirect_url ) localpart = map_username_to_mxid_localpart(username) @@ -223,9 +223,7 @@ async def handle_ticket_for_ui_auth( session_id: The UI Auth session ID. """ client_redirect_url = "" - user, _ = await self._validate_ticket( - ticket, "/_matrix/client/r0/auth/cas/ticket", client_redirect_url - ) + user, _ = await self._validate_ticket(ticket, request.path, client_redirect_url) localpart = map_username_to_mxid_localpart(user) user_id = UserID(localpart, self._hostname).to_string() From 32581bf832b6a669271b5f70af8b5d7a73dbb48e Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 1 Apr 2020 11:34:13 -0400 Subject: [PATCH 5/9] Clarify comments a bit more. --- synapse/handlers/cas_handler.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/synapse/handlers/cas_handler.py b/synapse/handlers/cas_handler.py index c9c85ad061fd..1362ce74a1b9 100644 --- a/synapse/handlers/cas_handler.py +++ b/synapse/handlers/cas_handler.py @@ -176,7 +176,8 @@ async def handle_ticket_for_login( self, request: SynapseRequest, client_redirect_url: str, ticket: str, ) -> None: """ - Validates a CAS ticket sent by the client and completes the login process. + Called once the user has successfully authenticated with the SSO, + validates a CAS ticket sent by the client and completes the login process. Registers the user if necessary, and then returns a redirect (with a login token) to the client. @@ -210,7 +211,9 @@ async def handle_ticket_for_ui_auth( self, request: SynapseRequest, ticket: str, session_id: str ) -> None: """ - Validates a CAS ticket sent by the client for user interactive authentication. + Called once the user has successfully authenticated with the SSO, + validates a CAS ticket sent by the client and completes user interactive + authentication. If successful, this completes the SSO step of UI auth and returns a an HTML page to the client. From 7e7e48628d07a917d0fa9581700faf6bfca1e301 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 1 Apr 2020 14:13:53 -0400 Subject: [PATCH 6/9] Do not use a separate endpoint for UI Auth for CAS. --- synapse/handlers/cas_handler.py | 96 +++++++++++----------------- synapse/rest/client/v1/login.py | 18 ++++-- synapse/rest/client/v2_alpha/auth.py | 32 +--------- 3 files changed, 52 insertions(+), 94 deletions(-) diff --git a/synapse/handlers/cas_handler.py b/synapse/handlers/cas_handler.py index 1362ce74a1b9..87f2a6303adb 100644 --- a/synapse/handlers/cas_handler.py +++ b/synapse/handlers/cas_handler.py @@ -48,44 +48,37 @@ def __init__(self, hs): self._http_client = hs.get_proxied_http_client() - def _build_service_param(self, service_redirect_endpoint: str, **kwargs) -> str: + def _build_service_param(self, args: Dict[str, str]) -> str: """ Generates a value to use as the "service" parameter when redirecting or querying the CAS service. Args: - service_redirect_endpoint: The homeserver endpoint to redirect - the client to after successful SSO negotiation. - kwargs: Additional arguments to include in the final redirect URL. + args: Additional arguments to include in the final redirect URL. Returns: The URL to use as a "service" parameter. """ return "%s%s?%s" % ( self._cas_service_url, - service_redirect_endpoint, - urllib.parse.urlencode(kwargs), + "/_matrix/client/r0/login/cas/ticket", + urllib.parse.urlencode(args), ) async def _validate_ticket( - self, ticket: str, service_redirect_endpoint: str, client_redirect_url: str + self, ticket: str, service_args: Dict[str, str] ) -> Tuple[str, Optional[str]]: """ Validate a CAS ticket with the server, parse the response, and return the user and display name. Args: ticket: The CAS ticket from the client. - service_redirect_endpoint: The homeserver endpoint that the client - accessed to validate the ticket. - client_redirect_url: The URL to redirect the client to after - validation is done. + service_args: Additional arguments to include in the service URL. """ uri = self._cas_server_url + "/proxyValidate" args = { "ticket": ticket, - "service": self._build_service_param( - service_redirect_endpoint, redirectUrl=client_redirect_url - ), + "service": self._build_service_param(service_args), } try: body = await self._http_client.get_raw(uri, args) @@ -154,26 +147,28 @@ def _parse_cas_response( ) return user, attributes - def get_redirect_url(self, service_redirect_endpoint: str, **kwargs) -> str: + def get_redirect_url(self, service_args: Dict[str, str]) -> str: """ Generates a URL to the CAS server where the client should be redirected. Args: - service_redirect_endpoint: The homeserver endpoint to redirect - the client to after successful SSO negotiation. - kwargs: Additional arguments to include in the final redirect URL. + service_args: Additional arguments to include in the final redirect URL. Returns: The URL to redirect the client to. """ args = urllib.parse.urlencode( - {"service": self._build_service_param(service_redirect_endpoint, **kwargs)} + {"service": self._build_service_param(service_args)} ) return "%s/login?%s" % (self._cas_server_url, args) - async def handle_ticket_for_login( - self, request: SynapseRequest, client_redirect_url: str, ticket: str, + async def handle_ticket( + self, + request: SynapseRequest, + ticket: str, + client_redirect_url: Optional[str], + session: Optional[str], ) -> None: """ Called once the user has successfully authenticated with the SSO, @@ -186,52 +181,35 @@ async def handle_ticket_for_login( request: the incoming request from the browser. We'll respond to it with a redirect. + ticket: The CAS ticket provided by the client. + client_redirect_url: the redirect_url the client gave us when it first started the process. - ticket: The CAS ticket provided by the client. + session_id: The UI Auth session ID, if applicable. """ - username, user_display_name = await self._validate_ticket( - ticket, request.path, client_redirect_url - ) + args = {} + if client_redirect_url: + args["redirectUrl"] = client_redirect_url + if session: + args["session"] = session + username, user_display_name = await self._validate_ticket(ticket, args) localpart = map_username_to_mxid_localpart(username) user_id = UserID(localpart, self._hostname).to_string() registered_user_id = await self._auth_handler.check_user_exists(user_id) - if not registered_user_id: - registered_user_id = await self._registration_handler.register_user( - localpart=localpart, default_display_name=user_display_name - ) - - self._auth_handler.complete_sso_login( - registered_user_id, request, client_redirect_url - ) - - async def handle_ticket_for_ui_auth( - self, request: SynapseRequest, ticket: str, session_id: str - ) -> None: - """ - Called once the user has successfully authenticated with the SSO, - validates a CAS ticket sent by the client and completes user interactive - authentication. - If successful, this completes the SSO step of UI auth and returns a - an HTML page to the client. - - Args: - request: the incoming request from the browser. - - ticket: The CAS ticket provided by the client. - - session_id: The UI Auth session ID. - """ - client_redirect_url = "" - user, _ = await self._validate_ticket(ticket, request.path, client_redirect_url) + if session: + self._auth_handler.complete_sso_ui_auth( + registered_user_id, session, request, + ) - localpart = map_username_to_mxid_localpart(user) - user_id = UserID(localpart, self._hostname).to_string() - registered_user_id = await self._auth_handler.check_user_exists(user_id) + else: + if not registered_user_id: + registered_user_id = await self._registration_handler.register_user( + localpart=localpart, default_display_name=user_display_name + ) - self._auth_handler.complete_sso_ui_auth( - registered_user_id, session_id, request, - ) + self._auth_handler.complete_sso_login( + registered_user_id, request, client_redirect_url + ) diff --git a/synapse/rest/client/v1/login.py b/synapse/rest/client/v1/login.py index ffaff41c4127..4de2f97d06c2 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -426,7 +426,7 @@ def __init__(self, hs): def get_sso_url(self, client_redirect_url: bytes) -> bytes: return self._cas_handler.get_redirect_url( - "/_matrix/client/r0/login/cas/ticket", redirectUrl=client_redirect_url + {"redirectUrl": client_redirect_url} ).encode("ascii") @@ -438,10 +438,20 @@ def __init__(self, hs): self._cas_handler = hs.get_cas_handler() async def on_GET(self, request: SynapseRequest) -> None: - client_redirect_url = parse_string(request, "redirectUrl", required=True) + client_redirect_url = parse_string(request, "redirectUrl") ticket = parse_string(request, "ticket", required=True) - await self._cas_handler.handle_ticket_for_login( - request, client_redirect_url, ticket + + # Maybe get a session ID (if this ticket is from user interactive + # authentication). + session = parse_string(request, "session") + + # Either client_redirect_url or session must be provided. + if not client_redirect_url and not session: + message = "Missing string query parameter redirectUrl or session" + raise SynapseError(400, message, errcode=Codes.MISSING_PARAM) + + await self._cas_handler.handle_ticket( + request, ticket, client_redirect_url, session ) diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py index 6a9deef984df..13f9604407e8 100644 --- a/synapse/rest/client/v2_alpha/auth.py +++ b/synapse/rest/client/v2_alpha/auth.py @@ -145,7 +145,7 @@ def on_GET(self, request, stagetype): # Generate a request to CAS that redirects back to an endpoint # to verify the successful authentication. sso_redirect_url = self._cas_handler.get_redirect_url( - "/_matrix/client/r0/auth/cas/ticket", session=session, + {"session": session}, ) elif self._saml_enabled: @@ -239,35 +239,5 @@ def on_OPTIONS(self, _): return 200, {} -class CasAuthTicketServlet(RestServlet): - """ - Completes a user interactive authentication session when using CAS. - - It is called after the user has completed SSO with the CAS provider and - received a ticket in response. It does the following: - - * Retrieves the CAS ticket and the UI auth session from the request. - * Validates the CAS ticket. - * Marks the UI auth session as complete. - """ - - PATTERNS = client_patterns(r"/auth/cas/ticket") - - def __init__(self, hs): - super(CasAuthTicketServlet, self).__init__() - self._cas_handler = hs.get_cas_handler() - - async def on_GET(self, request): - ticket = parse_string(request, "ticket", required=True) - # Pull the UI Auth session ID out. - session_id = parse_string(request, "session", required=True) - - return await self._cas_handler.handle_ticket_for_ui_auth( - request, ticket, session_id - ) - - def register_servlets(hs, http_server): AuthRestServlet(hs).register(http_server) - if hs.config.cas_enabled: - CasAuthTicketServlet(hs).register(http_server) From fce9db19df35b2cf64becdc438f22557791a4e1b Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 1 Apr 2020 15:07:02 -0400 Subject: [PATCH 7/9] Add changelog entry. --- changelog.d/7186.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7186.feature diff --git a/changelog.d/7186.feature b/changelog.d/7186.feature new file mode 100644 index 000000000000..01057aa396ba --- /dev/null +++ b/changelog.d/7186.feature @@ -0,0 +1 @@ +Support SSO in the user interactive authentication workflow. From f68df97edd45949a804b9380e9171f86bc21e3ce Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 2 Apr 2020 10:21:40 -0400 Subject: [PATCH 8/9] Update comments based on suggestions. Co-Authored-By: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- synapse/handlers/cas_handler.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/synapse/handlers/cas_handler.py b/synapse/handlers/cas_handler.py index 87f2a6303adb..d1ab9d576642 100644 --- a/synapse/handlers/cas_handler.py +++ b/synapse/handlers/cas_handler.py @@ -74,6 +74,7 @@ async def _validate_ticket( Args: ticket: The CAS ticket from the client. service_args: Additional arguments to include in the service URL. + Should be the same as those passed to `get_redirect_url`. """ uri = self._cas_server_url + "/proxyValidate" args = { @@ -149,7 +150,7 @@ def _parse_cas_response( def get_redirect_url(self, service_args: Dict[str, str]) -> str: """ - Generates a URL to the CAS server where the client should be redirected. + Generates a URL for the CAS server where the client should be redirected. Args: service_args: Additional arguments to include in the final redirect URL. @@ -171,22 +172,23 @@ async def handle_ticket( session: Optional[str], ) -> None: """ - Called once the user has successfully authenticated with the SSO, - validates a CAS ticket sent by the client and completes the login process. + Called once the user has successfully authenticated with the SSO. + validates a CAS ticket sent by the client and completes the auth process. Registers the user if necessary, and then returns a redirect (with a login token) to the client. Args: request: the incoming request from the browser. We'll - respond to it with a redirect. + respond to it with a redirect or an HTML page. ticket: The CAS ticket provided by the client. - client_redirect_url: the redirect_url the client gave us when - it first started the process. + client_redirect_url: the redirectUrl parameter from the `/cas/ticket` HTTP request, if given. + This should be the same as the redirectUrl from the original `/login/sso/redirect` request. - session_id: The UI Auth session ID, if applicable. + session_id: The session_id parameter from the `/cas/ticket` HTTP request, if given. + This should be the UI Auth session id. """ args = {} if client_redirect_url: From 654db21db2dc39ac63ecb64af66f956667ed27bd Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 2 Apr 2020 10:25:47 -0400 Subject: [PATCH 9/9] Update comment to call-out different flows. --- synapse/handlers/cas_handler.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/synapse/handlers/cas_handler.py b/synapse/handlers/cas_handler.py index d1ab9d576642..d977badf35d9 100644 --- a/synapse/handlers/cas_handler.py +++ b/synapse/handlers/cas_handler.py @@ -173,10 +173,14 @@ async def handle_ticket( ) -> None: """ Called once the user has successfully authenticated with the SSO. - validates a CAS ticket sent by the client and completes the auth process. + Validates a CAS ticket sent by the client and completes the auth process. - Registers the user if necessary, and then returns a redirect (with - a login token) to the client. + If the user interactive authentication session is provided, marks the + UI Auth session as complete, then returns an HTML page notifying the + user they are done. + + Otherwise, this registers the user if necessary, and then returns a + redirect (with a login token) to the client. Args: request: the incoming request from the browser. We'll @@ -187,7 +191,7 @@ async def handle_ticket( client_redirect_url: the redirectUrl parameter from the `/cas/ticket` HTTP request, if given. This should be the same as the redirectUrl from the original `/login/sso/redirect` request. - session_id: The session_id parameter from the `/cas/ticket` HTTP request, if given. + session: The session parameter from the `/cas/ticket` HTTP request, if given. This should be the UI Auth session id. """ args = {}