Exemple #1
0
    async def handle_submit_username_request(self, request: SynapseRequest,
                                             localpart: str,
                                             session_id: str) -> None:
        """Handle a request to the username-picker 'submit' endpoint

        Will serve an HTTP response to the request.

        Args:
            request: HTTP request
            localpart: localpart requested by the user
            session_id: ID of the username mapping session, extracted from a cookie
        """
        self._expire_old_sessions()
        session = self._username_mapping_sessions.get(session_id)
        if not session:
            logger.info("Couldn't find session id %s", session_id)
            raise SynapseError(400, "unknown session")

        logger.info("[session %s] Registering localpart %s", session_id,
                    localpart)

        attributes = UserAttributes(
            localpart=localpart,
            display_name=session.display_name,
            emails=session.emails,
        )

        # the following will raise a 400 error if the username has been taken in the
        # meantime.
        user_id = await self._register_mapped_user(
            attributes,
            session.auth_provider_id,
            session.remote_user_id,
            get_request_user_agent(request),
            request.getClientIP(),
        )

        logger.info("[session %s] Registered userid %s", session_id, user_id)

        # delete the mapping session and the cookie
        del self._username_mapping_sessions[session_id]

        # delete the cookie
        request.addCookie(
            USERNAME_MAPPING_SESSION_COOKIE_NAME,
            b"",
            expires=b"Thu, 01 Jan 1970 00:00:00 GMT",
            path=b"/",
        )

        await self._auth_handler.complete_sso_login(
            user_id,
            request,
            session.client_redirect_url,
            session.extra_login_attributes,
        )
Exemple #2
0
    async def handle_redirect_request(
        self, request: SynapseRequest, client_redirect_url: bytes
    ) -> None:
        """Handle an incoming request to /login/sso/redirect

        It redirects the browser to the authorization endpoint with a few
        parameters:

          - ``client_id``: the client ID set in ``oidc_config.client_id``
          - ``response_type``: ``code``
          - ``redirect_uri``: the callback URL ; ``{base url}/_synapse/oidc/callback``
          - ``scope``: the list of scopes set in ``oidc_config.scopes``
          - ``state``: a random string
          - ``nonce``: a random string

        In addition to redirecting the client, we are setting a cookie with
        a signed macaroon token containing the state, the nonce and the
        client_redirect_url params. Those are then checked when the client
        comes back from the provider.


        Args:
            request: the incoming request from the browser.
                We'll respond to it with a redirect and a cookie.
            client_redirect_url: the URL that we should redirect the client to
                when everything is done
        """

        state = generate_token()
        nonce = generate_token()

        cookie = self._generate_oidc_session_token(
            state=state, nonce=nonce, client_redirect_url=client_redirect_url.decode(),
        )
        request.addCookie(
            SESSION_COOKIE_NAME,
            cookie,
            path="/_synapse/oidc",
            max_age="3600",
            httpOnly=True,
            sameSite="lax",
        )

        metadata = await self.load_metadata()
        authorization_endpoint = metadata.get("authorization_endpoint")
        uri = prepare_grant_uri(
            authorization_endpoint,
            client_id=self._client_auth.client_id,
            response_type="code",
            redirect_uri=self._callback_url,
            scope=self._scopes,
            state=state,
            nonce=nonce,
        )
        request.redirect(uri)
        finish_request(request)
Exemple #3
0
    async def handle_oidc_callback(self, request: SynapseRequest) -> None:
        """Handle an incoming request to /_synapse/oidc/callback

        Since we might want to display OIDC-related errors in a user-friendly
        way, we don't raise SynapseError from here. Instead, we call
        ``self._render_error`` which displays an HTML page for the error.

        Most of the OpenID Connect logic happens here:

          - first, we check if there was any error returned by the provider and
            display it
          - then we fetch the session cookie, decode and verify it
          - the ``state`` query parameter should match with the one stored in the
            session cookie
          - once we known this session is legit, exchange the code with the
            provider using the ``token_endpoint`` (see ``_exchange_code``)
          - once we have the token, use it to either extract the UserInfo from
            the ``id_token`` (``_parse_id_token``), or use the ``access_token``
            to fetch UserInfo from the ``userinfo_endpoint``
            (``_fetch_userinfo``)
          - map those UserInfo to a Matrix user (``_map_userinfo_to_user``) and
            finish the login

        Args:
            request: the incoming request from the browser.
        """

        # The provider might redirect with an error.
        # In that case, just display it as-is.
        if b"error" in request.args:
            # error response from the auth server. see:
            #  https://tools.ietf.org/html/rfc6749#section-4.1.2.1
            #  https://openid.net/specs/openid-connect-core-1_0.html#AuthError
            error = request.args[b"error"][0].decode()
            description = request.args.get(b"error_description", [b""])[0].decode()

            # Most of the errors returned by the provider could be due by
            # either the provider misbehaving or Synapse being misconfigured.
            # The only exception of that is "access_denied", where the user
            # probably cancelled the login flow. In other cases, log those errors.
            if error != "access_denied":
                logger.error("Error from the OIDC provider: %s %s", error, description)

            self._render_error(request, error, description)
            return

        # otherwise, it is presumably a successful response. see:
        #   https://tools.ietf.org/html/rfc6749#section-4.1.2

        # Fetch the session cookie
        session = request.getCookie(SESSION_COOKIE_NAME)  # type: Optional[bytes]
        if session is None:
            logger.info("No session cookie found")
            self._render_error(request, "missing_session", "No session cookie found")
            return

        # Remove the cookie. There is a good chance that if the callback failed
        # once, it will fail next time and the code will already be exchanged.
        # Removing it early avoids spamming the provider with token requests.
        request.addCookie(
            SESSION_COOKIE_NAME,
            b"",
            path="/_synapse/oidc",
            expires="Thu, Jan 01 1970 00:00:00 UTC",
            httpOnly=True,
            sameSite="lax",
        )

        # Check for the state query parameter
        if b"state" not in request.args:
            logger.info("State parameter is missing")
            self._render_error(request, "invalid_request", "State parameter is missing")
            return

        state = request.args[b"state"][0].decode()

        # Deserialize the session token and verify it.
        try:
            (
                nonce,
                client_redirect_url,
                ui_auth_session_id,
            ) = self._verify_oidc_session_token(session, state)
        except MacaroonDeserializationException as e:
            logger.exception("Invalid session")
            self._render_error(request, "invalid_session", str(e))
            return
        except MacaroonInvalidSignatureException as e:
            logger.exception("Could not verify session")
            self._render_error(request, "mismatching_session", str(e))
            return

        # Exchange the code with the provider
        if b"code" not in request.args:
            logger.info("Code parameter is missing")
            self._render_error(request, "invalid_request", "Code parameter is missing")
            return

        logger.debug("Exchanging code")
        code = request.args[b"code"][0].decode()
        try:
            token = await self._exchange_code(code)
        except OidcError as e:
            logger.exception("Could not exchange code")
            self._render_error(request, e.error, e.error_description)
            return

        logger.debug("Successfully obtained OAuth2 access token")

        # Now that we have a token, get the userinfo, either by decoding the
        # `id_token` or by fetching the `userinfo_endpoint`.
        if self._uses_userinfo:
            logger.debug("Fetching userinfo")
            try:
                userinfo = await self._fetch_userinfo(token)
            except Exception as e:
                logger.exception("Could not fetch userinfo")
                self._render_error(request, "fetch_error", str(e))
                return
        else:
            logger.debug("Extracting userinfo from id_token")
            try:
                userinfo = await self._parse_id_token(token, nonce=nonce)
            except Exception as e:
                logger.exception("Invalid id_token")
                self._render_error(request, "invalid_token", str(e))
                return

        # Call the mapper to register/login the user
        try:
            user_id = await self._map_userinfo_to_user(userinfo, token)
        except MappingException as e:
            logger.exception("Could not map user")
            self._render_error(request, "mapping_error", str(e))
            return

        # and finally complete the login
        if ui_auth_session_id:
            await self._auth_handler.complete_sso_ui_auth(
                user_id, ui_auth_session_id, request
            )
        else:
            await self._auth_handler.complete_sso_login(
                user_id, request, client_redirect_url
            )
Exemple #4
0
    async def handle_redirect_request(
        self,
        request: SynapseRequest,
        client_redirect_url: Optional[bytes],
        ui_auth_session_id: Optional[str] = None,
    ) -> str:
        """Handle an incoming request to /login/sso/redirect

        It returns a redirect to the authorization endpoint with a few
        parameters:

          - ``client_id``: the client ID set in ``oidc_config.client_id``
          - ``response_type``: ``code``
          - ``redirect_uri``: the callback URL ; ``{base url}/_synapse/client/oidc/callback``
          - ``scope``: the list of scopes set in ``oidc_config.scopes``
          - ``state``: a random string
          - ``nonce``: a random string

        In addition generating a redirect URL, we are setting a cookie with
        a signed macaroon token containing the state, the nonce and the
        client_redirect_url params. Those are then checked when the client
        comes back from the provider.

        Args:
            request: the incoming request from the browser.
                We'll respond to it with a redirect and a cookie.
            client_redirect_url: the URL that we should redirect the client to
                when everything is done (or None for UI Auth)
            ui_auth_session_id: The session ID of the ongoing UI Auth (or
                None if this is a login).

        Returns:
            The redirect URL to the authorization endpoint.

        """

        state = generate_token()
        nonce = generate_token()

        if not client_redirect_url:
            client_redirect_url = b""

        cookie = self._token_generator.generate_oidc_session_token(
            state=state,
            session_data=OidcSessionData(
                idp_id=self.idp_id,
                nonce=nonce,
                client_redirect_url=client_redirect_url.decode(),
                ui_auth_session_id=ui_auth_session_id,
            ),
        )
        request.addCookie(
            SESSION_COOKIE_NAME,
            cookie,
            path="/_synapse/client/oidc",
            max_age="3600",
            httpOnly=True,
            sameSite="lax",
        )

        metadata = await self.load_metadata()
        authorization_endpoint = metadata.get("authorization_endpoint")
        return prepare_grant_uri(
            authorization_endpoint,
            client_id=self._client_auth.client_id,
            response_type="code",
            redirect_uri=self._callback_url,
            scope=self._scopes,
            state=state,
            nonce=nonce,
        )
Exemple #5
0
    async def handle_oidc_callback(self, request: SynapseRequest) -> None:
        """Handle an incoming request to /_synapse/client/oidc/callback

        Since we might want to display OIDC-related errors in a user-friendly
        way, we don't raise SynapseError from here. Instead, we call
        ``self._sso_handler.render_error`` which displays an HTML page for the error.

        Most of the OpenID Connect logic happens here:

          - first, we check if there was any error returned by the provider and
            display it
          - then we fetch the session cookie, decode and verify it
          - the ``state`` query parameter should match with the one stored in the
            session cookie

        Once we know the session is legit, we then delegate to the OIDC Provider
        implementation, which will exchange the code with the provider and complete the
        login/authentication.

        Args:
            request: the incoming request from the browser.
        """

        # The provider might redirect with an error.
        # In that case, just display it as-is.
        if b"error" in request.args:
            # error response from the auth server. see:
            #  https://tools.ietf.org/html/rfc6749#section-4.1.2.1
            #  https://openid.net/specs/openid-connect-core-1_0.html#AuthError
            error = request.args[b"error"][0].decode()
            description = request.args.get(b"error_description",
                                           [b""])[0].decode()

            # Most of the errors returned by the provider could be due by
            # either the provider misbehaving or Synapse being misconfigured.
            # The only exception of that is "access_denied", where the user
            # probably cancelled the login flow. In other cases, log those errors.
            if error != "access_denied":
                logger.error("Error from the OIDC provider: %s %s", error,
                             description)

            self._sso_handler.render_error(request, error, description)
            return

        # otherwise, it is presumably a successful response. see:
        #   https://tools.ietf.org/html/rfc6749#section-4.1.2

        # Fetch the session cookie
        session = request.getCookie(
            SESSION_COOKIE_NAME)  # type: Optional[bytes]
        if session is None:
            logger.info("No session cookie found")
            self._sso_handler.render_error(request, "missing_session",
                                           "No session cookie found")
            return

        # Remove the cookie. There is a good chance that if the callback failed
        # once, it will fail next time and the code will already be exchanged.
        # Removing it early avoids spamming the provider with token requests.
        request.addCookie(
            SESSION_COOKIE_NAME,
            b"",
            path="/_synapse/oidc",
            expires="Thu, Jan 01 1970 00:00:00 UTC",
            httpOnly=True,
            sameSite="lax",
        )

        # Check for the state query parameter
        if b"state" not in request.args:
            logger.info("State parameter is missing")
            self._sso_handler.render_error(request, "invalid_request",
                                           "State parameter is missing")
            return

        state = request.args[b"state"][0].decode()

        # Deserialize the session token and verify it.
        try:
            session_data = self._token_generator.verify_oidc_session_token(
                session, state)
        except (MacaroonDeserializationException, ValueError) as e:
            logger.exception("Invalid session")
            self._sso_handler.render_error(request, "invalid_session", str(e))
            return
        except MacaroonInvalidSignatureException as e:
            logger.exception("Could not verify session")
            self._sso_handler.render_error(request, "mismatching_session",
                                           str(e))
            return

        oidc_provider = self._providers.get(session_data.idp_id)
        if not oidc_provider:
            logger.error("OIDC session uses unknown IdP %r", oidc_provider)
            self._sso_handler.render_error(request, "unknown_idp",
                                           "Unknown IdP")
            return

        if b"code" not in request.args:
            logger.info("Code parameter is missing")
            self._sso_handler.render_error(request, "invalid_request",
                                           "Code parameter is missing")
            return

        code = request.args[b"code"][0].decode()

        await oidc_provider.handle_oidc_callback(request, session_data, code)