test_oauth_delegation.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. # Copyright 2023 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. import os
  15. from unittest.mock import Mock
  16. from synapse.config import ConfigError
  17. from synapse.config.homeserver import HomeServerConfig
  18. from synapse.module_api import ModuleApi
  19. from synapse.types import JsonDict
  20. from tests.server import get_clock, setup_test_homeserver
  21. from tests.unittest import TestCase, skip_unless
  22. from tests.utils import default_config
  23. try:
  24. import authlib # noqa: F401
  25. HAS_AUTHLIB = True
  26. except ImportError:
  27. HAS_AUTHLIB = False
  28. # These are a few constants that are used as config parameters in the tests.
  29. SERVER_NAME = "test"
  30. ISSUER = "https://issuer/"
  31. CLIENT_ID = "test-client-id"
  32. CLIENT_SECRET = "test-client-secret"
  33. BASE_URL = "https://synapse/"
  34. class CustomAuthModule:
  35. """A module which registers a password auth provider."""
  36. @staticmethod
  37. def parse_config(config: JsonDict) -> None:
  38. pass
  39. def __init__(self, config: None, api: ModuleApi):
  40. api.register_password_auth_provider_callbacks(
  41. auth_checkers={("m.login.password", ("password",)): Mock()},
  42. )
  43. @skip_unless(HAS_AUTHLIB, "requires authlib")
  44. class MSC3861OAuthDelegation(TestCase):
  45. """Test that the Homeserver fails to initialize if the config is invalid."""
  46. def setUp(self) -> None:
  47. self.config_dict: JsonDict = {
  48. **default_config("test"),
  49. "public_baseurl": BASE_URL,
  50. "enable_registration": False,
  51. "experimental_features": {
  52. "msc3861": {
  53. "enabled": True,
  54. "issuer": ISSUER,
  55. "client_id": CLIENT_ID,
  56. "client_auth_method": "client_secret_post",
  57. "client_secret": CLIENT_SECRET,
  58. }
  59. },
  60. }
  61. def parse_config(self) -> HomeServerConfig:
  62. config = HomeServerConfig()
  63. config.parse_config_dict(self.config_dict, "", "")
  64. return config
  65. def test_client_secret_post_works(self) -> None:
  66. self.config_dict["experimental_features"]["msc3861"].update(
  67. client_auth_method="client_secret_post",
  68. client_secret=CLIENT_SECRET,
  69. )
  70. self.parse_config()
  71. def test_client_secret_post_requires_client_secret(self) -> None:
  72. self.config_dict["experimental_features"]["msc3861"].update(
  73. client_auth_method="client_secret_post",
  74. client_secret=None,
  75. )
  76. with self.assertRaises(ConfigError):
  77. self.parse_config()
  78. def test_client_secret_basic_works(self) -> None:
  79. self.config_dict["experimental_features"]["msc3861"].update(
  80. client_auth_method="client_secret_basic",
  81. client_secret=CLIENT_SECRET,
  82. )
  83. self.parse_config()
  84. def test_client_secret_basic_requires_client_secret(self) -> None:
  85. self.config_dict["experimental_features"]["msc3861"].update(
  86. client_auth_method="client_secret_basic",
  87. client_secret=None,
  88. )
  89. with self.assertRaises(ConfigError):
  90. self.parse_config()
  91. def test_client_secret_jwt_works(self) -> None:
  92. self.config_dict["experimental_features"]["msc3861"].update(
  93. client_auth_method="client_secret_jwt",
  94. client_secret=CLIENT_SECRET,
  95. )
  96. self.parse_config()
  97. def test_client_secret_jwt_requires_client_secret(self) -> None:
  98. self.config_dict["experimental_features"]["msc3861"].update(
  99. client_auth_method="client_secret_jwt",
  100. client_secret=None,
  101. )
  102. with self.assertRaises(ConfigError):
  103. self.parse_config()
  104. def test_invalid_client_auth_method(self) -> None:
  105. self.config_dict["experimental_features"]["msc3861"].update(
  106. client_auth_method="invalid",
  107. )
  108. with self.assertRaises(ConfigError):
  109. self.parse_config()
  110. def test_private_key_jwt_requires_jwk(self) -> None:
  111. self.config_dict["experimental_features"]["msc3861"].update(
  112. client_auth_method="private_key_jwt",
  113. )
  114. with self.assertRaises(ConfigError):
  115. self.parse_config()
  116. def test_private_key_jwt_works(self) -> None:
  117. self.config_dict["experimental_features"]["msc3861"].update(
  118. client_auth_method="private_key_jwt",
  119. jwk={
  120. "p": "-frVdP_tZ-J_nIR6HNMDq1N7aunwm51nAqNnhqIyuA8ikx7LlQED1tt2LD3YEvYyW8nxE2V95HlCRZXQPMiRJBFOsbmYkzl2t-MpavTaObB_fct_JqcRtdXddg4-_ihdjRDwUOreq_dpWh6MIKsC3UyekfkHmeEJg5YpOTL15j8",
  121. "kty": "RSA",
  122. "q": "oFw-Enr_YozQB1ab-kawn4jY3yHi8B1nSmYT0s8oTCflrmps5BFJfCkHL5ij3iY15z0o2m0N-jjB1oSJ98O4RayEEYNQlHnTNTl0kRIWzpoqblHUIxVcahIpP_xTovBJzwi8XXoLGqHOOMA-r40LSyVgP2Ut8D9qBwV6_UfT0LU",
  123. "d": "WFkDPYo4b4LIS64D_QtQfGGuAObPvc3HFfp9VZXyq3SJR58XZRHE0jqtlEMNHhOTgbMYS3w8nxPQ_qVzY-5hs4fIanwvB64mAoOGl0qMHO65DTD_WsGFwzYClJPBVniavkLE2Hmpu8IGe6lGliN8vREC6_4t69liY-XcN_ECboVtC2behKkLOEASOIMuS7YcKAhTJFJwkl1dqDlliEn5A4u4xy7nuWQz3juB1OFdKlwGA5dfhDNglhoLIwNnkLsUPPFO-WB5ZNEW35xxHOToxj4bShvDuanVA6mJPtTKjz0XibjB36bj_nF_j7EtbE2PdGJ2KevAVgElR4lqS4ISgQ",
  124. "e": "AQAB",
  125. "kid": "test",
  126. "qi": "cPfNk8l8W5exVNNea4d7QZZ8Qr8LgHghypYAxz8PQh1fNa8Ya1SNUDVzC2iHHhszxxA0vB9C7jGze8dBrvnzWYF1XvQcqNIVVgHhD57R1Nm3dj2NoHIKe0Cu4bCUtP8xnZQUN4KX7y4IIcgRcBWG1hT6DEYZ4BxqicnBXXNXAUI",
  127. "dp": "dKlMHvslV1sMBQaKWpNb3gPq0B13TZhqr3-E2_8sPlvJ3fD8P4CmwwnOn50JDuhY3h9jY5L06sBwXjspYISVv8hX-ndMLkEeF3lrJeA5S70D8rgakfZcPIkffm3tlf1Ok3v5OzoxSv3-67Df4osMniyYwDUBCB5Oq1tTx77xpU8",
  128. "dq": "S4ooU1xNYYcjl9FcuJEEMqKsRrAXzzSKq6laPTwIp5dDwt2vXeAm1a4eDHXC-6rUSZGt5PbqVqzV4s-cjnJMI8YYkIdjNg4NSE1Ac_YpeDl3M3Colb5CQlU7yUB7xY2bt0NOOFp9UJZYJrOo09mFMGjy5eorsbitoZEbVqS3SuE",
  129. "n": "nJbYKqFwnURKimaviyDFrNLD3gaKR1JW343Qem25VeZxoMq1665RHVoO8n1oBm4ClZdjIiZiVdpyqzD5-Ow12YQgQEf1ZHP3CCcOQQhU57Rh5XvScTe5IxYVkEW32IW2mp_CJ6WfjYpfeL4azarVk8H3Vr59d1rSrKTVVinVdZer9YLQyC_rWAQNtHafPBMrf6RYiNGV9EiYn72wFIXlLlBYQ9Fx7bfe1PaL6qrQSsZP3_rSpuvVdLh1lqGeCLR0pyclA9uo5m2tMyCXuuGQLbA_QJm5xEc7zd-WFdux2eXF045oxnSZ_kgQt-pdN7AxGWOVvwoTf9am6mSkEdv6iw",
  130. },
  131. )
  132. self.parse_config()
  133. def test_registration_cannot_be_enabled(self) -> None:
  134. self.config_dict["enable_registration"] = True
  135. with self.assertRaises(ConfigError):
  136. self.parse_config()
  137. def test_user_consent_cannot_be_enabled(self) -> None:
  138. tmpdir = self.mktemp()
  139. os.mkdir(tmpdir)
  140. self.config_dict["user_consent"] = {
  141. "require_at_registration": True,
  142. "version": "1",
  143. "template_dir": tmpdir,
  144. "server_notice_content": {
  145. "msgtype": "m.text",
  146. "body": "foo",
  147. },
  148. }
  149. with self.assertRaises(ConfigError):
  150. self.parse_config()
  151. def test_password_config_cannot_be_enabled(self) -> None:
  152. self.config_dict["password_config"] = {"enabled": True}
  153. with self.assertRaises(ConfigError):
  154. self.parse_config()
  155. def test_oidc_sso_cannot_be_enabled(self) -> None:
  156. self.config_dict["oidc_providers"] = [
  157. {
  158. "idp_id": "microsoft",
  159. "idp_name": "Microsoft",
  160. "issuer": "https://login.microsoftonline.com/<tenant id>/v2.0",
  161. "client_id": "<client id>",
  162. "client_secret": "<client secret>",
  163. "scopes": ["openid", "profile"],
  164. "authorization_endpoint": "https://login.microsoftonline.com/<tenant id>/oauth2/v2.0/authorize",
  165. "token_endpoint": "https://login.microsoftonline.com/<tenant id>/oauth2/v2.0/token",
  166. "userinfo_endpoint": "https://graph.microsoft.com/oidc/userinfo",
  167. }
  168. ]
  169. with self.assertRaises(ConfigError):
  170. self.parse_config()
  171. def test_cas_sso_cannot_be_enabled(self) -> None:
  172. self.config_dict["cas_config"] = {
  173. "enabled": True,
  174. "server_url": "https://cas-server.com",
  175. "displayname_attribute": "name",
  176. "required_attributes": {"userGroup": "staff", "department": "None"},
  177. }
  178. with self.assertRaises(ConfigError):
  179. self.parse_config()
  180. def test_auth_providers_cannot_be_enabled(self) -> None:
  181. self.config_dict["modules"] = [
  182. {
  183. "module": f"{__name__}.{CustomAuthModule.__qualname__}",
  184. "config": {},
  185. }
  186. ]
  187. # This requires actually setting up an HS, as the module will be run on setup,
  188. # which should raise as the module tries to register an auth provider
  189. config = self.parse_config()
  190. reactor, clock = get_clock()
  191. with self.assertRaises(ConfigError):
  192. setup_test_homeserver(
  193. self.addCleanup, reactor=reactor, clock=clock, config=config
  194. )
  195. def test_jwt_auth_cannot_be_enabled(self) -> None:
  196. self.config_dict["jwt_config"] = {
  197. "enabled": True,
  198. "secret": "my-secret-token",
  199. "algorithm": "HS256",
  200. }
  201. with self.assertRaises(ConfigError):
  202. self.parse_config()
  203. def test_login_via_existing_session_cannot_be_enabled(self) -> None:
  204. self.config_dict["login_via_existing_session"] = {"enabled": True}
  205. with self.assertRaises(ConfigError):
  206. self.parse_config()
  207. def test_captcha_cannot_be_enabled(self) -> None:
  208. self.config_dict.update(
  209. enable_registration_captcha=True,
  210. recaptcha_public_key="test",
  211. recaptcha_private_key="test",
  212. )
  213. with self.assertRaises(ConfigError):
  214. self.parse_config()
  215. def test_refreshable_tokens_cannot_be_enabled(self) -> None:
  216. self.config_dict.update(
  217. refresh_token_lifetime="24h",
  218. refreshable_access_token_lifetime="10m",
  219. nonrefreshable_access_token_lifetime="24h",
  220. )
  221. with self.assertRaises(ConfigError):
  222. self.parse_config()
  223. def test_session_lifetime_cannot_be_set(self) -> None:
  224. self.config_dict["session_lifetime"] = "24h"
  225. with self.assertRaises(ConfigError):
  226. self.parse_config()
  227. def test_enable_3pid_changes_cannot_be_enabled(self) -> None:
  228. self.config_dict["enable_3pid_changes"] = True
  229. with self.assertRaises(ConfigError):
  230. self.parse_config()