From 7e7e48628d07a917d0fa9581700faf6bfca1e301 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 1 Apr 2020 14:13:53 -0400 Subject: [PATCH] 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 1362ce74a1..87f2a6303a 100644 --- a/synapse/handlers/cas_handler.py +++ b/synapse/handlers/cas_handler.py @@ -48,44 +48,37 @@ class CasHandler: 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 @@ class CasHandler: ) 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 @@ class CasHandler: 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 + + if session: + self._auth_handler.complete_sso_ui_auth( + registered_user_id, session, request, ) - self._auth_handler.complete_sso_login( - registered_user_id, request, client_redirect_url - ) + else: + if not registered_user_id: + registered_user_id = await self._registration_handler.register_user( + localpart=localpart, default_display_name=user_display_name + ) - 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) - - 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) - - 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 ffaff41c41..4de2f97d06 100644 --- a/synapse/rest/client/v1/login.py +++ b/synapse/rest/client/v1/login.py @@ -426,7 +426,7 @@ class CasRedirectServlet(BaseSSORedirectServlet): 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 @@ class CasTicketServlet(RestServlet): 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 6a9deef984..13f9604407 100644 --- a/synapse/rest/client/v2_alpha/auth.py +++ b/synapse/rest/client/v2_alpha/auth.py @@ -145,7 +145,7 @@ class AuthRestServlet(RestServlet): # 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 @@ class AuthRestServlet(RestServlet): 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)