summary refs log tree commit diff
path: root/synapse/handlers/cas_handler.py
diff options
context:
space:
mode:
authorPatrick Cloke <clokep@users.noreply.github.com>2020-04-03 15:35:05 -0400
committerGitHub <noreply@github.com>2020-04-03 15:35:05 -0400
commit694d8bed0e56366f080a49db0f930d635ca6cdf4 (patch)
treeb4a5fddf3cb5d5ae124bba9788483b6477fc1747 /synapse/handlers/cas_handler.py
parentExtend web_client_location to handle absolute URLs (#7006) (diff)
downloadsynapse-694d8bed0e56366f080a49db0f930d635ca6cdf4.tar.xz
Support CAS in UI Auth flows. (#7186)
Diffstat (limited to 'synapse/handlers/cas_handler.py')
-rw-r--r--synapse/handlers/cas_handler.py161
1 files changed, 89 insertions, 72 deletions
diff --git a/synapse/handlers/cas_handler.py b/synapse/handlers/cas_handler.py
index f8dc274b78..d977badf35 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,47 @@ class CasHandler:
 
         self._http_client = hs.get_proxied_http_client()
 
-    def _build_service_param(self, client_redirect_url: AnyStr) -> 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:
+            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,
             "/_matrix/client/r0/login/cas/ticket",
-            urllib.parse.urlencode({"redirectUrl": client_redirect_url}),
+            urllib.parse.urlencode(args),
         )
 
-    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_args: Dict[str, 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_args: Additional arguments to include in the service URL.
+                Should be the same as those passed to `get_redirect_url`.
         """
-        user, attributes = self._parse_cas_response(cas_response_body)
+        uri = self._cas_server_url + "/proxyValidate"
+        args = {
+            "ticket": ticket,
+            "service": self._build_service_param(service_args),
+        }
+        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 +103,7 @@ class CasHandler:
                 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,78 +148,74 @@ class CasHandler:
             )
         return user, attributes
 
-    async def _on_successful_auth(
-        self,
-        username: str,
-        request: SynapseRequest,
-        client_redirect_url: str,
-        user_display_name: Optional[str] = None,
-    ) -> None:
-        """Called once the user has successfully authenticated with the SSO.
-
-        Registers the user if necessary, and then returns a redirect (with
-        a login token) to the client.
+    def get_redirect_url(self, service_args: Dict[str, str]) -> str:
+        """
+        Generates a URL for the CAS server where the client should be redirected.
 
         Args:
-            username: the remote user id. We'll map this onto
-                something sane for a MXID localpath.
+            service_args: Additional arguments to include in the final redirect URL.
 
-            request: the incoming request from the browser. We'll
-                respond to it with a redirect.
+        Returns:
+            The URL to redirect the client to.
+        """
+        args = urllib.parse.urlencode(
+            {"service": self._build_service_param(service_args)}
+        )
 
-            client_redirect_url: the redirect_url the client gave us when
-                it first started the process.
+        return "%s/login?%s" % (self._cas_server_url, args)
 
-            user_display_name: if set, and we have to register a new user,
-                we will set their displayname to this.
+    async def handle_ticket(
+        self,
+        request: SynapseRequest,
+        ticket: str,
+        client_redirect_url: Optional[str],
+        session: Optional[str],
+    ) -> None:
         """
-        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
-            )
+        Called once the user has successfully authenticated with the SSO.
+        Validates a CAS ticket sent by the client and completes the auth process.
 
-        self._auth_handler.complete_sso_login(
-            registered_user_id, request, client_redirect_url
-        )
+        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.
 
-    def handle_redirect_request(self, client_redirect_url: bytes) -> bytes:
-        """
-        Generates a URL to the CAS server where the client should be redirected.
+        Otherwise, this registers the user if necessary, and then returns a
+        redirect (with a login token) to the client.
 
         Args:
-            client_redirect_url: The final URL the client should go to after the
-                user has negotiated SSO.
+            request: the incoming request from the browser. We'll
+                respond to it with a redirect or an HTML page.
 
-        Returns:
-            The URL to redirect to.
-        """
-        args = urllib.parse.urlencode(
-            {"service": self._build_service_param(client_redirect_url)}
-        )
+            ticket: The CAS ticket provided by the client.
 
-        return ("%s/login?%s" % (self._cas_server_url, args)).encode("ascii")
+            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.
 
-    async def handle_ticket_request(
-        self, request: SynapseRequest, client_redirect_url: str, ticket: str
-    ) -> None:
+            session: The session parameter from the `/cas/ticket` HTTP request, if given.
+                This should be the UI Auth session id.
         """
-        Validates a CAS ticket sent by the client for login/registration.
+        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)
 
-        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(username)
+        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)
+        if session:
+            self._auth_handler.complete_sso_ui_auth(
+                registered_user_id, session, request,
+            )
+
+        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_login(
+                registered_user_id, request, client_redirect_url
+            )