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