test_auth.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  1. # Copyright 2018 New Vector
  2. # Copyright 2020-2021 The Matrix.org Foundation C.I.C
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. from typing import Optional, Union
  16. from twisted.internet.defer import succeed
  17. import synapse.rest.admin
  18. from synapse.api.constants import LoginType
  19. from synapse.handlers.ui_auth.checkers import UserInteractiveAuthChecker
  20. from synapse.rest.client import account, auth, devices, login, register
  21. from synapse.rest.synapse.client import build_synapse_client_resource_tree
  22. from synapse.types import JsonDict, UserID
  23. from tests import unittest
  24. from tests.handlers.test_oidc import HAS_OIDC
  25. from tests.rest.client.utils import TEST_OIDC_CONFIG
  26. from tests.server import FakeChannel
  27. from tests.unittest import override_config, skip_unless
  28. class DummyRecaptchaChecker(UserInteractiveAuthChecker):
  29. def __init__(self, hs):
  30. super().__init__(hs)
  31. self.recaptcha_attempts = []
  32. def check_auth(self, authdict, clientip):
  33. self.recaptcha_attempts.append((authdict, clientip))
  34. return succeed(True)
  35. class FallbackAuthTests(unittest.HomeserverTestCase):
  36. servlets = [
  37. auth.register_servlets,
  38. register.register_servlets,
  39. ]
  40. hijack_auth = False
  41. def make_homeserver(self, reactor, clock):
  42. config = self.default_config()
  43. config["enable_registration_captcha"] = True
  44. config["recaptcha_public_key"] = "brokencake"
  45. config["registrations_require_3pid"] = []
  46. hs = self.setup_test_homeserver(config=config)
  47. return hs
  48. def prepare(self, reactor, clock, hs):
  49. self.recaptcha_checker = DummyRecaptchaChecker(hs)
  50. auth_handler = hs.get_auth_handler()
  51. auth_handler.checkers[LoginType.RECAPTCHA] = self.recaptcha_checker
  52. def register(self, expected_response: int, body: JsonDict) -> FakeChannel:
  53. """Make a register request."""
  54. channel = self.make_request("POST", "register", body)
  55. self.assertEqual(channel.code, expected_response)
  56. return channel
  57. def recaptcha(
  58. self,
  59. session: str,
  60. expected_post_response: int,
  61. post_session: Optional[str] = None,
  62. ) -> None:
  63. """Get and respond to a fallback recaptcha. Returns the second request."""
  64. if post_session is None:
  65. post_session = session
  66. channel = self.make_request(
  67. "GET", "auth/m.login.recaptcha/fallback/web?session=" + session
  68. )
  69. self.assertEqual(channel.code, 200)
  70. channel = self.make_request(
  71. "POST",
  72. "auth/m.login.recaptcha/fallback/web?session="
  73. + post_session
  74. + "&g-recaptcha-response=a",
  75. )
  76. self.assertEqual(channel.code, expected_post_response)
  77. # The recaptcha handler is called with the response given
  78. attempts = self.recaptcha_checker.recaptcha_attempts
  79. self.assertEqual(len(attempts), 1)
  80. self.assertEqual(attempts[0][0]["response"], "a")
  81. def test_fallback_captcha(self):
  82. """Ensure that fallback auth via a captcha works."""
  83. # Returns a 401 as per the spec
  84. channel = self.register(
  85. 401,
  86. {"username": "user", "type": "m.login.password", "password": "bar"},
  87. )
  88. # Grab the session
  89. session = channel.json_body["session"]
  90. # Assert our configured public key is being given
  91. self.assertEqual(
  92. channel.json_body["params"]["m.login.recaptcha"]["public_key"], "brokencake"
  93. )
  94. # Complete the recaptcha step.
  95. self.recaptcha(session, 200)
  96. # also complete the dummy auth
  97. self.register(200, {"auth": {"session": session, "type": "m.login.dummy"}})
  98. # Now we should have fulfilled a complete auth flow, including
  99. # the recaptcha fallback step, we can then send a
  100. # request to the register API with the session in the authdict.
  101. channel = self.register(200, {"auth": {"session": session}})
  102. # We're given a registered user.
  103. self.assertEqual(channel.json_body["user_id"], "@user:test")
  104. def test_complete_operation_unknown_session(self):
  105. """
  106. Attempting to mark an invalid session as complete should error.
  107. """
  108. # Make the initial request to register. (Later on a different password
  109. # will be used.)
  110. # Returns a 401 as per the spec
  111. channel = self.register(
  112. 401, {"username": "user", "type": "m.login.password", "password": "bar"}
  113. )
  114. # Grab the session
  115. session = channel.json_body["session"]
  116. # Assert our configured public key is being given
  117. self.assertEqual(
  118. channel.json_body["params"]["m.login.recaptcha"]["public_key"], "brokencake"
  119. )
  120. # Attempt to complete the recaptcha step with an unknown session.
  121. # This results in an error.
  122. self.recaptcha(session, 400, session + "unknown")
  123. class UIAuthTests(unittest.HomeserverTestCase):
  124. servlets = [
  125. auth.register_servlets,
  126. devices.register_servlets,
  127. login.register_servlets,
  128. synapse.rest.admin.register_servlets_for_client_rest_resource,
  129. register.register_servlets,
  130. ]
  131. def default_config(self):
  132. config = super().default_config()
  133. # public_baseurl uses an http:// scheme because FakeChannel.isSecure() returns
  134. # False, so synapse will see the requested uri as http://..., so using http in
  135. # the public_baseurl stops Synapse trying to redirect to https.
  136. config["public_baseurl"] = "http://synapse.test"
  137. if HAS_OIDC:
  138. # we enable OIDC as a way of testing SSO flows
  139. oidc_config = {}
  140. oidc_config.update(TEST_OIDC_CONFIG)
  141. oidc_config["allow_existing_users"] = True
  142. config["oidc_config"] = oidc_config
  143. return config
  144. def create_resource_dict(self):
  145. resource_dict = super().create_resource_dict()
  146. resource_dict.update(build_synapse_client_resource_tree(self.hs))
  147. return resource_dict
  148. def prepare(self, reactor, clock, hs):
  149. self.user_pass = "pass"
  150. self.user = self.register_user("test", self.user_pass)
  151. self.device_id = "dev1"
  152. self.user_tok = self.login("test", self.user_pass, self.device_id)
  153. def delete_device(
  154. self,
  155. access_token: str,
  156. device: str,
  157. expected_response: int,
  158. body: Union[bytes, JsonDict] = b"",
  159. ) -> FakeChannel:
  160. """Delete an individual device."""
  161. channel = self.make_request(
  162. "DELETE",
  163. "devices/" + device,
  164. body,
  165. access_token=access_token,
  166. )
  167. # Ensure the response is sane.
  168. self.assertEqual(channel.code, expected_response)
  169. return channel
  170. def delete_devices(self, expected_response: int, body: JsonDict) -> FakeChannel:
  171. """Delete 1 or more devices."""
  172. # Note that this uses the delete_devices endpoint so that we can modify
  173. # the payload half-way through some tests.
  174. channel = self.make_request(
  175. "POST",
  176. "delete_devices",
  177. body,
  178. access_token=self.user_tok,
  179. )
  180. # Ensure the response is sane.
  181. self.assertEqual(channel.code, expected_response)
  182. return channel
  183. def test_ui_auth(self):
  184. """
  185. Test user interactive authentication outside of registration.
  186. """
  187. # Attempt to delete this device.
  188. # Returns a 401 as per the spec
  189. channel = self.delete_device(self.user_tok, self.device_id, 401)
  190. # Grab the session
  191. session = channel.json_body["session"]
  192. # Ensure that flows are what is expected.
  193. self.assertIn({"stages": ["m.login.password"]}, channel.json_body["flows"])
  194. # Make another request providing the UI auth flow.
  195. self.delete_device(
  196. self.user_tok,
  197. self.device_id,
  198. 200,
  199. {
  200. "auth": {
  201. "type": "m.login.password",
  202. "identifier": {"type": "m.id.user", "user": self.user},
  203. "password": self.user_pass,
  204. "session": session,
  205. },
  206. },
  207. )
  208. def test_grandfathered_identifier(self):
  209. """Check behaviour without "identifier" dict
  210. Synapse used to require clients to submit a "user" field for m.login.password
  211. UIA - check that still works.
  212. """
  213. channel = self.delete_device(self.user_tok, self.device_id, 401)
  214. session = channel.json_body["session"]
  215. # Make another request providing the UI auth flow.
  216. self.delete_device(
  217. self.user_tok,
  218. self.device_id,
  219. 200,
  220. {
  221. "auth": {
  222. "type": "m.login.password",
  223. "user": self.user,
  224. "password": self.user_pass,
  225. "session": session,
  226. },
  227. },
  228. )
  229. def test_can_change_body(self):
  230. """
  231. The client dict can be modified during the user interactive authentication session.
  232. Note that it is not spec compliant to modify the client dict during a
  233. user interactive authentication session, but many clients currently do.
  234. When Synapse is updated to be spec compliant, the call to re-use the
  235. session ID should be rejected.
  236. """
  237. # Create a second login.
  238. self.login("test", self.user_pass, "dev2")
  239. # Attempt to delete the first device.
  240. # Returns a 401 as per the spec
  241. channel = self.delete_devices(401, {"devices": [self.device_id]})
  242. # Grab the session
  243. session = channel.json_body["session"]
  244. # Ensure that flows are what is expected.
  245. self.assertIn({"stages": ["m.login.password"]}, channel.json_body["flows"])
  246. # Make another request providing the UI auth flow, but try to delete the
  247. # second device.
  248. self.delete_devices(
  249. 200,
  250. {
  251. "devices": ["dev2"],
  252. "auth": {
  253. "type": "m.login.password",
  254. "identifier": {"type": "m.id.user", "user": self.user},
  255. "password": self.user_pass,
  256. "session": session,
  257. },
  258. },
  259. )
  260. def test_cannot_change_uri(self):
  261. """
  262. The initial requested URI cannot be modified during the user interactive authentication session.
  263. """
  264. # Create a second login.
  265. self.login("test", self.user_pass, "dev2")
  266. # Attempt to delete the first device.
  267. # Returns a 401 as per the spec
  268. channel = self.delete_device(self.user_tok, self.device_id, 401)
  269. # Grab the session
  270. session = channel.json_body["session"]
  271. # Ensure that flows are what is expected.
  272. self.assertIn({"stages": ["m.login.password"]}, channel.json_body["flows"])
  273. # Make another request providing the UI auth flow, but try to delete the
  274. # second device. This results in an error.
  275. #
  276. # This makes use of the fact that the device ID is embedded into the URL.
  277. self.delete_device(
  278. self.user_tok,
  279. "dev2",
  280. 403,
  281. {
  282. "auth": {
  283. "type": "m.login.password",
  284. "identifier": {"type": "m.id.user", "user": self.user},
  285. "password": self.user_pass,
  286. "session": session,
  287. },
  288. },
  289. )
  290. @unittest.override_config({"ui_auth": {"session_timeout": "5s"}})
  291. def test_can_reuse_session(self):
  292. """
  293. The session can be reused if configured.
  294. Compare to test_cannot_change_uri.
  295. """
  296. # Create a second and third login.
  297. self.login("test", self.user_pass, "dev2")
  298. self.login("test", self.user_pass, "dev3")
  299. # Attempt to delete a device. This works since the user just logged in.
  300. self.delete_device(self.user_tok, "dev2", 200)
  301. # Move the clock forward past the validation timeout.
  302. self.reactor.advance(6)
  303. # Deleting another devices throws the user into UI auth.
  304. channel = self.delete_device(self.user_tok, "dev3", 401)
  305. # Grab the session
  306. session = channel.json_body["session"]
  307. # Ensure that flows are what is expected.
  308. self.assertIn({"stages": ["m.login.password"]}, channel.json_body["flows"])
  309. # Make another request providing the UI auth flow.
  310. self.delete_device(
  311. self.user_tok,
  312. "dev3",
  313. 200,
  314. {
  315. "auth": {
  316. "type": "m.login.password",
  317. "identifier": {"type": "m.id.user", "user": self.user},
  318. "password": self.user_pass,
  319. "session": session,
  320. },
  321. },
  322. )
  323. # Make another request, but try to delete the first device. This works
  324. # due to re-using the previous session.
  325. #
  326. # Note that *no auth* information is provided, not even a session iD!
  327. self.delete_device(self.user_tok, self.device_id, 200)
  328. @skip_unless(HAS_OIDC, "requires OIDC")
  329. @override_config({"oidc_config": TEST_OIDC_CONFIG})
  330. def test_ui_auth_via_sso(self):
  331. """Test a successful UI Auth flow via SSO
  332. This includes:
  333. * hitting the UIA SSO redirect endpoint
  334. * checking it serves a confirmation page which links to the OIDC provider
  335. * calling back to the synapse oidc callback
  336. * checking that the original operation succeeds
  337. """
  338. # log the user in
  339. remote_user_id = UserID.from_string(self.user).localpart
  340. login_resp = self.helper.login_via_oidc(remote_user_id)
  341. self.assertEqual(login_resp["user_id"], self.user)
  342. # initiate a UI Auth process by attempting to delete the device
  343. channel = self.delete_device(self.user_tok, self.device_id, 401)
  344. # check that SSO is offered
  345. flows = channel.json_body["flows"]
  346. self.assertIn({"stages": ["m.login.sso"]}, flows)
  347. # run the UIA-via-SSO flow
  348. session_id = channel.json_body["session"]
  349. channel = self.helper.auth_via_oidc(
  350. {"sub": remote_user_id}, ui_auth_session_id=session_id
  351. )
  352. # that should serve a confirmation page
  353. self.assertEqual(channel.code, 200, channel.result)
  354. # and now the delete request should succeed.
  355. self.delete_device(
  356. self.user_tok,
  357. self.device_id,
  358. 200,
  359. body={"auth": {"session": session_id}},
  360. )
  361. @skip_unless(HAS_OIDC, "requires OIDC")
  362. @override_config({"oidc_config": TEST_OIDC_CONFIG})
  363. def test_does_not_offer_password_for_sso_user(self):
  364. login_resp = self.helper.login_via_oidc("username")
  365. user_tok = login_resp["access_token"]
  366. device_id = login_resp["device_id"]
  367. # now call the device deletion API: we should get the option to auth with SSO
  368. # and not password.
  369. channel = self.delete_device(user_tok, device_id, 401)
  370. flows = channel.json_body["flows"]
  371. self.assertEqual(flows, [{"stages": ["m.login.sso"]}])
  372. def test_does_not_offer_sso_for_password_user(self):
  373. channel = self.delete_device(self.user_tok, self.device_id, 401)
  374. flows = channel.json_body["flows"]
  375. self.assertEqual(flows, [{"stages": ["m.login.password"]}])
  376. @skip_unless(HAS_OIDC, "requires OIDC")
  377. @override_config({"oidc_config": TEST_OIDC_CONFIG})
  378. def test_offers_both_flows_for_upgraded_user(self):
  379. """A user that had a password and then logged in with SSO should get both flows"""
  380. login_resp = self.helper.login_via_oidc(UserID.from_string(self.user).localpart)
  381. self.assertEqual(login_resp["user_id"], self.user)
  382. channel = self.delete_device(self.user_tok, self.device_id, 401)
  383. flows = channel.json_body["flows"]
  384. # we have no particular expectations of ordering here
  385. self.assertIn({"stages": ["m.login.password"]}, flows)
  386. self.assertIn({"stages": ["m.login.sso"]}, flows)
  387. self.assertEqual(len(flows), 2)
  388. @skip_unless(HAS_OIDC, "requires OIDC")
  389. @override_config({"oidc_config": TEST_OIDC_CONFIG})
  390. def test_ui_auth_fails_for_incorrect_sso_user(self):
  391. """If the user tries to authenticate with the wrong SSO user, they get an error"""
  392. # log the user in
  393. login_resp = self.helper.login_via_oidc(UserID.from_string(self.user).localpart)
  394. self.assertEqual(login_resp["user_id"], self.user)
  395. # start a UI Auth flow by attempting to delete a device
  396. channel = self.delete_device(self.user_tok, self.device_id, 401)
  397. flows = channel.json_body["flows"]
  398. self.assertIn({"stages": ["m.login.sso"]}, flows)
  399. session_id = channel.json_body["session"]
  400. # do the OIDC auth, but auth as the wrong user
  401. channel = self.helper.auth_via_oidc(
  402. {"sub": "wrong_user"}, ui_auth_session_id=session_id
  403. )
  404. # that should return a failure message
  405. self.assertSubstring("We were unable to validate", channel.text_body)
  406. # ... and the delete op should now fail with a 403
  407. self.delete_device(
  408. self.user_tok, self.device_id, 403, body={"auth": {"session": session_id}}
  409. )
  410. class RefreshAuthTests(unittest.HomeserverTestCase):
  411. servlets = [
  412. auth.register_servlets,
  413. account.register_servlets,
  414. login.register_servlets,
  415. synapse.rest.admin.register_servlets_for_client_rest_resource,
  416. register.register_servlets,
  417. ]
  418. hijack_auth = False
  419. def prepare(self, reactor, clock, hs):
  420. self.user_pass = "pass"
  421. self.user = self.register_user("test", self.user_pass)
  422. def test_login_issue_refresh_token(self):
  423. """
  424. A login response should include a refresh_token only if asked.
  425. """
  426. # Test login
  427. body = {"type": "m.login.password", "user": "test", "password": self.user_pass}
  428. login_without_refresh = self.make_request(
  429. "POST", "/_matrix/client/r0/login", body
  430. )
  431. self.assertEqual(login_without_refresh.code, 200, login_without_refresh.result)
  432. self.assertNotIn("refresh_token", login_without_refresh.json_body)
  433. login_with_refresh = self.make_request(
  434. "POST",
  435. "/_matrix/client/r0/login?org.matrix.msc2918.refresh_token=true",
  436. body,
  437. )
  438. self.assertEqual(login_with_refresh.code, 200, login_with_refresh.result)
  439. self.assertIn("refresh_token", login_with_refresh.json_body)
  440. self.assertIn("expires_in_ms", login_with_refresh.json_body)
  441. def test_register_issue_refresh_token(self):
  442. """
  443. A register response should include a refresh_token only if asked.
  444. """
  445. register_without_refresh = self.make_request(
  446. "POST",
  447. "/_matrix/client/r0/register",
  448. {
  449. "username": "test2",
  450. "password": self.user_pass,
  451. "auth": {"type": LoginType.DUMMY},
  452. },
  453. )
  454. self.assertEqual(
  455. register_without_refresh.code, 200, register_without_refresh.result
  456. )
  457. self.assertNotIn("refresh_token", register_without_refresh.json_body)
  458. register_with_refresh = self.make_request(
  459. "POST",
  460. "/_matrix/client/r0/register?org.matrix.msc2918.refresh_token=true",
  461. {
  462. "username": "test3",
  463. "password": self.user_pass,
  464. "auth": {"type": LoginType.DUMMY},
  465. },
  466. )
  467. self.assertEqual(register_with_refresh.code, 200, register_with_refresh.result)
  468. self.assertIn("refresh_token", register_with_refresh.json_body)
  469. self.assertIn("expires_in_ms", register_with_refresh.json_body)
  470. def test_token_refresh(self):
  471. """
  472. A refresh token can be used to issue a new access token.
  473. """
  474. body = {"type": "m.login.password", "user": "test", "password": self.user_pass}
  475. login_response = self.make_request(
  476. "POST",
  477. "/_matrix/client/r0/login?org.matrix.msc2918.refresh_token=true",
  478. body,
  479. )
  480. self.assertEqual(login_response.code, 200, login_response.result)
  481. refresh_response = self.make_request(
  482. "POST",
  483. "/_matrix/client/unstable/org.matrix.msc2918.refresh_token/refresh",
  484. {"refresh_token": login_response.json_body["refresh_token"]},
  485. )
  486. self.assertEqual(refresh_response.code, 200, refresh_response.result)
  487. self.assertIn("access_token", refresh_response.json_body)
  488. self.assertIn("refresh_token", refresh_response.json_body)
  489. self.assertIn("expires_in_ms", refresh_response.json_body)
  490. # The access and refresh tokens should be different from the original ones after refresh
  491. self.assertNotEqual(
  492. login_response.json_body["access_token"],
  493. refresh_response.json_body["access_token"],
  494. )
  495. self.assertNotEqual(
  496. login_response.json_body["refresh_token"],
  497. refresh_response.json_body["refresh_token"],
  498. )
  499. @override_config({"access_token_lifetime": "1m"})
  500. def test_refresh_token_expiration(self):
  501. """
  502. The access token should have some time as specified in the config.
  503. """
  504. body = {"type": "m.login.password", "user": "test", "password": self.user_pass}
  505. login_response = self.make_request(
  506. "POST",
  507. "/_matrix/client/r0/login?org.matrix.msc2918.refresh_token=true",
  508. body,
  509. )
  510. self.assertEqual(login_response.code, 200, login_response.result)
  511. self.assertApproximates(
  512. login_response.json_body["expires_in_ms"], 60 * 1000, 100
  513. )
  514. refresh_response = self.make_request(
  515. "POST",
  516. "/_matrix/client/unstable/org.matrix.msc2918.refresh_token/refresh",
  517. {"refresh_token": login_response.json_body["refresh_token"]},
  518. )
  519. self.assertEqual(refresh_response.code, 200, refresh_response.result)
  520. self.assertApproximates(
  521. refresh_response.json_body["expires_in_ms"], 60 * 1000, 100
  522. )
  523. def test_refresh_token_invalidation(self):
  524. """Refresh tokens are invalidated after first use of the next token.
  525. A refresh token is considered invalid if:
  526. - it was already used at least once
  527. - and either
  528. - the next access token was used
  529. - the next refresh token was used
  530. The chain of tokens goes like this:
  531. login -|-> first_refresh -> third_refresh (fails)
  532. |-> second_refresh -> fifth_refresh
  533. |-> fourth_refresh (fails)
  534. """
  535. body = {"type": "m.login.password", "user": "test", "password": self.user_pass}
  536. login_response = self.make_request(
  537. "POST",
  538. "/_matrix/client/r0/login?org.matrix.msc2918.refresh_token=true",
  539. body,
  540. )
  541. self.assertEqual(login_response.code, 200, login_response.result)
  542. # This first refresh should work properly
  543. first_refresh_response = self.make_request(
  544. "POST",
  545. "/_matrix/client/unstable/org.matrix.msc2918.refresh_token/refresh",
  546. {"refresh_token": login_response.json_body["refresh_token"]},
  547. )
  548. self.assertEqual(
  549. first_refresh_response.code, 200, first_refresh_response.result
  550. )
  551. # This one as well, since the token in the first one was never used
  552. second_refresh_response = self.make_request(
  553. "POST",
  554. "/_matrix/client/unstable/org.matrix.msc2918.refresh_token/refresh",
  555. {"refresh_token": login_response.json_body["refresh_token"]},
  556. )
  557. self.assertEqual(
  558. second_refresh_response.code, 200, second_refresh_response.result
  559. )
  560. # This one should not, since the token from the first refresh is not valid anymore
  561. third_refresh_response = self.make_request(
  562. "POST",
  563. "/_matrix/client/unstable/org.matrix.msc2918.refresh_token/refresh",
  564. {"refresh_token": first_refresh_response.json_body["refresh_token"]},
  565. )
  566. self.assertEqual(
  567. third_refresh_response.code, 401, third_refresh_response.result
  568. )
  569. # The associated access token should also be invalid
  570. whoami_response = self.make_request(
  571. "GET",
  572. "/_matrix/client/r0/account/whoami",
  573. access_token=first_refresh_response.json_body["access_token"],
  574. )
  575. self.assertEqual(whoami_response.code, 401, whoami_response.result)
  576. # But all other tokens should work (they will expire after some time)
  577. for access_token in [
  578. second_refresh_response.json_body["access_token"],
  579. login_response.json_body["access_token"],
  580. ]:
  581. whoami_response = self.make_request(
  582. "GET", "/_matrix/client/r0/account/whoami", access_token=access_token
  583. )
  584. self.assertEqual(whoami_response.code, 200, whoami_response.result)
  585. # Now that the access token from the last valid refresh was used once, refreshing with the N-1 token should fail
  586. fourth_refresh_response = self.make_request(
  587. "POST",
  588. "/_matrix/client/unstable/org.matrix.msc2918.refresh_token/refresh",
  589. {"refresh_token": login_response.json_body["refresh_token"]},
  590. )
  591. self.assertEqual(
  592. fourth_refresh_response.code, 403, fourth_refresh_response.result
  593. )
  594. # But refreshing from the last valid refresh token still works
  595. fifth_refresh_response = self.make_request(
  596. "POST",
  597. "/_matrix/client/unstable/org.matrix.msc2918.refresh_token/refresh",
  598. {"refresh_token": second_refresh_response.json_body["refresh_token"]},
  599. )
  600. self.assertEqual(
  601. fifth_refresh_response.code, 200, fifth_refresh_response.result
  602. )