Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 
 

1929 linhas
73 KiB

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