Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 
 
 
 
 

1723 righe
65 KiB

  1. # -*- coding: utf-8 -*-
  2. # Copyright 2014 - 2016 OpenMarket Ltd
  3. # Copyright 2017 Vector Creations Ltd
  4. # Copyright 2019 - 2020 The Matrix.org Foundation C.I.C.
  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 time
  19. import unicodedata
  20. import urllib.parse
  21. from typing import (
  22. TYPE_CHECKING,
  23. Any,
  24. Awaitable,
  25. Callable,
  26. Dict,
  27. Iterable,
  28. List,
  29. Mapping,
  30. Optional,
  31. Tuple,
  32. Union,
  33. )
  34. import attr
  35. import bcrypt
  36. import pymacaroons
  37. from twisted.web.server import Request
  38. from synapse.api.constants import LoginType
  39. from synapse.api.errors import (
  40. AuthError,
  41. Codes,
  42. InteractiveAuthIncompleteError,
  43. LoginError,
  44. StoreError,
  45. SynapseError,
  46. UserDeactivatedError,
  47. )
  48. from synapse.api.ratelimiting import Ratelimiter
  49. from synapse.handlers._base import BaseHandler
  50. from synapse.handlers.ui_auth import (
  51. INTERACTIVE_AUTH_CHECKERS,
  52. UIAuthSessionDataConstants,
  53. )
  54. from synapse.handlers.ui_auth.checkers import UserInteractiveAuthChecker
  55. from synapse.http import get_request_user_agent
  56. from synapse.http.server import finish_request, respond_with_html
  57. from synapse.http.site import SynapseRequest
  58. from synapse.logging.context import defer_to_thread
  59. from synapse.metrics.background_process_metrics import run_as_background_process
  60. from synapse.module_api import ModuleApi
  61. from synapse.storage.roommember import ProfileInfo
  62. from synapse.types import JsonDict, Requester, UserID
  63. from synapse.util import stringutils as stringutils
  64. from synapse.util.async_helpers import maybe_awaitable
  65. from synapse.util.msisdn import phone_number_to_msisdn
  66. from synapse.util.threepids import canonicalise_email
  67. if TYPE_CHECKING:
  68. from synapse.app.homeserver import HomeServer
  69. logger = logging.getLogger(__name__)
  70. def convert_client_dict_legacy_fields_to_identifier(
  71. submission: JsonDict,
  72. ) -> Dict[str, str]:
  73. """
  74. Convert a legacy-formatted login submission to an identifier dict.
  75. Legacy login submissions (used in both login and user-interactive authentication)
  76. provide user-identifying information at the top-level instead.
  77. These are now deprecated and replaced with identifiers:
  78. https://matrix.org/docs/spec/client_server/r0.6.1#identifier-types
  79. Args:
  80. submission: The client dict to convert
  81. Returns:
  82. The matching identifier dict
  83. Raises:
  84. SynapseError: If the format of the client dict is invalid
  85. """
  86. identifier = submission.get("identifier", {})
  87. # Generate an m.id.user identifier if "user" parameter is present
  88. user = submission.get("user")
  89. if user:
  90. identifier = {"type": "m.id.user", "user": user}
  91. # Generate an m.id.thirdparty identifier if "medium" and "address" parameters are present
  92. medium = submission.get("medium")
  93. address = submission.get("address")
  94. if medium and address:
  95. identifier = {
  96. "type": "m.id.thirdparty",
  97. "medium": medium,
  98. "address": address,
  99. }
  100. # We've converted valid, legacy login submissions to an identifier. If the
  101. # submission still doesn't have an identifier, it's invalid
  102. if not identifier:
  103. raise SynapseError(400, "Invalid login submission", Codes.INVALID_PARAM)
  104. # Ensure the identifier has a type
  105. if "type" not in identifier:
  106. raise SynapseError(
  107. 400,
  108. "'identifier' dict has no key 'type'",
  109. errcode=Codes.MISSING_PARAM,
  110. )
  111. return identifier
  112. def login_id_phone_to_thirdparty(identifier: JsonDict) -> Dict[str, str]:
  113. """
  114. Convert a phone login identifier type to a generic threepid identifier.
  115. Args:
  116. identifier: Login identifier dict of type 'm.id.phone'
  117. Returns:
  118. An equivalent m.id.thirdparty identifier dict
  119. """
  120. if "country" not in identifier or (
  121. # The specification requires a "phone" field, while Synapse used to require a "number"
  122. # field. Accept both for backwards compatibility.
  123. "phone" not in identifier
  124. and "number" not in identifier
  125. ):
  126. raise SynapseError(
  127. 400, "Invalid phone-type identifier", errcode=Codes.INVALID_PARAM
  128. )
  129. # Accept both "phone" and "number" as valid keys in m.id.phone
  130. phone_number = identifier.get("phone", identifier["number"])
  131. # Convert user-provided phone number to a consistent representation
  132. msisdn = phone_number_to_msisdn(identifier["country"], phone_number)
  133. return {
  134. "type": "m.id.thirdparty",
  135. "medium": "msisdn",
  136. "address": msisdn,
  137. }
  138. @attr.s(slots=True)
  139. class SsoLoginExtraAttributes:
  140. """Data we track about SAML2 sessions"""
  141. # time the session was created, in milliseconds
  142. creation_time = attr.ib(type=int)
  143. extra_attributes = attr.ib(type=JsonDict)
  144. class AuthHandler(BaseHandler):
  145. SESSION_EXPIRE_MS = 48 * 60 * 60 * 1000
  146. def __init__(self, hs: "HomeServer"):
  147. super().__init__(hs)
  148. self.checkers = {} # type: Dict[str, UserInteractiveAuthChecker]
  149. for auth_checker_class in INTERACTIVE_AUTH_CHECKERS:
  150. inst = auth_checker_class(hs)
  151. if inst.is_enabled():
  152. self.checkers[inst.AUTH_TYPE] = inst # type: ignore
  153. self.bcrypt_rounds = hs.config.bcrypt_rounds
  154. # we can't use hs.get_module_api() here, because to do so will create an
  155. # import loop.
  156. #
  157. # TODO: refactor this class to separate the lower-level stuff that
  158. # ModuleApi can use from the higher-level stuff that uses ModuleApi, as
  159. # better way to break the loop
  160. account_handler = ModuleApi(hs, self)
  161. self.password_providers = [
  162. PasswordProvider.load(module, config, account_handler)
  163. for module, config in hs.config.password_providers
  164. ]
  165. logger.info("Extra password_providers: %s", self.password_providers)
  166. self.hs = hs # FIXME better possibility to access registrationHandler later?
  167. self.macaroon_gen = hs.get_macaroon_generator()
  168. self._password_enabled = hs.config.password_enabled
  169. self._password_localdb_enabled = hs.config.password_localdb_enabled
  170. # start out by assuming PASSWORD is enabled; we will remove it later if not.
  171. login_types = set()
  172. if self._password_localdb_enabled:
  173. login_types.add(LoginType.PASSWORD)
  174. for provider in self.password_providers:
  175. login_types.update(provider.get_supported_login_types().keys())
  176. if not self._password_enabled:
  177. login_types.discard(LoginType.PASSWORD)
  178. # Some clients just pick the first type in the list. In this case, we want
  179. # them to use PASSWORD (rather than token or whatever), so we want to make sure
  180. # that comes first, where it's present.
  181. self._supported_login_types = []
  182. if LoginType.PASSWORD in login_types:
  183. self._supported_login_types.append(LoginType.PASSWORD)
  184. login_types.remove(LoginType.PASSWORD)
  185. self._supported_login_types.extend(login_types)
  186. # Ratelimiter for failed auth during UIA. Uses same ratelimit config
  187. # as per `rc_login.failed_attempts`.
  188. self._failed_uia_attempts_ratelimiter = Ratelimiter(
  189. clock=self.clock,
  190. rate_hz=self.hs.config.rc_login_failed_attempts.per_second,
  191. burst_count=self.hs.config.rc_login_failed_attempts.burst_count,
  192. )
  193. # The number of seconds to keep a UI auth session active.
  194. self._ui_auth_session_timeout = hs.config.ui_auth_session_timeout
  195. # Ratelimitier for failed /login attempts
  196. self._failed_login_attempts_ratelimiter = Ratelimiter(
  197. clock=hs.get_clock(),
  198. rate_hz=self.hs.config.rc_login_failed_attempts.per_second,
  199. burst_count=self.hs.config.rc_login_failed_attempts.burst_count,
  200. )
  201. self._clock = self.hs.get_clock()
  202. # Expire old UI auth sessions after a period of time.
  203. if hs.config.run_background_tasks:
  204. self._clock.looping_call(
  205. run_as_background_process,
  206. 5 * 60 * 1000,
  207. "expire_old_sessions",
  208. self._expire_old_sessions,
  209. )
  210. # Load the SSO HTML templates.
  211. # The following template is shown to the user during a client login via SSO,
  212. # after the SSO completes and before redirecting them back to their client.
  213. # It notifies the user they are about to give access to their matrix account
  214. # to the client.
  215. self._sso_redirect_confirm_template = hs.config.sso_redirect_confirm_template
  216. # The following template is shown during user interactive authentication
  217. # in the fallback auth scenario. It notifies the user that they are
  218. # authenticating for an operation to occur on their account.
  219. self._sso_auth_confirm_template = hs.config.sso_auth_confirm_template
  220. # The following template is shown during the SSO authentication process if
  221. # the account is deactivated.
  222. self._sso_account_deactivated_template = (
  223. hs.config.sso_account_deactivated_template
  224. )
  225. self._server_name = hs.config.server_name
  226. # cast to tuple for use with str.startswith
  227. self._whitelisted_sso_clients = tuple(hs.config.sso_client_whitelist)
  228. # A mapping of user ID to extra attributes to include in the login
  229. # response.
  230. self._extra_attributes = {} # type: Dict[str, SsoLoginExtraAttributes]
  231. async def validate_user_via_ui_auth(
  232. self,
  233. requester: Requester,
  234. request: SynapseRequest,
  235. request_body: Dict[str, Any],
  236. description: str,
  237. ) -> Tuple[dict, Optional[str]]:
  238. """
  239. Checks that the user is who they claim to be, via a UI auth.
  240. This is used for things like device deletion and password reset where
  241. the user already has a valid access token, but we want to double-check
  242. that it isn't stolen by re-authenticating them.
  243. Args:
  244. requester: The user, as given by the access token
  245. request: The request sent by the client.
  246. request_body: The body of the request sent by the client
  247. description: A human readable string to be displayed to the user that
  248. describes the operation happening on their account.
  249. Returns:
  250. A tuple of (params, session_id).
  251. 'params' contains the parameters for this request (which may
  252. have been given only in a previous call).
  253. 'session_id' is the ID of this session, either passed in by the
  254. client or assigned by this call. This is None if UI auth was
  255. skipped (by re-using a previous validation).
  256. Raises:
  257. InteractiveAuthIncompleteError if the client has not yet completed
  258. any of the permitted login flows
  259. AuthError if the client has completed a login flow, and it gives
  260. a different user to `requester`
  261. LimitExceededError if the ratelimiter's failed request count for this
  262. user is too high to proceed
  263. """
  264. if self._ui_auth_session_timeout:
  265. last_validated = await self.store.get_access_token_last_validated(
  266. requester.access_token_id
  267. )
  268. if self.clock.time_msec() - last_validated < self._ui_auth_session_timeout:
  269. # Return the input parameters, minus the auth key, which matches
  270. # the logic in check_ui_auth.
  271. request_body.pop("auth", None)
  272. return request_body, None
  273. requester_user_id = requester.user.to_string()
  274. # Check if we should be ratelimited due to too many previous failed attempts
  275. self._failed_uia_attempts_ratelimiter.ratelimit(requester_user_id, update=False)
  276. # build a list of supported flows
  277. supported_ui_auth_types = await self._get_available_ui_auth_types(
  278. requester.user
  279. )
  280. flows = [[login_type] for login_type in supported_ui_auth_types]
  281. def get_new_session_data() -> JsonDict:
  282. return {UIAuthSessionDataConstants.REQUEST_USER_ID: requester_user_id}
  283. try:
  284. result, params, session_id = await self.check_ui_auth(
  285. flows,
  286. request,
  287. request_body,
  288. description,
  289. get_new_session_data,
  290. )
  291. except LoginError:
  292. # Update the ratelimiter to say we failed (`can_do_action` doesn't raise).
  293. self._failed_uia_attempts_ratelimiter.can_do_action(requester_user_id)
  294. raise
  295. # find the completed login type
  296. for login_type in supported_ui_auth_types:
  297. if login_type not in result:
  298. continue
  299. validated_user_id = result[login_type]
  300. break
  301. else:
  302. # this can't happen
  303. raise Exception("check_auth returned True but no successful login type")
  304. # check that the UI auth matched the access token
  305. if validated_user_id != requester_user_id:
  306. raise AuthError(403, "Invalid auth")
  307. # Note that the access token has been validated.
  308. await self.store.update_access_token_last_validated(requester.access_token_id)
  309. return params, session_id
  310. async def _get_available_ui_auth_types(self, user: UserID) -> Iterable[str]:
  311. """Get a list of the authentication types this user can use"""
  312. ui_auth_types = set()
  313. # if the HS supports password auth, and the user has a non-null password, we
  314. # support password auth
  315. if self._password_localdb_enabled and self._password_enabled:
  316. lookupres = await self._find_user_id_and_pwd_hash(user.to_string())
  317. if lookupres:
  318. _, password_hash = lookupres
  319. if password_hash:
  320. ui_auth_types.add(LoginType.PASSWORD)
  321. # also allow auth from password providers
  322. for provider in self.password_providers:
  323. for t in provider.get_supported_login_types().keys():
  324. if t == LoginType.PASSWORD and not self._password_enabled:
  325. continue
  326. ui_auth_types.add(t)
  327. # if sso is enabled, allow the user to log in via SSO iff they have a mapping
  328. # from sso to mxid.
  329. if await self.hs.get_sso_handler().get_identity_providers_for_user(
  330. user.to_string()
  331. ):
  332. ui_auth_types.add(LoginType.SSO)
  333. return ui_auth_types
  334. def get_enabled_auth_types(self):
  335. """Return the enabled user-interactive authentication types
  336. Returns the UI-Auth types which are supported by the homeserver's current
  337. config.
  338. """
  339. return self.checkers.keys()
  340. async def check_ui_auth(
  341. self,
  342. flows: List[List[str]],
  343. request: SynapseRequest,
  344. clientdict: Dict[str, Any],
  345. description: str,
  346. get_new_session_data: Optional[Callable[[], JsonDict]] = None,
  347. ) -> Tuple[dict, dict, str]:
  348. """
  349. Takes a dictionary sent by the client in the login / registration
  350. protocol and handles the User-Interactive Auth flow.
  351. If no auth flows have been completed successfully, raises an
  352. InteractiveAuthIncompleteError. To handle this, you can use
  353. synapse.rest.client.v2_alpha._base.interactive_auth_handler as a
  354. decorator.
  355. Args:
  356. flows: A list of login flows. Each flow is an ordered list of
  357. strings representing auth-types. At least one full
  358. flow must be completed in order for auth to be successful.
  359. request: The request sent by the client.
  360. clientdict: The dictionary from the client root level, not the
  361. 'auth' key: this method prompts for auth if none is sent.
  362. description: A human readable string to be displayed to the user that
  363. describes the operation happening on their account.
  364. get_new_session_data:
  365. an optional callback which will be called when starting a new session.
  366. it should return data to be stored as part of the session.
  367. The keys of the returned data should be entries in
  368. UIAuthSessionDataConstants.
  369. Returns:
  370. A tuple of (creds, params, session_id).
  371. 'creds' contains the authenticated credentials of each stage.
  372. 'params' contains the parameters for this request (which may
  373. have been given only in a previous call).
  374. 'session_id' is the ID of this session, either passed in by the
  375. client or assigned by this call
  376. Raises:
  377. InteractiveAuthIncompleteError if the client has not yet completed
  378. all the stages in any of the permitted flows.
  379. """
  380. sid = None # type: Optional[str]
  381. authdict = clientdict.pop("auth", {})
  382. if "session" in authdict:
  383. sid = authdict["session"]
  384. # Convert the URI and method to strings.
  385. uri = request.uri.decode("utf-8") # type: ignore
  386. method = request.method.decode("utf-8")
  387. # If there's no session ID, create a new session.
  388. if not sid:
  389. new_session_data = get_new_session_data() if get_new_session_data else {}
  390. session = await self.store.create_ui_auth_session(
  391. clientdict, uri, method, description
  392. )
  393. for k, v in new_session_data.items():
  394. await self.set_session_data(session.session_id, k, v)
  395. else:
  396. try:
  397. session = await self.store.get_ui_auth_session(sid)
  398. except StoreError:
  399. raise SynapseError(400, "Unknown session ID: %s" % (sid,))
  400. # If the client provides parameters, update what is persisted,
  401. # otherwise use whatever was last provided.
  402. #
  403. # This was designed to allow the client to omit the parameters
  404. # and just supply the session in subsequent calls so it split
  405. # auth between devices by just sharing the session, (eg. so you
  406. # could continue registration from your phone having clicked the
  407. # email auth link on there). It's probably too open to abuse
  408. # because it lets unauthenticated clients store arbitrary objects
  409. # on a homeserver.
  410. #
  411. # Revisit: Assuming the REST APIs do sensible validation, the data
  412. # isn't arbitrary.
  413. #
  414. # Note that the registration endpoint explicitly removes the
  415. # "initial_device_display_name" parameter if it is provided
  416. # without a "password" parameter. See the changes to
  417. # synapse.rest.client.v2_alpha.register.RegisterRestServlet.on_POST
  418. # in commit 544722bad23fc31056b9240189c3cbbbf0ffd3f9.
  419. if not clientdict:
  420. clientdict = session.clientdict
  421. # Ensure that the queried operation does not vary between stages of
  422. # the UI authentication session. This is done by generating a stable
  423. # comparator and storing it during the initial query. Subsequent
  424. # queries ensure that this comparator has not changed.
  425. #
  426. # The comparator is based on the requested URI and HTTP method. The
  427. # client dict (minus the auth dict) should also be checked, but some
  428. # clients are not spec compliant, just warn for now if the client
  429. # dict changes.
  430. if (session.uri, session.method) != (uri, method):
  431. raise SynapseError(
  432. 403,
  433. "Requested operation has changed during the UI authentication session.",
  434. )
  435. if session.clientdict != clientdict:
  436. logger.warning(
  437. "Requested operation has changed during the UI "
  438. "authentication session. A future version of Synapse "
  439. "will remove this capability."
  440. )
  441. # For backwards compatibility, changes to the client dict are
  442. # persisted as clients modify them throughout their user interactive
  443. # authentication flow.
  444. await self.store.set_ui_auth_clientdict(sid, clientdict)
  445. user_agent = get_request_user_agent(request)
  446. clientip = request.getClientIP()
  447. await self.store.add_user_agent_ip_to_ui_auth_session(
  448. session.session_id, user_agent, clientip
  449. )
  450. if not authdict:
  451. raise InteractiveAuthIncompleteError(
  452. session.session_id, self._auth_dict_for_flows(flows, session.session_id)
  453. )
  454. # check auth type currently being presented
  455. errordict = {} # type: Dict[str, Any]
  456. if "type" in authdict:
  457. login_type = authdict["type"] # type: str
  458. try:
  459. result = await self._check_auth_dict(authdict, clientip)
  460. if result:
  461. await self.store.mark_ui_auth_stage_complete(
  462. session.session_id, login_type, result
  463. )
  464. except LoginError as e:
  465. # this step failed. Merge the error dict into the response
  466. # so that the client can have another go.
  467. errordict = e.error_dict()
  468. creds = await self.store.get_completed_ui_auth_stages(session.session_id)
  469. for f in flows:
  470. # If all the required credentials have been supplied, the user has
  471. # successfully completed the UI auth process!
  472. if len(set(f) - set(creds)) == 0:
  473. # it's very useful to know what args are stored, but this can
  474. # include the password in the case of registering, so only log
  475. # the keys (confusingly, clientdict may contain a password
  476. # param, creds is just what the user authed as for UI auth
  477. # and is not sensitive).
  478. logger.info(
  479. "Auth completed with creds: %r. Client dict has keys: %r",
  480. creds,
  481. list(clientdict),
  482. )
  483. return creds, clientdict, session.session_id
  484. ret = self._auth_dict_for_flows(flows, session.session_id)
  485. ret["completed"] = list(creds)
  486. ret.update(errordict)
  487. raise InteractiveAuthIncompleteError(session.session_id, ret)
  488. async def add_oob_auth(
  489. self, stagetype: str, authdict: Dict[str, Any], clientip: str
  490. ) -> bool:
  491. """
  492. Adds the result of out-of-band authentication into an existing auth
  493. session. Currently used for adding the result of fallback auth.
  494. """
  495. if stagetype not in self.checkers:
  496. raise LoginError(400, "", Codes.MISSING_PARAM)
  497. if "session" not in authdict:
  498. raise LoginError(400, "", Codes.MISSING_PARAM)
  499. result = await self.checkers[stagetype].check_auth(authdict, clientip)
  500. if result:
  501. await self.store.mark_ui_auth_stage_complete(
  502. authdict["session"], stagetype, result
  503. )
  504. return True
  505. return False
  506. def get_session_id(self, clientdict: Dict[str, Any]) -> Optional[str]:
  507. """
  508. Gets the session ID for a client given the client dictionary
  509. Args:
  510. clientdict: The dictionary sent by the client in the request
  511. Returns:
  512. The string session ID the client sent. If the client did
  513. not send a session ID, returns None.
  514. """
  515. sid = None
  516. if clientdict and "auth" in clientdict:
  517. authdict = clientdict["auth"]
  518. if "session" in authdict:
  519. sid = authdict["session"]
  520. return sid
  521. async def set_session_data(self, session_id: str, key: str, value: Any) -> None:
  522. """
  523. Store a key-value pair into the sessions data associated with this
  524. request. This data is stored server-side and cannot be modified by
  525. the client.
  526. Args:
  527. session_id: The ID of this session as returned from check_auth
  528. key: The key to store the data under. An entry from
  529. UIAuthSessionDataConstants.
  530. value: The data to store
  531. """
  532. try:
  533. await self.store.set_ui_auth_session_data(session_id, key, value)
  534. except StoreError:
  535. raise SynapseError(400, "Unknown session ID: %s" % (session_id,))
  536. async def get_session_data(
  537. self, session_id: str, key: str, default: Optional[Any] = None
  538. ) -> Any:
  539. """
  540. Retrieve data stored with set_session_data
  541. Args:
  542. session_id: The ID of this session as returned from check_auth
  543. key: The key the data was stored under. An entry from
  544. UIAuthSessionDataConstants.
  545. default: Value to return if the key has not been set
  546. """
  547. try:
  548. return await self.store.get_ui_auth_session_data(session_id, key, default)
  549. except StoreError:
  550. raise SynapseError(400, "Unknown session ID: %s" % (session_id,))
  551. async def _expire_old_sessions(self):
  552. """
  553. Invalidate any user interactive authentication sessions that have expired.
  554. """
  555. now = self._clock.time_msec()
  556. expiration_time = now - self.SESSION_EXPIRE_MS
  557. await self.store.delete_old_ui_auth_sessions(expiration_time)
  558. async def _check_auth_dict(
  559. self, authdict: Dict[str, Any], clientip: str
  560. ) -> Union[Dict[str, Any], str]:
  561. """Attempt to validate the auth dict provided by a client
  562. Args:
  563. authdict: auth dict provided by the client
  564. clientip: IP address of the client
  565. Returns:
  566. Result of the stage verification.
  567. Raises:
  568. StoreError if there was a problem accessing the database
  569. SynapseError if there was a problem with the request
  570. LoginError if there was an authentication problem.
  571. """
  572. login_type = authdict["type"]
  573. checker = self.checkers.get(login_type)
  574. if checker is not None:
  575. res = await checker.check_auth(authdict, clientip=clientip)
  576. return res
  577. # fall back to the v1 login flow
  578. canonical_id, _ = await self.validate_login(authdict)
  579. return canonical_id
  580. def _get_params_recaptcha(self) -> dict:
  581. return {"public_key": self.hs.config.recaptcha_public_key}
  582. def _get_params_terms(self) -> dict:
  583. return {
  584. "policies": {
  585. "privacy_policy": {
  586. "version": self.hs.config.user_consent_version,
  587. "en": {
  588. "name": self.hs.config.user_consent_policy_name,
  589. "url": "%s_matrix/consent?v=%s"
  590. % (
  591. self.hs.config.public_baseurl,
  592. self.hs.config.user_consent_version,
  593. ),
  594. },
  595. }
  596. }
  597. }
  598. def _auth_dict_for_flows(
  599. self,
  600. flows: List[List[str]],
  601. session_id: str,
  602. ) -> Dict[str, Any]:
  603. public_flows = []
  604. for f in flows:
  605. public_flows.append(f)
  606. get_params = {
  607. LoginType.RECAPTCHA: self._get_params_recaptcha,
  608. LoginType.TERMS: self._get_params_terms,
  609. }
  610. params = {} # type: Dict[str, Any]
  611. for f in public_flows:
  612. for stage in f:
  613. if stage in get_params and stage not in params:
  614. params[stage] = get_params[stage]()
  615. return {
  616. "session": session_id,
  617. "flows": [{"stages": f} for f in public_flows],
  618. "params": params,
  619. }
  620. async def get_access_token_for_user_id(
  621. self,
  622. user_id: str,
  623. device_id: Optional[str],
  624. valid_until_ms: Optional[int],
  625. puppets_user_id: Optional[str] = None,
  626. is_appservice_ghost: bool = False,
  627. ) -> str:
  628. """
  629. Creates a new access token for the user with the given user ID.
  630. The user is assumed to have been authenticated by some other
  631. mechanism (e.g. CAS), and the user_id converted to the canonical case.
  632. The device will be recorded in the table if it is not there already.
  633. Args:
  634. user_id: canonical User ID
  635. device_id: the device ID to associate with the tokens.
  636. None to leave the tokens unassociated with a device (deprecated:
  637. we should always have a device ID)
  638. valid_until_ms: when the token is valid until. None for
  639. no expiry.
  640. is_appservice_ghost: Whether the user is an application ghost user
  641. Returns:
  642. The access token for the user's session.
  643. Raises:
  644. StoreError if there was a problem storing the token.
  645. """
  646. fmt_expiry = ""
  647. if valid_until_ms is not None:
  648. fmt_expiry = time.strftime(
  649. " until %Y-%m-%d %H:%M:%S", time.localtime(valid_until_ms / 1000.0)
  650. )
  651. if puppets_user_id:
  652. logger.info(
  653. "Logging in user %s as %s%s", user_id, puppets_user_id, fmt_expiry
  654. )
  655. else:
  656. logger.info(
  657. "Logging in user %s on device %s%s", user_id, device_id, fmt_expiry
  658. )
  659. if (
  660. not is_appservice_ghost
  661. or self.hs.config.appservice.track_appservice_user_ips
  662. ):
  663. await self.auth.check_auth_blocking(user_id)
  664. access_token = self.macaroon_gen.generate_access_token(user_id)
  665. await self.store.add_access_token_to_user(
  666. user_id=user_id,
  667. token=access_token,
  668. device_id=device_id,
  669. valid_until_ms=valid_until_ms,
  670. puppets_user_id=puppets_user_id,
  671. )
  672. # the device *should* have been registered before we got here; however,
  673. # it's possible we raced against a DELETE operation. The thing we
  674. # really don't want is active access_tokens without a record of the
  675. # device, so we double-check it here.
  676. if device_id is not None:
  677. try:
  678. await self.store.get_device(user_id, device_id)
  679. except StoreError:
  680. await self.store.delete_access_token(access_token)
  681. raise StoreError(400, "Login raced against device deletion")
  682. return access_token
  683. async def check_user_exists(self, user_id: str) -> Optional[str]:
  684. """
  685. Checks to see if a user with the given id exists. Will check case
  686. insensitively, but return None if there are multiple inexact matches.
  687. Args:
  688. user_id: complete @user:id
  689. Returns:
  690. The canonical_user_id, or None if zero or multiple matches
  691. """
  692. res = await self._find_user_id_and_pwd_hash(user_id)
  693. if res is not None:
  694. return res[0]
  695. return None
  696. async def _find_user_id_and_pwd_hash(
  697. self, user_id: str
  698. ) -> Optional[Tuple[str, str]]:
  699. """Checks to see if a user with the given id exists. Will check case
  700. insensitively, but will return None if there are multiple inexact
  701. matches.
  702. Returns:
  703. A 2-tuple of `(canonical_user_id, password_hash)` or `None`
  704. if there is not exactly one match
  705. """
  706. user_infos = await self.store.get_users_by_id_case_insensitive(user_id)
  707. result = None
  708. if not user_infos:
  709. logger.warning("Attempted to login as %s but they do not exist", user_id)
  710. elif len(user_infos) == 1:
  711. # a single match (possibly not exact)
  712. result = user_infos.popitem()
  713. elif user_id in user_infos:
  714. # multiple matches, but one is exact
  715. result = (user_id, user_infos[user_id])
  716. else:
  717. # multiple matches, none of them exact
  718. logger.warning(
  719. "Attempted to login as %s but it matches more than one user "
  720. "inexactly: %r",
  721. user_id,
  722. user_infos.keys(),
  723. )
  724. return result
  725. def get_supported_login_types(self) -> Iterable[str]:
  726. """Get a the login types supported for the /login API
  727. By default this is just 'm.login.password' (unless password_enabled is
  728. False in the config file), but password auth providers can provide
  729. other login types.
  730. Returns:
  731. login types
  732. """
  733. return self._supported_login_types
  734. async def validate_login(
  735. self,
  736. login_submission: Dict[str, Any],
  737. ratelimit: bool = False,
  738. ) -> Tuple[str, Optional[Callable[[Dict[str, str]], Awaitable[None]]]]:
  739. """Authenticates the user for the /login API
  740. Also used by the user-interactive auth flow to validate auth types which don't
  741. have an explicit UIA handler, including m.password.auth.
  742. Args:
  743. login_submission: the whole of the login submission
  744. (including 'type' and other relevant fields)
  745. ratelimit: whether to apply the failed_login_attempt ratelimiter
  746. Returns:
  747. A tuple of the canonical user id, and optional callback
  748. to be called once the access token and device id are issued
  749. Raises:
  750. StoreError if there was a problem accessing the database
  751. SynapseError if there was a problem with the request
  752. LoginError if there was an authentication problem.
  753. """
  754. login_type = login_submission.get("type")
  755. if not isinstance(login_type, str):
  756. raise SynapseError(400, "Bad parameter: type", Codes.INVALID_PARAM)
  757. # ideally, we wouldn't be checking the identifier unless we know we have a login
  758. # method which uses it (https://github.com/matrix-org/synapse/issues/8836)
  759. #
  760. # But the auth providers' check_auth interface requires a username, so in
  761. # practice we can only support login methods which we can map to a username
  762. # anyway.
  763. # special case to check for "password" for the check_password interface
  764. # for the auth providers
  765. password = login_submission.get("password")
  766. if login_type == LoginType.PASSWORD:
  767. if not self._password_enabled:
  768. raise SynapseError(400, "Password login has been disabled.")
  769. if not isinstance(password, str):
  770. raise SynapseError(400, "Bad parameter: password", Codes.INVALID_PARAM)
  771. # map old-school login fields into new-school "identifier" fields.
  772. identifier_dict = convert_client_dict_legacy_fields_to_identifier(
  773. login_submission
  774. )
  775. # convert phone type identifiers to generic threepids
  776. if identifier_dict["type"] == "m.id.phone":
  777. identifier_dict = login_id_phone_to_thirdparty(identifier_dict)
  778. # convert threepid identifiers to user IDs
  779. if identifier_dict["type"] == "m.id.thirdparty":
  780. address = identifier_dict.get("address")
  781. medium = identifier_dict.get("medium")
  782. if medium is None or address is None:
  783. raise SynapseError(400, "Invalid thirdparty identifier")
  784. # For emails, canonicalise the address.
  785. # We store all email addresses canonicalised in the DB.
  786. # (See add_threepid in synapse/handlers/auth.py)
  787. if medium == "email":
  788. try:
  789. address = canonicalise_email(address)
  790. except ValueError as e:
  791. raise SynapseError(400, str(e))
  792. # We also apply account rate limiting using the 3PID as a key, as
  793. # otherwise using 3PID bypasses the ratelimiting based on user ID.
  794. if ratelimit:
  795. self._failed_login_attempts_ratelimiter.ratelimit(
  796. (medium, address), update=False
  797. )
  798. # Check for login providers that support 3pid login types
  799. if login_type == LoginType.PASSWORD:
  800. # we've already checked that there is a (valid) password field
  801. assert isinstance(password, str)
  802. (
  803. canonical_user_id,
  804. callback_3pid,
  805. ) = await self.check_password_provider_3pid(medium, address, password)
  806. if canonical_user_id:
  807. # Authentication through password provider and 3pid succeeded
  808. return canonical_user_id, callback_3pid
  809. # No password providers were able to handle this 3pid
  810. # Check local store
  811. user_id = await self.hs.get_datastore().get_user_id_by_threepid(
  812. medium, address
  813. )
  814. if not user_id:
  815. logger.warning(
  816. "unknown 3pid identifier medium %s, address %r", medium, address
  817. )
  818. # We mark that we've failed to log in here, as
  819. # `check_password_provider_3pid` might have returned `None` due
  820. # to an incorrect password, rather than the account not
  821. # existing.
  822. #
  823. # If it returned None but the 3PID was bound then we won't hit
  824. # this code path, which is fine as then the per-user ratelimit
  825. # will kick in below.
  826. if ratelimit:
  827. self._failed_login_attempts_ratelimiter.can_do_action(
  828. (medium, address)
  829. )
  830. raise LoginError(403, "", errcode=Codes.FORBIDDEN)
  831. identifier_dict = {"type": "m.id.user", "user": user_id}
  832. # by this point, the identifier should be an m.id.user: if it's anything
  833. # else, we haven't understood it.
  834. if identifier_dict["type"] != "m.id.user":
  835. raise SynapseError(400, "Unknown login identifier type")
  836. username = identifier_dict.get("user")
  837. if not username:
  838. raise SynapseError(400, "User identifier is missing 'user' key")
  839. if username.startswith("@"):
  840. qualified_user_id = username
  841. else:
  842. qualified_user_id = UserID(username, self.hs.hostname).to_string()
  843. # Check if we've hit the failed ratelimit (but don't update it)
  844. if ratelimit:
  845. self._failed_login_attempts_ratelimiter.ratelimit(
  846. qualified_user_id.lower(), update=False
  847. )
  848. try:
  849. return await self._validate_userid_login(username, login_submission)
  850. except LoginError:
  851. # The user has failed to log in, so we need to update the rate
  852. # limiter. Using `can_do_action` avoids us raising a ratelimit
  853. # exception and masking the LoginError. The actual ratelimiting
  854. # should have happened above.
  855. if ratelimit:
  856. self._failed_login_attempts_ratelimiter.can_do_action(
  857. qualified_user_id.lower()
  858. )
  859. raise
  860. async def _validate_userid_login(
  861. self,
  862. username: str,
  863. login_submission: Dict[str, Any],
  864. ) -> Tuple[str, Optional[Callable[[Dict[str, str]], Awaitable[None]]]]:
  865. """Helper for validate_login
  866. Handles login, once we've mapped 3pids onto userids
  867. Args:
  868. username: the username, from the identifier dict
  869. login_submission: the whole of the login submission
  870. (including 'type' and other relevant fields)
  871. Returns:
  872. A tuple of the canonical user id, and optional callback
  873. to be called once the access token and device id are issued
  874. Raises:
  875. StoreError if there was a problem accessing the database
  876. SynapseError if there was a problem with the request
  877. LoginError if there was an authentication problem.
  878. """
  879. if username.startswith("@"):
  880. qualified_user_id = username
  881. else:
  882. qualified_user_id = UserID(username, self.hs.hostname).to_string()
  883. login_type = login_submission.get("type")
  884. # we already checked that we have a valid login type
  885. assert isinstance(login_type, str)
  886. known_login_type = False
  887. for provider in self.password_providers:
  888. supported_login_types = provider.get_supported_login_types()
  889. if login_type not in supported_login_types:
  890. # this password provider doesn't understand this login type
  891. continue
  892. known_login_type = True
  893. login_fields = supported_login_types[login_type]
  894. missing_fields = []
  895. login_dict = {}
  896. for f in login_fields:
  897. if f not in login_submission:
  898. missing_fields.append(f)
  899. else:
  900. login_dict[f] = login_submission[f]
  901. if missing_fields:
  902. raise SynapseError(
  903. 400,
  904. "Missing parameters for login type %s: %s"
  905. % (login_type, missing_fields),
  906. )
  907. result = await provider.check_auth(username, login_type, login_dict)
  908. if result:
  909. return result
  910. if login_type == LoginType.PASSWORD and self._password_localdb_enabled:
  911. known_login_type = True
  912. # we've already checked that there is a (valid) password field
  913. password = login_submission["password"]
  914. assert isinstance(password, str)
  915. canonical_user_id = await self._check_local_password(
  916. qualified_user_id, password
  917. )
  918. if canonical_user_id:
  919. return canonical_user_id, None
  920. if not known_login_type:
  921. raise SynapseError(400, "Unknown login type %s" % login_type)
  922. # We raise a 403 here, but note that if we're doing user-interactive
  923. # login, it turns all LoginErrors into a 401 anyway.
  924. raise LoginError(403, "Invalid password", errcode=Codes.FORBIDDEN)
  925. async def check_password_provider_3pid(
  926. self, medium: str, address: str, password: str
  927. ) -> Tuple[Optional[str], Optional[Callable[[Dict[str, str]], Awaitable[None]]]]:
  928. """Check if a password provider is able to validate a thirdparty login
  929. Args:
  930. medium: The medium of the 3pid (ex. email).
  931. address: The address of the 3pid (ex. jdoe@example.com).
  932. password: The password of the user.
  933. Returns:
  934. A tuple of `(user_id, callback)`. If authentication is successful,
  935. `user_id`is the authenticated, canonical user ID. `callback` is
  936. then either a function to be later run after the server has
  937. completed login/registration, or `None`. If authentication was
  938. unsuccessful, `user_id` and `callback` are both `None`.
  939. """
  940. for provider in self.password_providers:
  941. result = await provider.check_3pid_auth(medium, address, password)
  942. if result:
  943. return result
  944. return None, None
  945. async def _check_local_password(self, user_id: str, password: str) -> Optional[str]:
  946. """Authenticate a user against the local password database.
  947. user_id is checked case insensitively, but will return None if there are
  948. multiple inexact matches.
  949. Args:
  950. user_id: complete @user:id
  951. password: the provided password
  952. Returns:
  953. The canonical_user_id, or None if unknown user/bad password
  954. """
  955. lookupres = await self._find_user_id_and_pwd_hash(user_id)
  956. if not lookupres:
  957. return None
  958. (user_id, password_hash) = lookupres
  959. # If the password hash is None, the account has likely been deactivated
  960. if not password_hash:
  961. deactivated = await self.store.get_user_deactivated_status(user_id)
  962. if deactivated:
  963. raise UserDeactivatedError("This account has been deactivated")
  964. result = await self.validate_hash(password, password_hash)
  965. if not result:
  966. logger.warning("Failed password login for user %s", user_id)
  967. return None
  968. return user_id
  969. async def validate_short_term_login_token_and_get_user_id(self, login_token: str):
  970. auth_api = self.hs.get_auth()
  971. user_id = None
  972. try:
  973. macaroon = pymacaroons.Macaroon.deserialize(login_token)
  974. user_id = auth_api.get_user_id_from_macaroon(macaroon)
  975. auth_api.validate_macaroon(macaroon, "login", user_id)
  976. except Exception:
  977. raise AuthError(403, "Invalid token", errcode=Codes.FORBIDDEN)
  978. await self.auth.check_auth_blocking(user_id)
  979. return user_id
  980. async def delete_access_token(self, access_token: str):
  981. """Invalidate a single access token
  982. Args:
  983. access_token: access token to be deleted
  984. """
  985. user_info = await self.auth.get_user_by_access_token(access_token)
  986. await self.store.delete_access_token(access_token)
  987. # see if any of our auth providers want to know about this
  988. for provider in self.password_providers:
  989. await provider.on_logged_out(
  990. user_id=user_info.user_id,
  991. device_id=user_info.device_id,
  992. access_token=access_token,
  993. )
  994. # delete pushers associated with this access token
  995. if user_info.token_id is not None:
  996. await self.hs.get_pusherpool().remove_pushers_by_access_token(
  997. user_info.user_id, (user_info.token_id,)
  998. )
  999. async def delete_access_tokens_for_user(
  1000. self,
  1001. user_id: str,
  1002. except_token_id: Optional[str] = None,
  1003. device_id: Optional[str] = None,
  1004. ):
  1005. """Invalidate access tokens belonging to a user
  1006. Args:
  1007. user_id: ID of user the tokens belong to
  1008. except_token_id: access_token ID which should *not* be deleted
  1009. device_id: ID of device the tokens are associated with.
  1010. If None, tokens associated with any device (or no device) will
  1011. be deleted
  1012. """
  1013. tokens_and_devices = await self.store.user_delete_access_tokens(
  1014. user_id, except_token_id=except_token_id, device_id=device_id
  1015. )
  1016. # see if any of our auth providers want to know about this
  1017. for provider in self.password_providers:
  1018. for token, token_id, device_id in tokens_and_devices:
  1019. await provider.on_logged_out(
  1020. user_id=user_id, device_id=device_id, access_token=token
  1021. )
  1022. # delete pushers associated with the access tokens
  1023. await self.hs.get_pusherpool().remove_pushers_by_access_token(
  1024. user_id, (token_id for _, token_id, _ in tokens_and_devices)
  1025. )
  1026. async def add_threepid(
  1027. self, user_id: str, medium: str, address: str, validated_at: int
  1028. ):
  1029. # check if medium has a valid value
  1030. if medium not in ["email", "msisdn"]:
  1031. raise SynapseError(
  1032. code=400,
  1033. msg=("'%s' is not a valid value for 'medium'" % (medium,)),
  1034. errcode=Codes.INVALID_PARAM,
  1035. )
  1036. # 'Canonicalise' email addresses down to lower case.
  1037. # We've now moving towards the homeserver being the entity that
  1038. # is responsible for validating threepids used for resetting passwords
  1039. # on accounts, so in future Synapse will gain knowledge of specific
  1040. # types (mediums) of threepid. For now, we still use the existing
  1041. # infrastructure, but this is the start of synapse gaining knowledge
  1042. # of specific types of threepid (and fixes the fact that checking
  1043. # for the presence of an email address during password reset was
  1044. # case sensitive).
  1045. if medium == "email":
  1046. address = canonicalise_email(address)
  1047. await self.store.user_add_threepid(
  1048. user_id, medium, address, validated_at, self.hs.get_clock().time_msec()
  1049. )
  1050. async def delete_threepid(
  1051. self, user_id: str, medium: str, address: str, id_server: Optional[str] = None
  1052. ) -> bool:
  1053. """Attempts to unbind the 3pid on the identity servers and deletes it
  1054. from the local database.
  1055. Args:
  1056. user_id: ID of user to remove the 3pid from.
  1057. medium: The medium of the 3pid being removed: "email" or "msisdn".
  1058. address: The 3pid address to remove.
  1059. id_server: Use the given identity server when unbinding
  1060. any threepids. If None then will attempt to unbind using the
  1061. identity server specified when binding (if known).
  1062. Returns:
  1063. Returns True if successfully unbound the 3pid on
  1064. the identity server, False if identity server doesn't support the
  1065. unbind API.
  1066. """
  1067. # 'Canonicalise' email addresses as per above
  1068. if medium == "email":
  1069. address = canonicalise_email(address)
  1070. identity_handler = self.hs.get_identity_handler()
  1071. result = await identity_handler.try_unbind_threepid(
  1072. user_id, {"medium": medium, "address": address, "id_server": id_server}
  1073. )
  1074. await self.store.user_delete_threepid(user_id, medium, address)
  1075. return result
  1076. async def hash(self, password: str) -> str:
  1077. """Computes a secure hash of password.
  1078. Args:
  1079. password: Password to hash.
  1080. Returns:
  1081. Hashed password.
  1082. """
  1083. def _do_hash():
  1084. # Normalise the Unicode in the password
  1085. pw = unicodedata.normalize("NFKC", password)
  1086. return bcrypt.hashpw(
  1087. pw.encode("utf8") + self.hs.config.password_pepper.encode("utf8"),
  1088. bcrypt.gensalt(self.bcrypt_rounds),
  1089. ).decode("ascii")
  1090. return await defer_to_thread(self.hs.get_reactor(), _do_hash)
  1091. async def validate_hash(
  1092. self, password: str, stored_hash: Union[bytes, str]
  1093. ) -> bool:
  1094. """Validates that self.hash(password) == stored_hash.
  1095. Args:
  1096. password: Password to hash.
  1097. stored_hash: Expected hash value.
  1098. Returns:
  1099. Whether self.hash(password) == stored_hash.
  1100. """
  1101. def _do_validate_hash(checked_hash: bytes):
  1102. # Normalise the Unicode in the password
  1103. pw = unicodedata.normalize("NFKC", password)
  1104. return bcrypt.checkpw(
  1105. pw.encode("utf8") + self.hs.config.password_pepper.encode("utf8"),
  1106. checked_hash,
  1107. )
  1108. if stored_hash:
  1109. if not isinstance(stored_hash, bytes):
  1110. stored_hash = stored_hash.encode("ascii")
  1111. return await defer_to_thread(
  1112. self.hs.get_reactor(), _do_validate_hash, stored_hash
  1113. )
  1114. else:
  1115. return False
  1116. async def start_sso_ui_auth(self, request: SynapseRequest, session_id: str) -> str:
  1117. """
  1118. Get the HTML for the SSO redirect confirmation page.
  1119. Args:
  1120. request: The incoming HTTP request
  1121. session_id: The user interactive authentication session ID.
  1122. Returns:
  1123. The HTML to render.
  1124. """
  1125. try:
  1126. session = await self.store.get_ui_auth_session(session_id)
  1127. except StoreError:
  1128. raise SynapseError(400, "Unknown session ID: %s" % (session_id,))
  1129. user_id_to_verify = await self.get_session_data(
  1130. session_id, UIAuthSessionDataConstants.REQUEST_USER_ID
  1131. ) # type: str
  1132. idps = await self.hs.get_sso_handler().get_identity_providers_for_user(
  1133. user_id_to_verify
  1134. )
  1135. if not idps:
  1136. # we checked that the user had some remote identities before offering an SSO
  1137. # flow, so either it's been deleted or the client has requested SSO despite
  1138. # it not being offered.
  1139. raise SynapseError(400, "User has no SSO identities")
  1140. # for now, just pick one
  1141. idp_id, sso_auth_provider = next(iter(idps.items()))
  1142. if len(idps) > 0:
  1143. logger.warning(
  1144. "User %r has previously logged in with multiple SSO IdPs; arbitrarily "
  1145. "picking %r",
  1146. user_id_to_verify,
  1147. idp_id,
  1148. )
  1149. redirect_url = await sso_auth_provider.handle_redirect_request(
  1150. request, None, session_id
  1151. )
  1152. return self._sso_auth_confirm_template.render(
  1153. description=session.description,
  1154. redirect_url=redirect_url,
  1155. idp=sso_auth_provider,
  1156. )
  1157. async def complete_sso_login(
  1158. self,
  1159. registered_user_id: str,
  1160. request: Request,
  1161. client_redirect_url: str,
  1162. extra_attributes: Optional[JsonDict] = None,
  1163. new_user: bool = False,
  1164. ):
  1165. """Having figured out a mxid for this user, complete the HTTP request
  1166. Args:
  1167. registered_user_id: The registered user ID to complete SSO login for.
  1168. request: The request to complete.
  1169. client_redirect_url: The URL to which to redirect the user at the end of the
  1170. process.
  1171. extra_attributes: Extra attributes which will be passed to the client
  1172. during successful login. Must be JSON serializable.
  1173. new_user: True if we should use wording appropriate to a user who has just
  1174. registered.
  1175. """
  1176. # If the account has been deactivated, do not proceed with the login
  1177. # flow.
  1178. deactivated = await self.store.get_user_deactivated_status(registered_user_id)
  1179. if deactivated:
  1180. respond_with_html(request, 403, self._sso_account_deactivated_template)
  1181. return
  1182. profile = await self.store.get_profileinfo(
  1183. UserID.from_string(registered_user_id).localpart
  1184. )
  1185. self._complete_sso_login(
  1186. registered_user_id,
  1187. request,
  1188. client_redirect_url,
  1189. extra_attributes,
  1190. new_user=new_user,
  1191. user_profile_data=profile,
  1192. )
  1193. def _complete_sso_login(
  1194. self,
  1195. registered_user_id: str,
  1196. request: Request,
  1197. client_redirect_url: str,
  1198. extra_attributes: Optional[JsonDict] = None,
  1199. new_user: bool = False,
  1200. user_profile_data: Optional[ProfileInfo] = None,
  1201. ):
  1202. """
  1203. The synchronous portion of complete_sso_login.
  1204. This exists purely for backwards compatibility of synapse.module_api.ModuleApi.
  1205. """
  1206. if user_profile_data is None:
  1207. user_profile_data = ProfileInfo(None, None)
  1208. # Store any extra attributes which will be passed in the login response.
  1209. # Note that this is per-user so it may overwrite a previous value, this
  1210. # is considered OK since the newest SSO attributes should be most valid.
  1211. if extra_attributes:
  1212. self._extra_attributes[registered_user_id] = SsoLoginExtraAttributes(
  1213. self._clock.time_msec(),
  1214. extra_attributes,
  1215. )
  1216. # Create a login token
  1217. login_token = self.macaroon_gen.generate_short_term_login_token(
  1218. registered_user_id
  1219. )
  1220. # Append the login token to the original redirect URL (i.e. with its query
  1221. # parameters kept intact) to build the URL to which the template needs to
  1222. # redirect the users once they have clicked on the confirmation link.
  1223. redirect_url = self.add_query_param_to_url(
  1224. client_redirect_url, "loginToken", login_token
  1225. )
  1226. # if the client is whitelisted, we can redirect straight to it
  1227. if client_redirect_url.startswith(self._whitelisted_sso_clients):
  1228. request.redirect(redirect_url)
  1229. finish_request(request)
  1230. return
  1231. # Otherwise, serve the redirect confirmation page.
  1232. # Remove the query parameters from the redirect URL to get a shorter version of
  1233. # it. This is only to display a human-readable URL in the template, but not the
  1234. # URL we redirect users to.
  1235. url_parts = urllib.parse.urlsplit(client_redirect_url)
  1236. if url_parts.scheme == "https":
  1237. # for an https uri, just show the netloc (ie, the hostname. Specifically,
  1238. # the bit between "//" and "/"; this includes any potential
  1239. # "username:password@" prefix.)
  1240. display_url = url_parts.netloc
  1241. else:
  1242. # for other uris, strip the query-params (including the login token) and
  1243. # fragment.
  1244. display_url = urllib.parse.urlunsplit(
  1245. (url_parts.scheme, url_parts.netloc, url_parts.path, "", "")
  1246. )
  1247. html = self._sso_redirect_confirm_template.render(
  1248. display_url=display_url,
  1249. redirect_url=redirect_url,
  1250. server_name=self._server_name,
  1251. new_user=new_user,
  1252. user_id=registered_user_id,
  1253. user_profile=user_profile_data,
  1254. )
  1255. respond_with_html(request, 200, html)
  1256. async def _sso_login_callback(self, login_result: JsonDict) -> None:
  1257. """
  1258. A login callback which might add additional attributes to the login response.
  1259. Args:
  1260. login_result: The data to be sent to the client. Includes the user
  1261. ID and access token.
  1262. """
  1263. # Expire attributes before processing. Note that there shouldn't be any
  1264. # valid logins that still have extra attributes.
  1265. self._expire_sso_extra_attributes()
  1266. extra_attributes = self._extra_attributes.get(login_result["user_id"])
  1267. if extra_attributes:
  1268. login_result.update(extra_attributes.extra_attributes)
  1269. def _expire_sso_extra_attributes(self) -> None:
  1270. """
  1271. Iterate through the mapping of user IDs to extra attributes and remove any that are no longer valid.
  1272. """
  1273. # TODO This should match the amount of time the macaroon is valid for.
  1274. LOGIN_TOKEN_EXPIRATION_TIME = 2 * 60 * 1000
  1275. expire_before = self._clock.time_msec() - LOGIN_TOKEN_EXPIRATION_TIME
  1276. to_expire = set()
  1277. for user_id, data in self._extra_attributes.items():
  1278. if data.creation_time < expire_before:
  1279. to_expire.add(user_id)
  1280. for user_id in to_expire:
  1281. logger.debug("Expiring extra attributes for user %s", user_id)
  1282. del self._extra_attributes[user_id]
  1283. @staticmethod
  1284. def add_query_param_to_url(url: str, param_name: str, param: Any):
  1285. url_parts = list(urllib.parse.urlparse(url))
  1286. query = urllib.parse.parse_qsl(url_parts[4], keep_blank_values=True)
  1287. query.append((param_name, param))
  1288. url_parts[4] = urllib.parse.urlencode(query)
  1289. return urllib.parse.urlunparse(url_parts)
  1290. @attr.s(slots=True)
  1291. class MacaroonGenerator:
  1292. hs = attr.ib()
  1293. def generate_access_token(
  1294. self, user_id: str, extra_caveats: Optional[List[str]] = None
  1295. ) -> str:
  1296. extra_caveats = extra_caveats or []
  1297. macaroon = self._generate_base_macaroon(user_id)
  1298. macaroon.add_first_party_caveat("type = access")
  1299. # Include a nonce, to make sure that each login gets a different
  1300. # access token.
  1301. macaroon.add_first_party_caveat(
  1302. "nonce = %s" % (stringutils.random_string_with_symbols(16),)
  1303. )
  1304. for caveat in extra_caveats:
  1305. macaroon.add_first_party_caveat(caveat)
  1306. return macaroon.serialize()
  1307. def generate_short_term_login_token(
  1308. self, user_id: str, duration_in_ms: int = (2 * 60 * 1000)
  1309. ) -> str:
  1310. macaroon = self._generate_base_macaroon(user_id)
  1311. macaroon.add_first_party_caveat("type = login")
  1312. now = self.hs.get_clock().time_msec()
  1313. expiry = now + duration_in_ms
  1314. macaroon.add_first_party_caveat("time < %d" % (expiry,))
  1315. return macaroon.serialize()
  1316. def generate_delete_pusher_token(self, user_id: str) -> str:
  1317. macaroon = self._generate_base_macaroon(user_id)
  1318. macaroon.add_first_party_caveat("type = delete_pusher")
  1319. return macaroon.serialize()
  1320. def _generate_base_macaroon(self, user_id: str) -> pymacaroons.Macaroon:
  1321. macaroon = pymacaroons.Macaroon(
  1322. location=self.hs.config.server_name,
  1323. identifier="key",
  1324. key=self.hs.config.macaroon_secret_key,
  1325. )
  1326. macaroon.add_first_party_caveat("gen = 1")
  1327. macaroon.add_first_party_caveat("user_id = %s" % (user_id,))
  1328. return macaroon
  1329. class PasswordProvider:
  1330. """Wrapper for a password auth provider module
  1331. This class abstracts out all of the backwards-compatibility hacks for
  1332. password providers, to provide a consistent interface.
  1333. """
  1334. @classmethod
  1335. def load(cls, module, config, module_api: ModuleApi) -> "PasswordProvider":
  1336. try:
  1337. pp = module(config=config, account_handler=module_api)
  1338. except Exception as e:
  1339. logger.error("Error while initializing %r: %s", module, e)
  1340. raise
  1341. return cls(pp, module_api)
  1342. def __init__(self, pp, module_api: ModuleApi):
  1343. self._pp = pp
  1344. self._module_api = module_api
  1345. self._supported_login_types = {}
  1346. # grandfather in check_password support
  1347. if hasattr(self._pp, "check_password"):
  1348. self._supported_login_types[LoginType.PASSWORD] = ("password",)
  1349. g = getattr(self._pp, "get_supported_login_types", None)
  1350. if g:
  1351. self._supported_login_types.update(g())
  1352. def __str__(self):
  1353. return str(self._pp)
  1354. def get_supported_login_types(self) -> Mapping[str, Iterable[str]]:
  1355. """Get the login types supported by this password provider
  1356. Returns a map from a login type identifier (such as m.login.password) to an
  1357. iterable giving the fields which must be provided by the user in the submission
  1358. to the /login API.
  1359. This wrapper adds m.login.password to the list if the underlying password
  1360. provider supports the check_password() api.
  1361. """
  1362. return self._supported_login_types
  1363. async def check_auth(
  1364. self, username: str, login_type: str, login_dict: JsonDict
  1365. ) -> Optional[Tuple[str, Optional[Callable]]]:
  1366. """Check if the user has presented valid login credentials
  1367. This wrapper also calls check_password() if the underlying password provider
  1368. supports the check_password() api and the login type is m.login.password.
  1369. Args:
  1370. username: user id presented by the client. Either an MXID or an unqualified
  1371. username.
  1372. login_type: the login type being attempted - one of the types returned by
  1373. get_supported_login_types()
  1374. login_dict: the dictionary of login secrets passed by the client.
  1375. Returns: (user_id, callback) where `user_id` is the fully-qualified mxid of the
  1376. user, and `callback` is an optional callback which will be called with the
  1377. result from the /login call (including access_token, device_id, etc.)
  1378. """
  1379. # first grandfather in a call to check_password
  1380. if login_type == LoginType.PASSWORD:
  1381. g = getattr(self._pp, "check_password", None)
  1382. if g:
  1383. qualified_user_id = self._module_api.get_qualified_user_id(username)
  1384. is_valid = await self._pp.check_password(
  1385. qualified_user_id, login_dict["password"]
  1386. )
  1387. if is_valid:
  1388. return qualified_user_id, None
  1389. g = getattr(self._pp, "check_auth", None)
  1390. if not g:
  1391. return None
  1392. result = await g(username, login_type, login_dict)
  1393. # Check if the return value is a str or a tuple
  1394. if isinstance(result, str):
  1395. # If it's a str, set callback function to None
  1396. return result, None
  1397. return result
  1398. async def check_3pid_auth(
  1399. self, medium: str, address: str, password: str
  1400. ) -> Optional[Tuple[str, Optional[Callable]]]:
  1401. g = getattr(self._pp, "check_3pid_auth", None)
  1402. if not g:
  1403. return None
  1404. # This function is able to return a deferred that either
  1405. # resolves None, meaning authentication failure, or upon
  1406. # success, to a str (which is the user_id) or a tuple of
  1407. # (user_id, callback_func), where callback_func should be run
  1408. # after we've finished everything else
  1409. result = await g(medium, address, password)
  1410. # Check if the return value is a str or a tuple
  1411. if isinstance(result, str):
  1412. # If it's a str, set callback function to None
  1413. return result, None
  1414. return result
  1415. async def on_logged_out(
  1416. self, user_id: str, device_id: Optional[str], access_token: str
  1417. ) -> None:
  1418. g = getattr(self._pp, "on_logged_out", None)
  1419. if not g:
  1420. return
  1421. # This might return an awaitable, if it does block the log out
  1422. # until it completes.
  1423. await maybe_awaitable(
  1424. g(
  1425. user_id=user_id,
  1426. device_id=device_id,
  1427. access_token=access_token,
  1428. )
  1429. )