account.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900
  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. import logging
  18. import random
  19. from http import HTTPStatus
  20. from typing import TYPE_CHECKING
  21. from urllib.parse import urlparse
  22. from synapse.api.constants import LoginType
  23. from synapse.api.errors import (
  24. Codes,
  25. InteractiveAuthIncompleteError,
  26. SynapseError,
  27. ThreepidValidationError,
  28. )
  29. from synapse.config.emailconfig import ThreepidBehaviour
  30. from synapse.handlers.ui_auth import UIAuthSessionDataConstants
  31. from synapse.http.server import finish_request, respond_with_html
  32. from synapse.http.servlet import (
  33. RestServlet,
  34. assert_params_in_dict,
  35. parse_json_object_from_request,
  36. parse_string,
  37. )
  38. from synapse.metrics import threepid_send_requests
  39. from synapse.push.mailer import Mailer
  40. from synapse.util.msisdn import phone_number_to_msisdn
  41. from synapse.util.stringutils import assert_valid_client_secret, random_string
  42. from synapse.util.threepids import canonicalise_email, check_3pid_allowed
  43. from ._base import client_patterns, interactive_auth_handler
  44. if TYPE_CHECKING:
  45. from synapse.app.homeserver import HomeServer
  46. logger = logging.getLogger(__name__)
  47. class EmailPasswordRequestTokenRestServlet(RestServlet):
  48. PATTERNS = client_patterns("/account/password/email/requestToken$")
  49. def __init__(self, hs: "HomeServer"):
  50. super().__init__()
  51. self.hs = hs
  52. self.datastore = hs.get_datastore()
  53. self.config = hs.config
  54. self.identity_handler = hs.get_identity_handler()
  55. if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
  56. self.mailer = Mailer(
  57. hs=self.hs,
  58. app_name=self.config.email_app_name,
  59. template_html=self.config.email_password_reset_template_html,
  60. template_text=self.config.email_password_reset_template_text,
  61. )
  62. async def on_POST(self, request):
  63. if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF:
  64. if self.config.local_threepid_handling_disabled_due_to_email_config:
  65. logger.warning(
  66. "User password resets have been disabled due to lack of email config"
  67. )
  68. raise SynapseError(
  69. 400, "Email-based password resets have been disabled on this server"
  70. )
  71. body = parse_json_object_from_request(request)
  72. assert_params_in_dict(body, ["client_secret", "email", "send_attempt"])
  73. # Extract params from body
  74. client_secret = body["client_secret"]
  75. assert_valid_client_secret(client_secret)
  76. # Canonicalise the email address. The addresses are all stored canonicalised
  77. # in the database. This allows the user to reset his password without having to
  78. # know the exact spelling (eg. upper and lower case) of address in the database.
  79. # Stored in the database "foo@bar.com"
  80. # User requests with "FOO@bar.com" would raise a Not Found error
  81. try:
  82. email = canonicalise_email(body["email"])
  83. except ValueError as e:
  84. raise SynapseError(400, str(e))
  85. send_attempt = body["send_attempt"]
  86. next_link = body.get("next_link") # Optional param
  87. if next_link:
  88. # Raise if the provided next_link value isn't valid
  89. assert_valid_next_link(self.hs, next_link)
  90. self.identity_handler.ratelimit_request_token_requests(request, "email", email)
  91. # The email will be sent to the stored address.
  92. # This avoids a potential account hijack by requesting a password reset to
  93. # an email address which is controlled by the attacker but which, after
  94. # canonicalisation, matches the one in our database.
  95. existing_user_id = await self.hs.get_datastore().get_user_id_by_threepid(
  96. "email", email
  97. )
  98. if existing_user_id is None:
  99. if self.config.request_token_inhibit_3pid_errors:
  100. # Make the client think the operation succeeded. See the rationale in the
  101. # comments for request_token_inhibit_3pid_errors.
  102. # Also wait for some random amount of time between 100ms and 1s to make it
  103. # look like we did something.
  104. await self.hs.get_clock().sleep(random.randint(1, 10) / 10)
  105. return 200, {"sid": random_string(16)}
  106. raise SynapseError(400, "Email not found", Codes.THREEPID_NOT_FOUND)
  107. if self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
  108. assert self.hs.config.account_threepid_delegate_email
  109. # Have the configured identity server handle the request
  110. ret = await self.identity_handler.requestEmailToken(
  111. self.hs.config.account_threepid_delegate_email,
  112. email,
  113. client_secret,
  114. send_attempt,
  115. next_link,
  116. )
  117. else:
  118. # Send password reset emails from Synapse
  119. sid = await self.identity_handler.send_threepid_validation(
  120. email,
  121. client_secret,
  122. send_attempt,
  123. self.mailer.send_password_reset_mail,
  124. next_link,
  125. )
  126. # Wrap the session id in a JSON object
  127. ret = {"sid": sid}
  128. threepid_send_requests.labels(type="email", reason="password_reset").observe(
  129. send_attempt
  130. )
  131. return 200, ret
  132. class PasswordRestServlet(RestServlet):
  133. PATTERNS = client_patterns("/account/password$")
  134. def __init__(self, hs):
  135. super().__init__()
  136. self.hs = hs
  137. self.auth = hs.get_auth()
  138. self.auth_handler = hs.get_auth_handler()
  139. self.datastore = self.hs.get_datastore()
  140. self.password_policy_handler = hs.get_password_policy_handler()
  141. self._set_password_handler = hs.get_set_password_handler()
  142. @interactive_auth_handler
  143. async def on_POST(self, request):
  144. body = parse_json_object_from_request(request)
  145. # we do basic sanity checks here because the auth layer will store these
  146. # in sessions. Pull out the new password provided to us.
  147. new_password = body.pop("new_password", None)
  148. if new_password is not None:
  149. if not isinstance(new_password, str) or len(new_password) > 512:
  150. raise SynapseError(400, "Invalid password")
  151. self.password_policy_handler.validate_password(new_password)
  152. # there are two possibilities here. Either the user does not have an
  153. # access token, and needs to do a password reset; or they have one and
  154. # need to validate their identity.
  155. #
  156. # In the first case, we offer a couple of means of identifying
  157. # themselves (email and msisdn, though it's unclear if msisdn actually
  158. # works).
  159. #
  160. # In the second case, we require a password to confirm their identity.
  161. if self.auth.has_access_token(request):
  162. requester = await self.auth.get_user_by_req(request)
  163. try:
  164. params, session_id = await self.auth_handler.validate_user_via_ui_auth(
  165. requester,
  166. request,
  167. body,
  168. "modify your account password",
  169. )
  170. except InteractiveAuthIncompleteError as e:
  171. # The user needs to provide more steps to complete auth, but
  172. # they're not required to provide the password again.
  173. #
  174. # If a password is available now, hash the provided password and
  175. # store it for later.
  176. if new_password:
  177. password_hash = await self.auth_handler.hash(new_password)
  178. await self.auth_handler.set_session_data(
  179. e.session_id,
  180. UIAuthSessionDataConstants.PASSWORD_HASH,
  181. password_hash,
  182. )
  183. raise
  184. user_id = requester.user.to_string()
  185. else:
  186. requester = None
  187. try:
  188. result, params, session_id = await self.auth_handler.check_ui_auth(
  189. [[LoginType.EMAIL_IDENTITY]],
  190. request,
  191. body,
  192. "modify your account password",
  193. )
  194. except InteractiveAuthIncompleteError as e:
  195. # The user needs to provide more steps to complete auth, but
  196. # they're not required to provide the password again.
  197. #
  198. # If a password is available now, hash the provided password and
  199. # store it for later.
  200. if new_password:
  201. password_hash = await self.auth_handler.hash(new_password)
  202. await self.auth_handler.set_session_data(
  203. e.session_id,
  204. UIAuthSessionDataConstants.PASSWORD_HASH,
  205. password_hash,
  206. )
  207. raise
  208. if LoginType.EMAIL_IDENTITY in result:
  209. threepid = result[LoginType.EMAIL_IDENTITY]
  210. if "medium" not in threepid or "address" not in threepid:
  211. raise SynapseError(500, "Malformed threepid")
  212. if threepid["medium"] == "email":
  213. # For emails, canonicalise the address.
  214. # We store all email addresses canonicalised in the DB.
  215. # (See add_threepid in synapse/handlers/auth.py)
  216. try:
  217. threepid["address"] = canonicalise_email(threepid["address"])
  218. except ValueError as e:
  219. raise SynapseError(400, str(e))
  220. # if using email, we must know about the email they're authing with!
  221. threepid_user_id = await self.datastore.get_user_id_by_threepid(
  222. threepid["medium"], threepid["address"]
  223. )
  224. if not threepid_user_id:
  225. raise SynapseError(404, "Email address not found", Codes.NOT_FOUND)
  226. user_id = threepid_user_id
  227. else:
  228. logger.error("Auth succeeded but no known type! %r", result.keys())
  229. raise SynapseError(500, "", Codes.UNKNOWN)
  230. # If we have a password in this request, prefer it. Otherwise, use the
  231. # password hash from an earlier request.
  232. if new_password:
  233. password_hash = await self.auth_handler.hash(new_password)
  234. elif session_id is not None:
  235. password_hash = await self.auth_handler.get_session_data(
  236. session_id, UIAuthSessionDataConstants.PASSWORD_HASH, None
  237. )
  238. else:
  239. # UI validation was skipped, but the request did not include a new
  240. # password.
  241. password_hash = None
  242. if not password_hash:
  243. raise SynapseError(400, "Missing params: password", Codes.MISSING_PARAM)
  244. logout_devices = params.get("logout_devices", True)
  245. await self._set_password_handler.set_password(
  246. user_id, password_hash, logout_devices, requester
  247. )
  248. return 200, {}
  249. class DeactivateAccountRestServlet(RestServlet):
  250. PATTERNS = client_patterns("/account/deactivate$")
  251. def __init__(self, hs):
  252. super().__init__()
  253. self.hs = hs
  254. self.auth = hs.get_auth()
  255. self.auth_handler = hs.get_auth_handler()
  256. self._deactivate_account_handler = hs.get_deactivate_account_handler()
  257. @interactive_auth_handler
  258. async def on_POST(self, request):
  259. body = parse_json_object_from_request(request)
  260. erase = body.get("erase", False)
  261. if not isinstance(erase, bool):
  262. raise SynapseError(
  263. HTTPStatus.BAD_REQUEST,
  264. "Param 'erase' must be a boolean, if given",
  265. Codes.BAD_JSON,
  266. )
  267. requester = await self.auth.get_user_by_req(request)
  268. # allow ASes to deactivate their own users
  269. if requester.app_service:
  270. await self._deactivate_account_handler.deactivate_account(
  271. requester.user.to_string(), erase, requester
  272. )
  273. return 200, {}
  274. await self.auth_handler.validate_user_via_ui_auth(
  275. requester,
  276. request,
  277. body,
  278. "deactivate your account",
  279. )
  280. result = await self._deactivate_account_handler.deactivate_account(
  281. requester.user.to_string(),
  282. erase,
  283. requester,
  284. id_server=body.get("id_server"),
  285. )
  286. if result:
  287. id_server_unbind_result = "success"
  288. else:
  289. id_server_unbind_result = "no-support"
  290. return 200, {"id_server_unbind_result": id_server_unbind_result}
  291. class EmailThreepidRequestTokenRestServlet(RestServlet):
  292. PATTERNS = client_patterns("/account/3pid/email/requestToken$")
  293. def __init__(self, hs):
  294. super().__init__()
  295. self.hs = hs
  296. self.config = hs.config
  297. self.identity_handler = hs.get_identity_handler()
  298. self.store = self.hs.get_datastore()
  299. if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
  300. self.mailer = Mailer(
  301. hs=self.hs,
  302. app_name=self.config.email_app_name,
  303. template_html=self.config.email_add_threepid_template_html,
  304. template_text=self.config.email_add_threepid_template_text,
  305. )
  306. async def on_POST(self, request):
  307. if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF:
  308. if self.config.local_threepid_handling_disabled_due_to_email_config:
  309. logger.warning(
  310. "Adding emails have been disabled due to lack of an email config"
  311. )
  312. raise SynapseError(
  313. 400, "Adding an email to your account is disabled on this server"
  314. )
  315. body = parse_json_object_from_request(request)
  316. assert_params_in_dict(body, ["client_secret", "email", "send_attempt"])
  317. client_secret = body["client_secret"]
  318. assert_valid_client_secret(client_secret)
  319. # Canonicalise the email address. The addresses are all stored canonicalised
  320. # in the database.
  321. # This ensures that the validation email is sent to the canonicalised address
  322. # as it will later be entered into the database.
  323. # Otherwise the email will be sent to "FOO@bar.com" and stored as
  324. # "foo@bar.com" in database.
  325. try:
  326. email = canonicalise_email(body["email"])
  327. except ValueError as e:
  328. raise SynapseError(400, str(e))
  329. send_attempt = body["send_attempt"]
  330. next_link = body.get("next_link") # Optional param
  331. if not check_3pid_allowed(self.hs, "email", email):
  332. raise SynapseError(
  333. 403,
  334. "Your email domain is not authorized on this server",
  335. Codes.THREEPID_DENIED,
  336. )
  337. self.identity_handler.ratelimit_request_token_requests(request, "email", email)
  338. if next_link:
  339. # Raise if the provided next_link value isn't valid
  340. assert_valid_next_link(self.hs, next_link)
  341. existing_user_id = await self.store.get_user_id_by_threepid("email", email)
  342. if existing_user_id is not None:
  343. if self.config.request_token_inhibit_3pid_errors:
  344. # Make the client think the operation succeeded. See the rationale in the
  345. # comments for request_token_inhibit_3pid_errors.
  346. # Also wait for some random amount of time between 100ms and 1s to make it
  347. # look like we did something.
  348. await self.hs.get_clock().sleep(random.randint(1, 10) / 10)
  349. return 200, {"sid": random_string(16)}
  350. raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE)
  351. if self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
  352. assert self.hs.config.account_threepid_delegate_email
  353. # Have the configured identity server handle the request
  354. ret = await self.identity_handler.requestEmailToken(
  355. self.hs.config.account_threepid_delegate_email,
  356. email,
  357. client_secret,
  358. send_attempt,
  359. next_link,
  360. )
  361. else:
  362. # Send threepid validation emails from Synapse
  363. sid = await self.identity_handler.send_threepid_validation(
  364. email,
  365. client_secret,
  366. send_attempt,
  367. self.mailer.send_add_threepid_mail,
  368. next_link,
  369. )
  370. # Wrap the session id in a JSON object
  371. ret = {"sid": sid}
  372. threepid_send_requests.labels(type="email", reason="add_threepid").observe(
  373. send_attempt
  374. )
  375. return 200, ret
  376. class MsisdnThreepidRequestTokenRestServlet(RestServlet):
  377. PATTERNS = client_patterns("/account/3pid/msisdn/requestToken$")
  378. def __init__(self, hs: "HomeServer"):
  379. self.hs = hs
  380. super().__init__()
  381. self.store = self.hs.get_datastore()
  382. self.identity_handler = hs.get_identity_handler()
  383. async def on_POST(self, request):
  384. body = parse_json_object_from_request(request)
  385. assert_params_in_dict(
  386. body, ["client_secret", "country", "phone_number", "send_attempt"]
  387. )
  388. client_secret = body["client_secret"]
  389. assert_valid_client_secret(client_secret)
  390. country = body["country"]
  391. phone_number = body["phone_number"]
  392. send_attempt = body["send_attempt"]
  393. next_link = body.get("next_link") # Optional param
  394. msisdn = phone_number_to_msisdn(country, phone_number)
  395. if not check_3pid_allowed(self.hs, "msisdn", msisdn):
  396. raise SynapseError(
  397. 403,
  398. "Account phone numbers are not authorized on this server",
  399. Codes.THREEPID_DENIED,
  400. )
  401. self.identity_handler.ratelimit_request_token_requests(
  402. request, "msisdn", msisdn
  403. )
  404. if next_link:
  405. # Raise if the provided next_link value isn't valid
  406. assert_valid_next_link(self.hs, next_link)
  407. existing_user_id = await self.store.get_user_id_by_threepid("msisdn", msisdn)
  408. if existing_user_id is not None:
  409. if self.hs.config.request_token_inhibit_3pid_errors:
  410. # Make the client think the operation succeeded. See the rationale in the
  411. # comments for request_token_inhibit_3pid_errors.
  412. # Also wait for some random amount of time between 100ms and 1s to make it
  413. # look like we did something.
  414. await self.hs.get_clock().sleep(random.randint(1, 10) / 10)
  415. return 200, {"sid": random_string(16)}
  416. raise SynapseError(400, "MSISDN is already in use", Codes.THREEPID_IN_USE)
  417. if not self.hs.config.account_threepid_delegate_msisdn:
  418. logger.warning(
  419. "No upstream msisdn account_threepid_delegate configured on the server to "
  420. "handle this request"
  421. )
  422. raise SynapseError(
  423. 400,
  424. "Adding phone numbers to user account is not supported by this homeserver",
  425. )
  426. ret = await self.identity_handler.requestMsisdnToken(
  427. self.hs.config.account_threepid_delegate_msisdn,
  428. country,
  429. phone_number,
  430. client_secret,
  431. send_attempt,
  432. next_link,
  433. )
  434. threepid_send_requests.labels(type="msisdn", reason="add_threepid").observe(
  435. send_attempt
  436. )
  437. return 200, ret
  438. class AddThreepidEmailSubmitTokenServlet(RestServlet):
  439. """Handles 3PID validation token submission for adding an email to a user's account"""
  440. PATTERNS = client_patterns(
  441. "/add_threepid/email/submit_token$", releases=(), unstable=True
  442. )
  443. def __init__(self, hs):
  444. """
  445. Args:
  446. hs (synapse.server.HomeServer): server
  447. """
  448. super().__init__()
  449. self.config = hs.config
  450. self.clock = hs.get_clock()
  451. self.store = hs.get_datastore()
  452. if self.config.threepid_behaviour_email == ThreepidBehaviour.LOCAL:
  453. self._failure_email_template = (
  454. self.config.email_add_threepid_template_failure_html
  455. )
  456. async def on_GET(self, request):
  457. if self.config.threepid_behaviour_email == ThreepidBehaviour.OFF:
  458. if self.config.local_threepid_handling_disabled_due_to_email_config:
  459. logger.warning(
  460. "Adding emails have been disabled due to lack of an email config"
  461. )
  462. raise SynapseError(
  463. 400, "Adding an email to your account is disabled on this server"
  464. )
  465. elif self.config.threepid_behaviour_email == ThreepidBehaviour.REMOTE:
  466. raise SynapseError(
  467. 400,
  468. "This homeserver is not validating threepids. Use an identity server "
  469. "instead.",
  470. )
  471. sid = parse_string(request, "sid", required=True)
  472. token = parse_string(request, "token", required=True)
  473. client_secret = parse_string(request, "client_secret", required=True)
  474. assert_valid_client_secret(client_secret)
  475. # Attempt to validate a 3PID session
  476. try:
  477. # Mark the session as valid
  478. next_link = await self.store.validate_threepid_session(
  479. sid, client_secret, token, self.clock.time_msec()
  480. )
  481. # Perform a 302 redirect if next_link is set
  482. if next_link:
  483. request.setResponseCode(302)
  484. request.setHeader("Location", next_link)
  485. finish_request(request)
  486. return None
  487. # Otherwise show the success template
  488. html = self.config.email_add_threepid_template_success_html_content
  489. status_code = 200
  490. except ThreepidValidationError as e:
  491. status_code = e.code
  492. # Show a failure page with a reason
  493. template_vars = {"failure_reason": e.msg}
  494. html = self._failure_email_template.render(**template_vars)
  495. respond_with_html(request, status_code, html)
  496. class AddThreepidMsisdnSubmitTokenServlet(RestServlet):
  497. """Handles 3PID validation token submission for adding a phone number to a user's
  498. account
  499. """
  500. PATTERNS = client_patterns(
  501. "/add_threepid/msisdn/submit_token$", releases=(), unstable=True
  502. )
  503. def __init__(self, hs):
  504. """
  505. Args:
  506. hs (synapse.server.HomeServer): server
  507. """
  508. super().__init__()
  509. self.config = hs.config
  510. self.clock = hs.get_clock()
  511. self.store = hs.get_datastore()
  512. self.identity_handler = hs.get_identity_handler()
  513. async def on_POST(self, request):
  514. if not self.config.account_threepid_delegate_msisdn:
  515. raise SynapseError(
  516. 400,
  517. "This homeserver is not validating phone numbers. Use an identity server "
  518. "instead.",
  519. )
  520. body = parse_json_object_from_request(request)
  521. assert_params_in_dict(body, ["client_secret", "sid", "token"])
  522. assert_valid_client_secret(body["client_secret"])
  523. # Proxy submit_token request to msisdn threepid delegate
  524. response = await self.identity_handler.proxy_msisdn_submit_token(
  525. self.config.account_threepid_delegate_msisdn,
  526. body["client_secret"],
  527. body["sid"],
  528. body["token"],
  529. )
  530. return 200, response
  531. class ThreepidRestServlet(RestServlet):
  532. PATTERNS = client_patterns("/account/3pid$")
  533. def __init__(self, hs):
  534. super().__init__()
  535. self.hs = hs
  536. self.identity_handler = hs.get_identity_handler()
  537. self.auth = hs.get_auth()
  538. self.auth_handler = hs.get_auth_handler()
  539. self.datastore = self.hs.get_datastore()
  540. async def on_GET(self, request):
  541. requester = await self.auth.get_user_by_req(request)
  542. threepids = await self.datastore.user_get_threepids(requester.user.to_string())
  543. return 200, {"threepids": threepids}
  544. async def on_POST(self, request):
  545. if not self.hs.config.enable_3pid_changes:
  546. raise SynapseError(
  547. 400, "3PID changes are disabled on this server", Codes.FORBIDDEN
  548. )
  549. requester = await self.auth.get_user_by_req(request)
  550. user_id = requester.user.to_string()
  551. body = parse_json_object_from_request(request)
  552. threepid_creds = body.get("threePidCreds") or body.get("three_pid_creds")
  553. if threepid_creds is None:
  554. raise SynapseError(
  555. 400, "Missing param three_pid_creds", Codes.MISSING_PARAM
  556. )
  557. assert_params_in_dict(threepid_creds, ["client_secret", "sid"])
  558. sid = threepid_creds["sid"]
  559. client_secret = threepid_creds["client_secret"]
  560. assert_valid_client_secret(client_secret)
  561. validation_session = await self.identity_handler.validate_threepid_session(
  562. client_secret, sid
  563. )
  564. if validation_session:
  565. await self.auth_handler.add_threepid(
  566. user_id,
  567. validation_session["medium"],
  568. validation_session["address"],
  569. validation_session["validated_at"],
  570. )
  571. return 200, {}
  572. raise SynapseError(
  573. 400, "No validated 3pid session found", Codes.THREEPID_AUTH_FAILED
  574. )
  575. class ThreepidAddRestServlet(RestServlet):
  576. PATTERNS = client_patterns("/account/3pid/add$")
  577. def __init__(self, hs):
  578. super().__init__()
  579. self.hs = hs
  580. self.identity_handler = hs.get_identity_handler()
  581. self.auth = hs.get_auth()
  582. self.auth_handler = hs.get_auth_handler()
  583. @interactive_auth_handler
  584. async def on_POST(self, request):
  585. if not self.hs.config.enable_3pid_changes:
  586. raise SynapseError(
  587. 400, "3PID changes are disabled on this server", Codes.FORBIDDEN
  588. )
  589. requester = await self.auth.get_user_by_req(request)
  590. user_id = requester.user.to_string()
  591. body = parse_json_object_from_request(request)
  592. assert_params_in_dict(body, ["client_secret", "sid"])
  593. sid = body["sid"]
  594. client_secret = body["client_secret"]
  595. assert_valid_client_secret(client_secret)
  596. await self.auth_handler.validate_user_via_ui_auth(
  597. requester,
  598. request,
  599. body,
  600. "add a third-party identifier to your account",
  601. )
  602. validation_session = await self.identity_handler.validate_threepid_session(
  603. client_secret, sid
  604. )
  605. if validation_session:
  606. await self.auth_handler.add_threepid(
  607. user_id,
  608. validation_session["medium"],
  609. validation_session["address"],
  610. validation_session["validated_at"],
  611. )
  612. return 200, {}
  613. raise SynapseError(
  614. 400, "No validated 3pid session found", Codes.THREEPID_AUTH_FAILED
  615. )
  616. class ThreepidBindRestServlet(RestServlet):
  617. PATTERNS = client_patterns("/account/3pid/bind$")
  618. def __init__(self, hs):
  619. super().__init__()
  620. self.hs = hs
  621. self.identity_handler = hs.get_identity_handler()
  622. self.auth = hs.get_auth()
  623. async def on_POST(self, request):
  624. body = parse_json_object_from_request(request)
  625. assert_params_in_dict(body, ["id_server", "sid", "client_secret"])
  626. id_server = body["id_server"]
  627. sid = body["sid"]
  628. id_access_token = body.get("id_access_token") # optional
  629. client_secret = body["client_secret"]
  630. assert_valid_client_secret(client_secret)
  631. requester = await self.auth.get_user_by_req(request)
  632. user_id = requester.user.to_string()
  633. await self.identity_handler.bind_threepid(
  634. client_secret, sid, user_id, id_server, id_access_token
  635. )
  636. return 200, {}
  637. class ThreepidUnbindRestServlet(RestServlet):
  638. PATTERNS = client_patterns("/account/3pid/unbind$")
  639. def __init__(self, hs):
  640. super().__init__()
  641. self.hs = hs
  642. self.identity_handler = hs.get_identity_handler()
  643. self.auth = hs.get_auth()
  644. self.datastore = self.hs.get_datastore()
  645. async def on_POST(self, request):
  646. """Unbind the given 3pid from a specific identity server, or identity servers that are
  647. known to have this 3pid bound
  648. """
  649. requester = await self.auth.get_user_by_req(request)
  650. body = parse_json_object_from_request(request)
  651. assert_params_in_dict(body, ["medium", "address"])
  652. medium = body.get("medium")
  653. address = body.get("address")
  654. id_server = body.get("id_server")
  655. # Attempt to unbind the threepid from an identity server. If id_server is None, try to
  656. # unbind from all identity servers this threepid has been added to in the past
  657. result = await self.identity_handler.try_unbind_threepid(
  658. requester.user.to_string(),
  659. {"address": address, "medium": medium, "id_server": id_server},
  660. )
  661. return 200, {"id_server_unbind_result": "success" if result else "no-support"}
  662. class ThreepidDeleteRestServlet(RestServlet):
  663. PATTERNS = client_patterns("/account/3pid/delete$")
  664. def __init__(self, hs):
  665. super().__init__()
  666. self.hs = hs
  667. self.auth = hs.get_auth()
  668. self.auth_handler = hs.get_auth_handler()
  669. async def on_POST(self, request):
  670. if not self.hs.config.enable_3pid_changes:
  671. raise SynapseError(
  672. 400, "3PID changes are disabled on this server", Codes.FORBIDDEN
  673. )
  674. body = parse_json_object_from_request(request)
  675. assert_params_in_dict(body, ["medium", "address"])
  676. requester = await self.auth.get_user_by_req(request)
  677. user_id = requester.user.to_string()
  678. try:
  679. ret = await self.auth_handler.delete_threepid(
  680. user_id, body["medium"], body["address"], body.get("id_server")
  681. )
  682. except Exception:
  683. # NB. This endpoint should succeed if there is nothing to
  684. # delete, so it should only throw if something is wrong
  685. # that we ought to care about.
  686. logger.exception("Failed to remove threepid")
  687. raise SynapseError(500, "Failed to remove threepid")
  688. if ret:
  689. id_server_unbind_result = "success"
  690. else:
  691. id_server_unbind_result = "no-support"
  692. return 200, {"id_server_unbind_result": id_server_unbind_result}
  693. def assert_valid_next_link(hs: "HomeServer", next_link: str):
  694. """
  695. Raises a SynapseError if a given next_link value is invalid
  696. next_link is valid if the scheme is http(s) and the next_link.domain_whitelist config
  697. option is either empty or contains a domain that matches the one in the given next_link
  698. Args:
  699. hs: The homeserver object
  700. next_link: The next_link value given by the client
  701. Raises:
  702. SynapseError: If the next_link is invalid
  703. """
  704. valid = True
  705. # Parse the contents of the URL
  706. next_link_parsed = urlparse(next_link)
  707. # Scheme must not point to the local drive
  708. if next_link_parsed.scheme == "file":
  709. valid = False
  710. # If the domain whitelist is set, the domain must be in it
  711. if (
  712. valid
  713. and hs.config.next_link_domain_whitelist is not None
  714. and next_link_parsed.hostname not in hs.config.next_link_domain_whitelist
  715. ):
  716. valid = False
  717. if not valid:
  718. raise SynapseError(
  719. 400,
  720. "'next_link' domain not included in whitelist, or not http(s)",
  721. errcode=Codes.INVALID_PARAM,
  722. )
  723. class WhoamiRestServlet(RestServlet):
  724. PATTERNS = client_patterns("/account/whoami$")
  725. def __init__(self, hs):
  726. super().__init__()
  727. self.auth = hs.get_auth()
  728. async def on_GET(self, request):
  729. requester = await self.auth.get_user_by_req(request)
  730. return 200, {"user_id": requester.user.to_string()}
  731. def register_servlets(hs, http_server):
  732. EmailPasswordRequestTokenRestServlet(hs).register(http_server)
  733. PasswordRestServlet(hs).register(http_server)
  734. DeactivateAccountRestServlet(hs).register(http_server)
  735. EmailThreepidRequestTokenRestServlet(hs).register(http_server)
  736. MsisdnThreepidRequestTokenRestServlet(hs).register(http_server)
  737. AddThreepidEmailSubmitTokenServlet(hs).register(http_server)
  738. AddThreepidMsisdnSubmitTokenServlet(hs).register(http_server)
  739. ThreepidRestServlet(hs).register(http_server)
  740. ThreepidAddRestServlet(hs).register(http_server)
  741. ThreepidBindRestServlet(hs).register(http_server)
  742. ThreepidUnbindRestServlet(hs).register(http_server)
  743. ThreepidDeleteRestServlet(hs).register(http_server)
  744. WhoamiRestServlet(hs).register(http_server)