test_oauth_delegation.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793
  1. # Copyright 2022 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. from http import HTTPStatus
  15. from typing import Any, Dict, Union
  16. from unittest.mock import ANY, AsyncMock, Mock
  17. from urllib.parse import parse_qs
  18. from signedjson.key import (
  19. encode_verify_key_base64,
  20. generate_signing_key,
  21. get_verify_key,
  22. )
  23. from signedjson.sign import sign_json
  24. from twisted.test.proto_helpers import MemoryReactor
  25. from synapse.api.errors import (
  26. AuthError,
  27. Codes,
  28. InvalidClientTokenError,
  29. OAuthInsufficientScopeError,
  30. SynapseError,
  31. )
  32. from synapse.rest import admin
  33. from synapse.rest.client import account, devices, keys, login, logout, register
  34. from synapse.server import HomeServer
  35. from synapse.types import JsonDict
  36. from synapse.util import Clock
  37. from tests.test_utils import FakeResponse, get_awaitable_result
  38. from tests.unittest import HomeserverTestCase, skip_unless
  39. from tests.utils import mock_getRawHeaders
  40. try:
  41. import authlib # noqa: F401
  42. HAS_AUTHLIB = True
  43. except ImportError:
  44. HAS_AUTHLIB = False
  45. # These are a few constants that are used as config parameters in the tests.
  46. SERVER_NAME = "test"
  47. ISSUER = "https://issuer/"
  48. CLIENT_ID = "test-client-id"
  49. CLIENT_SECRET = "test-client-secret"
  50. BASE_URL = "https://synapse/"
  51. SCOPES = ["openid"]
  52. AUTHORIZATION_ENDPOINT = ISSUER + "authorize"
  53. TOKEN_ENDPOINT = ISSUER + "token"
  54. USERINFO_ENDPOINT = ISSUER + "userinfo"
  55. WELL_KNOWN = ISSUER + ".well-known/openid-configuration"
  56. JWKS_URI = ISSUER + ".well-known/jwks.json"
  57. INTROSPECTION_ENDPOINT = ISSUER + "introspect"
  58. SYNAPSE_ADMIN_SCOPE = "urn:synapse:admin:*"
  59. MATRIX_USER_SCOPE = "urn:matrix:org.matrix.msc2967.client:api:*"
  60. MATRIX_GUEST_SCOPE = "urn:matrix:org.matrix.msc2967.client:api:guest"
  61. MATRIX_DEVICE_SCOPE_PREFIX = "urn:matrix:org.matrix.msc2967.client:device:"
  62. DEVICE = "AABBCCDD"
  63. MATRIX_DEVICE_SCOPE = MATRIX_DEVICE_SCOPE_PREFIX + DEVICE
  64. SUBJECT = "abc-def-ghi"
  65. USERNAME = "test-user"
  66. USER_ID = "@" + USERNAME + ":" + SERVER_NAME
  67. async def get_json(url: str) -> JsonDict:
  68. # Mock get_json calls to handle jwks & oidc discovery endpoints
  69. if url == WELL_KNOWN:
  70. # Minimal discovery document, as defined in OpenID.Discovery
  71. # https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
  72. return {
  73. "issuer": ISSUER,
  74. "authorization_endpoint": AUTHORIZATION_ENDPOINT,
  75. "token_endpoint": TOKEN_ENDPOINT,
  76. "jwks_uri": JWKS_URI,
  77. "userinfo_endpoint": USERINFO_ENDPOINT,
  78. "introspection_endpoint": INTROSPECTION_ENDPOINT,
  79. "response_types_supported": ["code"],
  80. "subject_types_supported": ["public"],
  81. "id_token_signing_alg_values_supported": ["RS256"],
  82. }
  83. elif url == JWKS_URI:
  84. return {"keys": []}
  85. return {}
  86. @skip_unless(HAS_AUTHLIB, "requires authlib")
  87. class MSC3861OAuthDelegation(HomeserverTestCase):
  88. servlets = [
  89. account.register_servlets,
  90. devices.register_servlets,
  91. keys.register_servlets,
  92. register.register_servlets,
  93. login.register_servlets,
  94. logout.register_servlets,
  95. admin.register_servlets,
  96. ]
  97. def default_config(self) -> Dict[str, Any]:
  98. config = super().default_config()
  99. config["public_baseurl"] = BASE_URL
  100. config["disable_registration"] = True
  101. config["experimental_features"] = {
  102. "msc3861": {
  103. "enabled": True,
  104. "issuer": ISSUER,
  105. "client_id": CLIENT_ID,
  106. "client_auth_method": "client_secret_post",
  107. "client_secret": CLIENT_SECRET,
  108. }
  109. }
  110. return config
  111. def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
  112. self.http_client = Mock(spec=["get_json"])
  113. self.http_client.get_json.side_effect = get_json
  114. self.http_client.user_agent = b"Synapse Test"
  115. hs = self.setup_test_homeserver(proxied_http_client=self.http_client)
  116. self.auth = hs.get_auth()
  117. return hs
  118. def _assertParams(self) -> None:
  119. """Assert that the request parameters are correct."""
  120. params = parse_qs(self.http_client.request.call_args[1]["data"].decode("utf-8"))
  121. self.assertEqual(params["token"], ["mockAccessToken"])
  122. self.assertEqual(params["client_id"], [CLIENT_ID])
  123. self.assertEqual(params["client_secret"], [CLIENT_SECRET])
  124. def test_inactive_token(self) -> None:
  125. """The handler should return a 403 where the token is inactive."""
  126. self.http_client.request = AsyncMock(
  127. return_value=FakeResponse.json(
  128. code=200,
  129. payload={"active": False},
  130. )
  131. )
  132. request = Mock(args={})
  133. request.args[b"access_token"] = [b"mockAccessToken"]
  134. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  135. self.get_failure(self.auth.get_user_by_req(request), InvalidClientTokenError)
  136. self.http_client.get_json.assert_called_once_with(WELL_KNOWN)
  137. self.http_client.request.assert_called_once_with(
  138. method="POST", uri=INTROSPECTION_ENDPOINT, data=ANY, headers=ANY
  139. )
  140. self._assertParams()
  141. def test_active_no_scope(self) -> None:
  142. """The handler should return a 403 where no scope is given."""
  143. self.http_client.request = AsyncMock(
  144. return_value=FakeResponse.json(
  145. code=200,
  146. payload={"active": True},
  147. )
  148. )
  149. request = Mock(args={})
  150. request.args[b"access_token"] = [b"mockAccessToken"]
  151. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  152. self.get_failure(self.auth.get_user_by_req(request), InvalidClientTokenError)
  153. self.http_client.get_json.assert_called_once_with(WELL_KNOWN)
  154. self.http_client.request.assert_called_once_with(
  155. method="POST", uri=INTROSPECTION_ENDPOINT, data=ANY, headers=ANY
  156. )
  157. self._assertParams()
  158. def test_active_user_no_subject(self) -> None:
  159. """The handler should return a 500 when no subject is present."""
  160. self.http_client.request = AsyncMock(
  161. return_value=FakeResponse.json(
  162. code=200,
  163. payload={"active": True, "scope": " ".join([MATRIX_USER_SCOPE])},
  164. )
  165. )
  166. request = Mock(args={})
  167. request.args[b"access_token"] = [b"mockAccessToken"]
  168. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  169. self.get_failure(self.auth.get_user_by_req(request), InvalidClientTokenError)
  170. self.http_client.get_json.assert_called_once_with(WELL_KNOWN)
  171. self.http_client.request.assert_called_once_with(
  172. method="POST", uri=INTROSPECTION_ENDPOINT, data=ANY, headers=ANY
  173. )
  174. self._assertParams()
  175. def test_active_no_user_scope(self) -> None:
  176. """The handler should return a 500 when no subject is present."""
  177. self.http_client.request = AsyncMock(
  178. return_value=FakeResponse.json(
  179. code=200,
  180. payload={
  181. "active": True,
  182. "sub": SUBJECT,
  183. "scope": " ".join([MATRIX_DEVICE_SCOPE]),
  184. },
  185. )
  186. )
  187. request = Mock(args={})
  188. request.args[b"access_token"] = [b"mockAccessToken"]
  189. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  190. self.get_failure(self.auth.get_user_by_req(request), InvalidClientTokenError)
  191. self.http_client.get_json.assert_called_once_with(WELL_KNOWN)
  192. self.http_client.request.assert_called_once_with(
  193. method="POST", uri=INTROSPECTION_ENDPOINT, data=ANY, headers=ANY
  194. )
  195. self._assertParams()
  196. def test_active_admin_not_user(self) -> None:
  197. """The handler should raise when the scope has admin right but not user."""
  198. self.http_client.request = AsyncMock(
  199. return_value=FakeResponse.json(
  200. code=200,
  201. payload={
  202. "active": True,
  203. "sub": SUBJECT,
  204. "scope": " ".join([SYNAPSE_ADMIN_SCOPE]),
  205. "username": USERNAME,
  206. },
  207. )
  208. )
  209. request = Mock(args={})
  210. request.args[b"access_token"] = [b"mockAccessToken"]
  211. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  212. self.get_failure(self.auth.get_user_by_req(request), InvalidClientTokenError)
  213. self.http_client.get_json.assert_called_once_with(WELL_KNOWN)
  214. self.http_client.request.assert_called_once_with(
  215. method="POST", uri=INTROSPECTION_ENDPOINT, data=ANY, headers=ANY
  216. )
  217. self._assertParams()
  218. def test_active_admin(self) -> None:
  219. """The handler should return a requester with admin rights."""
  220. self.http_client.request = AsyncMock(
  221. return_value=FakeResponse.json(
  222. code=200,
  223. payload={
  224. "active": True,
  225. "sub": SUBJECT,
  226. "scope": " ".join([SYNAPSE_ADMIN_SCOPE, MATRIX_USER_SCOPE]),
  227. "username": USERNAME,
  228. },
  229. )
  230. )
  231. request = Mock(args={})
  232. request.args[b"access_token"] = [b"mockAccessToken"]
  233. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  234. requester = self.get_success(self.auth.get_user_by_req(request))
  235. self.http_client.get_json.assert_called_once_with(WELL_KNOWN)
  236. self.http_client.request.assert_called_once_with(
  237. method="POST", uri=INTROSPECTION_ENDPOINT, data=ANY, headers=ANY
  238. )
  239. self._assertParams()
  240. self.assertEqual(requester.user.to_string(), "@%s:%s" % (USERNAME, SERVER_NAME))
  241. self.assertEqual(requester.is_guest, False)
  242. self.assertEqual(requester.device_id, None)
  243. self.assertEqual(
  244. get_awaitable_result(self.auth.is_server_admin(requester)), True
  245. )
  246. def test_active_admin_highest_privilege(self) -> None:
  247. """The handler should resolve to the most permissive scope."""
  248. self.http_client.request = AsyncMock(
  249. return_value=FakeResponse.json(
  250. code=200,
  251. payload={
  252. "active": True,
  253. "sub": SUBJECT,
  254. "scope": " ".join(
  255. [SYNAPSE_ADMIN_SCOPE, MATRIX_USER_SCOPE, MATRIX_GUEST_SCOPE]
  256. ),
  257. "username": USERNAME,
  258. },
  259. )
  260. )
  261. request = Mock(args={})
  262. request.args[b"access_token"] = [b"mockAccessToken"]
  263. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  264. requester = self.get_success(self.auth.get_user_by_req(request))
  265. self.http_client.get_json.assert_called_once_with(WELL_KNOWN)
  266. self.http_client.request.assert_called_once_with(
  267. method="POST", uri=INTROSPECTION_ENDPOINT, data=ANY, headers=ANY
  268. )
  269. self._assertParams()
  270. self.assertEqual(requester.user.to_string(), "@%s:%s" % (USERNAME, SERVER_NAME))
  271. self.assertEqual(requester.is_guest, False)
  272. self.assertEqual(requester.device_id, None)
  273. self.assertEqual(
  274. get_awaitable_result(self.auth.is_server_admin(requester)), True
  275. )
  276. def test_active_user(self) -> None:
  277. """The handler should return a requester with normal user rights."""
  278. self.http_client.request = AsyncMock(
  279. return_value=FakeResponse.json(
  280. code=200,
  281. payload={
  282. "active": True,
  283. "sub": SUBJECT,
  284. "scope": " ".join([MATRIX_USER_SCOPE]),
  285. "username": USERNAME,
  286. },
  287. )
  288. )
  289. request = Mock(args={})
  290. request.args[b"access_token"] = [b"mockAccessToken"]
  291. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  292. requester = self.get_success(self.auth.get_user_by_req(request))
  293. self.http_client.get_json.assert_called_once_with(WELL_KNOWN)
  294. self.http_client.request.assert_called_once_with(
  295. method="POST", uri=INTROSPECTION_ENDPOINT, data=ANY, headers=ANY
  296. )
  297. self._assertParams()
  298. self.assertEqual(requester.user.to_string(), "@%s:%s" % (USERNAME, SERVER_NAME))
  299. self.assertEqual(requester.is_guest, False)
  300. self.assertEqual(requester.device_id, None)
  301. self.assertEqual(
  302. get_awaitable_result(self.auth.is_server_admin(requester)), False
  303. )
  304. def test_active_user_admin_impersonation(self) -> None:
  305. """The handler should return a requester with normal user rights
  306. and an user ID matching the one specified in query param `user_id`"""
  307. self.http_client.request = AsyncMock(
  308. return_value=FakeResponse.json(
  309. code=200,
  310. payload={
  311. "active": True,
  312. "sub": SUBJECT,
  313. "scope": " ".join([SYNAPSE_ADMIN_SCOPE, MATRIX_USER_SCOPE]),
  314. "username": USERNAME,
  315. },
  316. )
  317. )
  318. request = Mock(args={})
  319. request.args[b"access_token"] = [b"mockAccessToken"]
  320. impersonated_user_id = f"@{USERNAME}:{SERVER_NAME}"
  321. request.args[b"_oidc_admin_impersonate_user_id"] = [
  322. impersonated_user_id.encode("ascii")
  323. ]
  324. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  325. requester = self.get_success(self.auth.get_user_by_req(request))
  326. self.http_client.get_json.assert_called_once_with(WELL_KNOWN)
  327. self.http_client.request.assert_called_once_with(
  328. method="POST", uri=INTROSPECTION_ENDPOINT, data=ANY, headers=ANY
  329. )
  330. self._assertParams()
  331. self.assertEqual(requester.user.to_string(), impersonated_user_id)
  332. self.assertEqual(requester.is_guest, False)
  333. self.assertEqual(requester.device_id, None)
  334. self.assertEqual(
  335. get_awaitable_result(self.auth.is_server_admin(requester)), False
  336. )
  337. def test_active_user_with_device(self) -> None:
  338. """The handler should return a requester with normal user rights and a device ID."""
  339. self.http_client.request = AsyncMock(
  340. return_value=FakeResponse.json(
  341. code=200,
  342. payload={
  343. "active": True,
  344. "sub": SUBJECT,
  345. "scope": " ".join([MATRIX_USER_SCOPE, MATRIX_DEVICE_SCOPE]),
  346. "username": USERNAME,
  347. },
  348. )
  349. )
  350. request = Mock(args={})
  351. request.args[b"access_token"] = [b"mockAccessToken"]
  352. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  353. requester = self.get_success(self.auth.get_user_by_req(request))
  354. self.http_client.get_json.assert_called_once_with(WELL_KNOWN)
  355. self.http_client.request.assert_called_once_with(
  356. method="POST", uri=INTROSPECTION_ENDPOINT, data=ANY, headers=ANY
  357. )
  358. self._assertParams()
  359. self.assertEqual(requester.user.to_string(), "@%s:%s" % (USERNAME, SERVER_NAME))
  360. self.assertEqual(requester.is_guest, False)
  361. self.assertEqual(
  362. get_awaitable_result(self.auth.is_server_admin(requester)), False
  363. )
  364. self.assertEqual(requester.device_id, DEVICE)
  365. def test_multiple_devices(self) -> None:
  366. """The handler should raise an error if multiple devices are found in the scope."""
  367. self.http_client.request = AsyncMock(
  368. return_value=FakeResponse.json(
  369. code=200,
  370. payload={
  371. "active": True,
  372. "sub": SUBJECT,
  373. "scope": " ".join(
  374. [
  375. MATRIX_USER_SCOPE,
  376. f"{MATRIX_DEVICE_SCOPE_PREFIX}AABBCC",
  377. f"{MATRIX_DEVICE_SCOPE_PREFIX}DDEEFF",
  378. ]
  379. ),
  380. "username": USERNAME,
  381. },
  382. )
  383. )
  384. request = Mock(args={})
  385. request.args[b"access_token"] = [b"mockAccessToken"]
  386. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  387. self.get_failure(self.auth.get_user_by_req(request), AuthError)
  388. def test_active_guest_not_allowed(self) -> None:
  389. """The handler should return an insufficient scope error."""
  390. self.http_client.request = AsyncMock(
  391. return_value=FakeResponse.json(
  392. code=200,
  393. payload={
  394. "active": True,
  395. "sub": SUBJECT,
  396. "scope": " ".join([MATRIX_GUEST_SCOPE, MATRIX_DEVICE_SCOPE]),
  397. "username": USERNAME,
  398. },
  399. )
  400. )
  401. request = Mock(args={})
  402. request.args[b"access_token"] = [b"mockAccessToken"]
  403. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  404. error = self.get_failure(
  405. self.auth.get_user_by_req(request), OAuthInsufficientScopeError
  406. )
  407. self.http_client.get_json.assert_called_once_with(WELL_KNOWN)
  408. self.http_client.request.assert_called_once_with(
  409. method="POST", uri=INTROSPECTION_ENDPOINT, data=ANY, headers=ANY
  410. )
  411. self._assertParams()
  412. self.assertEqual(
  413. getattr(error.value, "headers", {})["WWW-Authenticate"],
  414. 'Bearer error="insufficient_scope", scope="urn:matrix:org.matrix.msc2967.client:api:*"',
  415. )
  416. def test_active_guest_allowed(self) -> None:
  417. """The handler should return a requester with guest user rights and a device ID."""
  418. self.http_client.request = AsyncMock(
  419. return_value=FakeResponse.json(
  420. code=200,
  421. payload={
  422. "active": True,
  423. "sub": SUBJECT,
  424. "scope": " ".join([MATRIX_GUEST_SCOPE, MATRIX_DEVICE_SCOPE]),
  425. "username": USERNAME,
  426. },
  427. )
  428. )
  429. request = Mock(args={})
  430. request.args[b"access_token"] = [b"mockAccessToken"]
  431. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  432. requester = self.get_success(
  433. self.auth.get_user_by_req(request, allow_guest=True)
  434. )
  435. self.http_client.get_json.assert_called_once_with(WELL_KNOWN)
  436. self.http_client.request.assert_called_once_with(
  437. method="POST", uri=INTROSPECTION_ENDPOINT, data=ANY, headers=ANY
  438. )
  439. self._assertParams()
  440. self.assertEqual(requester.user.to_string(), "@%s:%s" % (USERNAME, SERVER_NAME))
  441. self.assertEqual(requester.is_guest, True)
  442. self.assertEqual(
  443. get_awaitable_result(self.auth.is_server_admin(requester)), False
  444. )
  445. self.assertEqual(requester.device_id, DEVICE)
  446. def test_unavailable_introspection_endpoint(self) -> None:
  447. """The handler should return an internal server error."""
  448. request = Mock(args={})
  449. request.args[b"access_token"] = [b"mockAccessToken"]
  450. request.requestHeaders.getRawHeaders = mock_getRawHeaders()
  451. # The introspection endpoint is returning an error.
  452. self.http_client.request = AsyncMock(
  453. return_value=FakeResponse(code=500, body=b"Internal Server Error")
  454. )
  455. error = self.get_failure(self.auth.get_user_by_req(request), SynapseError)
  456. self.assertEqual(error.value.code, 503)
  457. # The introspection endpoint request fails.
  458. self.http_client.request = AsyncMock(side_effect=Exception())
  459. error = self.get_failure(self.auth.get_user_by_req(request), SynapseError)
  460. self.assertEqual(error.value.code, 503)
  461. # The introspection endpoint does not return a JSON object.
  462. self.http_client.request = AsyncMock(
  463. return_value=FakeResponse.json(
  464. code=200, payload=["this is an array", "not an object"]
  465. )
  466. )
  467. error = self.get_failure(self.auth.get_user_by_req(request), SynapseError)
  468. self.assertEqual(error.value.code, 503)
  469. # The introspection endpoint does not return valid JSON.
  470. self.http_client.request = AsyncMock(
  471. return_value=FakeResponse(code=200, body=b"this is not valid JSON")
  472. )
  473. error = self.get_failure(self.auth.get_user_by_req(request), SynapseError)
  474. self.assertEqual(error.value.code, 503)
  475. def test_introspection_token_cache(self) -> None:
  476. access_token = "open_sesame"
  477. self.http_client.request = AsyncMock(
  478. return_value=FakeResponse.json(
  479. code=200,
  480. payload={"active": "true", "scope": "guest", "jti": access_token},
  481. )
  482. )
  483. # first call should cache response
  484. # Mpyp ignores below are due to mypy not understanding the dynamic substitution of msc3861 auth code
  485. # for regular auth code via the config
  486. self.get_success(
  487. self.auth._introspect_token(access_token) # type: ignore[attr-defined]
  488. )
  489. introspection_token = self.auth._token_cache.get(access_token) # type: ignore[attr-defined]
  490. self.assertEqual(introspection_token["jti"], access_token)
  491. # there's been one http request
  492. self.http_client.request.assert_called_once()
  493. # second call should pull from cache, there should still be only one http request
  494. token = self.get_success(self.auth._introspect_token(access_token)) # type: ignore[attr-defined]
  495. self.http_client.request.assert_called_once()
  496. self.assertEqual(token["jti"], access_token)
  497. # advance past five minutes and check that cache expired - there should be more than one http call now
  498. self.reactor.advance(360)
  499. token_2 = self.get_success(self.auth._introspect_token(access_token)) # type: ignore[attr-defined]
  500. self.assertEqual(self.http_client.request.call_count, 2)
  501. self.assertEqual(token_2["jti"], access_token)
  502. # test that if a cached token is expired, a fresh token will be pulled from authorizing server - first add a
  503. # token with a soon-to-expire `exp` field to the cache
  504. self.http_client.request = AsyncMock(
  505. return_value=FakeResponse.json(
  506. code=200,
  507. payload={
  508. "active": "true",
  509. "scope": "guest",
  510. "jti": "stale",
  511. "exp": self.clock.time() + 100,
  512. },
  513. )
  514. )
  515. self.get_success(
  516. self.auth._introspect_token("stale") # type: ignore[attr-defined]
  517. )
  518. introspection_token = self.auth._token_cache.get("stale") # type: ignore[attr-defined]
  519. self.assertEqual(introspection_token["jti"], "stale")
  520. self.assertEqual(self.http_client.request.call_count, 1)
  521. # advance the reactor past the token expiry but less than the cache expiry
  522. self.reactor.advance(120)
  523. self.assertEqual(self.auth._token_cache.get("stale"), introspection_token) # type: ignore[attr-defined]
  524. # check that the next call causes another http request (which will fail because the token is technically expired
  525. # but the important thing is we discard the token from the cache and try the network)
  526. self.get_failure(
  527. self.auth._introspect_token("stale"), InvalidClientTokenError # type: ignore[attr-defined]
  528. )
  529. self.assertEqual(self.http_client.request.call_count, 2)
  530. def test_revocation_endpoint(self) -> None:
  531. # mock introspection response and then admin verification response
  532. self.http_client.request = AsyncMock(
  533. side_effect=[
  534. FakeResponse.json(
  535. code=200, payload={"active": True, "jti": "open_sesame"}
  536. ),
  537. FakeResponse.json(
  538. code=200,
  539. payload={
  540. "active": True,
  541. "sub": SUBJECT,
  542. "scope": " ".join([SYNAPSE_ADMIN_SCOPE, MATRIX_USER_SCOPE]),
  543. "username": USERNAME,
  544. },
  545. ),
  546. ]
  547. )
  548. # cache a token to delete
  549. introspection_token = self.get_success(
  550. self.auth._introspect_token("open_sesame") # type: ignore[attr-defined]
  551. )
  552. self.assertEqual(self.auth._token_cache.get("open_sesame"), introspection_token) # type: ignore[attr-defined]
  553. # delete the revoked token
  554. introspection_token_id = "open_sesame"
  555. url = f"/_synapse/admin/v1/OIDC_token_revocation/{introspection_token_id}"
  556. channel = self.make_request("DELETE", url, access_token="mockAccessToken")
  557. self.assertEqual(channel.code, 200)
  558. self.assertEqual(self.auth._token_cache.get("open_sesame"), None) # type: ignore[attr-defined]
  559. def make_device_keys(self, user_id: str, device_id: str) -> JsonDict:
  560. # We only generate a master key to simplify the test.
  561. master_signing_key = generate_signing_key(device_id)
  562. master_verify_key = encode_verify_key_base64(get_verify_key(master_signing_key))
  563. return {
  564. "master_key": sign_json(
  565. {
  566. "user_id": user_id,
  567. "usage": ["master"],
  568. "keys": {"ed25519:" + master_verify_key: master_verify_key},
  569. },
  570. user_id,
  571. master_signing_key,
  572. ),
  573. }
  574. def test_cross_signing(self) -> None:
  575. """Try uploading device keys with OAuth delegation enabled."""
  576. self.http_client.request = AsyncMock(
  577. return_value=FakeResponse.json(
  578. code=200,
  579. payload={
  580. "active": True,
  581. "sub": SUBJECT,
  582. "scope": " ".join([MATRIX_USER_SCOPE, MATRIX_DEVICE_SCOPE]),
  583. "username": USERNAME,
  584. },
  585. )
  586. )
  587. keys_upload_body = self.make_device_keys(USER_ID, DEVICE)
  588. channel = self.make_request(
  589. "POST",
  590. "/_matrix/client/v3/keys/device_signing/upload",
  591. keys_upload_body,
  592. access_token="mockAccessToken",
  593. )
  594. self.assertEqual(channel.code, 200, channel.json_body)
  595. channel = self.make_request(
  596. "POST",
  597. "/_matrix/client/v3/keys/device_signing/upload",
  598. keys_upload_body,
  599. access_token="mockAccessToken",
  600. )
  601. self.assertEqual(channel.code, HTTPStatus.NOT_IMPLEMENTED, channel.json_body)
  602. def expect_unauthorized(
  603. self, method: str, path: str, content: Union[bytes, str, JsonDict] = ""
  604. ) -> None:
  605. channel = self.make_request(method, path, content, shorthand=False)
  606. self.assertEqual(channel.code, 401, channel.json_body)
  607. def expect_unrecognized(
  608. self, method: str, path: str, content: Union[bytes, str, JsonDict] = ""
  609. ) -> None:
  610. channel = self.make_request(method, path, content)
  611. self.assertEqual(channel.code, 404, channel.json_body)
  612. self.assertEqual(
  613. channel.json_body["errcode"], Codes.UNRECOGNIZED, channel.json_body
  614. )
  615. def test_uia_endpoints(self) -> None:
  616. """Test that endpoints that were removed in MSC2964 are no longer available."""
  617. # This is just an endpoint that should remain visible (but requires auth):
  618. self.expect_unauthorized("GET", "/_matrix/client/v3/devices")
  619. # This remains usable, but will require a uia scope:
  620. self.expect_unauthorized(
  621. "POST", "/_matrix/client/v3/keys/device_signing/upload"
  622. )
  623. def test_3pid_endpoints(self) -> None:
  624. """Test that 3pid account management endpoints that were removed in MSC2964 are no longer available."""
  625. # Remains and requires auth:
  626. self.expect_unauthorized("GET", "/_matrix/client/v3/account/3pid")
  627. self.expect_unauthorized(
  628. "POST",
  629. "/_matrix/client/v3/account/3pid/bind",
  630. {
  631. "client_secret": "foo",
  632. "id_access_token": "bar",
  633. "id_server": "foo",
  634. "sid": "bar",
  635. },
  636. )
  637. self.expect_unauthorized("POST", "/_matrix/client/v3/account/3pid/unbind", {})
  638. # These are gone:
  639. self.expect_unrecognized(
  640. "POST", "/_matrix/client/v3/account/3pid"
  641. ) # deprecated
  642. self.expect_unrecognized("POST", "/_matrix/client/v3/account/3pid/add")
  643. self.expect_unrecognized("POST", "/_matrix/client/v3/account/3pid/delete")
  644. self.expect_unrecognized(
  645. "POST", "/_matrix/client/v3/account/3pid/email/requestToken"
  646. )
  647. self.expect_unrecognized(
  648. "POST", "/_matrix/client/v3/account/3pid/msisdn/requestToken"
  649. )
  650. def test_account_management_endpoints_removed(self) -> None:
  651. """Test that account management endpoints that were removed in MSC2964 are no longer available."""
  652. self.expect_unrecognized("POST", "/_matrix/client/v3/account/deactivate")
  653. self.expect_unrecognized("POST", "/_matrix/client/v3/account/password")
  654. self.expect_unrecognized(
  655. "POST", "/_matrix/client/v3/account/password/email/requestToken"
  656. )
  657. self.expect_unrecognized(
  658. "POST", "/_matrix/client/v3/account/password/msisdn/requestToken"
  659. )
  660. def test_registration_endpoints_removed(self) -> None:
  661. """Test that registration endpoints that were removed in MSC2964 are no longer available."""
  662. self.expect_unrecognized(
  663. "GET", "/_matrix/client/v1/register/m.login.registration_token/validity"
  664. )
  665. # This is still available for AS registrations
  666. # self.expect_unrecognized("POST", "/_matrix/client/v3/register")
  667. self.expect_unrecognized("GET", "/_matrix/client/v3/register/available")
  668. self.expect_unrecognized(
  669. "POST", "/_matrix/client/v3/register/email/requestToken"
  670. )
  671. self.expect_unrecognized(
  672. "POST", "/_matrix/client/v3/register/msisdn/requestToken"
  673. )
  674. def test_session_management_endpoints_removed(self) -> None:
  675. """Test that session management endpoints that were removed in MSC2964 are no longer available."""
  676. self.expect_unrecognized("GET", "/_matrix/client/v3/login")
  677. self.expect_unrecognized("POST", "/_matrix/client/v3/login")
  678. self.expect_unrecognized("GET", "/_matrix/client/v3/login/sso/redirect")
  679. self.expect_unrecognized("POST", "/_matrix/client/v3/logout")
  680. self.expect_unrecognized("POST", "/_matrix/client/v3/logout/all")
  681. self.expect_unrecognized("POST", "/_matrix/client/v3/refresh")
  682. self.expect_unrecognized("GET", "/_matrix/static/client/login")
  683. def test_device_management_endpoints_removed(self) -> None:
  684. """Test that device management endpoints that were removed in MSC2964 are no longer available."""
  685. self.expect_unrecognized("POST", "/_matrix/client/v3/delete_devices")
  686. self.expect_unrecognized("DELETE", "/_matrix/client/v3/devices/{DEVICE}")
  687. def test_openid_endpoints_removed(self) -> None:
  688. """Test that OpenID id_token endpoints that were removed in MSC2964 are no longer available."""
  689. self.expect_unrecognized(
  690. "POST", "/_matrix/client/v3/user/{USERNAME}/openid/request_token"
  691. )
  692. def test_admin_api_endpoints_removed(self) -> None:
  693. """Test that admin API endpoints that were removed in MSC2964 are no longer available."""
  694. self.expect_unrecognized("GET", "/_synapse/admin/v1/registration_tokens")
  695. self.expect_unrecognized("POST", "/_synapse/admin/v1/registration_tokens/new")
  696. self.expect_unrecognized("GET", "/_synapse/admin/v1/registration_tokens/abcd")
  697. self.expect_unrecognized("PUT", "/_synapse/admin/v1/registration_tokens/abcd")
  698. self.expect_unrecognized(
  699. "DELETE", "/_synapse/admin/v1/registration_tokens/abcd"
  700. )
  701. self.expect_unrecognized("POST", "/_synapse/admin/v1/reset_password/foo")
  702. self.expect_unrecognized("POST", "/_synapse/admin/v1/users/foo/login")
  703. self.expect_unrecognized("GET", "/_synapse/admin/v1/register")
  704. self.expect_unrecognized("POST", "/_synapse/admin/v1/register")
  705. self.expect_unrecognized("GET", "/_synapse/admin/v1/users/foo/admin")
  706. self.expect_unrecognized("PUT", "/_synapse/admin/v1/users/foo/admin")
  707. self.expect_unrecognized("POST", "/_synapse/admin/v1/account_validity/validity")