identity.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810
  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. """Utilities for interacting with Identity Servers"""
  17. import logging
  18. import urllib.parse
  19. from typing import TYPE_CHECKING, Awaitable, Callable, Dict, List, Optional, Tuple
  20. from synapse.api.errors import (
  21. CodeMessageException,
  22. Codes,
  23. HttpResponseException,
  24. SynapseError,
  25. )
  26. from synapse.api.ratelimiting import Ratelimiter
  27. from synapse.http import RequestTimedOutError
  28. from synapse.http.client import SimpleHttpClient
  29. from synapse.http.site import SynapseRequest
  30. from synapse.types import JsonDict, Requester
  31. from synapse.util import json_decoder
  32. from synapse.util.hash import sha256_and_url_safe_base64
  33. from synapse.util.stringutils import (
  34. assert_valid_client_secret,
  35. random_string,
  36. valid_id_server_location,
  37. )
  38. if TYPE_CHECKING:
  39. from synapse.server import HomeServer
  40. logger = logging.getLogger(__name__)
  41. id_server_scheme = "https://"
  42. class IdentityHandler:
  43. def __init__(self, hs: "HomeServer"):
  44. self.store = hs.get_datastores().main
  45. # An HTTP client for contacting trusted URLs.
  46. self.http_client = SimpleHttpClient(hs)
  47. # An HTTP client for contacting identity servers specified by clients.
  48. self.blacklisting_http_client = SimpleHttpClient(
  49. hs,
  50. ip_blacklist=hs.config.server.federation_ip_range_blacklist,
  51. ip_whitelist=hs.config.server.federation_ip_range_whitelist,
  52. )
  53. self.federation_http_client = hs.get_federation_http_client()
  54. self.hs = hs
  55. self._web_client_location = hs.config.email.invite_client_location
  56. # Ratelimiters for `/requestToken` endpoints.
  57. self._3pid_validation_ratelimiter_ip = Ratelimiter(
  58. store=self.store,
  59. clock=hs.get_clock(),
  60. rate_hz=hs.config.ratelimiting.rc_3pid_validation.per_second,
  61. burst_count=hs.config.ratelimiting.rc_3pid_validation.burst_count,
  62. )
  63. self._3pid_validation_ratelimiter_address = Ratelimiter(
  64. store=self.store,
  65. clock=hs.get_clock(),
  66. rate_hz=hs.config.ratelimiting.rc_3pid_validation.per_second,
  67. burst_count=hs.config.ratelimiting.rc_3pid_validation.burst_count,
  68. )
  69. async def ratelimit_request_token_requests(
  70. self,
  71. request: SynapseRequest,
  72. medium: str,
  73. address: str,
  74. ) -> None:
  75. """Used to ratelimit requests to `/requestToken` by IP and address.
  76. Args:
  77. request: The associated request
  78. medium: The type of threepid, e.g. "msisdn" or "email"
  79. address: The actual threepid ID, e.g. the phone number or email address
  80. """
  81. await self._3pid_validation_ratelimiter_ip.ratelimit(
  82. None, (medium, request.getClientAddress().host)
  83. )
  84. await self._3pid_validation_ratelimiter_address.ratelimit(
  85. None, (medium, address)
  86. )
  87. async def threepid_from_creds(
  88. self, id_server: str, creds: Dict[str, str]
  89. ) -> Optional[JsonDict]:
  90. """
  91. Retrieve and validate a threepid identifier from a "credentials" dictionary against a
  92. given identity server
  93. Args:
  94. id_server: The identity server to validate 3PIDs against. Must be a
  95. complete URL including the protocol (http(s)://)
  96. creds: Dictionary containing the following keys:
  97. * client_secret|clientSecret: A unique secret str provided by the client
  98. * sid: The ID of the validation session
  99. Returns:
  100. A dictionary consisting of response params to the /getValidated3pid
  101. endpoint of the Identity Service API, or None if the threepid was not found
  102. """
  103. client_secret = creds.get("client_secret") or creds.get("clientSecret")
  104. if not client_secret:
  105. raise SynapseError(
  106. 400, "Missing param client_secret in creds", errcode=Codes.MISSING_PARAM
  107. )
  108. assert_valid_client_secret(client_secret)
  109. session_id = creds.get("sid")
  110. if not session_id:
  111. raise SynapseError(
  112. 400, "Missing param session_id in creds", errcode=Codes.MISSING_PARAM
  113. )
  114. query_params = {"sid": session_id, "client_secret": client_secret}
  115. url = id_server + "/_matrix/identity/api/v1/3pid/getValidated3pid"
  116. try:
  117. data = await self.http_client.get_json(url, query_params)
  118. except RequestTimedOutError:
  119. raise SynapseError(500, "Timed out contacting identity server")
  120. except HttpResponseException as e:
  121. logger.info(
  122. "%s returned %i for threepid validation for: %s",
  123. id_server,
  124. e.code,
  125. creds,
  126. )
  127. return None
  128. # Old versions of Sydent return a 200 http code even on a failed validation
  129. # check. Thus, in addition to the HttpResponseException check above (which
  130. # checks for non-200 errors), we need to make sure validation_session isn't
  131. # actually an error, identified by the absence of a "medium" key
  132. # See https://github.com/matrix-org/sydent/issues/215 for details
  133. if "medium" in data:
  134. return data
  135. logger.info("%s reported non-validated threepid: %s", id_server, creds)
  136. return None
  137. async def bind_threepid(
  138. self,
  139. client_secret: str,
  140. sid: str,
  141. mxid: str,
  142. id_server: str,
  143. id_access_token: str,
  144. ) -> JsonDict:
  145. """Bind a 3PID to an identity server
  146. Args:
  147. client_secret: A unique secret provided by the client
  148. sid: The ID of the validation session
  149. mxid: The MXID to bind the 3PID to
  150. id_server: The domain of the identity server to query
  151. id_access_token: The access token to authenticate to the identity
  152. server with
  153. Raises:
  154. SynapseError: On any of the following conditions
  155. - the supplied id_server is not a valid identity server name
  156. - we failed to contact the supplied identity server
  157. Returns:
  158. The response from the identity server
  159. """
  160. logger.debug("Proxying threepid bind request for %s to %s", mxid, id_server)
  161. if not valid_id_server_location(id_server):
  162. raise SynapseError(
  163. 400,
  164. "id_server must be a valid hostname with optional port and path components",
  165. )
  166. bind_data = {"sid": sid, "client_secret": client_secret, "mxid": mxid}
  167. bind_url = "https://%s/_matrix/identity/v2/3pid/bind" % (id_server,)
  168. headers = {"Authorization": create_id_access_token_header(id_access_token)}
  169. try:
  170. # Use the blacklisting http client as this call is only to identity servers
  171. # provided by a client
  172. data = await self.blacklisting_http_client.post_json_get_json(
  173. bind_url, bind_data, headers=headers
  174. )
  175. # Remember where we bound the threepid
  176. await self.store.add_user_bound_threepid(
  177. user_id=mxid,
  178. medium=data["medium"],
  179. address=data["address"],
  180. id_server=id_server,
  181. )
  182. return data
  183. except HttpResponseException as e:
  184. logger.error("3PID bind failed with Matrix error: %r", e)
  185. raise e.to_synapse_error()
  186. except RequestTimedOutError:
  187. raise SynapseError(500, "Timed out contacting identity server")
  188. except CodeMessageException as e:
  189. data = json_decoder.decode(e.msg) # XXX WAT?
  190. return data
  191. async def try_unbind_threepid(self, mxid: str, threepid: dict) -> bool:
  192. """Attempt to remove a 3PID from an identity server, or if one is not provided, all
  193. identity servers we're aware the binding is present on
  194. Args:
  195. mxid: Matrix user ID of binding to be removed
  196. threepid: Dict with medium & address of binding to be
  197. removed, and an optional id_server.
  198. Raises:
  199. SynapseError: If we failed to contact the identity server
  200. Returns:
  201. True on success, otherwise False if the identity
  202. server doesn't support unbinding (or no identity server found to
  203. contact).
  204. """
  205. if threepid.get("id_server"):
  206. id_servers = [threepid["id_server"]]
  207. else:
  208. id_servers = await self.store.get_id_servers_user_bound(
  209. user_id=mxid, medium=threepid["medium"], address=threepid["address"]
  210. )
  211. # We don't know where to unbind, so we don't have a choice but to return
  212. if not id_servers:
  213. return False
  214. changed = True
  215. for id_server in id_servers:
  216. changed &= await self.try_unbind_threepid_with_id_server(
  217. mxid, threepid, id_server
  218. )
  219. return changed
  220. async def try_unbind_threepid_with_id_server(
  221. self, mxid: str, threepid: dict, id_server: str
  222. ) -> bool:
  223. """Removes a binding from an identity server
  224. Args:
  225. mxid: Matrix user ID of binding to be removed
  226. threepid: Dict with medium & address of binding to be removed
  227. id_server: Identity server to unbind from
  228. Raises:
  229. SynapseError: On any of the following conditions
  230. - the supplied id_server is not a valid identity server name
  231. - we failed to contact the supplied identity server
  232. Returns:
  233. True on success, otherwise False if the identity
  234. server doesn't support unbinding
  235. """
  236. if not valid_id_server_location(id_server):
  237. raise SynapseError(
  238. 400,
  239. "id_server must be a valid hostname with optional port and path components",
  240. )
  241. url = "https://%s/_matrix/identity/v2/3pid/unbind" % (id_server,)
  242. url_bytes = b"/_matrix/identity/v2/3pid/unbind"
  243. content = {
  244. "mxid": mxid,
  245. "threepid": {"medium": threepid["medium"], "address": threepid["address"]},
  246. }
  247. # we abuse the federation http client to sign the request, but we have to send it
  248. # using the normal http client since we don't want the SRV lookup and want normal
  249. # 'browser-like' HTTPS.
  250. auth_headers = self.federation_http_client.build_auth_headers(
  251. destination=None,
  252. method=b"POST",
  253. url_bytes=url_bytes,
  254. content=content,
  255. destination_is=id_server.encode("ascii"),
  256. )
  257. headers = {b"Authorization": auth_headers}
  258. try:
  259. # Use the blacklisting http client as this call is only to identity servers
  260. # provided by a client
  261. await self.blacklisting_http_client.post_json_get_json(
  262. url, content, headers
  263. )
  264. changed = True
  265. except HttpResponseException as e:
  266. changed = False
  267. if e.code in (400, 404, 501):
  268. # The remote server probably doesn't support unbinding (yet)
  269. logger.warning("Received %d response while unbinding threepid", e.code)
  270. else:
  271. logger.error("Failed to unbind threepid on identity server: %s", e)
  272. raise SynapseError(500, "Failed to contact identity server")
  273. except RequestTimedOutError:
  274. raise SynapseError(500, "Timed out contacting identity server")
  275. await self.store.remove_user_bound_threepid(
  276. user_id=mxid,
  277. medium=threepid["medium"],
  278. address=threepid["address"],
  279. id_server=id_server,
  280. )
  281. return changed
  282. async def send_threepid_validation(
  283. self,
  284. email_address: str,
  285. client_secret: str,
  286. send_attempt: int,
  287. send_email_func: Callable[[str, str, str, str], Awaitable],
  288. next_link: Optional[str] = None,
  289. ) -> str:
  290. """Send a threepid validation email for password reset or
  291. registration purposes
  292. Args:
  293. email_address: The user's email address
  294. client_secret: The provided client secret
  295. send_attempt: Which send attempt this is
  296. send_email_func: A function that takes an email address, token,
  297. client_secret and session_id, sends an email
  298. and returns an Awaitable.
  299. next_link: The URL to redirect the user to after validation
  300. Returns:
  301. The new session_id upon success
  302. Raises:
  303. SynapseError is an error occurred when sending the email
  304. """
  305. # Check that this email/client_secret/send_attempt combo is new or
  306. # greater than what we've seen previously
  307. session = await self.store.get_threepid_validation_session(
  308. "email", client_secret, address=email_address, validated=False
  309. )
  310. # Check to see if a session already exists and that it is not yet
  311. # marked as validated
  312. if session and session.get("validated_at") is None:
  313. session_id = session["session_id"]
  314. last_send_attempt = session["last_send_attempt"]
  315. # Check that the send_attempt is higher than previous attempts
  316. if send_attempt <= last_send_attempt:
  317. # If not, just return a success without sending an email
  318. return session_id
  319. else:
  320. # An non-validated session does not exist yet.
  321. # Generate a session id
  322. session_id = random_string(16)
  323. if next_link:
  324. # Manipulate the next_link to add the sid, because the caller won't get
  325. # it until we send a response, by which time we've sent the mail.
  326. if "?" in next_link:
  327. next_link += "&"
  328. else:
  329. next_link += "?"
  330. next_link += "sid=" + urllib.parse.quote(session_id)
  331. # Generate a new validation token
  332. token = random_string(32)
  333. # Send the mail with the link containing the token, client_secret
  334. # and session_id
  335. try:
  336. await send_email_func(email_address, token, client_secret, session_id)
  337. except Exception:
  338. logger.exception(
  339. "Error sending threepid validation email to %s", email_address
  340. )
  341. raise SynapseError(500, "An error was encountered when sending the email")
  342. token_expires = (
  343. self.hs.get_clock().time_msec()
  344. + self.hs.config.email.email_validation_token_lifetime
  345. )
  346. await self.store.start_or_continue_validation_session(
  347. "email",
  348. email_address,
  349. session_id,
  350. client_secret,
  351. send_attempt,
  352. next_link,
  353. token,
  354. token_expires,
  355. )
  356. return session_id
  357. async def requestMsisdnToken(
  358. self,
  359. id_server: str,
  360. country: str,
  361. phone_number: str,
  362. client_secret: str,
  363. send_attempt: int,
  364. next_link: Optional[str] = None,
  365. ) -> JsonDict:
  366. """
  367. Request an external server send an SMS message on our behalf for the purposes of
  368. threepid validation.
  369. Args:
  370. id_server: The identity server to proxy to
  371. country: The country code of the phone number
  372. phone_number: The number to send the message to
  373. client_secret: The unique client_secret sends by the user
  374. send_attempt: Which attempt this is
  375. next_link: A link to redirect the user to once they submit the token
  376. Returns:
  377. The json response body from the server
  378. """
  379. params = {
  380. "country": country,
  381. "phone_number": phone_number,
  382. "client_secret": client_secret,
  383. "send_attempt": send_attempt,
  384. }
  385. if next_link:
  386. params["next_link"] = next_link
  387. try:
  388. data = await self.http_client.post_json_get_json(
  389. id_server + "/_matrix/identity/api/v1/validate/msisdn/requestToken",
  390. params,
  391. )
  392. except HttpResponseException as e:
  393. logger.info("Proxied requestToken failed: %r", e)
  394. raise e.to_synapse_error()
  395. except RequestTimedOutError:
  396. raise SynapseError(500, "Timed out contacting identity server")
  397. # we need to tell the client to send the token back to us, since it doesn't
  398. # otherwise know where to send it, so add submit_url response parameter
  399. # (see also MSC2078)
  400. data["submit_url"] = (
  401. self.hs.config.server.public_baseurl
  402. + "_matrix/client/unstable/add_threepid/msisdn/submit_token"
  403. )
  404. return data
  405. async def validate_threepid_session(
  406. self, client_secret: str, sid: str
  407. ) -> Optional[JsonDict]:
  408. """Validates a threepid session with only the client secret and session ID
  409. Tries validating against any configured account_threepid_delegates as well as locally.
  410. Args:
  411. client_secret: A secret provided by the client
  412. sid: The ID of the session
  413. Returns:
  414. The json response if validation was successful, otherwise None
  415. """
  416. # XXX: We shouldn't need to keep wrapping and unwrapping this value
  417. threepid_creds = {"client_secret": client_secret, "sid": sid}
  418. # We don't actually know which medium this 3PID is. Thus we first assume it's email,
  419. # and if validation fails we try msisdn
  420. validation_session = None
  421. # Try to validate as email
  422. if self.hs.config.email.can_verify_email:
  423. # Get a validated session matching these details
  424. validation_session = await self.store.get_threepid_validation_session(
  425. "email", client_secret, sid=sid, validated=True
  426. )
  427. if validation_session:
  428. return validation_session
  429. # Try to validate as msisdn
  430. if self.hs.config.registration.account_threepid_delegate_msisdn:
  431. # Ask our delegated msisdn identity server
  432. validation_session = await self.threepid_from_creds(
  433. self.hs.config.registration.account_threepid_delegate_msisdn,
  434. threepid_creds,
  435. )
  436. return validation_session
  437. async def proxy_msisdn_submit_token(
  438. self, id_server: str, client_secret: str, sid: str, token: str
  439. ) -> JsonDict:
  440. """Proxy a POST submitToken request to an identity server for verification purposes
  441. Args:
  442. id_server: The identity server URL to contact
  443. client_secret: Secret provided by the client
  444. sid: The ID of the session
  445. token: The verification token
  446. Raises:
  447. SynapseError: If we failed to contact the identity server
  448. Returns:
  449. The response dict from the identity server
  450. """
  451. body = {"client_secret": client_secret, "sid": sid, "token": token}
  452. try:
  453. return await self.http_client.post_json_get_json(
  454. id_server + "/_matrix/identity/api/v1/validate/msisdn/submitToken",
  455. body,
  456. )
  457. except RequestTimedOutError:
  458. raise SynapseError(500, "Timed out contacting identity server")
  459. except HttpResponseException as e:
  460. logger.warning("Error contacting msisdn account_threepid_delegate: %s", e)
  461. raise SynapseError(400, "Error contacting the identity server")
  462. async def lookup_3pid(
  463. self, id_server: str, medium: str, address: str, id_access_token: str
  464. ) -> Optional[str]:
  465. """Looks up a 3pid in the passed identity server.
  466. Args:
  467. id_server: The server name (including port, if required)
  468. of the identity server to use.
  469. medium: The type of the third party identifier (e.g. "email").
  470. address: The third party identifier (e.g. "foo@example.com").
  471. id_access_token: The access token to authenticate to the identity
  472. server with
  473. Returns:
  474. the matrix ID of the 3pid, or None if it is not recognized.
  475. """
  476. try:
  477. results = await self._lookup_3pid_v2(
  478. id_server, id_access_token, medium, address
  479. )
  480. return results
  481. except Exception as e:
  482. logger.warning("Error when looking up hashing details: %s", e)
  483. return None
  484. async def _lookup_3pid_v2(
  485. self, id_server: str, id_access_token: str, medium: str, address: str
  486. ) -> Optional[str]:
  487. """Looks up a 3pid in the passed identity server using v2 lookup.
  488. Args:
  489. id_server: The server name (including port, if required)
  490. of the identity server to use.
  491. id_access_token: The access token to authenticate to the identity server with
  492. medium: The type of the third party identifier (e.g. "email").
  493. address: The third party identifier (e.g. "foo@example.com").
  494. Returns:
  495. the matrix ID of the 3pid, or None if it is not recognised.
  496. """
  497. # Check what hashing details are supported by this identity server
  498. try:
  499. hash_details = await self.blacklisting_http_client.get_json(
  500. "%s%s/_matrix/identity/v2/hash_details" % (id_server_scheme, id_server),
  501. {"access_token": id_access_token},
  502. )
  503. except RequestTimedOutError:
  504. raise SynapseError(500, "Timed out contacting identity server")
  505. if not isinstance(hash_details, dict):
  506. logger.warning(
  507. "Got non-dict object when checking hash details of %s%s: %s",
  508. id_server_scheme,
  509. id_server,
  510. hash_details,
  511. )
  512. raise SynapseError(
  513. 400,
  514. "Non-dict object from %s%s during v2 hash_details request: %s"
  515. % (id_server_scheme, id_server, hash_details),
  516. )
  517. # Extract information from hash_details
  518. supported_lookup_algorithms = hash_details.get("algorithms")
  519. lookup_pepper = hash_details.get("lookup_pepper")
  520. if (
  521. not supported_lookup_algorithms
  522. or not isinstance(supported_lookup_algorithms, list)
  523. or not lookup_pepper
  524. or not isinstance(lookup_pepper, str)
  525. ):
  526. raise SynapseError(
  527. 400,
  528. "Invalid hash details received from identity server %s%s: %s"
  529. % (id_server_scheme, id_server, hash_details),
  530. )
  531. # Check if any of the supported lookup algorithms are present
  532. if LookupAlgorithm.SHA256 in supported_lookup_algorithms:
  533. # Perform a hashed lookup
  534. lookup_algorithm = LookupAlgorithm.SHA256
  535. # Hash address, medium and the pepper with sha256
  536. to_hash = "%s %s %s" % (address, medium, lookup_pepper)
  537. lookup_value = sha256_and_url_safe_base64(to_hash)
  538. elif LookupAlgorithm.NONE in supported_lookup_algorithms:
  539. # Perform a non-hashed lookup
  540. lookup_algorithm = LookupAlgorithm.NONE
  541. # Combine together plaintext address and medium
  542. lookup_value = "%s %s" % (address, medium)
  543. else:
  544. logger.warning(
  545. "None of the provided lookup algorithms of %s are supported: %s",
  546. id_server,
  547. supported_lookup_algorithms,
  548. )
  549. raise SynapseError(
  550. 400,
  551. "Provided identity server does not support any v2 lookup "
  552. "algorithms that this homeserver supports.",
  553. )
  554. # Authenticate with identity server given the access token from the client
  555. headers = {"Authorization": create_id_access_token_header(id_access_token)}
  556. try:
  557. lookup_results = await self.blacklisting_http_client.post_json_get_json(
  558. "%s%s/_matrix/identity/v2/lookup" % (id_server_scheme, id_server),
  559. {
  560. "addresses": [lookup_value],
  561. "algorithm": lookup_algorithm,
  562. "pepper": lookup_pepper,
  563. },
  564. headers=headers,
  565. )
  566. except RequestTimedOutError:
  567. raise SynapseError(500, "Timed out contacting identity server")
  568. except Exception as e:
  569. logger.warning("Error when performing a v2 3pid lookup: %s", e)
  570. raise SynapseError(
  571. 500, "Unknown error occurred during identity server lookup"
  572. )
  573. # Check for a mapping from what we looked up to an MXID
  574. if "mappings" not in lookup_results or not isinstance(
  575. lookup_results["mappings"], dict
  576. ):
  577. logger.warning("No results from 3pid lookup")
  578. return None
  579. # Return the MXID if it's available, or None otherwise
  580. mxid = lookup_results["mappings"].get(lookup_value)
  581. return mxid
  582. async def ask_id_server_for_third_party_invite(
  583. self,
  584. requester: Requester,
  585. id_server: str,
  586. medium: str,
  587. address: str,
  588. room_id: str,
  589. inviter_user_id: str,
  590. room_alias: str,
  591. room_avatar_url: str,
  592. room_join_rules: str,
  593. room_name: str,
  594. room_type: Optional[str],
  595. inviter_display_name: str,
  596. inviter_avatar_url: str,
  597. id_access_token: str,
  598. ) -> Tuple[str, List[Dict[str, str]], Dict[str, str], str]:
  599. """
  600. Asks an identity server for a third party invite.
  601. Args:
  602. requester
  603. id_server: hostname + optional port for the identity server.
  604. medium: The literal string "email".
  605. address: The third party address being invited.
  606. room_id: The ID of the room to which the user is invited.
  607. inviter_user_id: The user ID of the inviter.
  608. room_alias: An alias for the room, for cosmetic notifications.
  609. room_avatar_url: The URL of the room's avatar, for cosmetic
  610. notifications.
  611. room_join_rules: The join rules of the email (e.g. "public").
  612. room_name: The m.room.name of the room.
  613. room_type: The type of the room from its m.room.create event (e.g "m.space").
  614. inviter_display_name: The current display name of the
  615. inviter.
  616. inviter_avatar_url: The URL of the inviter's avatar.
  617. id_access_token: The access token to authenticate to the identity
  618. server with
  619. Returns:
  620. A tuple containing:
  621. token: The token which must be signed to prove authenticity.
  622. public_keys ([{"public_key": str, "key_validity_url": str}]):
  623. public_key is a base64-encoded ed25519 public key.
  624. fallback_public_key: One element from public_keys.
  625. display_name: A user-friendly name to represent the invited user.
  626. """
  627. invite_config = {
  628. "medium": medium,
  629. "address": address,
  630. "room_id": room_id,
  631. "room_alias": room_alias,
  632. "room_avatar_url": room_avatar_url,
  633. "room_join_rules": room_join_rules,
  634. "room_name": room_name,
  635. "sender": inviter_user_id,
  636. "sender_display_name": inviter_display_name,
  637. "sender_avatar_url": inviter_avatar_url,
  638. }
  639. if room_type is not None:
  640. invite_config["room_type"] = room_type
  641. # If a custom web client location is available, include it in the request.
  642. if self._web_client_location:
  643. invite_config["org.matrix.web_client_location"] = self._web_client_location
  644. # Add the identity service access token to the JSON body and use the v2
  645. # Identity Service endpoints
  646. data = None
  647. key_validity_url = "%s%s/_matrix/identity/v2/pubkey/isvalid" % (
  648. id_server_scheme,
  649. id_server,
  650. )
  651. url = "%s%s/_matrix/identity/v2/store-invite" % (id_server_scheme, id_server)
  652. try:
  653. data = await self.blacklisting_http_client.post_json_get_json(
  654. url,
  655. invite_config,
  656. {"Authorization": create_id_access_token_header(id_access_token)},
  657. )
  658. except RequestTimedOutError:
  659. raise SynapseError(500, "Timed out contacting identity server")
  660. token = data["token"]
  661. public_keys = data.get("public_keys", [])
  662. if "public_key" in data:
  663. fallback_public_key = {
  664. "public_key": data["public_key"],
  665. "key_validity_url": key_validity_url,
  666. }
  667. else:
  668. fallback_public_key = public_keys[0]
  669. if not public_keys:
  670. public_keys.append(fallback_public_key)
  671. display_name = data["display_name"]
  672. return token, public_keys, fallback_public_key, display_name
  673. def create_id_access_token_header(id_access_token: str) -> List[str]:
  674. """Create an Authorization header for passing to SimpleHttpClient as the header value
  675. of an HTTP request.
  676. Args:
  677. id_access_token: An identity server access token.
  678. Returns:
  679. The ascii-encoded bearer token encased in a list.
  680. """
  681. # Prefix with Bearer
  682. bearer_token = "Bearer %s" % id_access_token
  683. # Encode headers to standard ascii
  684. bearer_token.encode("ascii")
  685. # Return as a list as that's how SimpleHttpClient takes header values
  686. return [bearer_token]
  687. class LookupAlgorithm:
  688. """
  689. Supported hashing algorithms when performing a 3PID lookup.
  690. SHA256 - Hashing an (address, medium, pepper) combo with sha256, then url-safe base64
  691. encoding
  692. NONE - Not performing any hashing. Simply sending an (address, medium) combo in plaintext
  693. """
  694. SHA256 = "sha256"
  695. NONE = "none"