auth.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916
  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. from twisted.internet import defer, threads
  17. from ._base import BaseHandler
  18. from synapse.api.constants import LoginType
  19. from synapse.api.errors import (
  20. AuthError, Codes, InteractiveAuthIncompleteError, LoginError, StoreError,
  21. SynapseError,
  22. )
  23. from synapse.module_api import ModuleApi
  24. from synapse.types import UserID
  25. from synapse.util.async import run_on_reactor
  26. from synapse.util.caches.expiringcache import ExpiringCache
  27. from synapse.util.logcontext import make_deferred_yieldable
  28. from twisted.web.client import PartialDownloadError
  29. import logging
  30. import bcrypt
  31. import pymacaroons
  32. import simplejson
  33. import synapse.util.stringutils as stringutils
  34. logger = logging.getLogger(__name__)
  35. class AuthHandler(BaseHandler):
  36. SESSION_EXPIRE_MS = 48 * 60 * 60 * 1000
  37. def __init__(self, hs):
  38. """
  39. Args:
  40. hs (synapse.server.HomeServer):
  41. """
  42. super(AuthHandler, self).__init__(hs)
  43. self.checkers = {
  44. LoginType.RECAPTCHA: self._check_recaptcha,
  45. LoginType.EMAIL_IDENTITY: self._check_email_identity,
  46. LoginType.MSISDN: self._check_msisdn,
  47. LoginType.DUMMY: self._check_dummy_auth,
  48. }
  49. self.bcrypt_rounds = hs.config.bcrypt_rounds
  50. # This is not a cache per se, but a store of all current sessions that
  51. # expire after N hours
  52. self.sessions = ExpiringCache(
  53. cache_name="register_sessions",
  54. clock=hs.get_clock(),
  55. expiry_ms=self.SESSION_EXPIRE_MS,
  56. reset_expiry_on_get=True,
  57. )
  58. account_handler = ModuleApi(hs, self)
  59. self.password_providers = [
  60. module(config=config, account_handler=account_handler)
  61. for module, config in hs.config.password_providers
  62. ]
  63. logger.info("Extra password_providers: %r", self.password_providers)
  64. self.hs = hs # FIXME better possibility to access registrationHandler later?
  65. self.macaroon_gen = hs.get_macaroon_generator()
  66. self._password_enabled = hs.config.password_enabled
  67. # we keep this as a list despite the O(N^2) implication so that we can
  68. # keep PASSWORD first and avoid confusing clients which pick the first
  69. # type in the list. (NB that the spec doesn't require us to do so and
  70. # clients which favour types that they don't understand over those that
  71. # they do are technically broken)
  72. login_types = []
  73. if self._password_enabled:
  74. login_types.append(LoginType.PASSWORD)
  75. for provider in self.password_providers:
  76. if hasattr(provider, "get_supported_login_types"):
  77. for t in provider.get_supported_login_types().keys():
  78. if t not in login_types:
  79. login_types.append(t)
  80. self._supported_login_types = login_types
  81. @defer.inlineCallbacks
  82. def validate_user_via_ui_auth(self, requester, request_body, clientip):
  83. """
  84. Checks that the user is who they claim to be, via a UI auth.
  85. This is used for things like device deletion and password reset where
  86. the user already has a valid access token, but we want to double-check
  87. that it isn't stolen by re-authenticating them.
  88. Args:
  89. requester (Requester): The user, as given by the access token
  90. request_body (dict): The body of the request sent by the client
  91. clientip (str): The IP address of the client.
  92. Returns:
  93. defer.Deferred[dict]: the parameters for this request (which may
  94. have been given only in a previous call).
  95. Raises:
  96. InteractiveAuthIncompleteError if the client has not yet completed
  97. any of the permitted login flows
  98. AuthError if the client has completed a login flow, and it gives
  99. a different user to `requester`
  100. """
  101. # build a list of supported flows
  102. flows = [
  103. [login_type] for login_type in self._supported_login_types
  104. ]
  105. result, params, _ = yield self.check_auth(
  106. flows, request_body, clientip,
  107. )
  108. # find the completed login type
  109. for login_type in self._supported_login_types:
  110. if login_type not in result:
  111. continue
  112. user_id = result[login_type]
  113. break
  114. else:
  115. # this can't happen
  116. raise Exception(
  117. "check_auth returned True but no successful login type",
  118. )
  119. # check that the UI auth matched the access token
  120. if user_id != requester.user.to_string():
  121. raise AuthError(403, "Invalid auth")
  122. defer.returnValue(params)
  123. @defer.inlineCallbacks
  124. def check_auth(self, flows, clientdict, clientip):
  125. """
  126. Takes a dictionary sent by the client in the login / registration
  127. protocol and handles the User-Interactive Auth flow.
  128. As a side effect, this function fills in the 'creds' key on the user's
  129. session with a map, which maps each auth-type (str) to the relevant
  130. identity authenticated by that auth-type (mostly str, but for captcha, bool).
  131. If no auth flows have been completed successfully, raises an
  132. InteractiveAuthIncompleteError. To handle this, you can use
  133. synapse.rest.client.v2_alpha._base.interactive_auth_handler as a
  134. decorator.
  135. Args:
  136. flows (list): A list of login flows. Each flow is an ordered list of
  137. strings representing auth-types. At least one full
  138. flow must be completed in order for auth to be successful.
  139. clientdict: The dictionary from the client root level, not the
  140. 'auth' key: this method prompts for auth if none is sent.
  141. clientip (str): The IP address of the client.
  142. Returns:
  143. defer.Deferred[dict, dict, str]: a deferred tuple of
  144. (creds, params, session_id).
  145. 'creds' contains the authenticated credentials of each stage.
  146. 'params' contains the parameters for this request (which may
  147. have been given only in a previous call).
  148. 'session_id' is the ID of this session, either passed in by the
  149. client or assigned by this call
  150. Raises:
  151. InteractiveAuthIncompleteError if the client has not yet completed
  152. all the stages in any of the permitted flows.
  153. """
  154. authdict = None
  155. sid = None
  156. if clientdict and 'auth' in clientdict:
  157. authdict = clientdict['auth']
  158. del clientdict['auth']
  159. if 'session' in authdict:
  160. sid = authdict['session']
  161. session = self._get_session_info(sid)
  162. if len(clientdict) > 0:
  163. # This was designed to allow the client to omit the parameters
  164. # and just supply the session in subsequent calls so it split
  165. # auth between devices by just sharing the session, (eg. so you
  166. # could continue registration from your phone having clicked the
  167. # email auth link on there). It's probably too open to abuse
  168. # because it lets unauthenticated clients store arbitrary objects
  169. # on a home server.
  170. # Revisit: Assumimg the REST APIs do sensible validation, the data
  171. # isn't arbintrary.
  172. session['clientdict'] = clientdict
  173. self._save_session(session)
  174. elif 'clientdict' in session:
  175. clientdict = session['clientdict']
  176. if not authdict:
  177. raise InteractiveAuthIncompleteError(
  178. self._auth_dict_for_flows(flows, session),
  179. )
  180. if 'creds' not in session:
  181. session['creds'] = {}
  182. creds = session['creds']
  183. # check auth type currently being presented
  184. errordict = {}
  185. if 'type' in authdict:
  186. login_type = authdict['type']
  187. try:
  188. result = yield self._check_auth_dict(authdict, clientip)
  189. if result:
  190. creds[login_type] = result
  191. self._save_session(session)
  192. except LoginError as e:
  193. if login_type == LoginType.EMAIL_IDENTITY:
  194. # riot used to have a bug where it would request a new
  195. # validation token (thus sending a new email) each time it
  196. # got a 401 with a 'flows' field.
  197. # (https://github.com/vector-im/vector-web/issues/2447).
  198. #
  199. # Grandfather in the old behaviour for now to avoid
  200. # breaking old riot deployments.
  201. raise
  202. # this step failed. Merge the error dict into the response
  203. # so that the client can have another go.
  204. errordict = e.error_dict()
  205. for f in flows:
  206. if len(set(f) - set(creds.keys())) == 0:
  207. # it's very useful to know what args are stored, but this can
  208. # include the password in the case of registering, so only log
  209. # the keys (confusingly, clientdict may contain a password
  210. # param, creds is just what the user authed as for UI auth
  211. # and is not sensitive).
  212. logger.info(
  213. "Auth completed with creds: %r. Client dict has keys: %r",
  214. creds, clientdict.keys()
  215. )
  216. defer.returnValue((creds, clientdict, session['id']))
  217. ret = self._auth_dict_for_flows(flows, session)
  218. ret['completed'] = creds.keys()
  219. ret.update(errordict)
  220. raise InteractiveAuthIncompleteError(
  221. ret,
  222. )
  223. @defer.inlineCallbacks
  224. def add_oob_auth(self, stagetype, authdict, clientip):
  225. """
  226. Adds the result of out-of-band authentication into an existing auth
  227. session. Currently used for adding the result of fallback auth.
  228. """
  229. if stagetype not in self.checkers:
  230. raise LoginError(400, "", Codes.MISSING_PARAM)
  231. if 'session' not in authdict:
  232. raise LoginError(400, "", Codes.MISSING_PARAM)
  233. sess = self._get_session_info(
  234. authdict['session']
  235. )
  236. if 'creds' not in sess:
  237. sess['creds'] = {}
  238. creds = sess['creds']
  239. result = yield self.checkers[stagetype](authdict, clientip)
  240. if result:
  241. creds[stagetype] = result
  242. self._save_session(sess)
  243. defer.returnValue(True)
  244. defer.returnValue(False)
  245. def get_session_id(self, clientdict):
  246. """
  247. Gets the session ID for a client given the client dictionary
  248. Args:
  249. clientdict: The dictionary sent by the client in the request
  250. Returns:
  251. str|None: The string session ID the client sent. If the client did
  252. not send a session ID, returns None.
  253. """
  254. sid = None
  255. if clientdict and 'auth' in clientdict:
  256. authdict = clientdict['auth']
  257. if 'session' in authdict:
  258. sid = authdict['session']
  259. return sid
  260. def set_session_data(self, session_id, key, value):
  261. """
  262. Store a key-value pair into the sessions data associated with this
  263. request. This data is stored server-side and cannot be modified by
  264. the client.
  265. Args:
  266. session_id (string): The ID of this session as returned from check_auth
  267. key (string): The key to store the data under
  268. value (any): The data to store
  269. """
  270. sess = self._get_session_info(session_id)
  271. sess.setdefault('serverdict', {})[key] = value
  272. self._save_session(sess)
  273. def get_session_data(self, session_id, key, default=None):
  274. """
  275. Retrieve data stored with set_session_data
  276. Args:
  277. session_id (string): The ID of this session as returned from check_auth
  278. key (string): The key to store the data under
  279. default (any): Value to return if the key has not been set
  280. """
  281. sess = self._get_session_info(session_id)
  282. return sess.setdefault('serverdict', {}).get(key, default)
  283. @defer.inlineCallbacks
  284. def _check_auth_dict(self, authdict, clientip):
  285. """Attempt to validate the auth dict provided by a client
  286. Args:
  287. authdict (object): auth dict provided by the client
  288. clientip (str): IP address of the client
  289. Returns:
  290. Deferred: result of the stage verification.
  291. Raises:
  292. StoreError if there was a problem accessing the database
  293. SynapseError if there was a problem with the request
  294. LoginError if there was an authentication problem.
  295. """
  296. login_type = authdict['type']
  297. checker = self.checkers.get(login_type)
  298. if checker is not None:
  299. res = yield checker(authdict, clientip)
  300. defer.returnValue(res)
  301. # build a v1-login-style dict out of the authdict and fall back to the
  302. # v1 code
  303. user_id = authdict.get("user")
  304. if user_id is None:
  305. raise SynapseError(400, "", Codes.MISSING_PARAM)
  306. (canonical_id, callback) = yield self.validate_login(user_id, authdict)
  307. defer.returnValue(canonical_id)
  308. @defer.inlineCallbacks
  309. def _check_recaptcha(self, authdict, clientip):
  310. try:
  311. user_response = authdict["response"]
  312. except KeyError:
  313. # Client tried to provide captcha but didn't give the parameter:
  314. # bad request.
  315. raise LoginError(
  316. 400, "Captcha response is required",
  317. errcode=Codes.CAPTCHA_NEEDED
  318. )
  319. logger.info(
  320. "Submitting recaptcha response %s with remoteip %s",
  321. user_response, clientip
  322. )
  323. # TODO: get this from the homeserver rather than creating a new one for
  324. # each request
  325. try:
  326. client = self.hs.get_simple_http_client()
  327. resp_body = yield client.post_urlencoded_get_json(
  328. self.hs.config.recaptcha_siteverify_api,
  329. args={
  330. 'secret': self.hs.config.recaptcha_private_key,
  331. 'response': user_response,
  332. 'remoteip': clientip,
  333. }
  334. )
  335. except PartialDownloadError as pde:
  336. # Twisted is silly
  337. data = pde.response
  338. resp_body = simplejson.loads(data)
  339. if 'success' in resp_body:
  340. # Note that we do NOT check the hostname here: we explicitly
  341. # intend the CAPTCHA to be presented by whatever client the
  342. # user is using, we just care that they have completed a CAPTCHA.
  343. logger.info(
  344. "%s reCAPTCHA from hostname %s",
  345. "Successful" if resp_body['success'] else "Failed",
  346. resp_body.get('hostname')
  347. )
  348. if resp_body['success']:
  349. defer.returnValue(True)
  350. raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
  351. def _check_email_identity(self, authdict, _):
  352. return self._check_threepid('email', authdict)
  353. def _check_msisdn(self, authdict, _):
  354. return self._check_threepid('msisdn', authdict)
  355. @defer.inlineCallbacks
  356. def _check_dummy_auth(self, authdict, _):
  357. yield run_on_reactor()
  358. defer.returnValue(True)
  359. @defer.inlineCallbacks
  360. def _check_threepid(self, medium, authdict):
  361. yield run_on_reactor()
  362. if 'threepid_creds' not in authdict:
  363. raise LoginError(400, "Missing threepid_creds", Codes.MISSING_PARAM)
  364. threepid_creds = authdict['threepid_creds']
  365. identity_handler = self.hs.get_handlers().identity_handler
  366. logger.info("Getting validated threepid. threepidcreds: %r", (threepid_creds,))
  367. threepid = yield identity_handler.threepid_from_creds(threepid_creds)
  368. if not threepid:
  369. raise LoginError(401, "", errcode=Codes.UNAUTHORIZED)
  370. if threepid['medium'] != medium:
  371. raise LoginError(
  372. 401,
  373. "Expecting threepid of type '%s', got '%s'" % (
  374. medium, threepid['medium'],
  375. ),
  376. errcode=Codes.UNAUTHORIZED
  377. )
  378. threepid['threepid_creds'] = authdict['threepid_creds']
  379. defer.returnValue(threepid)
  380. def _get_params_recaptcha(self):
  381. return {"public_key": self.hs.config.recaptcha_public_key}
  382. def _auth_dict_for_flows(self, flows, session):
  383. public_flows = []
  384. for f in flows:
  385. public_flows.append(f)
  386. get_params = {
  387. LoginType.RECAPTCHA: self._get_params_recaptcha,
  388. }
  389. params = {}
  390. for f in public_flows:
  391. for stage in f:
  392. if stage in get_params and stage not in params:
  393. params[stage] = get_params[stage]()
  394. return {
  395. "session": session['id'],
  396. "flows": [{"stages": f} for f in public_flows],
  397. "params": params
  398. }
  399. def _get_session_info(self, session_id):
  400. if session_id not in self.sessions:
  401. session_id = None
  402. if not session_id:
  403. # create a new session
  404. while session_id is None or session_id in self.sessions:
  405. session_id = stringutils.random_string(24)
  406. self.sessions[session_id] = {
  407. "id": session_id,
  408. }
  409. return self.sessions[session_id]
  410. @defer.inlineCallbacks
  411. def get_access_token_for_user_id(self, user_id, device_id=None):
  412. """
  413. Creates a new access token for the user with the given user ID.
  414. The user is assumed to have been authenticated by some other
  415. machanism (e.g. CAS), and the user_id converted to the canonical case.
  416. The device will be recorded in the table if it is not there already.
  417. Args:
  418. user_id (str): canonical User ID
  419. device_id (str|None): the device ID to associate with the tokens.
  420. None to leave the tokens unassociated with a device (deprecated:
  421. we should always have a device ID)
  422. Returns:
  423. The access token for the user's session.
  424. Raises:
  425. StoreError if there was a problem storing the token.
  426. """
  427. logger.info("Logging in user %s on device %s", user_id, device_id)
  428. access_token = yield self.issue_access_token(user_id, device_id)
  429. # the device *should* have been registered before we got here; however,
  430. # it's possible we raced against a DELETE operation. The thing we
  431. # really don't want is active access_tokens without a record of the
  432. # device, so we double-check it here.
  433. if device_id is not None:
  434. try:
  435. yield self.store.get_device(user_id, device_id)
  436. except StoreError:
  437. yield self.store.delete_access_token(access_token)
  438. raise StoreError(400, "Login raced against device deletion")
  439. defer.returnValue(access_token)
  440. @defer.inlineCallbacks
  441. def check_user_exists(self, user_id):
  442. """
  443. Checks to see if a user with the given id exists. Will check case
  444. insensitively, but return None if there are multiple inexact matches.
  445. Args:
  446. (str) user_id: complete @user:id
  447. Returns:
  448. defer.Deferred: (str) canonical_user_id, or None if zero or
  449. multiple matches
  450. """
  451. res = yield self._find_user_id_and_pwd_hash(user_id)
  452. if res is not None:
  453. defer.returnValue(res[0])
  454. defer.returnValue(None)
  455. @defer.inlineCallbacks
  456. def _find_user_id_and_pwd_hash(self, user_id):
  457. """Checks to see if a user with the given id exists. Will check case
  458. insensitively, but will return None if there are multiple inexact
  459. matches.
  460. Returns:
  461. tuple: A 2-tuple of `(canonical_user_id, password_hash)`
  462. None: if there is not exactly one match
  463. """
  464. user_infos = yield self.store.get_users_by_id_case_insensitive(user_id)
  465. result = None
  466. if not user_infos:
  467. logger.warn("Attempted to login as %s but they do not exist", user_id)
  468. elif len(user_infos) == 1:
  469. # a single match (possibly not exact)
  470. result = user_infos.popitem()
  471. elif user_id in user_infos:
  472. # multiple matches, but one is exact
  473. result = (user_id, user_infos[user_id])
  474. else:
  475. # multiple matches, none of them exact
  476. logger.warn(
  477. "Attempted to login as %s but it matches more than one user "
  478. "inexactly: %r",
  479. user_id, user_infos.keys()
  480. )
  481. defer.returnValue(result)
  482. def get_supported_login_types(self):
  483. """Get a the login types supported for the /login API
  484. By default this is just 'm.login.password' (unless password_enabled is
  485. False in the config file), but password auth providers can provide
  486. other login types.
  487. Returns:
  488. Iterable[str]: login types
  489. """
  490. return self._supported_login_types
  491. @defer.inlineCallbacks
  492. def validate_login(self, username, login_submission):
  493. """Authenticates the user for the /login API
  494. Also used by the user-interactive auth flow to validate
  495. m.login.password auth types.
  496. Args:
  497. username (str): username supplied by the user
  498. login_submission (dict): the whole of the login submission
  499. (including 'type' and other relevant fields)
  500. Returns:
  501. Deferred[str, func]: canonical user id, and optional callback
  502. to be called once the access token and device id are issued
  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. if username.startswith('@'):
  509. qualified_user_id = username
  510. else:
  511. qualified_user_id = UserID(
  512. username, self.hs.hostname
  513. ).to_string()
  514. login_type = login_submission.get("type")
  515. known_login_type = False
  516. # special case to check for "password" for the check_password interface
  517. # for the auth providers
  518. password = login_submission.get("password")
  519. if login_type == LoginType.PASSWORD:
  520. if not self._password_enabled:
  521. raise SynapseError(400, "Password login has been disabled.")
  522. if not password:
  523. raise SynapseError(400, "Missing parameter: password")
  524. for provider in self.password_providers:
  525. if (hasattr(provider, "check_password")
  526. and login_type == LoginType.PASSWORD):
  527. known_login_type = True
  528. is_valid = yield provider.check_password(
  529. qualified_user_id, password,
  530. )
  531. if is_valid:
  532. defer.returnValue((qualified_user_id, None))
  533. if (not hasattr(provider, "get_supported_login_types")
  534. or not hasattr(provider, "check_auth")):
  535. # this password provider doesn't understand custom login types
  536. continue
  537. supported_login_types = provider.get_supported_login_types()
  538. if login_type not in supported_login_types:
  539. # this password provider doesn't understand this login type
  540. continue
  541. known_login_type = True
  542. login_fields = supported_login_types[login_type]
  543. missing_fields = []
  544. login_dict = {}
  545. for f in login_fields:
  546. if f not in login_submission:
  547. missing_fields.append(f)
  548. else:
  549. login_dict[f] = login_submission[f]
  550. if missing_fields:
  551. raise SynapseError(
  552. 400, "Missing parameters for login type %s: %s" % (
  553. login_type,
  554. missing_fields,
  555. ),
  556. )
  557. result = yield provider.check_auth(
  558. username, login_type, login_dict,
  559. )
  560. if result:
  561. if isinstance(result, str):
  562. result = (result, None)
  563. defer.returnValue(result)
  564. if login_type == LoginType.PASSWORD:
  565. known_login_type = True
  566. canonical_user_id = yield self._check_local_password(
  567. qualified_user_id, password,
  568. )
  569. if canonical_user_id:
  570. defer.returnValue((canonical_user_id, None))
  571. if not known_login_type:
  572. raise SynapseError(400, "Unknown login type %s" % login_type)
  573. # unknown username or invalid password. We raise a 403 here, but note
  574. # that if we're doing user-interactive login, it turns all LoginErrors
  575. # into a 401 anyway.
  576. raise LoginError(
  577. 403, "Invalid password",
  578. errcode=Codes.FORBIDDEN
  579. )
  580. @defer.inlineCallbacks
  581. def _check_local_password(self, user_id, password):
  582. """Authenticate a user against the local password database.
  583. user_id is checked case insensitively, but will return None if there are
  584. multiple inexact matches.
  585. Args:
  586. user_id (str): complete @user:id
  587. Returns:
  588. (str) the canonical_user_id, or None if unknown user / bad password
  589. """
  590. lookupres = yield self._find_user_id_and_pwd_hash(user_id)
  591. if not lookupres:
  592. defer.returnValue(None)
  593. (user_id, password_hash) = lookupres
  594. result = yield self.validate_hash(password, password_hash)
  595. if not result:
  596. logger.warn("Failed password login for user %s", user_id)
  597. defer.returnValue(None)
  598. defer.returnValue(user_id)
  599. @defer.inlineCallbacks
  600. def issue_access_token(self, user_id, device_id=None):
  601. access_token = self.macaroon_gen.generate_access_token(user_id)
  602. yield self.store.add_access_token_to_user(user_id, access_token,
  603. device_id)
  604. defer.returnValue(access_token)
  605. def validate_short_term_login_token_and_get_user_id(self, login_token):
  606. auth_api = self.hs.get_auth()
  607. try:
  608. macaroon = pymacaroons.Macaroon.deserialize(login_token)
  609. user_id = auth_api.get_user_id_from_macaroon(macaroon)
  610. auth_api.validate_macaroon(macaroon, "login", True, user_id)
  611. return user_id
  612. except Exception:
  613. raise AuthError(403, "Invalid token", errcode=Codes.FORBIDDEN)
  614. @defer.inlineCallbacks
  615. def delete_access_token(self, access_token):
  616. """Invalidate a single access token
  617. Args:
  618. access_token (str): access token to be deleted
  619. Returns:
  620. Deferred
  621. """
  622. user_info = yield self.auth.get_user_by_access_token(access_token)
  623. yield self.store.delete_access_token(access_token)
  624. # see if any of our auth providers want to know about this
  625. for provider in self.password_providers:
  626. if hasattr(provider, "on_logged_out"):
  627. yield provider.on_logged_out(
  628. user_id=str(user_info["user"]),
  629. device_id=user_info["device_id"],
  630. access_token=access_token,
  631. )
  632. # delete pushers associated with this access token
  633. if user_info["token_id"] is not None:
  634. yield self.hs.get_pusherpool().remove_pushers_by_access_token(
  635. str(user_info["user"]), (user_info["token_id"], )
  636. )
  637. @defer.inlineCallbacks
  638. def delete_access_tokens_for_user(self, user_id, except_token_id=None,
  639. device_id=None):
  640. """Invalidate access tokens belonging to a user
  641. Args:
  642. user_id (str): ID of user the tokens belong to
  643. except_token_id (str|None): access_token ID which should *not* be
  644. deleted
  645. device_id (str|None): ID of device the tokens are associated with.
  646. If None, tokens associated with any device (or no device) will
  647. be deleted
  648. Returns:
  649. Deferred
  650. """
  651. tokens_and_devices = yield self.store.user_delete_access_tokens(
  652. user_id, except_token_id=except_token_id, device_id=device_id,
  653. )
  654. # see if any of our auth providers want to know about this
  655. for provider in self.password_providers:
  656. if hasattr(provider, "on_logged_out"):
  657. for token, token_id, device_id in tokens_and_devices:
  658. yield provider.on_logged_out(
  659. user_id=user_id,
  660. device_id=device_id,
  661. access_token=token,
  662. )
  663. # delete pushers associated with the access tokens
  664. yield self.hs.get_pusherpool().remove_pushers_by_access_token(
  665. user_id, (token_id for _, token_id, _ in tokens_and_devices),
  666. )
  667. @defer.inlineCallbacks
  668. def add_threepid(self, user_id, medium, address, validated_at):
  669. # 'Canonicalise' email addresses down to lower case.
  670. # We've now moving towards the Home Server being the entity that
  671. # is responsible for validating threepids used for resetting passwords
  672. # on accounts, so in future Synapse will gain knowledge of specific
  673. # types (mediums) of threepid. For now, we still use the existing
  674. # infrastructure, but this is the start of synapse gaining knowledge
  675. # of specific types of threepid (and fixes the fact that checking
  676. # for the presence of an email address during password reset was
  677. # case sensitive).
  678. if medium == 'email':
  679. address = address.lower()
  680. yield self.store.user_add_threepid(
  681. user_id, medium, address, validated_at,
  682. self.hs.get_clock().time_msec()
  683. )
  684. @defer.inlineCallbacks
  685. def delete_threepid(self, user_id, medium, address):
  686. # 'Canonicalise' email addresses as per above
  687. if medium == 'email':
  688. address = address.lower()
  689. ret = yield self.store.user_delete_threepid(
  690. user_id, medium, address,
  691. )
  692. defer.returnValue(ret)
  693. def _save_session(self, session):
  694. # TODO: Persistent storage
  695. logger.debug("Saving session %s", session)
  696. session["last_used"] = self.hs.get_clock().time_msec()
  697. self.sessions[session["id"]] = session
  698. def hash(self, password):
  699. """Computes a secure hash of password.
  700. Args:
  701. password (str): Password to hash.
  702. Returns:
  703. Deferred(str): Hashed password.
  704. """
  705. def _do_hash():
  706. return bcrypt.hashpw(password.encode('utf8') + self.hs.config.password_pepper,
  707. bcrypt.gensalt(self.bcrypt_rounds))
  708. return make_deferred_yieldable(threads.deferToThread(_do_hash))
  709. def validate_hash(self, password, stored_hash):
  710. """Validates that self.hash(password) == stored_hash.
  711. Args:
  712. password (str): Password to hash.
  713. stored_hash (str): Expected hash value.
  714. Returns:
  715. Deferred(bool): Whether self.hash(password) == stored_hash.
  716. """
  717. def _do_validate_hash():
  718. return bcrypt.checkpw(
  719. password.encode('utf8') + self.hs.config.password_pepper,
  720. stored_hash.encode('utf8')
  721. )
  722. if stored_hash:
  723. return make_deferred_yieldable(threads.deferToThread(_do_validate_hash))
  724. else:
  725. return defer.succeed(False)
  726. class MacaroonGeneartor(object):
  727. def __init__(self, hs):
  728. self.clock = hs.get_clock()
  729. self.server_name = hs.config.server_name
  730. self.macaroon_secret_key = hs.config.macaroon_secret_key
  731. def generate_access_token(self, user_id, extra_caveats=None):
  732. extra_caveats = extra_caveats or []
  733. macaroon = self._generate_base_macaroon(user_id)
  734. macaroon.add_first_party_caveat("type = access")
  735. # Include a nonce, to make sure that each login gets a different
  736. # access token.
  737. macaroon.add_first_party_caveat("nonce = %s" % (
  738. stringutils.random_string_with_symbols(16),
  739. ))
  740. for caveat in extra_caveats:
  741. macaroon.add_first_party_caveat(caveat)
  742. return macaroon.serialize()
  743. def generate_short_term_login_token(self, user_id, duration_in_ms=(2 * 60 * 1000)):
  744. macaroon = self._generate_base_macaroon(user_id)
  745. macaroon.add_first_party_caveat("type = login")
  746. now = self.clock.time_msec()
  747. expiry = now + duration_in_ms
  748. macaroon.add_first_party_caveat("time < %d" % (expiry,))
  749. return macaroon.serialize()
  750. def generate_delete_pusher_token(self, user_id):
  751. macaroon = self._generate_base_macaroon(user_id)
  752. macaroon.add_first_party_caveat("type = delete_pusher")
  753. return macaroon.serialize()
  754. def _generate_base_macaroon(self, user_id):
  755. macaroon = pymacaroons.Macaroon(
  756. location=self.server_name,
  757. identifier="key",
  758. key=self.macaroon_secret_key)
  759. macaroon.add_first_party_caveat("gen = 1")
  760. macaroon.add_first_party_caveat("user_id = %s" % (user_id,))
  761. return macaroon