You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

917 lines
35 KiB

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