auth.py 51 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2014 - 2016 OpenMarket Ltd
  3. # Copyright 2017 Vector Creations Ltd
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. import inspect
  17. import logging
  18. import time
  19. import unicodedata
  20. import urllib.parse
  21. from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
  22. import attr
  23. import bcrypt # type: ignore[import]
  24. import pymacaroons
  25. from synapse.api.constants import LoginType
  26. from synapse.api.errors import (
  27. AuthError,
  28. Codes,
  29. InteractiveAuthIncompleteError,
  30. LoginError,
  31. StoreError,
  32. SynapseError,
  33. UserDeactivatedError,
  34. )
  35. from synapse.api.ratelimiting import Ratelimiter
  36. from synapse.handlers.ui_auth import INTERACTIVE_AUTH_CHECKERS
  37. from synapse.handlers.ui_auth.checkers import UserInteractiveAuthChecker
  38. from synapse.http.server import finish_request, respond_with_html
  39. from synapse.http.site import SynapseRequest
  40. from synapse.logging.context import defer_to_thread
  41. from synapse.metrics.background_process_metrics import run_as_background_process
  42. from synapse.module_api import ModuleApi
  43. from synapse.types import JsonDict, Requester, UserID
  44. from synapse.util import stringutils as stringutils
  45. from synapse.util.msisdn import phone_number_to_msisdn
  46. from synapse.util.threepids import canonicalise_email
  47. from ._base import BaseHandler
  48. logger = logging.getLogger(__name__)
  49. def convert_client_dict_legacy_fields_to_identifier(
  50. submission: JsonDict,
  51. ) -> Dict[str, str]:
  52. """
  53. Convert a legacy-formatted login submission to an identifier dict.
  54. Legacy login submissions (used in both login and user-interactive authentication)
  55. provide user-identifying information at the top-level instead.
  56. These are now deprecated and replaced with identifiers:
  57. https://matrix.org/docs/spec/client_server/r0.6.1#identifier-types
  58. Args:
  59. submission: The client dict to convert
  60. Returns:
  61. The matching identifier dict
  62. Raises:
  63. SynapseError: If the format of the client dict is invalid
  64. """
  65. identifier = submission.get("identifier", {})
  66. # Generate an m.id.user identifier if "user" parameter is present
  67. user = submission.get("user")
  68. if user:
  69. identifier = {"type": "m.id.user", "user": user}
  70. # Generate an m.id.thirdparty identifier if "medium" and "address" parameters are present
  71. medium = submission.get("medium")
  72. address = submission.get("address")
  73. if medium and address:
  74. identifier = {
  75. "type": "m.id.thirdparty",
  76. "medium": medium,
  77. "address": address,
  78. }
  79. # We've converted valid, legacy login submissions to an identifier. If the
  80. # submission still doesn't have an identifier, it's invalid
  81. if not identifier:
  82. raise SynapseError(400, "Invalid login submission", Codes.INVALID_PARAM)
  83. # Ensure the identifier has a type
  84. if "type" not in identifier:
  85. raise SynapseError(
  86. 400, "'identifier' dict has no key 'type'", errcode=Codes.MISSING_PARAM,
  87. )
  88. return identifier
  89. def login_id_phone_to_thirdparty(identifier: JsonDict) -> Dict[str, str]:
  90. """
  91. Convert a phone login identifier type to a generic threepid identifier.
  92. Args:
  93. identifier: Login identifier dict of type 'm.id.phone'
  94. Returns:
  95. An equivalent m.id.thirdparty identifier dict
  96. """
  97. if "country" not in identifier or (
  98. # The specification requires a "phone" field, while Synapse used to require a "number"
  99. # field. Accept both for backwards compatibility.
  100. "phone" not in identifier
  101. and "number" not in identifier
  102. ):
  103. raise SynapseError(
  104. 400, "Invalid phone-type identifier", errcode=Codes.INVALID_PARAM
  105. )
  106. # Accept both "phone" and "number" as valid keys in m.id.phone
  107. phone_number = identifier.get("phone", identifier["number"])
  108. # Convert user-provided phone number to a consistent representation
  109. msisdn = phone_number_to_msisdn(identifier["country"], phone_number)
  110. return {
  111. "type": "m.id.thirdparty",
  112. "medium": "msisdn",
  113. "address": msisdn,
  114. }
  115. @attr.s(slots=True)
  116. class SsoLoginExtraAttributes:
  117. """Data we track about SAML2 sessions"""
  118. # time the session was created, in milliseconds
  119. creation_time = attr.ib(type=int)
  120. extra_attributes = attr.ib(type=JsonDict)
  121. class AuthHandler(BaseHandler):
  122. SESSION_EXPIRE_MS = 48 * 60 * 60 * 1000
  123. def __init__(self, hs):
  124. """
  125. Args:
  126. hs (synapse.server.HomeServer):
  127. """
  128. super().__init__(hs)
  129. self.checkers = {} # type: Dict[str, UserInteractiveAuthChecker]
  130. for auth_checker_class in INTERACTIVE_AUTH_CHECKERS:
  131. inst = auth_checker_class(hs)
  132. if inst.is_enabled():
  133. self.checkers[inst.AUTH_TYPE] = inst # type: ignore
  134. self.bcrypt_rounds = hs.config.bcrypt_rounds
  135. account_handler = ModuleApi(hs, self)
  136. self.password_providers = [
  137. module(config=config, account_handler=account_handler)
  138. for module, config in hs.config.password_providers
  139. ]
  140. logger.info("Extra password_providers: %r", self.password_providers)
  141. self.hs = hs # FIXME better possibility to access registrationHandler later?
  142. self.macaroon_gen = hs.get_macaroon_generator()
  143. self._password_enabled = hs.config.password_enabled
  144. self._sso_enabled = (
  145. hs.config.cas_enabled or hs.config.saml2_enabled or hs.config.oidc_enabled
  146. )
  147. # we keep this as a list despite the O(N^2) implication so that we can
  148. # keep PASSWORD first and avoid confusing clients which pick the first
  149. # type in the list. (NB that the spec doesn't require us to do so and
  150. # clients which favour types that they don't understand over those that
  151. # they do are technically broken)
  152. login_types = []
  153. if self._password_enabled:
  154. login_types.append(LoginType.PASSWORD)
  155. for provider in self.password_providers:
  156. if hasattr(provider, "get_supported_login_types"):
  157. for t in provider.get_supported_login_types().keys():
  158. if t not in login_types:
  159. login_types.append(t)
  160. self._supported_login_types = login_types
  161. # Login types and UI Auth types have a heavy overlap, but are not
  162. # necessarily identical. Login types have SSO (and other login types)
  163. # added in the rest layer, see synapse.rest.client.v1.login.LoginRestServerlet.on_GET.
  164. ui_auth_types = login_types.copy()
  165. if self._sso_enabled:
  166. ui_auth_types.append(LoginType.SSO)
  167. self._supported_ui_auth_types = ui_auth_types
  168. # Ratelimiter for failed auth during UIA. Uses same ratelimit config
  169. # as per `rc_login.failed_attempts`.
  170. self._failed_uia_attempts_ratelimiter = Ratelimiter(
  171. clock=self.clock,
  172. rate_hz=self.hs.config.rc_login_failed_attempts.per_second,
  173. burst_count=self.hs.config.rc_login_failed_attempts.burst_count,
  174. )
  175. self._clock = self.hs.get_clock()
  176. # Expire old UI auth sessions after a period of time.
  177. if hs.config.worker_app is None:
  178. self._clock.looping_call(
  179. run_as_background_process,
  180. 5 * 60 * 1000,
  181. "expire_old_sessions",
  182. self._expire_old_sessions,
  183. )
  184. # Load the SSO HTML templates.
  185. # The following template is shown to the user during a client login via SSO,
  186. # after the SSO completes and before redirecting them back to their client.
  187. # It notifies the user they are about to give access to their matrix account
  188. # to the client.
  189. self._sso_redirect_confirm_template = hs.config.sso_redirect_confirm_template
  190. # The following template is shown during user interactive authentication
  191. # in the fallback auth scenario. It notifies the user that they are
  192. # authenticating for an operation to occur on their account.
  193. self._sso_auth_confirm_template = hs.config.sso_auth_confirm_template
  194. # The following template is shown after a successful user interactive
  195. # authentication session. It tells the user they can close the window.
  196. self._sso_auth_success_template = hs.config.sso_auth_success_template
  197. # The following template is shown during the SSO authentication process if
  198. # the account is deactivated.
  199. self._sso_account_deactivated_template = (
  200. hs.config.sso_account_deactivated_template
  201. )
  202. self._server_name = hs.config.server_name
  203. # cast to tuple for use with str.startswith
  204. self._whitelisted_sso_clients = tuple(hs.config.sso_client_whitelist)
  205. # A mapping of user ID to extra attributes to include in the login
  206. # response.
  207. self._extra_attributes = {} # type: Dict[str, SsoLoginExtraAttributes]
  208. async def validate_user_via_ui_auth(
  209. self,
  210. requester: Requester,
  211. request: SynapseRequest,
  212. request_body: Dict[str, Any],
  213. clientip: str,
  214. description: str,
  215. ) -> Tuple[dict, str]:
  216. """
  217. Checks that the user is who they claim to be, via a UI auth.
  218. This is used for things like device deletion and password reset where
  219. the user already has a valid access token, but we want to double-check
  220. that it isn't stolen by re-authenticating them.
  221. Args:
  222. requester: The user, as given by the access token
  223. request: The request sent by the client.
  224. request_body: The body of the request sent by the client
  225. clientip: The IP address of the client.
  226. description: A human readable string to be displayed to the user that
  227. describes the operation happening on their account.
  228. Returns:
  229. A tuple of (params, session_id).
  230. 'params' contains the parameters for this request (which may
  231. have been given only in a previous call).
  232. 'session_id' is the ID of this session, either passed in by the
  233. client or assigned by this call
  234. Raises:
  235. InteractiveAuthIncompleteError if the client has not yet completed
  236. any of the permitted login flows
  237. AuthError if the client has completed a login flow, and it gives
  238. a different user to `requester`
  239. LimitExceededError if the ratelimiter's failed request count for this
  240. user is too high to proceed
  241. """
  242. user_id = requester.user.to_string()
  243. # Check if we should be ratelimited due to too many previous failed attempts
  244. self._failed_uia_attempts_ratelimiter.ratelimit(user_id, update=False)
  245. # build a list of supported flows
  246. flows = [[login_type] for login_type in self._supported_ui_auth_types]
  247. try:
  248. result, params, session_id = await self.check_ui_auth(
  249. flows, request, request_body, clientip, description
  250. )
  251. except LoginError:
  252. # Update the ratelimiter to say we failed (`can_do_action` doesn't raise).
  253. self._failed_uia_attempts_ratelimiter.can_do_action(user_id)
  254. raise
  255. # find the completed login type
  256. for login_type in self._supported_ui_auth_types:
  257. if login_type not in result:
  258. continue
  259. user_id = result[login_type]
  260. break
  261. else:
  262. # this can't happen
  263. raise Exception("check_auth returned True but no successful login type")
  264. # check that the UI auth matched the access token
  265. if user_id != requester.user.to_string():
  266. raise AuthError(403, "Invalid auth")
  267. return params, session_id
  268. def get_enabled_auth_types(self):
  269. """Return the enabled user-interactive authentication types
  270. Returns the UI-Auth types which are supported by the homeserver's current
  271. config.
  272. """
  273. return self.checkers.keys()
  274. async def check_ui_auth(
  275. self,
  276. flows: List[List[str]],
  277. request: SynapseRequest,
  278. clientdict: Dict[str, Any],
  279. clientip: str,
  280. description: str,
  281. ) -> Tuple[dict, dict, str]:
  282. """
  283. Takes a dictionary sent by the client in the login / registration
  284. protocol and handles the User-Interactive Auth flow.
  285. If no auth flows have been completed successfully, raises an
  286. InteractiveAuthIncompleteError. To handle this, you can use
  287. synapse.rest.client.v2_alpha._base.interactive_auth_handler as a
  288. decorator.
  289. Args:
  290. flows: A list of login flows. Each flow is an ordered list of
  291. strings representing auth-types. At least one full
  292. flow must be completed in order for auth to be successful.
  293. request: The request sent by the client.
  294. clientdict: The dictionary from the client root level, not the
  295. 'auth' key: this method prompts for auth if none is sent.
  296. clientip: The IP address of the client.
  297. description: A human readable string to be displayed to the user that
  298. describes the operation happening on their account.
  299. Returns:
  300. A tuple of (creds, params, session_id).
  301. 'creds' contains the authenticated credentials of each stage.
  302. 'params' contains the parameters for this request (which may
  303. have been given only in a previous call).
  304. 'session_id' is the ID of this session, either passed in by the
  305. client or assigned by this call
  306. Raises:
  307. InteractiveAuthIncompleteError if the client has not yet completed
  308. all the stages in any of the permitted flows.
  309. """
  310. authdict = None
  311. sid = None # type: Optional[str]
  312. if clientdict and "auth" in clientdict:
  313. authdict = clientdict["auth"]
  314. del clientdict["auth"]
  315. if "session" in authdict:
  316. sid = authdict["session"]
  317. # Convert the URI and method to strings.
  318. uri = request.uri.decode("utf-8")
  319. method = request.method.decode("utf-8")
  320. # If there's no session ID, create a new session.
  321. if not sid:
  322. session = await self.store.create_ui_auth_session(
  323. clientdict, uri, method, description
  324. )
  325. else:
  326. try:
  327. session = await self.store.get_ui_auth_session(sid)
  328. except StoreError:
  329. raise SynapseError(400, "Unknown session ID: %s" % (sid,))
  330. # If the client provides parameters, update what is persisted,
  331. # otherwise use whatever was last provided.
  332. #
  333. # This was designed to allow the client to omit the parameters
  334. # and just supply the session in subsequent calls so it split
  335. # auth between devices by just sharing the session, (eg. so you
  336. # could continue registration from your phone having clicked the
  337. # email auth link on there). It's probably too open to abuse
  338. # because it lets unauthenticated clients store arbitrary objects
  339. # on a homeserver.
  340. #
  341. # Revisit: Assuming the REST APIs do sensible validation, the data
  342. # isn't arbitrary.
  343. #
  344. # Note that the registration endpoint explicitly removes the
  345. # "initial_device_display_name" parameter if it is provided
  346. # without a "password" parameter. See the changes to
  347. # synapse.rest.client.v2_alpha.register.RegisterRestServlet.on_POST
  348. # in commit 544722bad23fc31056b9240189c3cbbbf0ffd3f9.
  349. if not clientdict:
  350. clientdict = session.clientdict
  351. # Ensure that the queried operation does not vary between stages of
  352. # the UI authentication session. This is done by generating a stable
  353. # comparator and storing it during the initial query. Subsequent
  354. # queries ensure that this comparator has not changed.
  355. #
  356. # The comparator is based on the requested URI and HTTP method. The
  357. # client dict (minus the auth dict) should also be checked, but some
  358. # clients are not spec compliant, just warn for now if the client
  359. # dict changes.
  360. if (session.uri, session.method) != (uri, method):
  361. raise SynapseError(
  362. 403,
  363. "Requested operation has changed during the UI authentication session.",
  364. )
  365. if session.clientdict != clientdict:
  366. logger.warning(
  367. "Requested operation has changed during the UI "
  368. "authentication session. A future version of Synapse "
  369. "will remove this capability."
  370. )
  371. # For backwards compatibility, changes to the client dict are
  372. # persisted as clients modify them throughout their user interactive
  373. # authentication flow.
  374. await self.store.set_ui_auth_clientdict(sid, clientdict)
  375. user_agent = request.requestHeaders.getRawHeaders(b"User-Agent", default=[b""])[
  376. 0
  377. ].decode("ascii", "surrogateescape")
  378. await self.store.add_user_agent_ip_to_ui_auth_session(
  379. session.session_id, user_agent, clientip
  380. )
  381. if not authdict:
  382. raise InteractiveAuthIncompleteError(
  383. session.session_id, self._auth_dict_for_flows(flows, session.session_id)
  384. )
  385. # check auth type currently being presented
  386. errordict = {} # type: Dict[str, Any]
  387. if "type" in authdict:
  388. login_type = authdict["type"] # type: str
  389. try:
  390. result = await self._check_auth_dict(authdict, clientip)
  391. if result:
  392. await self.store.mark_ui_auth_stage_complete(
  393. session.session_id, login_type, result
  394. )
  395. except LoginError as e:
  396. if login_type == LoginType.EMAIL_IDENTITY:
  397. # riot used to have a bug where it would request a new
  398. # validation token (thus sending a new email) each time it
  399. # got a 401 with a 'flows' field.
  400. # (https://github.com/vector-im/vector-web/issues/2447).
  401. #
  402. # Grandfather in the old behaviour for now to avoid
  403. # breaking old riot deployments.
  404. raise
  405. # this step failed. Merge the error dict into the response
  406. # so that the client can have another go.
  407. errordict = e.error_dict()
  408. creds = await self.store.get_completed_ui_auth_stages(session.session_id)
  409. for f in flows:
  410. if len(set(f) - set(creds)) == 0:
  411. # it's very useful to know what args are stored, but this can
  412. # include the password in the case of registering, so only log
  413. # the keys (confusingly, clientdict may contain a password
  414. # param, creds is just what the user authed as for UI auth
  415. # and is not sensitive).
  416. logger.info(
  417. "Auth completed with creds: %r. Client dict has keys: %r",
  418. creds,
  419. list(clientdict),
  420. )
  421. return creds, clientdict, session.session_id
  422. ret = self._auth_dict_for_flows(flows, session.session_id)
  423. ret["completed"] = list(creds)
  424. ret.update(errordict)
  425. raise InteractiveAuthIncompleteError(session.session_id, ret)
  426. async def add_oob_auth(
  427. self, stagetype: str, authdict: Dict[str, Any], clientip: str
  428. ) -> bool:
  429. """
  430. Adds the result of out-of-band authentication into an existing auth
  431. session. Currently used for adding the result of fallback auth.
  432. """
  433. if stagetype not in self.checkers:
  434. raise LoginError(400, "", Codes.MISSING_PARAM)
  435. if "session" not in authdict:
  436. raise LoginError(400, "", Codes.MISSING_PARAM)
  437. result = await self.checkers[stagetype].check_auth(authdict, clientip)
  438. if result:
  439. await self.store.mark_ui_auth_stage_complete(
  440. authdict["session"], stagetype, result
  441. )
  442. return True
  443. return False
  444. def get_session_id(self, clientdict: Dict[str, Any]) -> Optional[str]:
  445. """
  446. Gets the session ID for a client given the client dictionary
  447. Args:
  448. clientdict: The dictionary sent by the client in the request
  449. Returns:
  450. The string session ID the client sent. If the client did
  451. not send a session ID, returns None.
  452. """
  453. sid = None
  454. if clientdict and "auth" in clientdict:
  455. authdict = clientdict["auth"]
  456. if "session" in authdict:
  457. sid = authdict["session"]
  458. return sid
  459. async def set_session_data(self, session_id: str, key: str, value: Any) -> None:
  460. """
  461. Store a key-value pair into the sessions data associated with this
  462. request. This data is stored server-side and cannot be modified by
  463. the client.
  464. Args:
  465. session_id: The ID of this session as returned from check_auth
  466. key: The key to store the data under
  467. value: The data to store
  468. """
  469. try:
  470. await self.store.set_ui_auth_session_data(session_id, key, value)
  471. except StoreError:
  472. raise SynapseError(400, "Unknown session ID: %s" % (session_id,))
  473. async def get_session_data(
  474. self, session_id: str, key: str, default: Optional[Any] = None
  475. ) -> Any:
  476. """
  477. Retrieve data stored with set_session_data
  478. Args:
  479. session_id: The ID of this session as returned from check_auth
  480. key: The key to store the data under
  481. default: Value to return if the key has not been set
  482. """
  483. try:
  484. return await self.store.get_ui_auth_session_data(session_id, key, default)
  485. except StoreError:
  486. raise SynapseError(400, "Unknown session ID: %s" % (session_id,))
  487. async def _expire_old_sessions(self):
  488. """
  489. Invalidate any user interactive authentication sessions that have expired.
  490. """
  491. now = self._clock.time_msec()
  492. expiration_time = now - self.SESSION_EXPIRE_MS
  493. await self.store.delete_old_ui_auth_sessions(expiration_time)
  494. async def _check_auth_dict(
  495. self, authdict: Dict[str, Any], clientip: str
  496. ) -> Union[Dict[str, Any], str]:
  497. """Attempt to validate the auth dict provided by a client
  498. Args:
  499. authdict: auth dict provided by the client
  500. clientip: IP address of the client
  501. Returns:
  502. Result of the stage verification.
  503. Raises:
  504. StoreError if there was a problem accessing the database
  505. SynapseError if there was a problem with the request
  506. LoginError if there was an authentication problem.
  507. """
  508. login_type = authdict["type"]
  509. checker = self.checkers.get(login_type)
  510. if checker is not None:
  511. res = await checker.check_auth(authdict, clientip=clientip)
  512. return res
  513. # build a v1-login-style dict out of the authdict and fall back to the
  514. # v1 code
  515. user_id = authdict.get("user")
  516. if user_id is None:
  517. raise SynapseError(400, "", Codes.MISSING_PARAM)
  518. (canonical_id, callback) = await self.validate_login(user_id, authdict)
  519. return canonical_id
  520. def _get_params_recaptcha(self) -> dict:
  521. return {"public_key": self.hs.config.recaptcha_public_key}
  522. def _get_params_terms(self) -> dict:
  523. return {
  524. "policies": {
  525. "privacy_policy": {
  526. "version": self.hs.config.user_consent_version,
  527. "en": {
  528. "name": self.hs.config.user_consent_policy_name,
  529. "url": "%s_matrix/consent?v=%s"
  530. % (
  531. self.hs.config.public_baseurl,
  532. self.hs.config.user_consent_version,
  533. ),
  534. },
  535. }
  536. }
  537. }
  538. def _auth_dict_for_flows(
  539. self, flows: List[List[str]], session_id: str,
  540. ) -> Dict[str, Any]:
  541. public_flows = []
  542. for f in flows:
  543. public_flows.append(f)
  544. get_params = {
  545. LoginType.RECAPTCHA: self._get_params_recaptcha,
  546. LoginType.TERMS: self._get_params_terms,
  547. }
  548. params = {} # type: Dict[str, Any]
  549. for f in public_flows:
  550. for stage in f:
  551. if stage in get_params and stage not in params:
  552. params[stage] = get_params[stage]()
  553. return {
  554. "session": session_id,
  555. "flows": [{"stages": f} for f in public_flows],
  556. "params": params,
  557. }
  558. async def get_access_token_for_user_id(
  559. self, user_id: str, device_id: Optional[str], valid_until_ms: Optional[int]
  560. ):
  561. """
  562. Creates a new access token for the user with the given user ID.
  563. The user is assumed to have been authenticated by some other
  564. machanism (e.g. CAS), and the user_id converted to the canonical case.
  565. The device will be recorded in the table if it is not there already.
  566. Args:
  567. user_id: canonical User ID
  568. device_id: the device ID to associate with the tokens.
  569. None to leave the tokens unassociated with a device (deprecated:
  570. we should always have a device ID)
  571. valid_until_ms: when the token is valid until. None for
  572. no expiry.
  573. Returns:
  574. The access token for the user's session.
  575. Raises:
  576. StoreError if there was a problem storing the token.
  577. """
  578. fmt_expiry = ""
  579. if valid_until_ms is not None:
  580. fmt_expiry = time.strftime(
  581. " until %Y-%m-%d %H:%M:%S", time.localtime(valid_until_ms / 1000.0)
  582. )
  583. logger.info("Logging in user %s on device %s%s", user_id, device_id, fmt_expiry)
  584. await self.auth.check_auth_blocking(user_id)
  585. access_token = self.macaroon_gen.generate_access_token(user_id)
  586. await self.store.add_access_token_to_user(
  587. user_id, access_token, device_id, valid_until_ms
  588. )
  589. # the device *should* have been registered before we got here; however,
  590. # it's possible we raced against a DELETE operation. The thing we
  591. # really don't want is active access_tokens without a record of the
  592. # device, so we double-check it here.
  593. if device_id is not None:
  594. try:
  595. await self.store.get_device(user_id, device_id)
  596. except StoreError:
  597. await self.store.delete_access_token(access_token)
  598. raise StoreError(400, "Login raced against device deletion")
  599. return access_token
  600. async def check_user_exists(self, user_id: str) -> Optional[str]:
  601. """
  602. Checks to see if a user with the given id exists. Will check case
  603. insensitively, but return None if there are multiple inexact matches.
  604. Args:
  605. user_id: complete @user:id
  606. Returns:
  607. The canonical_user_id, or None if zero or multiple matches
  608. """
  609. res = await self._find_user_id_and_pwd_hash(user_id)
  610. if res is not None:
  611. return res[0]
  612. return None
  613. async def _find_user_id_and_pwd_hash(
  614. self, user_id: str
  615. ) -> Optional[Tuple[str, str]]:
  616. """Checks to see if a user with the given id exists. Will check case
  617. insensitively, but will return None if there are multiple inexact
  618. matches.
  619. Returns:
  620. A 2-tuple of `(canonical_user_id, password_hash)` or `None`
  621. if there is not exactly one match
  622. """
  623. user_infos = await self.store.get_users_by_id_case_insensitive(user_id)
  624. result = None
  625. if not user_infos:
  626. logger.warning("Attempted to login as %s but they do not exist", user_id)
  627. elif len(user_infos) == 1:
  628. # a single match (possibly not exact)
  629. result = user_infos.popitem()
  630. elif user_id in user_infos:
  631. # multiple matches, but one is exact
  632. result = (user_id, user_infos[user_id])
  633. else:
  634. # multiple matches, none of them exact
  635. logger.warning(
  636. "Attempted to login as %s but it matches more than one user "
  637. "inexactly: %r",
  638. user_id,
  639. user_infos.keys(),
  640. )
  641. return result
  642. def get_supported_login_types(self) -> Iterable[str]:
  643. """Get a the login types supported for the /login API
  644. By default this is just 'm.login.password' (unless password_enabled is
  645. False in the config file), but password auth providers can provide
  646. other login types.
  647. Returns:
  648. login types
  649. """
  650. return self._supported_login_types
  651. async def validate_login(
  652. self, username: str, login_submission: Dict[str, Any]
  653. ) -> Tuple[str, Optional[Callable[[Dict[str, str]], None]]]:
  654. """Authenticates the user for the /login API
  655. Also used by the user-interactive auth flow to validate
  656. m.login.password auth types.
  657. Args:
  658. username: username supplied by the user
  659. login_submission: the whole of the login submission
  660. (including 'type' and other relevant fields)
  661. Returns:
  662. A tuple of the canonical user id, and optional callback
  663. to be called once the access token and device id are issued
  664. Raises:
  665. StoreError if there was a problem accessing the database
  666. SynapseError if there was a problem with the request
  667. LoginError if there was an authentication problem.
  668. """
  669. if username.startswith("@"):
  670. qualified_user_id = username
  671. else:
  672. qualified_user_id = UserID(username, self.hs.hostname).to_string()
  673. login_type = login_submission.get("type")
  674. known_login_type = False
  675. # special case to check for "password" for the check_password interface
  676. # for the auth providers
  677. password = login_submission.get("password")
  678. if login_type == LoginType.PASSWORD:
  679. if not self._password_enabled:
  680. raise SynapseError(400, "Password login has been disabled.")
  681. if not password:
  682. raise SynapseError(400, "Missing parameter: password")
  683. for provider in self.password_providers:
  684. if hasattr(provider, "check_password") and login_type == LoginType.PASSWORD:
  685. known_login_type = True
  686. is_valid = await provider.check_password(qualified_user_id, password)
  687. if is_valid:
  688. return qualified_user_id, None
  689. if not hasattr(provider, "get_supported_login_types") or not hasattr(
  690. provider, "check_auth"
  691. ):
  692. # this password provider doesn't understand custom login types
  693. continue
  694. supported_login_types = provider.get_supported_login_types()
  695. if login_type not in supported_login_types:
  696. # this password provider doesn't understand this login type
  697. continue
  698. known_login_type = True
  699. login_fields = supported_login_types[login_type]
  700. missing_fields = []
  701. login_dict = {}
  702. for f in login_fields:
  703. if f not in login_submission:
  704. missing_fields.append(f)
  705. else:
  706. login_dict[f] = login_submission[f]
  707. if missing_fields:
  708. raise SynapseError(
  709. 400,
  710. "Missing parameters for login type %s: %s"
  711. % (login_type, missing_fields),
  712. )
  713. result = await provider.check_auth(username, login_type, login_dict)
  714. if result:
  715. if isinstance(result, str):
  716. result = (result, None)
  717. return result
  718. if login_type == LoginType.PASSWORD and self.hs.config.password_localdb_enabled:
  719. known_login_type = True
  720. canonical_user_id = await self._check_local_password(
  721. qualified_user_id, password # type: ignore
  722. )
  723. if canonical_user_id:
  724. return canonical_user_id, None
  725. if not known_login_type:
  726. raise SynapseError(400, "Unknown login type %s" % login_type)
  727. # We raise a 403 here, but note that if we're doing user-interactive
  728. # login, it turns all LoginErrors into a 401 anyway.
  729. raise LoginError(403, "Invalid password", errcode=Codes.FORBIDDEN)
  730. async def check_password_provider_3pid(
  731. self, medium: str, address: str, password: str
  732. ) -> Tuple[Optional[str], Optional[Callable[[Dict[str, str]], None]]]:
  733. """Check if a password provider is able to validate a thirdparty login
  734. Args:
  735. medium: The medium of the 3pid (ex. email).
  736. address: The address of the 3pid (ex. jdoe@example.com).
  737. password: The password of the user.
  738. Returns:
  739. A tuple of `(user_id, callback)`. If authentication is successful,
  740. `user_id`is the authenticated, canonical user ID. `callback` is
  741. then either a function to be later run after the server has
  742. completed login/registration, or `None`. If authentication was
  743. unsuccessful, `user_id` and `callback` are both `None`.
  744. """
  745. for provider in self.password_providers:
  746. if hasattr(provider, "check_3pid_auth"):
  747. # This function is able to return a deferred that either
  748. # resolves None, meaning authentication failure, or upon
  749. # success, to a str (which is the user_id) or a tuple of
  750. # (user_id, callback_func), where callback_func should be run
  751. # after we've finished everything else
  752. result = await provider.check_3pid_auth(medium, address, password)
  753. if result:
  754. # Check if the return value is a str or a tuple
  755. if isinstance(result, str):
  756. # If it's a str, set callback function to None
  757. result = (result, None)
  758. return result
  759. return None, None
  760. async def _check_local_password(self, user_id: str, password: str) -> Optional[str]:
  761. """Authenticate a user against the local password database.
  762. user_id is checked case insensitively, but will return None if there are
  763. multiple inexact matches.
  764. Args:
  765. user_id: complete @user:id
  766. password: the provided password
  767. Returns:
  768. The canonical_user_id, or None if unknown user/bad password
  769. """
  770. lookupres = await self._find_user_id_and_pwd_hash(user_id)
  771. if not lookupres:
  772. return None
  773. (user_id, password_hash) = lookupres
  774. # If the password hash is None, the account has likely been deactivated
  775. if not password_hash:
  776. deactivated = await self.store.get_user_deactivated_status(user_id)
  777. if deactivated:
  778. raise UserDeactivatedError("This account has been deactivated")
  779. result = await self.validate_hash(password, password_hash)
  780. if not result:
  781. logger.warning("Failed password login for user %s", user_id)
  782. return None
  783. return user_id
  784. async def validate_short_term_login_token_and_get_user_id(self, login_token: str):
  785. auth_api = self.hs.get_auth()
  786. user_id = None
  787. try:
  788. macaroon = pymacaroons.Macaroon.deserialize(login_token)
  789. user_id = auth_api.get_user_id_from_macaroon(macaroon)
  790. auth_api.validate_macaroon(macaroon, "login", user_id)
  791. except Exception:
  792. raise AuthError(403, "Invalid token", errcode=Codes.FORBIDDEN)
  793. await self.auth.check_auth_blocking(user_id)
  794. return user_id
  795. async def delete_access_token(self, access_token: str):
  796. """Invalidate a single access token
  797. Args:
  798. access_token: access token to be deleted
  799. """
  800. user_info = await self.auth.get_user_by_access_token(access_token)
  801. await self.store.delete_access_token(access_token)
  802. # see if any of our auth providers want to know about this
  803. for provider in self.password_providers:
  804. if hasattr(provider, "on_logged_out"):
  805. # This might return an awaitable, if it does block the log out
  806. # until it completes.
  807. result = provider.on_logged_out(
  808. user_id=str(user_info["user"]),
  809. device_id=user_info["device_id"],
  810. access_token=access_token,
  811. )
  812. if inspect.isawaitable(result):
  813. await result
  814. # delete pushers associated with this access token
  815. if user_info["token_id"] is not None:
  816. await self.hs.get_pusherpool().remove_pushers_by_access_token(
  817. str(user_info["user"]), (user_info["token_id"],)
  818. )
  819. async def delete_access_tokens_for_user(
  820. self,
  821. user_id: str,
  822. except_token_id: Optional[str] = None,
  823. device_id: Optional[str] = None,
  824. ):
  825. """Invalidate access tokens belonging to a user
  826. Args:
  827. user_id: ID of user the tokens belong to
  828. except_token_id: access_token ID which should *not* be deleted
  829. device_id: ID of device the tokens are associated with.
  830. If None, tokens associated with any device (or no device) will
  831. be deleted
  832. """
  833. tokens_and_devices = await self.store.user_delete_access_tokens(
  834. user_id, except_token_id=except_token_id, device_id=device_id
  835. )
  836. # see if any of our auth providers want to know about this
  837. for provider in self.password_providers:
  838. if hasattr(provider, "on_logged_out"):
  839. for token, token_id, device_id in tokens_and_devices:
  840. await provider.on_logged_out(
  841. user_id=user_id, device_id=device_id, access_token=token
  842. )
  843. # delete pushers associated with the access tokens
  844. await self.hs.get_pusherpool().remove_pushers_by_access_token(
  845. user_id, (token_id for _, token_id, _ in tokens_and_devices)
  846. )
  847. async def add_threepid(
  848. self, user_id: str, medium: str, address: str, validated_at: int
  849. ):
  850. # check if medium has a valid value
  851. if medium not in ["email", "msisdn"]:
  852. raise SynapseError(
  853. code=400,
  854. msg=("'%s' is not a valid value for 'medium'" % (medium,)),
  855. errcode=Codes.INVALID_PARAM,
  856. )
  857. # 'Canonicalise' email addresses down to lower case.
  858. # We've now moving towards the homeserver being the entity that
  859. # is responsible for validating threepids used for resetting passwords
  860. # on accounts, so in future Synapse will gain knowledge of specific
  861. # types (mediums) of threepid. For now, we still use the existing
  862. # infrastructure, but this is the start of synapse gaining knowledge
  863. # of specific types of threepid (and fixes the fact that checking
  864. # for the presence of an email address during password reset was
  865. # case sensitive).
  866. if medium == "email":
  867. address = canonicalise_email(address)
  868. await self.store.user_add_threepid(
  869. user_id, medium, address, validated_at, self.hs.get_clock().time_msec()
  870. )
  871. async def delete_threepid(
  872. self, user_id: str, medium: str, address: str, id_server: Optional[str] = None
  873. ) -> bool:
  874. """Attempts to unbind the 3pid on the identity servers and deletes it
  875. from the local database.
  876. Args:
  877. user_id: ID of user to remove the 3pid from.
  878. medium: The medium of the 3pid being removed: "email" or "msisdn".
  879. address: The 3pid address to remove.
  880. id_server: Use the given identity server when unbinding
  881. any threepids. If None then will attempt to unbind using the
  882. identity server specified when binding (if known).
  883. Returns:
  884. Returns True if successfully unbound the 3pid on
  885. the identity server, False if identity server doesn't support the
  886. unbind API.
  887. """
  888. # 'Canonicalise' email addresses as per above
  889. if medium == "email":
  890. address = canonicalise_email(address)
  891. identity_handler = self.hs.get_handlers().identity_handler
  892. result = await identity_handler.try_unbind_threepid(
  893. user_id, {"medium": medium, "address": address, "id_server": id_server}
  894. )
  895. await self.store.user_delete_threepid(user_id, medium, address)
  896. return result
  897. async def hash(self, password: str) -> str:
  898. """Computes a secure hash of password.
  899. Args:
  900. password: Password to hash.
  901. Returns:
  902. Hashed password.
  903. """
  904. def _do_hash():
  905. # Normalise the Unicode in the password
  906. pw = unicodedata.normalize("NFKC", password)
  907. return bcrypt.hashpw(
  908. pw.encode("utf8") + self.hs.config.password_pepper.encode("utf8"),
  909. bcrypt.gensalt(self.bcrypt_rounds),
  910. ).decode("ascii")
  911. return await defer_to_thread(self.hs.get_reactor(), _do_hash)
  912. async def validate_hash(
  913. self, password: str, stored_hash: Union[bytes, str]
  914. ) -> bool:
  915. """Validates that self.hash(password) == stored_hash.
  916. Args:
  917. password: Password to hash.
  918. stored_hash: Expected hash value.
  919. Returns:
  920. Whether self.hash(password) == stored_hash.
  921. """
  922. def _do_validate_hash():
  923. # Normalise the Unicode in the password
  924. pw = unicodedata.normalize("NFKC", password)
  925. return bcrypt.checkpw(
  926. pw.encode("utf8") + self.hs.config.password_pepper.encode("utf8"),
  927. stored_hash,
  928. )
  929. if stored_hash:
  930. if not isinstance(stored_hash, bytes):
  931. stored_hash = stored_hash.encode("ascii")
  932. return await defer_to_thread(self.hs.get_reactor(), _do_validate_hash)
  933. else:
  934. return False
  935. async def start_sso_ui_auth(self, redirect_url: str, session_id: str) -> str:
  936. """
  937. Get the HTML for the SSO redirect confirmation page.
  938. Args:
  939. redirect_url: The URL to redirect to the SSO provider.
  940. session_id: The user interactive authentication session ID.
  941. Returns:
  942. The HTML to render.
  943. """
  944. try:
  945. session = await self.store.get_ui_auth_session(session_id)
  946. except StoreError:
  947. raise SynapseError(400, "Unknown session ID: %s" % (session_id,))
  948. return self._sso_auth_confirm_template.render(
  949. description=session.description, redirect_url=redirect_url,
  950. )
  951. async def complete_sso_ui_auth(
  952. self, registered_user_id: str, session_id: str, request: SynapseRequest,
  953. ):
  954. """Having figured out a mxid for this user, complete the HTTP request
  955. Args:
  956. registered_user_id: The registered user ID to complete SSO login for.
  957. request: The request to complete.
  958. client_redirect_url: The URL to which to redirect the user at the end of the
  959. process.
  960. """
  961. # Mark the stage of the authentication as successful.
  962. # Save the user who authenticated with SSO, this will be used to ensure
  963. # that the account be modified is also the person who logged in.
  964. await self.store.mark_ui_auth_stage_complete(
  965. session_id, LoginType.SSO, registered_user_id
  966. )
  967. # Render the HTML and return.
  968. html = self._sso_auth_success_template
  969. respond_with_html(request, 200, html)
  970. async def complete_sso_login(
  971. self,
  972. registered_user_id: str,
  973. request: SynapseRequest,
  974. client_redirect_url: str,
  975. extra_attributes: Optional[JsonDict] = None,
  976. ):
  977. """Having figured out a mxid for this user, complete the HTTP request
  978. Args:
  979. registered_user_id: The registered user ID to complete SSO login for.
  980. request: The request to complete.
  981. client_redirect_url: The URL to which to redirect the user at the end of the
  982. process.
  983. extra_attributes: Extra attributes which will be passed to the client
  984. during successful login. Must be JSON serializable.
  985. """
  986. # If the account has been deactivated, do not proceed with the login
  987. # flow.
  988. deactivated = await self.store.get_user_deactivated_status(registered_user_id)
  989. if deactivated:
  990. respond_with_html(request, 403, self._sso_account_deactivated_template)
  991. return
  992. self._complete_sso_login(
  993. registered_user_id, request, client_redirect_url, extra_attributes
  994. )
  995. def _complete_sso_login(
  996. self,
  997. registered_user_id: str,
  998. request: SynapseRequest,
  999. client_redirect_url: str,
  1000. extra_attributes: Optional[JsonDict] = None,
  1001. ):
  1002. """
  1003. The synchronous portion of complete_sso_login.
  1004. This exists purely for backwards compatibility of synapse.module_api.ModuleApi.
  1005. """
  1006. # Store any extra attributes which will be passed in the login response.
  1007. # Note that this is per-user so it may overwrite a previous value, this
  1008. # is considered OK since the newest SSO attributes should be most valid.
  1009. if extra_attributes:
  1010. self._extra_attributes[registered_user_id] = SsoLoginExtraAttributes(
  1011. self._clock.time_msec(), extra_attributes,
  1012. )
  1013. # Create a login token
  1014. login_token = self.macaroon_gen.generate_short_term_login_token(
  1015. registered_user_id
  1016. )
  1017. # Append the login token to the original redirect URL (i.e. with its query
  1018. # parameters kept intact) to build the URL to which the template needs to
  1019. # redirect the users once they have clicked on the confirmation link.
  1020. redirect_url = self.add_query_param_to_url(
  1021. client_redirect_url, "loginToken", login_token
  1022. )
  1023. # if the client is whitelisted, we can redirect straight to it
  1024. if client_redirect_url.startswith(self._whitelisted_sso_clients):
  1025. request.redirect(redirect_url)
  1026. finish_request(request)
  1027. return
  1028. # Otherwise, serve the redirect confirmation page.
  1029. # Remove the query parameters from the redirect URL to get a shorter version of
  1030. # it. This is only to display a human-readable URL in the template, but not the
  1031. # URL we redirect users to.
  1032. redirect_url_no_params = client_redirect_url.split("?")[0]
  1033. html = self._sso_redirect_confirm_template.render(
  1034. display_url=redirect_url_no_params,
  1035. redirect_url=redirect_url,
  1036. server_name=self._server_name,
  1037. )
  1038. respond_with_html(request, 200, html)
  1039. async def _sso_login_callback(self, login_result: JsonDict) -> None:
  1040. """
  1041. A login callback which might add additional attributes to the login response.
  1042. Args:
  1043. login_result: The data to be sent to the client. Includes the user
  1044. ID and access token.
  1045. """
  1046. # Expire attributes before processing. Note that there shouldn't be any
  1047. # valid logins that still have extra attributes.
  1048. self._expire_sso_extra_attributes()
  1049. extra_attributes = self._extra_attributes.get(login_result["user_id"])
  1050. if extra_attributes:
  1051. login_result.update(extra_attributes.extra_attributes)
  1052. def _expire_sso_extra_attributes(self) -> None:
  1053. """
  1054. Iterate through the mapping of user IDs to extra attributes and remove any that are no longer valid.
  1055. """
  1056. # TODO This should match the amount of time the macaroon is valid for.
  1057. LOGIN_TOKEN_EXPIRATION_TIME = 2 * 60 * 1000
  1058. expire_before = self._clock.time_msec() - LOGIN_TOKEN_EXPIRATION_TIME
  1059. to_expire = set()
  1060. for user_id, data in self._extra_attributes.items():
  1061. if data.creation_time < expire_before:
  1062. to_expire.add(user_id)
  1063. for user_id in to_expire:
  1064. logger.debug("Expiring extra attributes for user %s", user_id)
  1065. del self._extra_attributes[user_id]
  1066. @staticmethod
  1067. def add_query_param_to_url(url: str, param_name: str, param: Any):
  1068. url_parts = list(urllib.parse.urlparse(url))
  1069. query = dict(urllib.parse.parse_qsl(url_parts[4]))
  1070. query.update({param_name: param})
  1071. url_parts[4] = urllib.parse.urlencode(query)
  1072. return urllib.parse.urlunparse(url_parts)
  1073. @attr.s(slots=True)
  1074. class MacaroonGenerator:
  1075. hs = attr.ib()
  1076. def generate_access_token(
  1077. self, user_id: str, extra_caveats: Optional[List[str]] = None
  1078. ) -> str:
  1079. extra_caveats = extra_caveats or []
  1080. macaroon = self._generate_base_macaroon(user_id)
  1081. macaroon.add_first_party_caveat("type = access")
  1082. # Include a nonce, to make sure that each login gets a different
  1083. # access token.
  1084. macaroon.add_first_party_caveat(
  1085. "nonce = %s" % (stringutils.random_string_with_symbols(16),)
  1086. )
  1087. for caveat in extra_caveats:
  1088. macaroon.add_first_party_caveat(caveat)
  1089. return macaroon.serialize()
  1090. def generate_short_term_login_token(
  1091. self, user_id: str, duration_in_ms: int = (2 * 60 * 1000)
  1092. ) -> str:
  1093. macaroon = self._generate_base_macaroon(user_id)
  1094. macaroon.add_first_party_caveat("type = login")
  1095. now = self.hs.get_clock().time_msec()
  1096. expiry = now + duration_in_ms
  1097. macaroon.add_first_party_caveat("time < %d" % (expiry,))
  1098. return macaroon.serialize()
  1099. def generate_delete_pusher_token(self, user_id: str) -> str:
  1100. macaroon = self._generate_base_macaroon(user_id)
  1101. macaroon.add_first_party_caveat("type = delete_pusher")
  1102. return macaroon.serialize()
  1103. def _generate_base_macaroon(self, user_id: str) -> pymacaroons.Macaroon:
  1104. macaroon = pymacaroons.Macaroon(
  1105. location=self.hs.config.server_name,
  1106. identifier="key",
  1107. key=self.hs.config.macaroon_secret_key,
  1108. )
  1109. macaroon.add_first_party_caveat("gen = 1")
  1110. macaroon.add_first_party_caveat("user_id = %s" % (user_id,))
  1111. return macaroon