identity.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2015, 2016 OpenMarket Ltd
  3. # Copyright 2017 Vector Creations Ltd
  4. # Copyright 2018 New Vector Ltd
  5. #
  6. # Licensed under the Apache License, Version 2.0 (the "License");
  7. # you may not use this file except in compliance with the License.
  8. # You may obtain a copy of the License at
  9. #
  10. # http://www.apache.org/licenses/LICENSE-2.0
  11. #
  12. # Unless required by applicable law or agreed to in writing, software
  13. # distributed under the License is distributed on an "AS IS" BASIS,
  14. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. # See the License for the specific language governing permissions and
  16. # limitations under the License.
  17. """Utilities for interacting with Identity Servers"""
  18. import logging
  19. from canonicaljson import json
  20. from twisted.internet import defer
  21. from synapse.api.errors import (
  22. CodeMessageException,
  23. Codes,
  24. HttpResponseException,
  25. SynapseError,
  26. )
  27. from ._base import BaseHandler
  28. logger = logging.getLogger(__name__)
  29. class IdentityHandler(BaseHandler):
  30. def __init__(self, hs):
  31. super(IdentityHandler, self).__init__(hs)
  32. self.http_client = hs.get_simple_http_client()
  33. self.federation_http_client = hs.get_http_client()
  34. self.trusted_id_servers = set(hs.config.trusted_third_party_id_servers)
  35. self.trust_any_id_server_just_for_testing_do_not_use = (
  36. hs.config.use_insecure_ssl_client_just_for_testing_do_not_use
  37. )
  38. def _should_trust_id_server(self, id_server):
  39. if id_server not in self.trusted_id_servers:
  40. if self.trust_any_id_server_just_for_testing_do_not_use:
  41. logger.warn(
  42. "Trusting untrustworthy ID server %r even though it isn't"
  43. " in the trusted id list for testing because"
  44. " 'use_insecure_ssl_client_just_for_testing_do_not_use'"
  45. " is set in the config",
  46. id_server,
  47. )
  48. else:
  49. return False
  50. return True
  51. @defer.inlineCallbacks
  52. def threepid_from_creds(self, creds):
  53. if "id_server" in creds:
  54. id_server = creds["id_server"]
  55. elif "idServer" in creds:
  56. id_server = creds["idServer"]
  57. else:
  58. raise SynapseError(400, "No id_server in creds")
  59. if "client_secret" in creds:
  60. client_secret = creds["client_secret"]
  61. elif "clientSecret" in creds:
  62. client_secret = creds["clientSecret"]
  63. else:
  64. raise SynapseError(400, "No client_secret in creds")
  65. if not self._should_trust_id_server(id_server):
  66. logger.warn(
  67. "%s is not a trusted ID server: rejecting 3pid " + "credentials",
  68. id_server,
  69. )
  70. return None
  71. try:
  72. data = yield self.http_client.get_json(
  73. "https://%s%s"
  74. % (id_server, "/_matrix/identity/api/v1/3pid/getValidated3pid"),
  75. {"sid": creds["sid"], "client_secret": client_secret},
  76. )
  77. except HttpResponseException as e:
  78. logger.info("getValidated3pid failed with Matrix error: %r", e)
  79. raise e.to_synapse_error()
  80. if "medium" in data:
  81. return data
  82. return None
  83. @defer.inlineCallbacks
  84. def bind_threepid(self, creds, mxid):
  85. logger.debug("binding threepid %r to %s", creds, mxid)
  86. data = None
  87. if "id_server" in creds:
  88. id_server = creds["id_server"]
  89. elif "idServer" in creds:
  90. id_server = creds["idServer"]
  91. else:
  92. raise SynapseError(400, "No id_server in creds")
  93. if "client_secret" in creds:
  94. client_secret = creds["client_secret"]
  95. elif "clientSecret" in creds:
  96. client_secret = creds["clientSecret"]
  97. else:
  98. raise SynapseError(400, "No client_secret in creds")
  99. try:
  100. data = yield self.http_client.post_json_get_json(
  101. "https://%s%s" % (id_server, "/_matrix/identity/api/v1/3pid/bind"),
  102. {"sid": creds["sid"], "client_secret": client_secret, "mxid": mxid},
  103. )
  104. logger.debug("bound threepid %r to %s", creds, mxid)
  105. # Remember where we bound the threepid
  106. yield self.store.add_user_bound_threepid(
  107. user_id=mxid,
  108. medium=data["medium"],
  109. address=data["address"],
  110. id_server=id_server,
  111. )
  112. except CodeMessageException as e:
  113. data = json.loads(e.msg) # XXX WAT?
  114. return data
  115. @defer.inlineCallbacks
  116. def try_unbind_threepid(self, mxid, threepid):
  117. """Removes a binding from an identity server
  118. Args:
  119. mxid (str): Matrix user ID of binding to be removed
  120. threepid (dict): Dict with medium & address of binding to be
  121. removed, and an optional id_server.
  122. Raises:
  123. SynapseError: If we failed to contact the identity server
  124. Returns:
  125. Deferred[bool]: True on success, otherwise False if the identity
  126. server doesn't support unbinding (or no identity server found to
  127. contact).
  128. """
  129. if threepid.get("id_server"):
  130. id_servers = [threepid["id_server"]]
  131. else:
  132. id_servers = yield self.store.get_id_servers_user_bound(
  133. user_id=mxid, medium=threepid["medium"], address=threepid["address"]
  134. )
  135. # We don't know where to unbind, so we don't have a choice but to return
  136. if not id_servers:
  137. return False
  138. changed = True
  139. for id_server in id_servers:
  140. changed &= yield self.try_unbind_threepid_with_id_server(
  141. mxid, threepid, id_server
  142. )
  143. return changed
  144. @defer.inlineCallbacks
  145. def try_unbind_threepid_with_id_server(self, mxid, threepid, id_server):
  146. """Removes a binding from an identity server
  147. Args:
  148. mxid (str): Matrix user ID of binding to be removed
  149. threepid (dict): Dict with medium & address of binding to be removed
  150. id_server (str): Identity server to unbind from
  151. Raises:
  152. SynapseError: If we failed to contact the identity server
  153. Returns:
  154. Deferred[bool]: True on success, otherwise False if the identity
  155. server doesn't support unbinding
  156. """
  157. url = "https://%s/_matrix/identity/api/v1/3pid/unbind" % (id_server,)
  158. content = {
  159. "mxid": mxid,
  160. "threepid": {"medium": threepid["medium"], "address": threepid["address"]},
  161. }
  162. # we abuse the federation http client to sign the request, but we have to send it
  163. # using the normal http client since we don't want the SRV lookup and want normal
  164. # 'browser-like' HTTPS.
  165. auth_headers = self.federation_http_client.build_auth_headers(
  166. destination=None,
  167. method="POST",
  168. url_bytes="/_matrix/identity/api/v1/3pid/unbind".encode("ascii"),
  169. content=content,
  170. destination_is=id_server,
  171. )
  172. headers = {b"Authorization": auth_headers}
  173. try:
  174. yield self.http_client.post_json_get_json(url, content, headers)
  175. changed = True
  176. except HttpResponseException as e:
  177. changed = False
  178. if e.code in (400, 404, 501):
  179. # The remote server probably doesn't support unbinding (yet)
  180. logger.warn("Received %d response while unbinding threepid", e.code)
  181. else:
  182. logger.error("Failed to unbind threepid on identity server: %s", e)
  183. raise SynapseError(502, "Failed to contact identity server")
  184. yield self.store.remove_user_bound_threepid(
  185. user_id=mxid,
  186. medium=threepid["medium"],
  187. address=threepid["address"],
  188. id_server=id_server,
  189. )
  190. return changed
  191. @defer.inlineCallbacks
  192. def requestEmailToken(
  193. self, id_server, email, client_secret, send_attempt, next_link=None
  194. ):
  195. if not self._should_trust_id_server(id_server):
  196. raise SynapseError(
  197. 400, "Untrusted ID server '%s'" % id_server, Codes.SERVER_NOT_TRUSTED
  198. )
  199. params = {
  200. "email": email,
  201. "client_secret": client_secret,
  202. "send_attempt": send_attempt,
  203. }
  204. if next_link:
  205. params.update({"next_link": next_link})
  206. try:
  207. data = yield self.http_client.post_json_get_json(
  208. "https://%s%s"
  209. % (id_server, "/_matrix/identity/api/v1/validate/email/requestToken"),
  210. params,
  211. )
  212. return data
  213. except HttpResponseException as e:
  214. logger.info("Proxied requestToken failed: %r", e)
  215. raise e.to_synapse_error()
  216. @defer.inlineCallbacks
  217. def requestMsisdnToken(
  218. self, id_server, country, phone_number, client_secret, send_attempt, **kwargs
  219. ):
  220. if not self._should_trust_id_server(id_server):
  221. raise SynapseError(
  222. 400, "Untrusted ID server '%s'" % id_server, Codes.SERVER_NOT_TRUSTED
  223. )
  224. params = {
  225. "country": country,
  226. "phone_number": phone_number,
  227. "client_secret": client_secret,
  228. "send_attempt": send_attempt,
  229. }
  230. params.update(kwargs)
  231. try:
  232. data = yield self.http_client.post_json_get_json(
  233. "https://%s%s"
  234. % (id_server, "/_matrix/identity/api/v1/validate/msisdn/requestToken"),
  235. params,
  236. )
  237. return data
  238. except HttpResponseException as e:
  239. logger.info("Proxied requestToken failed: %r", e)
  240. raise e.to_synapse_error()