account.py 34 KB

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