test_password_providers.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801
  1. # Copyright 2020 The Matrix.org Foundation C.I.C.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """Tests for the password_auth_provider interface"""
  15. from typing import Any, Type, Union
  16. from unittest.mock import Mock
  17. from twisted.internet import defer
  18. import synapse
  19. from synapse.handlers.auth import load_legacy_password_auth_providers
  20. from synapse.module_api import ModuleApi
  21. from synapse.rest.client import devices, login, logout
  22. from synapse.types import JsonDict
  23. from tests import unittest
  24. from tests.server import FakeChannel
  25. from tests.unittest import override_config
  26. # (possibly experimental) login flows we expect to appear in the list after the normal
  27. # ones
  28. ADDITIONAL_LOGIN_FLOWS = [
  29. {"type": "m.login.application_service"},
  30. {"type": "uk.half-shot.msc2778.login.application_service"},
  31. ]
  32. # a mock instance which the dummy auth providers delegate to, so we can see what's going
  33. # on
  34. mock_password_provider = Mock()
  35. class LegacyPasswordOnlyAuthProvider:
  36. """A legacy password_provider which only implements `check_password`."""
  37. @staticmethod
  38. def parse_config(self):
  39. pass
  40. def __init__(self, config, account_handler):
  41. pass
  42. def check_password(self, *args):
  43. return mock_password_provider.check_password(*args)
  44. class LegacyCustomAuthProvider:
  45. """A legacy password_provider which implements a custom login type."""
  46. @staticmethod
  47. def parse_config(self):
  48. pass
  49. def __init__(self, config, account_handler):
  50. pass
  51. def get_supported_login_types(self):
  52. return {"test.login_type": ["test_field"]}
  53. def check_auth(self, *args):
  54. return mock_password_provider.check_auth(*args)
  55. class CustomAuthProvider:
  56. """A module which registers password_auth_provider callbacks for a custom login type."""
  57. @staticmethod
  58. def parse_config(self):
  59. pass
  60. def __init__(self, config, api: ModuleApi):
  61. api.register_password_auth_provider_callbacks(
  62. auth_checkers={("test.login_type", ("test_field",)): self.check_auth},
  63. )
  64. def check_auth(self, *args):
  65. return mock_password_provider.check_auth(*args)
  66. class LegacyPasswordCustomAuthProvider:
  67. """A password_provider which implements password login via `check_auth`, as well
  68. as a custom type."""
  69. @staticmethod
  70. def parse_config(self):
  71. pass
  72. def __init__(self, config, account_handler):
  73. pass
  74. def get_supported_login_types(self):
  75. return {"m.login.password": ["password"], "test.login_type": ["test_field"]}
  76. def check_auth(self, *args):
  77. return mock_password_provider.check_auth(*args)
  78. class PasswordCustomAuthProvider:
  79. """A module which registers password_auth_provider callbacks for a custom login type.
  80. as well as a password login"""
  81. @staticmethod
  82. def parse_config(self):
  83. pass
  84. def __init__(self, config, api: ModuleApi):
  85. api.register_password_auth_provider_callbacks(
  86. auth_checkers={
  87. ("test.login_type", ("test_field",)): self.check_auth,
  88. ("m.login.password", ("password",)): self.check_auth,
  89. },
  90. )
  91. pass
  92. def check_auth(self, *args):
  93. return mock_password_provider.check_auth(*args)
  94. def check_pass(self, *args):
  95. return mock_password_provider.check_password(*args)
  96. def legacy_providers_config(*providers: Type[Any]) -> dict:
  97. """Returns a config dict that will enable the given legacy password auth providers"""
  98. return {
  99. "password_providers": [
  100. {"module": "%s.%s" % (__name__, provider.__qualname__), "config": {}}
  101. for provider in providers
  102. ]
  103. }
  104. def providers_config(*providers: Type[Any]) -> dict:
  105. """Returns a config dict that will enable the given modules"""
  106. return {
  107. "modules": [
  108. {"module": "%s.%s" % (__name__, provider.__qualname__), "config": {}}
  109. for provider in providers
  110. ]
  111. }
  112. class PasswordAuthProviderTests(unittest.HomeserverTestCase):
  113. servlets = [
  114. synapse.rest.admin.register_servlets,
  115. login.register_servlets,
  116. devices.register_servlets,
  117. logout.register_servlets,
  118. ]
  119. def setUp(self):
  120. # we use a global mock device, so make sure we are starting with a clean slate
  121. mock_password_provider.reset_mock()
  122. super().setUp()
  123. def make_homeserver(self, reactor, clock):
  124. hs = self.setup_test_homeserver()
  125. # Load the modules into the homeserver
  126. module_api = hs.get_module_api()
  127. for module, config in hs.config.modules.loaded_modules:
  128. module(config=config, api=module_api)
  129. load_legacy_password_auth_providers(hs)
  130. return hs
  131. @override_config(legacy_providers_config(LegacyPasswordOnlyAuthProvider))
  132. def test_password_only_auth_progiver_login_legacy(self):
  133. self.password_only_auth_provider_login_test_body()
  134. def password_only_auth_provider_login_test_body(self):
  135. # login flows should only have m.login.password
  136. flows = self._get_login_flows()
  137. self.assertEqual(flows, [{"type": "m.login.password"}] + ADDITIONAL_LOGIN_FLOWS)
  138. # check_password must return an awaitable
  139. mock_password_provider.check_password.return_value = defer.succeed(True)
  140. channel = self._send_password_login("u", "p")
  141. self.assertEqual(channel.code, 200, channel.result)
  142. self.assertEqual("@u:test", channel.json_body["user_id"])
  143. mock_password_provider.check_password.assert_called_once_with("@u:test", "p")
  144. mock_password_provider.reset_mock()
  145. # login with mxid should work too
  146. channel = self._send_password_login("@u:bz", "p")
  147. self.assertEqual(channel.code, 200, channel.result)
  148. self.assertEqual("@u:bz", channel.json_body["user_id"])
  149. mock_password_provider.check_password.assert_called_once_with("@u:bz", "p")
  150. mock_password_provider.reset_mock()
  151. # try a weird username / pass. Honestly it's unclear what we *expect* to happen
  152. # in these cases, but at least we can guard against the API changing
  153. # unexpectedly
  154. channel = self._send_password_login(" USER🙂NAME ", " pASS\U0001F622word ")
  155. self.assertEqual(channel.code, 200, channel.result)
  156. self.assertEqual("@ USER🙂NAME :test", channel.json_body["user_id"])
  157. mock_password_provider.check_password.assert_called_once_with(
  158. "@ USER🙂NAME :test", " pASS😢word "
  159. )
  160. @override_config(legacy_providers_config(LegacyPasswordOnlyAuthProvider))
  161. def test_password_only_auth_provider_ui_auth_legacy(self):
  162. self.password_only_auth_provider_ui_auth_test_body()
  163. def password_only_auth_provider_ui_auth_test_body(self):
  164. """UI Auth should delegate correctly to the password provider"""
  165. # create the user, otherwise access doesn't work
  166. module_api = self.hs.get_module_api()
  167. self.get_success(module_api.register_user("u"))
  168. # log in twice, to get two devices
  169. mock_password_provider.check_password.return_value = defer.succeed(True)
  170. tok1 = self.login("u", "p")
  171. self.login("u", "p", device_id="dev2")
  172. mock_password_provider.reset_mock()
  173. # have the auth provider deny the request to start with
  174. mock_password_provider.check_password.return_value = defer.succeed(False)
  175. # make the initial request which returns a 401
  176. session = self._start_delete_device_session(tok1, "dev2")
  177. mock_password_provider.check_password.assert_not_called()
  178. # Make another request providing the UI auth flow.
  179. channel = self._authed_delete_device(tok1, "dev2", session, "u", "p")
  180. self.assertEqual(channel.code, 401) # XXX why not a 403?
  181. self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN")
  182. mock_password_provider.check_password.assert_called_once_with("@u:test", "p")
  183. mock_password_provider.reset_mock()
  184. # Finally, check the request goes through when we allow it
  185. mock_password_provider.check_password.return_value = defer.succeed(True)
  186. channel = self._authed_delete_device(tok1, "dev2", session, "u", "p")
  187. self.assertEqual(channel.code, 200)
  188. mock_password_provider.check_password.assert_called_once_with("@u:test", "p")
  189. @override_config(legacy_providers_config(LegacyPasswordOnlyAuthProvider))
  190. def test_local_user_fallback_login_legacy(self):
  191. self.local_user_fallback_login_test_body()
  192. def local_user_fallback_login_test_body(self):
  193. """rejected login should fall back to local db"""
  194. self.register_user("localuser", "localpass")
  195. # check_password must return an awaitable
  196. mock_password_provider.check_password.return_value = defer.succeed(False)
  197. channel = self._send_password_login("u", "p")
  198. self.assertEqual(channel.code, 403, channel.result)
  199. channel = self._send_password_login("localuser", "localpass")
  200. self.assertEqual(channel.code, 200, channel.result)
  201. self.assertEqual("@localuser:test", channel.json_body["user_id"])
  202. @override_config(legacy_providers_config(LegacyPasswordOnlyAuthProvider))
  203. def test_local_user_fallback_ui_auth_legacy(self):
  204. self.local_user_fallback_ui_auth_test_body()
  205. def local_user_fallback_ui_auth_test_body(self):
  206. """rejected login should fall back to local db"""
  207. self.register_user("localuser", "localpass")
  208. # have the auth provider deny the request
  209. mock_password_provider.check_password.return_value = defer.succeed(False)
  210. # log in twice, to get two devices
  211. tok1 = self.login("localuser", "localpass")
  212. self.login("localuser", "localpass", device_id="dev2")
  213. mock_password_provider.check_password.reset_mock()
  214. # first delete should give a 401
  215. session = self._start_delete_device_session(tok1, "dev2")
  216. mock_password_provider.check_password.assert_not_called()
  217. # Wrong password
  218. channel = self._authed_delete_device(tok1, "dev2", session, "localuser", "xxx")
  219. self.assertEqual(channel.code, 401) # XXX why not a 403?
  220. self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN")
  221. mock_password_provider.check_password.assert_called_once_with(
  222. "@localuser:test", "xxx"
  223. )
  224. mock_password_provider.reset_mock()
  225. # Right password
  226. channel = self._authed_delete_device(
  227. tok1, "dev2", session, "localuser", "localpass"
  228. )
  229. self.assertEqual(channel.code, 200)
  230. mock_password_provider.check_password.assert_called_once_with(
  231. "@localuser:test", "localpass"
  232. )
  233. @override_config(
  234. {
  235. **legacy_providers_config(LegacyPasswordOnlyAuthProvider),
  236. "password_config": {"localdb_enabled": False},
  237. }
  238. )
  239. def test_no_local_user_fallback_login_legacy(self):
  240. self.no_local_user_fallback_login_test_body()
  241. def no_local_user_fallback_login_test_body(self):
  242. """localdb_enabled can block login with the local password"""
  243. self.register_user("localuser", "localpass")
  244. # check_password must return an awaitable
  245. mock_password_provider.check_password.return_value = defer.succeed(False)
  246. channel = self._send_password_login("localuser", "localpass")
  247. self.assertEqual(channel.code, 403)
  248. self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN")
  249. mock_password_provider.check_password.assert_called_once_with(
  250. "@localuser:test", "localpass"
  251. )
  252. @override_config(
  253. {
  254. **legacy_providers_config(LegacyPasswordOnlyAuthProvider),
  255. "password_config": {"localdb_enabled": False},
  256. }
  257. )
  258. def test_no_local_user_fallback_ui_auth_legacy(self):
  259. self.no_local_user_fallback_ui_auth_test_body()
  260. def no_local_user_fallback_ui_auth_test_body(self):
  261. """localdb_enabled can block ui auth with the local password"""
  262. self.register_user("localuser", "localpass")
  263. # allow login via the auth provider
  264. mock_password_provider.check_password.return_value = defer.succeed(True)
  265. # log in twice, to get two devices
  266. tok1 = self.login("localuser", "p")
  267. self.login("localuser", "p", device_id="dev2")
  268. mock_password_provider.check_password.reset_mock()
  269. # first delete should give a 401
  270. channel = self._delete_device(tok1, "dev2")
  271. self.assertEqual(channel.code, 401)
  272. # m.login.password UIA is permitted because the auth provider allows it,
  273. # even though the localdb does not.
  274. self.assertEqual(channel.json_body["flows"], [{"stages": ["m.login.password"]}])
  275. session = channel.json_body["session"]
  276. mock_password_provider.check_password.assert_not_called()
  277. # now try deleting with the local password
  278. mock_password_provider.check_password.return_value = defer.succeed(False)
  279. channel = self._authed_delete_device(
  280. tok1, "dev2", session, "localuser", "localpass"
  281. )
  282. self.assertEqual(channel.code, 401) # XXX why not a 403?
  283. self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN")
  284. mock_password_provider.check_password.assert_called_once_with(
  285. "@localuser:test", "localpass"
  286. )
  287. @override_config(
  288. {
  289. **legacy_providers_config(LegacyPasswordOnlyAuthProvider),
  290. "password_config": {"enabled": False},
  291. }
  292. )
  293. def test_password_auth_disabled_legacy(self):
  294. self.password_auth_disabled_test_body()
  295. def password_auth_disabled_test_body(self):
  296. """password auth doesn't work if it's disabled across the board"""
  297. # login flows should be empty
  298. flows = self._get_login_flows()
  299. self.assertEqual(flows, ADDITIONAL_LOGIN_FLOWS)
  300. # login shouldn't work and should be rejected with a 400 ("unknown login type")
  301. channel = self._send_password_login("u", "p")
  302. self.assertEqual(channel.code, 400, channel.result)
  303. mock_password_provider.check_password.assert_not_called()
  304. @override_config(legacy_providers_config(LegacyCustomAuthProvider))
  305. def test_custom_auth_provider_login_legacy(self):
  306. self.custom_auth_provider_login_test_body()
  307. @override_config(providers_config(CustomAuthProvider))
  308. def test_custom_auth_provider_login(self):
  309. self.custom_auth_provider_login_test_body()
  310. def custom_auth_provider_login_test_body(self):
  311. # login flows should have the custom flow and m.login.password, since we
  312. # haven't disabled local password lookup.
  313. # (password must come first, because reasons)
  314. flows = self._get_login_flows()
  315. self.assertEqual(
  316. flows,
  317. [{"type": "m.login.password"}, {"type": "test.login_type"}]
  318. + ADDITIONAL_LOGIN_FLOWS,
  319. )
  320. # login with missing param should be rejected
  321. channel = self._send_login("test.login_type", "u")
  322. self.assertEqual(channel.code, 400, channel.result)
  323. mock_password_provider.check_auth.assert_not_called()
  324. mock_password_provider.check_auth.return_value = defer.succeed(
  325. ("@user:bz", None)
  326. )
  327. channel = self._send_login("test.login_type", "u", test_field="y")
  328. self.assertEqual(channel.code, 200, channel.result)
  329. self.assertEqual("@user:bz", channel.json_body["user_id"])
  330. mock_password_provider.check_auth.assert_called_once_with(
  331. "u", "test.login_type", {"test_field": "y"}
  332. )
  333. mock_password_provider.reset_mock()
  334. # try a weird username. Again, it's unclear what we *expect* to happen
  335. # in these cases, but at least we can guard against the API changing
  336. # unexpectedly
  337. mock_password_provider.check_auth.return_value = defer.succeed(
  338. ("@ MALFORMED! :bz", None)
  339. )
  340. channel = self._send_login("test.login_type", " USER🙂NAME ", test_field=" abc ")
  341. self.assertEqual(channel.code, 200, channel.result)
  342. self.assertEqual("@ MALFORMED! :bz", channel.json_body["user_id"])
  343. mock_password_provider.check_auth.assert_called_once_with(
  344. " USER🙂NAME ", "test.login_type", {"test_field": " abc "}
  345. )
  346. @override_config(legacy_providers_config(LegacyCustomAuthProvider))
  347. def test_custom_auth_provider_ui_auth_legacy(self):
  348. self.custom_auth_provider_ui_auth_test_body()
  349. @override_config(providers_config(CustomAuthProvider))
  350. def test_custom_auth_provider_ui_auth(self):
  351. self.custom_auth_provider_ui_auth_test_body()
  352. def custom_auth_provider_ui_auth_test_body(self):
  353. # register the user and log in twice, to get two devices
  354. self.register_user("localuser", "localpass")
  355. tok1 = self.login("localuser", "localpass")
  356. self.login("localuser", "localpass", device_id="dev2")
  357. # make the initial request which returns a 401
  358. channel = self._delete_device(tok1, "dev2")
  359. self.assertEqual(channel.code, 401)
  360. # Ensure that flows are what is expected.
  361. self.assertIn({"stages": ["m.login.password"]}, channel.json_body["flows"])
  362. self.assertIn({"stages": ["test.login_type"]}, channel.json_body["flows"])
  363. session = channel.json_body["session"]
  364. # missing param
  365. body = {
  366. "auth": {
  367. "type": "test.login_type",
  368. "identifier": {"type": "m.id.user", "user": "localuser"},
  369. "session": session,
  370. },
  371. }
  372. channel = self._delete_device(tok1, "dev2", body)
  373. self.assertEqual(channel.code, 400)
  374. # there's a perfectly good M_MISSING_PARAM errcode, but heaven forfend we should
  375. # use it...
  376. self.assertIn("Missing parameters", channel.json_body["error"])
  377. mock_password_provider.check_auth.assert_not_called()
  378. mock_password_provider.reset_mock()
  379. # right params, but authing as the wrong user
  380. mock_password_provider.check_auth.return_value = defer.succeed(
  381. ("@user:bz", None)
  382. )
  383. body["auth"]["test_field"] = "foo"
  384. channel = self._delete_device(tok1, "dev2", body)
  385. self.assertEqual(channel.code, 403)
  386. self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN")
  387. mock_password_provider.check_auth.assert_called_once_with(
  388. "localuser", "test.login_type", {"test_field": "foo"}
  389. )
  390. mock_password_provider.reset_mock()
  391. # and finally, succeed
  392. mock_password_provider.check_auth.return_value = defer.succeed(
  393. ("@localuser:test", None)
  394. )
  395. channel = self._delete_device(tok1, "dev2", body)
  396. self.assertEqual(channel.code, 200)
  397. mock_password_provider.check_auth.assert_called_once_with(
  398. "localuser", "test.login_type", {"test_field": "foo"}
  399. )
  400. @override_config(legacy_providers_config(LegacyCustomAuthProvider))
  401. def test_custom_auth_provider_callback_legacy(self):
  402. self.custom_auth_provider_callback_test_body()
  403. @override_config(providers_config(CustomAuthProvider))
  404. def test_custom_auth_provider_callback(self):
  405. self.custom_auth_provider_callback_test_body()
  406. def custom_auth_provider_callback_test_body(self):
  407. callback = Mock(return_value=defer.succeed(None))
  408. mock_password_provider.check_auth.return_value = defer.succeed(
  409. ("@user:bz", callback)
  410. )
  411. channel = self._send_login("test.login_type", "u", test_field="y")
  412. self.assertEqual(channel.code, 200, channel.result)
  413. self.assertEqual("@user:bz", channel.json_body["user_id"])
  414. mock_password_provider.check_auth.assert_called_once_with(
  415. "u", "test.login_type", {"test_field": "y"}
  416. )
  417. # check the args to the callback
  418. callback.assert_called_once()
  419. call_args, call_kwargs = callback.call_args
  420. # should be one positional arg
  421. self.assertEqual(len(call_args), 1)
  422. self.assertEqual(call_args[0]["user_id"], "@user:bz")
  423. for p in ["user_id", "access_token", "device_id", "home_server"]:
  424. self.assertIn(p, call_args[0])
  425. @override_config(
  426. {
  427. **legacy_providers_config(LegacyCustomAuthProvider),
  428. "password_config": {"enabled": False},
  429. }
  430. )
  431. def test_custom_auth_password_disabled_legacy(self):
  432. self.custom_auth_password_disabled_test_body()
  433. @override_config(
  434. {**providers_config(CustomAuthProvider), "password_config": {"enabled": False}}
  435. )
  436. def test_custom_auth_password_disabled(self):
  437. self.custom_auth_password_disabled_test_body()
  438. def custom_auth_password_disabled_test_body(self):
  439. """Test login with a custom auth provider where password login is disabled"""
  440. self.register_user("localuser", "localpass")
  441. flows = self._get_login_flows()
  442. self.assertEqual(flows, [{"type": "test.login_type"}] + ADDITIONAL_LOGIN_FLOWS)
  443. # login shouldn't work and should be rejected with a 400 ("unknown login type")
  444. channel = self._send_password_login("localuser", "localpass")
  445. self.assertEqual(channel.code, 400, channel.result)
  446. mock_password_provider.check_auth.assert_not_called()
  447. @override_config(
  448. {
  449. **legacy_providers_config(LegacyCustomAuthProvider),
  450. "password_config": {"enabled": False, "localdb_enabled": False},
  451. }
  452. )
  453. def test_custom_auth_password_disabled_localdb_enabled_legacy(self):
  454. self.custom_auth_password_disabled_localdb_enabled_test_body()
  455. @override_config(
  456. {
  457. **providers_config(CustomAuthProvider),
  458. "password_config": {"enabled": False, "localdb_enabled": False},
  459. }
  460. )
  461. def test_custom_auth_password_disabled_localdb_enabled(self):
  462. self.custom_auth_password_disabled_localdb_enabled_test_body()
  463. def custom_auth_password_disabled_localdb_enabled_test_body(self):
  464. """Check the localdb_enabled == enabled == False
  465. Regression test for https://github.com/matrix-org/synapse/issues/8914: check
  466. that setting *both* `localdb_enabled` *and* `password: enabled` to False doesn't
  467. cause an exception.
  468. """
  469. self.register_user("localuser", "localpass")
  470. flows = self._get_login_flows()
  471. self.assertEqual(flows, [{"type": "test.login_type"}] + ADDITIONAL_LOGIN_FLOWS)
  472. # login shouldn't work and should be rejected with a 400 ("unknown login type")
  473. channel = self._send_password_login("localuser", "localpass")
  474. self.assertEqual(channel.code, 400, channel.result)
  475. mock_password_provider.check_auth.assert_not_called()
  476. @override_config(
  477. {
  478. **legacy_providers_config(LegacyPasswordCustomAuthProvider),
  479. "password_config": {"enabled": False},
  480. }
  481. )
  482. def test_password_custom_auth_password_disabled_login_legacy(self):
  483. self.password_custom_auth_password_disabled_login_test_body()
  484. @override_config(
  485. {
  486. **providers_config(PasswordCustomAuthProvider),
  487. "password_config": {"enabled": False},
  488. }
  489. )
  490. def test_password_custom_auth_password_disabled_login(self):
  491. self.password_custom_auth_password_disabled_login_test_body()
  492. def password_custom_auth_password_disabled_login_test_body(self):
  493. """log in with a custom auth provider which implements password, but password
  494. login is disabled"""
  495. self.register_user("localuser", "localpass")
  496. flows = self._get_login_flows()
  497. self.assertEqual(flows, [{"type": "test.login_type"}] + ADDITIONAL_LOGIN_FLOWS)
  498. # login shouldn't work and should be rejected with a 400 ("unknown login type")
  499. channel = self._send_password_login("localuser", "localpass")
  500. self.assertEqual(channel.code, 400, channel.result)
  501. mock_password_provider.check_auth.assert_not_called()
  502. mock_password_provider.check_password.assert_not_called()
  503. @override_config(
  504. {
  505. **legacy_providers_config(LegacyPasswordCustomAuthProvider),
  506. "password_config": {"enabled": False},
  507. }
  508. )
  509. def test_password_custom_auth_password_disabled_ui_auth_legacy(self):
  510. self.password_custom_auth_password_disabled_ui_auth_test_body()
  511. @override_config(
  512. {
  513. **providers_config(PasswordCustomAuthProvider),
  514. "password_config": {"enabled": False},
  515. }
  516. )
  517. def test_password_custom_auth_password_disabled_ui_auth(self):
  518. self.password_custom_auth_password_disabled_ui_auth_test_body()
  519. def password_custom_auth_password_disabled_ui_auth_test_body(self):
  520. """UI Auth with a custom auth provider which implements password, but password
  521. login is disabled"""
  522. # register the user and log in twice via the test login type to get two devices,
  523. self.register_user("localuser", "localpass")
  524. mock_password_provider.check_auth.return_value = defer.succeed(
  525. ("@localuser:test", None)
  526. )
  527. channel = self._send_login("test.login_type", "localuser", test_field="")
  528. self.assertEqual(channel.code, 200, channel.result)
  529. tok1 = channel.json_body["access_token"]
  530. channel = self._send_login(
  531. "test.login_type", "localuser", test_field="", device_id="dev2"
  532. )
  533. self.assertEqual(channel.code, 200, channel.result)
  534. # make the initial request which returns a 401
  535. channel = self._delete_device(tok1, "dev2")
  536. self.assertEqual(channel.code, 401)
  537. # Ensure that flows are what is expected. In particular, "password" should *not*
  538. # be present.
  539. self.assertIn({"stages": ["test.login_type"]}, channel.json_body["flows"])
  540. session = channel.json_body["session"]
  541. mock_password_provider.reset_mock()
  542. # check that auth with password is rejected
  543. body = {
  544. "auth": {
  545. "type": "m.login.password",
  546. "identifier": {"type": "m.id.user", "user": "localuser"},
  547. "password": "localpass",
  548. "session": session,
  549. },
  550. }
  551. channel = self._delete_device(tok1, "dev2", body)
  552. self.assertEqual(channel.code, 400)
  553. self.assertEqual(
  554. "Password login has been disabled.", channel.json_body["error"]
  555. )
  556. mock_password_provider.check_auth.assert_not_called()
  557. mock_password_provider.check_password.assert_not_called()
  558. mock_password_provider.reset_mock()
  559. # successful auth
  560. body["auth"]["type"] = "test.login_type"
  561. body["auth"]["test_field"] = "x"
  562. channel = self._delete_device(tok1, "dev2", body)
  563. self.assertEqual(channel.code, 200)
  564. mock_password_provider.check_auth.assert_called_once_with(
  565. "localuser", "test.login_type", {"test_field": "x"}
  566. )
  567. mock_password_provider.check_password.assert_not_called()
  568. @override_config(
  569. {
  570. **legacy_providers_config(LegacyCustomAuthProvider),
  571. "password_config": {"localdb_enabled": False},
  572. }
  573. )
  574. def test_custom_auth_no_local_user_fallback_legacy(self):
  575. self.custom_auth_no_local_user_fallback_test_body()
  576. @override_config(
  577. {
  578. **providers_config(CustomAuthProvider),
  579. "password_config": {"localdb_enabled": False},
  580. }
  581. )
  582. def test_custom_auth_no_local_user_fallback(self):
  583. self.custom_auth_no_local_user_fallback_test_body()
  584. def custom_auth_no_local_user_fallback_test_body(self):
  585. """Test login with a custom auth provider where the local db is disabled"""
  586. self.register_user("localuser", "localpass")
  587. flows = self._get_login_flows()
  588. self.assertEqual(flows, [{"type": "test.login_type"}] + ADDITIONAL_LOGIN_FLOWS)
  589. # password login shouldn't work and should be rejected with a 400
  590. # ("unknown login type")
  591. channel = self._send_password_login("localuser", "localpass")
  592. self.assertEqual(channel.code, 400, channel.result)
  593. def test_on_logged_out(self):
  594. """Tests that the on_logged_out callback is called when the user logs out."""
  595. self.register_user("rin", "password")
  596. tok = self.login("rin", "password")
  597. self.called = False
  598. async def on_logged_out(user_id, device_id, access_token):
  599. self.called = True
  600. on_logged_out = Mock(side_effect=on_logged_out)
  601. self.hs.get_password_auth_provider().on_logged_out_callbacks.append(
  602. on_logged_out
  603. )
  604. channel = self.make_request(
  605. "POST",
  606. "/_matrix/client/v3/logout",
  607. {},
  608. access_token=tok,
  609. )
  610. self.assertEqual(channel.code, 200)
  611. on_logged_out.assert_called_once()
  612. self.assertTrue(self.called)
  613. def _get_login_flows(self) -> JsonDict:
  614. channel = self.make_request("GET", "/_matrix/client/r0/login")
  615. self.assertEqual(channel.code, 200, channel.result)
  616. return channel.json_body["flows"]
  617. def _send_password_login(self, user: str, password: str) -> FakeChannel:
  618. return self._send_login(type="m.login.password", user=user, password=password)
  619. def _send_login(self, type, user, **params) -> FakeChannel:
  620. params.update({"identifier": {"type": "m.id.user", "user": user}, "type": type})
  621. channel = self.make_request("POST", "/_matrix/client/r0/login", params)
  622. return channel
  623. def _start_delete_device_session(self, access_token, device_id) -> str:
  624. """Make an initial delete device request, and return the UI Auth session ID"""
  625. channel = self._delete_device(access_token, device_id)
  626. self.assertEqual(channel.code, 401)
  627. # Ensure that flows are what is expected.
  628. self.assertIn({"stages": ["m.login.password"]}, channel.json_body["flows"])
  629. return channel.json_body["session"]
  630. def _authed_delete_device(
  631. self,
  632. access_token: str,
  633. device_id: str,
  634. session: str,
  635. user_id: str,
  636. password: str,
  637. ) -> FakeChannel:
  638. """Make a delete device request, authenticating with the given uid/password"""
  639. return self._delete_device(
  640. access_token,
  641. device_id,
  642. {
  643. "auth": {
  644. "type": "m.login.password",
  645. "identifier": {"type": "m.id.user", "user": user_id},
  646. "password": password,
  647. "session": session,
  648. },
  649. },
  650. )
  651. def _delete_device(
  652. self,
  653. access_token: str,
  654. device: str,
  655. body: Union[JsonDict, bytes] = b"",
  656. ) -> FakeChannel:
  657. """Delete an individual device."""
  658. channel = self.make_request(
  659. "DELETE", "devices/" + device, body, access_token=access_token
  660. )
  661. return channel