1# Copyright 2015, 2016 OpenMarket Ltd
2# Copyright 2017 Vector Creations Ltd
3# Copyright 2018 New Vector Ltd
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Utilities for interacting with Identity Servers"""
18import logging
19import urllib.parse
20from typing import TYPE_CHECKING, Awaitable, Callable, Dict, List, Optional, Tuple
21
22from synapse.api.errors import (
23    CodeMessageException,
24    Codes,
25    HttpResponseException,
26    SynapseError,
27)
28from synapse.api.ratelimiting import Ratelimiter
29from synapse.config.emailconfig import ThreepidBehaviour
30from synapse.http import RequestTimedOutError
31from synapse.http.client import SimpleHttpClient
32from synapse.http.site import SynapseRequest
33from synapse.types import JsonDict, Requester
34from synapse.util import json_decoder
35from synapse.util.hash import sha256_and_url_safe_base64
36from synapse.util.stringutils import (
37    assert_valid_client_secret,
38    random_string,
39    valid_id_server_location,
40)
41
42if TYPE_CHECKING:
43    from synapse.server import HomeServer
44
45logger = logging.getLogger(__name__)
46
47id_server_scheme = "https://"
48
49
50class IdentityHandler:
51    def __init__(self, hs: "HomeServer"):
52        self.store = hs.get_datastore()
53        # An HTTP client for contacting trusted URLs.
54        self.http_client = SimpleHttpClient(hs)
55        # An HTTP client for contacting identity servers specified by clients.
56        self.blacklisting_http_client = SimpleHttpClient(
57            hs,
58            ip_blacklist=hs.config.server.federation_ip_range_blacklist,
59            ip_whitelist=hs.config.server.federation_ip_range_whitelist,
60        )
61        self.federation_http_client = hs.get_federation_http_client()
62        self.hs = hs
63
64        self._web_client_location = hs.config.email.invite_client_location
65
66        # Ratelimiters for `/requestToken` endpoints.
67        self._3pid_validation_ratelimiter_ip = Ratelimiter(
68            store=self.store,
69            clock=hs.get_clock(),
70            rate_hz=hs.config.ratelimiting.rc_3pid_validation.per_second,
71            burst_count=hs.config.ratelimiting.rc_3pid_validation.burst_count,
72        )
73        self._3pid_validation_ratelimiter_address = Ratelimiter(
74            store=self.store,
75            clock=hs.get_clock(),
76            rate_hz=hs.config.ratelimiting.rc_3pid_validation.per_second,
77            burst_count=hs.config.ratelimiting.rc_3pid_validation.burst_count,
78        )
79
80    async def ratelimit_request_token_requests(
81        self,
82        request: SynapseRequest,
83        medium: str,
84        address: str,
85    ) -> None:
86        """Used to ratelimit requests to `/requestToken` by IP and address.
87
88        Args:
89            request: The associated request
90            medium: The type of threepid, e.g. "msisdn" or "email"
91            address: The actual threepid ID, e.g. the phone number or email address
92        """
93
94        await self._3pid_validation_ratelimiter_ip.ratelimit(
95            None, (medium, request.getClientIP())
96        )
97        await self._3pid_validation_ratelimiter_address.ratelimit(
98            None, (medium, address)
99        )
100
101    async def threepid_from_creds(
102        self, id_server: str, creds: Dict[str, str]
103    ) -> Optional[JsonDict]:
104        """
105        Retrieve and validate a threepid identifier from a "credentials" dictionary against a
106        given identity server
107
108        Args:
109            id_server: The identity server to validate 3PIDs against. Must be a
110                complete URL including the protocol (http(s)://)
111            creds: Dictionary containing the following keys:
112                * client_secret|clientSecret: A unique secret str provided by the client
113                * sid: The ID of the validation session
114
115        Returns:
116            A dictionary consisting of response params to the /getValidated3pid
117            endpoint of the Identity Service API, or None if the threepid was not found
118        """
119        client_secret = creds.get("client_secret") or creds.get("clientSecret")
120        if not client_secret:
121            raise SynapseError(
122                400, "Missing param client_secret in creds", errcode=Codes.MISSING_PARAM
123            )
124        assert_valid_client_secret(client_secret)
125
126        session_id = creds.get("sid")
127        if not session_id:
128            raise SynapseError(
129                400, "Missing param session_id in creds", errcode=Codes.MISSING_PARAM
130            )
131
132        query_params = {"sid": session_id, "client_secret": client_secret}
133
134        url = id_server + "/_matrix/identity/api/v1/3pid/getValidated3pid"
135
136        try:
137            data = await self.http_client.get_json(url, query_params)
138        except RequestTimedOutError:
139            raise SynapseError(500, "Timed out contacting identity server")
140        except HttpResponseException as e:
141            logger.info(
142                "%s returned %i for threepid validation for: %s",
143                id_server,
144                e.code,
145                creds,
146            )
147            return None
148
149        # Old versions of Sydent return a 200 http code even on a failed validation
150        # check. Thus, in addition to the HttpResponseException check above (which
151        # checks for non-200 errors), we need to make sure validation_session isn't
152        # actually an error, identified by the absence of a "medium" key
153        # See https://github.com/matrix-org/sydent/issues/215 for details
154        if "medium" in data:
155            return data
156
157        logger.info("%s reported non-validated threepid: %s", id_server, creds)
158        return None
159
160    async def bind_threepid(
161        self,
162        client_secret: str,
163        sid: str,
164        mxid: str,
165        id_server: str,
166        id_access_token: Optional[str] = None,
167        use_v2: bool = True,
168    ) -> JsonDict:
169        """Bind a 3PID to an identity server
170
171        Args:
172            client_secret: A unique secret provided by the client
173            sid: The ID of the validation session
174            mxid: The MXID to bind the 3PID to
175            id_server: The domain of the identity server to query
176            id_access_token: The access token to authenticate to the identity
177                server with, if necessary. Required if use_v2 is true
178            use_v2: Whether to use v2 Identity Service API endpoints. Defaults to True
179
180        Raises:
181            SynapseError: On any of the following conditions
182                - the supplied id_server is not a valid identity server name
183                - we failed to contact the supplied identity server
184
185        Returns:
186            The response from the identity server
187        """
188        logger.debug("Proxying threepid bind request for %s to %s", mxid, id_server)
189
190        # If an id_access_token is not supplied, force usage of v1
191        if id_access_token is None:
192            use_v2 = False
193
194        if not valid_id_server_location(id_server):
195            raise SynapseError(
196                400,
197                "id_server must be a valid hostname with optional port and path components",
198            )
199
200        # Decide which API endpoint URLs to use
201        headers = {}
202        bind_data = {"sid": sid, "client_secret": client_secret, "mxid": mxid}
203        if use_v2:
204            bind_url = "https://%s/_matrix/identity/v2/3pid/bind" % (id_server,)
205            headers["Authorization"] = create_id_access_token_header(id_access_token)  # type: ignore
206        else:
207            bind_url = "https://%s/_matrix/identity/api/v1/3pid/bind" % (id_server,)
208
209        try:
210            # Use the blacklisting http client as this call is only to identity servers
211            # provided by a client
212            data = await self.blacklisting_http_client.post_json_get_json(
213                bind_url, bind_data, headers=headers
214            )
215
216            # Remember where we bound the threepid
217            await self.store.add_user_bound_threepid(
218                user_id=mxid,
219                medium=data["medium"],
220                address=data["address"],
221                id_server=id_server,
222            )
223
224            return data
225        except HttpResponseException as e:
226            if e.code != 404 or not use_v2:
227                logger.error("3PID bind failed with Matrix error: %r", e)
228                raise e.to_synapse_error()
229        except RequestTimedOutError:
230            raise SynapseError(500, "Timed out contacting identity server")
231        except CodeMessageException as e:
232            data = json_decoder.decode(e.msg)  # XXX WAT?
233            return data
234
235        logger.info("Got 404 when POSTing JSON %s, falling back to v1 URL", bind_url)
236        res = await self.bind_threepid(
237            client_secret, sid, mxid, id_server, id_access_token, use_v2=False
238        )
239        return res
240
241    async def try_unbind_threepid(self, mxid: str, threepid: dict) -> bool:
242        """Attempt to remove a 3PID from an identity server, or if one is not provided, all
243        identity servers we're aware the binding is present on
244
245        Args:
246            mxid: Matrix user ID of binding to be removed
247            threepid: Dict with medium & address of binding to be
248                removed, and an optional id_server.
249
250        Raises:
251            SynapseError: If we failed to contact the identity server
252
253        Returns:
254            True on success, otherwise False if the identity
255            server doesn't support unbinding (or no identity server found to
256            contact).
257        """
258        if threepid.get("id_server"):
259            id_servers = [threepid["id_server"]]
260        else:
261            id_servers = await self.store.get_id_servers_user_bound(
262                user_id=mxid, medium=threepid["medium"], address=threepid["address"]
263            )
264
265        # We don't know where to unbind, so we don't have a choice but to return
266        if not id_servers:
267            return False
268
269        changed = True
270        for id_server in id_servers:
271            changed &= await self.try_unbind_threepid_with_id_server(
272                mxid, threepid, id_server
273            )
274
275        return changed
276
277    async def try_unbind_threepid_with_id_server(
278        self, mxid: str, threepid: dict, id_server: str
279    ) -> bool:
280        """Removes a binding from an identity server
281
282        Args:
283            mxid: Matrix user ID of binding to be removed
284            threepid: Dict with medium & address of binding to be removed
285            id_server: Identity server to unbind from
286
287        Raises:
288            SynapseError: On any of the following conditions
289                - the supplied id_server is not a valid identity server name
290                - we failed to contact the supplied identity server
291
292        Returns:
293            True on success, otherwise False if the identity
294            server doesn't support unbinding
295        """
296
297        if not valid_id_server_location(id_server):
298            raise SynapseError(
299                400,
300                "id_server must be a valid hostname with optional port and path components",
301            )
302
303        url = "https://%s/_matrix/identity/api/v1/3pid/unbind" % (id_server,)
304        url_bytes = b"/_matrix/identity/api/v1/3pid/unbind"
305
306        content = {
307            "mxid": mxid,
308            "threepid": {"medium": threepid["medium"], "address": threepid["address"]},
309        }
310
311        # we abuse the federation http client to sign the request, but we have to send it
312        # using the normal http client since we don't want the SRV lookup and want normal
313        # 'browser-like' HTTPS.
314        auth_headers = self.federation_http_client.build_auth_headers(
315            destination=None,
316            method=b"POST",
317            url_bytes=url_bytes,
318            content=content,
319            destination_is=id_server.encode("ascii"),
320        )
321        headers = {b"Authorization": auth_headers}
322
323        try:
324            # Use the blacklisting http client as this call is only to identity servers
325            # provided by a client
326            await self.blacklisting_http_client.post_json_get_json(
327                url, content, headers
328            )
329            changed = True
330        except HttpResponseException as e:
331            changed = False
332            if e.code in (400, 404, 501):
333                # The remote server probably doesn't support unbinding (yet)
334                logger.warning("Received %d response while unbinding threepid", e.code)
335            else:
336                logger.error("Failed to unbind threepid on identity server: %s", e)
337                raise SynapseError(500, "Failed to contact identity server")
338        except RequestTimedOutError:
339            raise SynapseError(500, "Timed out contacting identity server")
340
341        await self.store.remove_user_bound_threepid(
342            user_id=mxid,
343            medium=threepid["medium"],
344            address=threepid["address"],
345            id_server=id_server,
346        )
347
348        return changed
349
350    async def send_threepid_validation(
351        self,
352        email_address: str,
353        client_secret: str,
354        send_attempt: int,
355        send_email_func: Callable[[str, str, str, str], Awaitable],
356        next_link: Optional[str] = None,
357    ) -> str:
358        """Send a threepid validation email for password reset or
359        registration purposes
360
361        Args:
362            email_address: The user's email address
363            client_secret: The provided client secret
364            send_attempt: Which send attempt this is
365            send_email_func: A function that takes an email address, token,
366                             client_secret and session_id, sends an email
367                             and returns an Awaitable.
368            next_link: The URL to redirect the user to after validation
369
370        Returns:
371            The new session_id upon success
372
373        Raises:
374            SynapseError is an error occurred when sending the email
375        """
376        # Check that this email/client_secret/send_attempt combo is new or
377        # greater than what we've seen previously
378        session = await self.store.get_threepid_validation_session(
379            "email", client_secret, address=email_address, validated=False
380        )
381
382        # Check to see if a session already exists and that it is not yet
383        # marked as validated
384        if session and session.get("validated_at") is None:
385            session_id = session["session_id"]
386            last_send_attempt = session["last_send_attempt"]
387
388            # Check that the send_attempt is higher than previous attempts
389            if send_attempt <= last_send_attempt:
390                # If not, just return a success without sending an email
391                return session_id
392        else:
393            # An non-validated session does not exist yet.
394            # Generate a session id
395            session_id = random_string(16)
396
397        if next_link:
398            # Manipulate the next_link to add the sid, because the caller won't get
399            # it until we send a response, by which time we've sent the mail.
400            if "?" in next_link:
401                next_link += "&"
402            else:
403                next_link += "?"
404            next_link += "sid=" + urllib.parse.quote(session_id)
405
406        # Generate a new validation token
407        token = random_string(32)
408
409        # Send the mail with the link containing the token, client_secret
410        # and session_id
411        try:
412            await send_email_func(email_address, token, client_secret, session_id)
413        except Exception:
414            logger.exception(
415                "Error sending threepid validation email to %s", email_address
416            )
417            raise SynapseError(500, "An error was encountered when sending the email")
418
419        token_expires = (
420            self.hs.get_clock().time_msec()
421            + self.hs.config.email.email_validation_token_lifetime
422        )
423
424        await self.store.start_or_continue_validation_session(
425            "email",
426            email_address,
427            session_id,
428            client_secret,
429            send_attempt,
430            next_link,
431            token,
432            token_expires,
433        )
434
435        return session_id
436
437    async def requestEmailToken(
438        self,
439        id_server: str,
440        email: str,
441        client_secret: str,
442        send_attempt: int,
443        next_link: Optional[str] = None,
444    ) -> JsonDict:
445        """
446        Request an external server send an email on our behalf for the purposes of threepid
447        validation.
448
449        Args:
450            id_server: The identity server to proxy to
451            email: The email to send the message to
452            client_secret: The unique client_secret sends by the user
453            send_attempt: Which attempt this is
454            next_link: A link to redirect the user to once they submit the token
455
456        Returns:
457            The json response body from the server
458        """
459        params = {
460            "email": email,
461            "client_secret": client_secret,
462            "send_attempt": send_attempt,
463        }
464        if next_link:
465            params["next_link"] = next_link
466
467        try:
468            data = await self.http_client.post_json_get_json(
469                id_server + "/_matrix/identity/api/v1/validate/email/requestToken",
470                params,
471            )
472            return data
473        except HttpResponseException as e:
474            logger.info("Proxied requestToken failed: %r", e)
475            raise e.to_synapse_error()
476        except RequestTimedOutError:
477            raise SynapseError(500, "Timed out contacting identity server")
478
479    async def requestMsisdnToken(
480        self,
481        id_server: str,
482        country: str,
483        phone_number: str,
484        client_secret: str,
485        send_attempt: int,
486        next_link: Optional[str] = None,
487    ) -> JsonDict:
488        """
489        Request an external server send an SMS message on our behalf for the purposes of
490        threepid validation.
491        Args:
492            id_server: The identity server to proxy to
493            country: The country code of the phone number
494            phone_number: The number to send the message to
495            client_secret: The unique client_secret sends by the user
496            send_attempt: Which attempt this is
497            next_link: A link to redirect the user to once they submit the token
498
499        Returns:
500            The json response body from the server
501        """
502        params = {
503            "country": country,
504            "phone_number": phone_number,
505            "client_secret": client_secret,
506            "send_attempt": send_attempt,
507        }
508        if next_link:
509            params["next_link"] = next_link
510
511        try:
512            data = await self.http_client.post_json_get_json(
513                id_server + "/_matrix/identity/api/v1/validate/msisdn/requestToken",
514                params,
515            )
516        except HttpResponseException as e:
517            logger.info("Proxied requestToken failed: %r", e)
518            raise e.to_synapse_error()
519        except RequestTimedOutError:
520            raise SynapseError(500, "Timed out contacting identity server")
521
522        # we need to tell the client to send the token back to us, since it doesn't
523        # otherwise know where to send it, so add submit_url response parameter
524        # (see also MSC2078)
525        data["submit_url"] = (
526            self.hs.config.server.public_baseurl
527            + "_matrix/client/unstable/add_threepid/msisdn/submit_token"
528        )
529        return data
530
531    async def validate_threepid_session(
532        self, client_secret: str, sid: str
533    ) -> Optional[JsonDict]:
534        """Validates a threepid session with only the client secret and session ID
535        Tries validating against any configured account_threepid_delegates as well as locally.
536
537        Args:
538            client_secret: A secret provided by the client
539            sid: The ID of the session
540
541        Returns:
542            The json response if validation was successful, otherwise None
543        """
544        # XXX: We shouldn't need to keep wrapping and unwrapping this value
545        threepid_creds = {"client_secret": client_secret, "sid": sid}
546
547        # We don't actually know which medium this 3PID is. Thus we first assume it's email,
548        # and if validation fails we try msisdn
549        validation_session = None
550
551        # Try to validate as email
552        if self.hs.config.email.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
553            # Remote emails will only be used if a valid identity server is provided.
554            assert (
555                self.hs.config.registration.account_threepid_delegate_email is not None
556            )
557
558            # Ask our delegated email identity server
559            validation_session = await self.threepid_from_creds(
560                self.hs.config.registration.account_threepid_delegate_email,
561                threepid_creds,
562            )
563        elif self.hs.config.email.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
564            # Get a validated session matching these details
565            validation_session = await self.store.get_threepid_validation_session(
566                "email", client_secret, sid=sid, validated=True
567            )
568
569        if validation_session:
570            return validation_session
571
572        # Try to validate as msisdn
573        if self.hs.config.registration.account_threepid_delegate_msisdn:
574            # Ask our delegated msisdn identity server
575            validation_session = await self.threepid_from_creds(
576                self.hs.config.registration.account_threepid_delegate_msisdn,
577                threepid_creds,
578            )
579
580        return validation_session
581
582    async def proxy_msisdn_submit_token(
583        self, id_server: str, client_secret: str, sid: str, token: str
584    ) -> JsonDict:
585        """Proxy a POST submitToken request to an identity server for verification purposes
586
587        Args:
588            id_server: The identity server URL to contact
589            client_secret: Secret provided by the client
590            sid: The ID of the session
591            token: The verification token
592
593        Raises:
594            SynapseError: If we failed to contact the identity server
595
596        Returns:
597            The response dict from the identity server
598        """
599        body = {"client_secret": client_secret, "sid": sid, "token": token}
600
601        try:
602            return await self.http_client.post_json_get_json(
603                id_server + "/_matrix/identity/api/v1/validate/msisdn/submitToken",
604                body,
605            )
606        except RequestTimedOutError:
607            raise SynapseError(500, "Timed out contacting identity server")
608        except HttpResponseException as e:
609            logger.warning("Error contacting msisdn account_threepid_delegate: %s", e)
610            raise SynapseError(400, "Error contacting the identity server")
611
612    async def lookup_3pid(
613        self,
614        id_server: str,
615        medium: str,
616        address: str,
617        id_access_token: Optional[str] = None,
618    ) -> Optional[str]:
619        """Looks up a 3pid in the passed identity server.
620
621        Args:
622            id_server: The server name (including port, if required)
623                of the identity server to use.
624            medium: The type of the third party identifier (e.g. "email").
625            address: The third party identifier (e.g. "foo@example.com").
626            id_access_token: The access token to authenticate to the identity
627                server with
628
629        Returns:
630            the matrix ID of the 3pid, or None if it is not recognized.
631        """
632        if id_access_token is not None:
633            try:
634                results = await self._lookup_3pid_v2(
635                    id_server, id_access_token, medium, address
636                )
637                return results
638
639            except Exception as e:
640                # Catch HttpResponseExcept for a non-200 response code
641                # Check if this identity server does not know about v2 lookups
642                if isinstance(e, HttpResponseException) and e.code == 404:
643                    # This is an old identity server that does not yet support v2 lookups
644                    logger.warning(
645                        "Attempted v2 lookup on v1 identity server %s. Falling "
646                        "back to v1",
647                        id_server,
648                    )
649                else:
650                    logger.warning("Error when looking up hashing details: %s", e)
651                    return None
652
653        return await self._lookup_3pid_v1(id_server, medium, address)
654
655    async def _lookup_3pid_v1(
656        self, id_server: str, medium: str, address: str
657    ) -> Optional[str]:
658        """Looks up a 3pid in the passed identity server using v1 lookup.
659
660        Args:
661            id_server: The server name (including port, if required)
662                of the identity server to use.
663            medium: The type of the third party identifier (e.g. "email").
664            address: The third party identifier (e.g. "foo@example.com").
665
666        Returns:
667            the matrix ID of the 3pid, or None if it is not recognized.
668        """
669        try:
670            data = await self.blacklisting_http_client.get_json(
671                "%s%s/_matrix/identity/api/v1/lookup" % (id_server_scheme, id_server),
672                {"medium": medium, "address": address},
673            )
674
675            if "mxid" in data:
676                # note: we used to verify the identity server's signature here, but no longer
677                # require or validate it. See the following for context:
678                # https://github.com/matrix-org/synapse/issues/5253#issuecomment-666246950
679                return data["mxid"]
680        except RequestTimedOutError:
681            raise SynapseError(500, "Timed out contacting identity server")
682        except OSError as e:
683            logger.warning("Error from v1 identity server lookup: %s" % (e,))
684
685        return None
686
687    async def _lookup_3pid_v2(
688        self, id_server: str, id_access_token: str, medium: str, address: str
689    ) -> Optional[str]:
690        """Looks up a 3pid in the passed identity server using v2 lookup.
691
692        Args:
693            id_server: The server name (including port, if required)
694                of the identity server to use.
695            id_access_token: The access token to authenticate to the identity server with
696            medium: The type of the third party identifier (e.g. "email").
697            address: The third party identifier (e.g. "foo@example.com").
698
699        Returns:
700            the matrix ID of the 3pid, or None if it is not recognised.
701        """
702        # Check what hashing details are supported by this identity server
703        try:
704            hash_details = await self.blacklisting_http_client.get_json(
705                "%s%s/_matrix/identity/v2/hash_details" % (id_server_scheme, id_server),
706                {"access_token": id_access_token},
707            )
708        except RequestTimedOutError:
709            raise SynapseError(500, "Timed out contacting identity server")
710
711        if not isinstance(hash_details, dict):
712            logger.warning(
713                "Got non-dict object when checking hash details of %s%s: %s",
714                id_server_scheme,
715                id_server,
716                hash_details,
717            )
718            raise SynapseError(
719                400,
720                "Non-dict object from %s%s during v2 hash_details request: %s"
721                % (id_server_scheme, id_server, hash_details),
722            )
723
724        # Extract information from hash_details
725        supported_lookup_algorithms = hash_details.get("algorithms")
726        lookup_pepper = hash_details.get("lookup_pepper")
727        if (
728            not supported_lookup_algorithms
729            or not isinstance(supported_lookup_algorithms, list)
730            or not lookup_pepper
731            or not isinstance(lookup_pepper, str)
732        ):
733            raise SynapseError(
734                400,
735                "Invalid hash details received from identity server %s%s: %s"
736                % (id_server_scheme, id_server, hash_details),
737            )
738
739        # Check if any of the supported lookup algorithms are present
740        if LookupAlgorithm.SHA256 in supported_lookup_algorithms:
741            # Perform a hashed lookup
742            lookup_algorithm = LookupAlgorithm.SHA256
743
744            # Hash address, medium and the pepper with sha256
745            to_hash = "%s %s %s" % (address, medium, lookup_pepper)
746            lookup_value = sha256_and_url_safe_base64(to_hash)
747
748        elif LookupAlgorithm.NONE in supported_lookup_algorithms:
749            # Perform a non-hashed lookup
750            lookup_algorithm = LookupAlgorithm.NONE
751
752            # Combine together plaintext address and medium
753            lookup_value = "%s %s" % (address, medium)
754
755        else:
756            logger.warning(
757                "None of the provided lookup algorithms of %s are supported: %s",
758                id_server,
759                supported_lookup_algorithms,
760            )
761            raise SynapseError(
762                400,
763                "Provided identity server does not support any v2 lookup "
764                "algorithms that this homeserver supports.",
765            )
766
767        # Authenticate with identity server given the access token from the client
768        headers = {"Authorization": create_id_access_token_header(id_access_token)}
769
770        try:
771            lookup_results = await self.blacklisting_http_client.post_json_get_json(
772                "%s%s/_matrix/identity/v2/lookup" % (id_server_scheme, id_server),
773                {
774                    "addresses": [lookup_value],
775                    "algorithm": lookup_algorithm,
776                    "pepper": lookup_pepper,
777                },
778                headers=headers,
779            )
780        except RequestTimedOutError:
781            raise SynapseError(500, "Timed out contacting identity server")
782        except Exception as e:
783            logger.warning("Error when performing a v2 3pid lookup: %s", e)
784            raise SynapseError(
785                500, "Unknown error occurred during identity server lookup"
786            )
787
788        # Check for a mapping from what we looked up to an MXID
789        if "mappings" not in lookup_results or not isinstance(
790            lookup_results["mappings"], dict
791        ):
792            logger.warning("No results from 3pid lookup")
793            return None
794
795        # Return the MXID if it's available, or None otherwise
796        mxid = lookup_results["mappings"].get(lookup_value)
797        return mxid
798
799    async def ask_id_server_for_third_party_invite(
800        self,
801        requester: Requester,
802        id_server: str,
803        medium: str,
804        address: str,
805        room_id: str,
806        inviter_user_id: str,
807        room_alias: str,
808        room_avatar_url: str,
809        room_join_rules: str,
810        room_name: str,
811        room_type: Optional[str],
812        inviter_display_name: str,
813        inviter_avatar_url: str,
814        id_access_token: Optional[str] = None,
815    ) -> Tuple[str, List[Dict[str, str]], Dict[str, str], str]:
816        """
817        Asks an identity server for a third party invite.
818
819        Args:
820            requester
821            id_server: hostname + optional port for the identity server.
822            medium: The literal string "email".
823            address: The third party address being invited.
824            room_id: The ID of the room to which the user is invited.
825            inviter_user_id: The user ID of the inviter.
826            room_alias: An alias for the room, for cosmetic notifications.
827            room_avatar_url: The URL of the room's avatar, for cosmetic
828                notifications.
829            room_join_rules: The join rules of the email (e.g. "public").
830            room_name: The m.room.name of the room.
831            room_type: The type of the room from its m.room.create event (e.g "m.space").
832            inviter_display_name: The current display name of the
833                inviter.
834            inviter_avatar_url: The URL of the inviter's avatar.
835            id_access_token (str|None): The access token to authenticate to the identity
836                server with
837
838        Returns:
839            A tuple containing:
840                token: The token which must be signed to prove authenticity.
841                public_keys ([{"public_key": str, "key_validity_url": str}]):
842                    public_key is a base64-encoded ed25519 public key.
843                fallback_public_key: One element from public_keys.
844                display_name: A user-friendly name to represent the invited user.
845        """
846        invite_config = {
847            "medium": medium,
848            "address": address,
849            "room_id": room_id,
850            "room_alias": room_alias,
851            "room_avatar_url": room_avatar_url,
852            "room_join_rules": room_join_rules,
853            "room_name": room_name,
854            "sender": inviter_user_id,
855            "sender_display_name": inviter_display_name,
856            "sender_avatar_url": inviter_avatar_url,
857        }
858
859        if room_type is not None:
860            invite_config["room_type"] = room_type
861            # TODO The unstable field is deprecated and should be removed in the future.
862            invite_config["org.matrix.msc3288.room_type"] = room_type
863
864        # If a custom web client location is available, include it in the request.
865        if self._web_client_location:
866            invite_config["org.matrix.web_client_location"] = self._web_client_location
867
868        # Add the identity service access token to the JSON body and use the v2
869        # Identity Service endpoints if id_access_token is present
870        data = None
871        base_url = "%s%s/_matrix/identity" % (id_server_scheme, id_server)
872
873        if id_access_token:
874            key_validity_url = "%s%s/_matrix/identity/v2/pubkey/isvalid" % (
875                id_server_scheme,
876                id_server,
877            )
878
879            # Attempt a v2 lookup
880            url = base_url + "/v2/store-invite"
881            try:
882                data = await self.blacklisting_http_client.post_json_get_json(
883                    url,
884                    invite_config,
885                    {"Authorization": create_id_access_token_header(id_access_token)},
886                )
887            except RequestTimedOutError:
888                raise SynapseError(500, "Timed out contacting identity server")
889            except HttpResponseException as e:
890                if e.code != 404:
891                    logger.info("Failed to POST %s with JSON: %s", url, e)
892                    raise e
893
894        if data is None:
895            key_validity_url = "%s%s/_matrix/identity/api/v1/pubkey/isvalid" % (
896                id_server_scheme,
897                id_server,
898            )
899            url = base_url + "/api/v1/store-invite"
900
901            try:
902                data = await self.blacklisting_http_client.post_json_get_json(
903                    url, invite_config
904                )
905            except RequestTimedOutError:
906                raise SynapseError(500, "Timed out contacting identity server")
907            except HttpResponseException as e:
908                logger.warning(
909                    "Error trying to call /store-invite on %s%s: %s",
910                    id_server_scheme,
911                    id_server,
912                    e,
913                )
914
915            if data is None:
916                # Some identity servers may only support application/x-www-form-urlencoded
917                # types. This is especially true with old instances of Sydent, see
918                # https://github.com/matrix-org/sydent/pull/170
919                try:
920                    data = await self.blacklisting_http_client.post_urlencoded_get_json(
921                        url, invite_config
922                    )
923                except HttpResponseException as e:
924                    logger.warning(
925                        "Error calling /store-invite on %s%s with fallback "
926                        "encoding: %s",
927                        id_server_scheme,
928                        id_server,
929                        e,
930                    )
931                    raise e
932
933        # TODO: Check for success
934        token = data["token"]
935        public_keys = data.get("public_keys", [])
936        if "public_key" in data:
937            fallback_public_key = {
938                "public_key": data["public_key"],
939                "key_validity_url": key_validity_url,
940            }
941        else:
942            fallback_public_key = public_keys[0]
943
944        if not public_keys:
945            public_keys.append(fallback_public_key)
946        display_name = data["display_name"]
947        return token, public_keys, fallback_public_key, display_name
948
949
950def create_id_access_token_header(id_access_token: str) -> List[str]:
951    """Create an Authorization header for passing to SimpleHttpClient as the header value
952    of an HTTP request.
953
954    Args:
955        id_access_token: An identity server access token.
956
957    Returns:
958        The ascii-encoded bearer token encased in a list.
959    """
960    # Prefix with Bearer
961    bearer_token = "Bearer %s" % id_access_token
962
963    # Encode headers to standard ascii
964    bearer_token.encode("ascii")
965
966    # Return as a list as that's how SimpleHttpClient takes header values
967    return [bearer_token]
968
969
970class LookupAlgorithm:
971    """
972    Supported hashing algorithms when performing a 3PID lookup.
973
974    SHA256 - Hashing an (address, medium, pepper) combo with sha256, then url-safe base64
975        encoding
976    NONE - Not performing any hashing. Simply sending an (address, medium) combo in plaintext
977    """
978
979    SHA256 = "sha256"
980    NONE = "none"
981