Exemplo n.º 1
0
    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 :/
        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
Exemplo n.º 2
0
        async def grandfather_existing_users() -> Optional[str]:
            if self._allow_existing_users:
                # If allowing existing users we want to generate a single localpart
                # and attempt to match it.
                attributes = await oidc_response_to_user_attributes(failures=0)

                user_id = UserID(attributes.localpart,
                                 self._server_name).to_string()
                users = await self._store.get_users_by_id_case_insensitive(
                    user_id)
                if users:
                    # If an existing matrix ID is returned, then use it.
                    if len(users) == 1:
                        previously_registered_user_id = next(iter(users))
                    elif user_id in users:
                        previously_registered_user_id = user_id
                    else:
                        # Do not attempt to continue generating Matrix IDs.
                        raise MappingException(
                            "Attempted to login as '{}' but it matches more than one user inexactly: {}"
                            .format(user_id, users))

                    return previously_registered_user_id

            return None
Exemplo n.º 3
0
        async def oidc_response_to_user_attributes(failures: int) -> UserAttributes:
            """
            Call the mapping provider to map the OIDC userinfo and token to user attributes.

            This is backwards compatibility for abstraction for the SSO handler.
            """
            if supports_failures:
                attributes = await self._user_mapping_provider.map_user_attributes(
                    userinfo, token, failures
                )
            else:
                # If the mapping provider does not support processing failures,
                # do not continually generate the same Matrix ID since it will
                # continue to already be in use. Note that the error raised is
                # arbitrary and will get turned into a MappingException.
                if failures:
                    raise MappingException(
                        "Mapping provider does not support de-duplicating Matrix IDs"
                    )

                attributes = await self._user_mapping_provider.map_user_attributes(  # type: ignore
                    userinfo, token
                )

            return UserAttributes(**attributes)
Exemplo n.º 4
0
 def get_remote_user_id(self, saml_response: saml2.response.AuthnResponse,
                        client_redirect_url: str) -> str:
     """Extracts the remote user id from the SAML response"""
     try:
         return saml_response.ava["uid"][0]
     except KeyError:
         logger.warning("SAML2 response lacks a 'uid' attestation")
         raise MappingException("'uid' not in SAML2 response")
Exemplo n.º 5
0
    def test_callback(self):
        """Code callback works and display errors if something went wrong.

        A lot of scenarios are tested here:
         - when the callback works, with userinfo from ID token
         - when the user mapping fails
         - when ID token verification fails
         - when the callback works, with userinfo fetched from the userinfo endpoint
         - when the userinfo fetching fails
         - when the code exchange fails
        """
        token = {
            "type": "bearer",
            "id_token": "id_token",
            "access_token": "access_token",
        }
        userinfo = {
            "sub": "foo",
            "preferred_username": "******",
        }
        user_id = "@foo:domain.org"
        self.handler._exchange_code = simple_async_mock(return_value=token)
        self.handler._parse_id_token = simple_async_mock(return_value=userinfo)
        self.handler._fetch_userinfo = simple_async_mock(return_value=userinfo)
        self.handler._map_userinfo_to_user = simple_async_mock(
            return_value=user_id)
        self.handler._auth_handler.complete_sso_login = simple_async_mock()
        request = Mock(spec=[
            "args",
            "getCookie",
            "addCookie",
            "requestHeaders",
            "getClientIP",
            "get_user_agent",
        ])

        code = "code"
        state = "state"
        nonce = "nonce"
        client_redirect_url = "http://client/redirect"
        user_agent = "Browser"
        ip_address = "10.0.0.1"
        request.getCookie.return_value = self.handler._generate_oidc_session_token(
            state=state,
            nonce=nonce,
            client_redirect_url=client_redirect_url,
            ui_auth_session_id=None,
        )

        request.args = {}
        request.args[b"code"] = [code.encode("utf-8")]
        request.args[b"state"] = [state.encode("utf-8")]

        request.getClientIP.return_value = ip_address
        request.get_user_agent.return_value = user_agent

        self.get_success(self.handler.handle_oidc_callback(request))

        self.handler._auth_handler.complete_sso_login.assert_called_once_with(
            user_id,
            request,
            client_redirect_url,
            {},
        )
        self.handler._exchange_code.assert_called_once_with(code)
        self.handler._parse_id_token.assert_called_once_with(token,
                                                             nonce=nonce)
        self.handler._map_userinfo_to_user.assert_called_once_with(
            userinfo, token, user_agent, ip_address)
        self.handler._fetch_userinfo.assert_not_called()
        self.render_error.assert_not_called()

        # Handle mapping errors
        self.handler._map_userinfo_to_user = simple_async_mock(
            raises=MappingException())
        self.get_success(self.handler.handle_oidc_callback(request))
        self.assertRenderedError("mapping_error")
        self.handler._map_userinfo_to_user = simple_async_mock(
            return_value=user_id)

        # Handle ID token errors
        self.handler._parse_id_token = simple_async_mock(raises=Exception())
        self.get_success(self.handler.handle_oidc_callback(request))
        self.assertRenderedError("invalid_token")

        self.handler._auth_handler.complete_sso_login.reset_mock()
        self.handler._exchange_code.reset_mock()
        self.handler._parse_id_token.reset_mock()
        self.handler._map_userinfo_to_user.reset_mock()
        self.handler._fetch_userinfo.reset_mock()

        # With userinfo fetching
        self.handler._scopes = []  # do not ask the "openid" scope
        self.get_success(self.handler.handle_oidc_callback(request))

        self.handler._auth_handler.complete_sso_login.assert_called_once_with(
            user_id,
            request,
            client_redirect_url,
            {},
        )
        self.handler._exchange_code.assert_called_once_with(code)
        self.handler._parse_id_token.assert_not_called()
        self.handler._map_userinfo_to_user.assert_called_once_with(
            userinfo, token, user_agent, ip_address)
        self.handler._fetch_userinfo.assert_called_once_with(token)
        self.render_error.assert_not_called()

        # Handle userinfo fetching error
        self.handler._fetch_userinfo = simple_async_mock(raises=Exception())
        self.get_success(self.handler.handle_oidc_callback(request))
        self.assertRenderedError("fetch_error")

        # Handle code exchange failure
        self.handler._exchange_code = simple_async_mock(
            raises=OidcError("invalid_request"))
        self.get_success(self.handler.handle_oidc_callback(request))
        self.assertRenderedError("invalid_request")
Exemplo n.º 6
0
    async def _complete_oidc_login(
        self,
        userinfo: UserInfo,
        token: Token,
        request: SynapseRequest,
        client_redirect_url: str,
    ) -> None:
        """Given a UserInfo response, complete the login flow

        UserInfo should have a claim that uniquely identifies users. This claim
        is usually `sub`, but can be configured with `oidc_config.subject_claim`.
        It is then used as an `external_id`.

        If we don't find the user that way, we should register the user,
        mapping the localpart and the display name from the UserInfo.

        If a user already exists with the mxid we've mapped and allow_existing_users
        is disabled, raise an exception.

        Otherwise, render a redirect back to the client_redirect_url with a loginToken.

        Args:
            userinfo: an object representing the user
            token: a dict with the tokens obtained from the provider
            request: The request to respond to
            client_redirect_url: The redirect URL passed in by the client.

        Raises:
            MappingException: if there was an error while mapping some properties
        """
        try:
            remote_user_id = self._remote_id_from_userinfo(userinfo)
        except Exception as e:
            raise MappingException(
                "Failed to extract subject from OIDC response: %s" % (e, ))

        # Older mapping providers don't accept the `failures` argument, so we
        # try and detect support.
        mapper_signature = inspect.signature(
            self._user_mapping_provider.map_user_attributes)
        supports_failures = "failures" in mapper_signature.parameters

        async def oidc_response_to_user_attributes(
                failures: int) -> UserAttributes:
            """
            Call the mapping provider to map the OIDC userinfo and token to user attributes.

            This is backwards compatibility for abstraction for the SSO handler.
            """
            if supports_failures:
                attributes = await self._user_mapping_provider.map_user_attributes(
                    userinfo, token, failures)
            else:
                # If the mapping provider does not support processing failures,
                # do not continually generate the same Matrix ID since it will
                # continue to already be in use. Note that the error raised is
                # arbitrary and will get turned into a MappingException.
                if failures:
                    raise MappingException(
                        "Mapping provider does not support de-duplicating Matrix IDs"
                    )

                attributes = await self._user_mapping_provider.map_user_attributes(  # type: ignore
                    userinfo, token)

            return UserAttributes(**attributes)

        async def grandfather_existing_users() -> Optional[str]:
            if self._allow_existing_users:
                # If allowing existing users we want to generate a single localpart
                # and attempt to match it.
                attributes = await oidc_response_to_user_attributes(failures=0)

                user_id = UserID(attributes.localpart,
                                 self._server_name).to_string()
                users = await self._store.get_users_by_id_case_insensitive(
                    user_id)
                if users:
                    # If an existing matrix ID is returned, then use it.
                    if len(users) == 1:
                        previously_registered_user_id = next(iter(users))
                    elif user_id in users:
                        previously_registered_user_id = user_id
                    else:
                        # Do not attempt to continue generating Matrix IDs.
                        raise MappingException(
                            "Attempted to login as '{}' but it matches more than one user inexactly: {}"
                            .format(user_id, users))

                    return previously_registered_user_id

            return None

        # Mapping providers might not have get_extra_attributes: only call this
        # method if it exists.
        extra_attributes = None
        get_extra_attributes = getattr(self._user_mapping_provider,
                                       "get_extra_attributes", None)
        if get_extra_attributes:
            extra_attributes = await get_extra_attributes(userinfo, token)

        await self._sso_handler.complete_sso_login_request(
            self.idp_id,
            remote_user_id,
            request,
            client_redirect_url,
            oidc_response_to_user_attributes,
            grandfather_existing_users,
            extra_attributes,
        )
Exemplo n.º 7
0
    async def _map_saml_response_to_user(
        self,
        saml2_auth: saml2.response.AuthnResponse,
        client_redirect_url: str,
        user_agent: str,
        ip_address: str,
    ) -> str:
        """
        Given a SAML response, retrieve the user ID for it and possibly register the user.

        Args:
            saml2_auth: The parsed SAML2 response.
            client_redirect_url: The redirect URL passed in by the client.
            user_agent: The user agent of the client making the request.
            ip_address: The IP address of the client making the request.

        Returns:
             The user ID associated with this response.

        Raises:
            MappingException if there was a problem mapping the response to a 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(
            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:
            """
            Call the mapping provider to map a SAML response to user attributes and coerce the result into the standard form.

            This is backwards compatibility for abstraction for the SSO handler.
            """
            # Call the mapping provider.
            result = self._user_mapping_provider.saml_response_to_user_attributes(
                saml2_auth, failures, client_redirect_url)
            # Remap some of the results.
            return UserAttributes(
                localpart=result.get("mxid_localpart"),
                display_name=result.get("displayname"),
                emails=result.get("emails", []),
            )

        async def grandfather_existing_users() -> Optional[str]:
            # backwards-compatibility hack: see if there is an existing user with a
            # suitable mapping from the uid
            if (self._grandfathered_mxid_source_attribute
                    and self._grandfathered_mxid_source_attribute
                    in saml2_auth.ava):
                attrval = saml2_auth.ava[
                    self._grandfathered_mxid_source_attribute][0]
                user_id = UserID(map_username_to_mxid_localpart(attrval),
                                 self.server_name).to_string()

                logger.debug(
                    "Looking for existing account based on mapped %s %s",
                    self._grandfathered_mxid_source_attribute,
                    user_id,
                )

                users = await self.store.get_users_by_id_case_insensitive(
                    user_id)
                if users:
                    registered_user_id = list(users.keys())[0]
                    logger.info("Grandfathering mapping to %s",
                                registered_user_id)
                    return registered_user_id

            return None

        with (await self._mapping_lock.queue(self._auth_provider_id)):
            return await self._sso_handler.get_mxid_from_sso(
                self._auth_provider_id,
                remote_user_id,
                user_agent,
                ip_address,
                saml_response_to_remapped_user_attributes,
                grandfather_existing_users,
            )
Exemplo n.º 8
0
    def test_callback(self):
        """Code callback works and display errors if something went wrong.

        A lot of scenarios are tested here:
         - when the callback works, with userinfo from ID token
         - when the user mapping fails
         - when ID token verification fails
         - when the callback works, with userinfo fetched from the userinfo endpoint
         - when the userinfo fetching fails
         - when the code exchange fails
        """

        # ensure that we are correctly testing the fallback when "get_extra_attributes"
        # is not implemented.
        mapping_provider = self.provider._user_mapping_provider
        with self.assertRaises(AttributeError):
            _ = mapping_provider.get_extra_attributes

        token = {
            "type": "bearer",
            "id_token": "id_token",
            "access_token": "access_token",
        }
        username = "******"
        userinfo = {
            "sub": "foo",
            "username": username,
        }
        expected_user_id = "@%s:%s" % (username, self.hs.hostname)
        self.provider._exchange_code = simple_async_mock(return_value=token)
        self.provider._parse_id_token = simple_async_mock(
            return_value=userinfo)
        self.provider._fetch_userinfo = simple_async_mock(
            return_value=userinfo)
        auth_handler = self.hs.get_auth_handler()
        auth_handler.complete_sso_login = simple_async_mock()

        code = "code"
        state = "state"
        nonce = "nonce"
        client_redirect_url = "http://client/redirect"
        user_agent = "Browser"
        ip_address = "10.0.0.1"
        session = self._generate_oidc_session_token(state, nonce,
                                                    client_redirect_url)
        request = _build_callback_request(code,
                                          state,
                                          session,
                                          user_agent=user_agent,
                                          ip_address=ip_address)

        self.get_success(self.handler.handle_oidc_callback(request))

        auth_handler.complete_sso_login.assert_called_once_with(
            expected_user_id,
            "oidc",
            request,
            client_redirect_url,
            None,
            new_user=True)
        self.provider._exchange_code.assert_called_once_with(code)
        self.provider._parse_id_token.assert_called_once_with(token,
                                                              nonce=nonce)
        self.provider._fetch_userinfo.assert_not_called()
        self.render_error.assert_not_called()

        # Handle mapping errors
        with patch.object(
                self.provider,
                "_remote_id_from_userinfo",
                new=Mock(side_effect=MappingException()),
        ):
            self.get_success(self.handler.handle_oidc_callback(request))
            self.assertRenderedError("mapping_error")

        # Handle ID token errors
        self.provider._parse_id_token = simple_async_mock(raises=Exception())
        self.get_success(self.handler.handle_oidc_callback(request))
        self.assertRenderedError("invalid_token")

        auth_handler.complete_sso_login.reset_mock()
        self.provider._exchange_code.reset_mock()
        self.provider._parse_id_token.reset_mock()
        self.provider._fetch_userinfo.reset_mock()

        # With userinfo fetching
        self.provider._scopes = []  # do not ask the "openid" scope
        self.get_success(self.handler.handle_oidc_callback(request))

        auth_handler.complete_sso_login.assert_called_once_with(
            expected_user_id,
            "oidc",
            request,
            client_redirect_url,
            None,
            new_user=False)
        self.provider._exchange_code.assert_called_once_with(code)
        self.provider._parse_id_token.assert_not_called()
        self.provider._fetch_userinfo.assert_called_once_with(token)
        self.render_error.assert_not_called()

        # Handle userinfo fetching error
        self.provider._fetch_userinfo = simple_async_mock(raises=Exception())
        self.get_success(self.handler.handle_oidc_callback(request))
        self.assertRenderedError("fetch_error")

        # Handle code exchange failure
        from synapse.handlers.oidc import OidcError

        self.provider._exchange_code = simple_async_mock(
            raises=OidcError("invalid_request"))
        self.get_success(self.handler.handle_oidc_callback(request))
        self.assertRenderedError("invalid_request")