You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

477 lines
17 KiB

4 months ago
  1. """Test module for KeycloakOpenID."""
  2. from typing import Tuple
  3. from unittest import mock
  4. import pytest
  5. from keycloak import KeycloakAdmin, KeycloakOpenID
  6. from keycloak.authorization import Authorization
  7. from keycloak.authorization.permission import Permission
  8. from keycloak.authorization.policy import Policy
  9. from keycloak.authorization.role import Role
  10. from keycloak.connection import ConnectionManager
  11. from keycloak.exceptions import (
  12. KeycloakAuthenticationError,
  13. KeycloakAuthorizationConfigError,
  14. KeycloakDeprecationError,
  15. KeycloakInvalidTokenError,
  16. KeycloakPostError,
  17. KeycloakRPTNotFound,
  18. )
  19. def test_keycloak_openid_init(env):
  20. """Test KeycloakOpenId's init method.
  21. :param env: Environment fixture
  22. :type env: KeycloakTestEnv
  23. """
  24. oid = KeycloakOpenID(
  25. server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}",
  26. realm_name="master",
  27. client_id="admin-cli",
  28. )
  29. assert oid.client_id == "admin-cli"
  30. assert oid.client_secret_key is None
  31. assert oid.realm_name == "master"
  32. assert isinstance(oid.connection, ConnectionManager)
  33. assert isinstance(oid.authorization, Authorization)
  34. def test_well_known(oid: KeycloakOpenID):
  35. """Test the well_known method.
  36. :param oid: Keycloak OpenID client
  37. :type oid: KeycloakOpenID
  38. """
  39. res = oid.well_known()
  40. assert res is not None
  41. assert res != dict()
  42. for key in [
  43. "acr_values_supported",
  44. "authorization_encryption_alg_values_supported",
  45. "authorization_encryption_enc_values_supported",
  46. "authorization_endpoint",
  47. "authorization_signing_alg_values_supported",
  48. "backchannel_authentication_endpoint",
  49. "backchannel_authentication_request_signing_alg_values_supported",
  50. "backchannel_logout_session_supported",
  51. "backchannel_logout_supported",
  52. "backchannel_token_delivery_modes_supported",
  53. "check_session_iframe",
  54. "claim_types_supported",
  55. "claims_parameter_supported",
  56. "claims_supported",
  57. "code_challenge_methods_supported",
  58. "device_authorization_endpoint",
  59. "end_session_endpoint",
  60. "frontchannel_logout_session_supported",
  61. "frontchannel_logout_supported",
  62. "grant_types_supported",
  63. "id_token_encryption_alg_values_supported",
  64. "id_token_encryption_enc_values_supported",
  65. "id_token_signing_alg_values_supported",
  66. "introspection_endpoint",
  67. "introspection_endpoint_auth_methods_supported",
  68. "introspection_endpoint_auth_signing_alg_values_supported",
  69. "issuer",
  70. "jwks_uri",
  71. "mtls_endpoint_aliases",
  72. "pushed_authorization_request_endpoint",
  73. "registration_endpoint",
  74. "request_object_encryption_alg_values_supported",
  75. "request_object_encryption_enc_values_supported",
  76. "request_object_signing_alg_values_supported",
  77. "request_parameter_supported",
  78. "request_uri_parameter_supported",
  79. "require_pushed_authorization_requests",
  80. "require_request_uri_registration",
  81. "response_modes_supported",
  82. "response_types_supported",
  83. "revocation_endpoint",
  84. "revocation_endpoint_auth_methods_supported",
  85. "revocation_endpoint_auth_signing_alg_values_supported",
  86. "scopes_supported",
  87. "subject_types_supported",
  88. "tls_client_certificate_bound_access_tokens",
  89. "token_endpoint",
  90. "token_endpoint_auth_methods_supported",
  91. "token_endpoint_auth_signing_alg_values_supported",
  92. "userinfo_encryption_alg_values_supported",
  93. "userinfo_encryption_enc_values_supported",
  94. "userinfo_endpoint",
  95. "userinfo_signing_alg_values_supported",
  96. ]:
  97. assert key in res
  98. def test_auth_url(env, oid: KeycloakOpenID):
  99. """Test the auth_url method.
  100. :param env: Environment fixture
  101. :type env: KeycloakTestEnv
  102. :param oid: Keycloak OpenID client
  103. :type oid: KeycloakOpenID
  104. """
  105. res = oid.auth_url(redirect_uri="http://test.test/*")
  106. assert (
  107. res
  108. == f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}/realms/{oid.realm_name}"
  109. + f"/protocol/openid-connect/auth?client_id={oid.client_id}&response_type=code"
  110. + "&redirect_uri=http://test.test/*&scope=email&state="
  111. )
  112. def test_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]):
  113. """Test the token method.
  114. :param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials
  115. :type oid_with_credentials: Tuple[KeycloakOpenID, str, str]
  116. """
  117. oid, username, password = oid_with_credentials
  118. token = oid.token(username=username, password=password)
  119. assert token == {
  120. "access_token": mock.ANY,
  121. "expires_in": mock.ANY,
  122. "id_token": mock.ANY,
  123. "not-before-policy": 0,
  124. "refresh_expires_in": mock.ANY,
  125. "refresh_token": mock.ANY,
  126. "scope": mock.ANY,
  127. "session_state": mock.ANY,
  128. "token_type": "Bearer",
  129. }
  130. # Test with dummy totp
  131. token = oid.token(username=username, password=password, totp="123456")
  132. assert token == {
  133. "access_token": mock.ANY,
  134. "expires_in": mock.ANY,
  135. "id_token": mock.ANY,
  136. "not-before-policy": 0,
  137. "refresh_expires_in": mock.ANY,
  138. "refresh_token": mock.ANY,
  139. "scope": mock.ANY,
  140. "session_state": mock.ANY,
  141. "token_type": "Bearer",
  142. }
  143. # Test with extra param
  144. token = oid.token(username=username, password=password, extra_param="foo")
  145. assert token == {
  146. "access_token": mock.ANY,
  147. "expires_in": mock.ANY,
  148. "id_token": mock.ANY,
  149. "not-before-policy": 0,
  150. "refresh_expires_in": mock.ANY,
  151. "refresh_token": mock.ANY,
  152. "scope": mock.ANY,
  153. "session_state": mock.ANY,
  154. "token_type": "Bearer",
  155. }
  156. def test_exchange_token(
  157. oid_with_credentials: Tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin
  158. ):
  159. """Test the exchange token method.
  160. :param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials
  161. :type oid_with_credentials: Tuple[KeycloakOpenID, str, str]
  162. :param admin: Keycloak Admin client
  163. :type admin: KeycloakAdmin
  164. """
  165. # Verify existing user
  166. oid, username, password = oid_with_credentials
  167. # Allow impersonation
  168. admin.change_current_realm(oid.realm_name)
  169. admin.assign_client_role(
  170. user_id=admin.get_user_id(username=username),
  171. client_id=admin.get_client_id(client_id="realm-management"),
  172. roles=[
  173. admin.get_client_role(
  174. client_id=admin.get_client_id(client_id="realm-management"),
  175. role_name="impersonation",
  176. )
  177. ],
  178. )
  179. token = oid.token(username=username, password=password)
  180. assert oid.userinfo(token=token["access_token"]) == {
  181. "email": f"{username}@test.test",
  182. "email_verified": True,
  183. "family_name": "last",
  184. "given_name": "first",
  185. "name": "first last",
  186. "preferred_username": username,
  187. "sub": mock.ANY,
  188. }
  189. # Exchange token with the new user
  190. new_token = oid.exchange_token(
  191. token=token["access_token"], audience=oid.client_id, subject=username
  192. )
  193. assert oid.userinfo(token=new_token["access_token"]) == {
  194. "email": f"{username}@test.test",
  195. "email_verified": True,
  196. "family_name": "last",
  197. "given_name": "first",
  198. "name": "first last",
  199. "preferred_username": username,
  200. "sub": mock.ANY,
  201. }
  202. assert token != new_token
  203. def test_logout(oid_with_credentials):
  204. """Test logout.
  205. :param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials
  206. :type oid_with_credentials: Tuple[KeycloakOpenID, str, str]
  207. """
  208. oid, username, password = oid_with_credentials
  209. token = oid.token(username=username, password=password)
  210. assert oid.userinfo(token=token["access_token"]) != dict()
  211. assert oid.logout(refresh_token=token["refresh_token"]) == dict()
  212. with pytest.raises(KeycloakAuthenticationError):
  213. oid.userinfo(token=token["access_token"])
  214. def test_certs(oid: KeycloakOpenID):
  215. """Test certificates.
  216. :param oid: Keycloak OpenID client
  217. :type oid: KeycloakOpenID
  218. """
  219. assert len(oid.certs()["keys"]) == 2
  220. def test_public_key(oid: KeycloakOpenID):
  221. """Test public key.
  222. :param oid: Keycloak OpenID client
  223. :type oid: KeycloakOpenID
  224. """
  225. assert oid.public_key() is not None
  226. def test_entitlement(
  227. oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin
  228. ):
  229. """Test entitlement.
  230. :param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization
  231. server with client credentials
  232. :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]
  233. :param admin: Keycloak Admin client
  234. :type admin: KeycloakAdmin
  235. """
  236. oid, username, password = oid_with_credentials_authz
  237. token = oid.token(username=username, password=password)
  238. resource_server_id = admin.get_client_authz_resources(
  239. client_id=admin.get_client_id(oid.client_id)
  240. )[0]["_id"]
  241. with pytest.raises(KeycloakDeprecationError):
  242. oid.entitlement(token=token["access_token"], resource_server_id=resource_server_id)
  243. def test_introspect(oid_with_credentials: Tuple[KeycloakOpenID, str, str]):
  244. """Test introspect.
  245. :param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials
  246. :type oid_with_credentials: Tuple[KeycloakOpenID, str, str]
  247. """
  248. oid, username, password = oid_with_credentials
  249. token = oid.token(username=username, password=password)
  250. assert oid.introspect(token=token["access_token"])["active"]
  251. assert oid.introspect(
  252. token=token["access_token"], rpt="some", token_type_hint="requesting_party_token"
  253. ) == {"active": False}
  254. with pytest.raises(KeycloakRPTNotFound):
  255. oid.introspect(token=token["access_token"], token_type_hint="requesting_party_token")
  256. def test_decode_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]):
  257. """Test decode token.
  258. :param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials
  259. :type oid_with_credentials: Tuple[KeycloakOpenID, str, str]
  260. """
  261. oid, username, password = oid_with_credentials
  262. token = oid.token(username=username, password=password)
  263. assert (
  264. oid.decode_token(
  265. token=token["access_token"],
  266. key="-----BEGIN PUBLIC KEY-----\n" + oid.public_key() + "\n-----END PUBLIC KEY-----",
  267. options={"verify_aud": False},
  268. )["preferred_username"]
  269. == username
  270. )
  271. def test_load_authorization_config(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]):
  272. """Test load authorization config.
  273. :param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization
  274. server with client credentials
  275. :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]
  276. """
  277. oid, username, password = oid_with_credentials_authz
  278. oid.load_authorization_config(path="tests/data/authz_settings.json")
  279. assert "test-authz-rb-policy" in oid.authorization.policies
  280. assert isinstance(oid.authorization.policies["test-authz-rb-policy"], Policy)
  281. assert len(oid.authorization.policies["test-authz-rb-policy"].roles) == 1
  282. assert isinstance(oid.authorization.policies["test-authz-rb-policy"].roles[0], Role)
  283. assert len(oid.authorization.policies["test-authz-rb-policy"].permissions) == 2
  284. assert isinstance(
  285. oid.authorization.policies["test-authz-rb-policy"].permissions[0], Permission
  286. )
  287. def test_get_policies(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]):
  288. """Test get policies.
  289. :param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization
  290. server with client credentials
  291. :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]
  292. """
  293. oid, username, password = oid_with_credentials_authz
  294. token = oid.token(username=username, password=password)
  295. with pytest.raises(KeycloakAuthorizationConfigError):
  296. oid.get_policies(token=token["access_token"])
  297. oid.load_authorization_config(path="tests/data/authz_settings.json")
  298. assert oid.get_policies(token=token["access_token"]) is None
  299. key = "-----BEGIN PUBLIC KEY-----\n" + oid.public_key() + "\n-----END PUBLIC KEY-----"
  300. orig_client_id = oid.client_id
  301. oid.client_id = "account"
  302. assert oid.get_policies(token=token["access_token"], method_token_info="decode", key=key) == []
  303. policy = Policy(name="test", type="role", logic="POSITIVE", decision_strategy="UNANIMOUS")
  304. policy.add_role(role="account/view-profile")
  305. oid.authorization.policies["test"] = policy
  306. assert [
  307. str(x)
  308. for x in oid.get_policies(token=token["access_token"], method_token_info="decode", key=key)
  309. ] == ["Policy: test (role)"]
  310. assert [
  311. repr(x)
  312. for x in oid.get_policies(token=token["access_token"], method_token_info="decode", key=key)
  313. ] == ["<Policy: test (role)>"]
  314. oid.client_id = orig_client_id
  315. oid.logout(refresh_token=token["refresh_token"])
  316. with pytest.raises(KeycloakInvalidTokenError):
  317. oid.get_policies(token=token["access_token"])
  318. def test_get_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]):
  319. """Test get policies.
  320. :param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization
  321. server with client credentials
  322. :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]
  323. """
  324. oid, username, password = oid_with_credentials_authz
  325. token = oid.token(username=username, password=password)
  326. with pytest.raises(KeycloakAuthorizationConfigError):
  327. oid.get_permissions(token=token["access_token"])
  328. oid.load_authorization_config(path="tests/data/authz_settings.json")
  329. assert oid.get_permissions(token=token["access_token"]) is None
  330. key = "-----BEGIN PUBLIC KEY-----\n" + oid.public_key() + "\n-----END PUBLIC KEY-----"
  331. orig_client_id = oid.client_id
  332. oid.client_id = "account"
  333. assert (
  334. oid.get_permissions(token=token["access_token"], method_token_info="decode", key=key) == []
  335. )
  336. policy = Policy(name="test", type="role", logic="POSITIVE", decision_strategy="UNANIMOUS")
  337. policy.add_role(role="account/view-profile")
  338. policy.add_permission(
  339. permission=Permission(
  340. name="test-perm", type="resource", logic="POSITIVE", decision_strategy="UNANIMOUS"
  341. )
  342. )
  343. oid.authorization.policies["test"] = policy
  344. assert [
  345. str(x)
  346. for x in oid.get_permissions(
  347. token=token["access_token"], method_token_info="decode", key=key
  348. )
  349. ] == ["Permission: test-perm (resource)"]
  350. assert [
  351. repr(x)
  352. for x in oid.get_permissions(
  353. token=token["access_token"], method_token_info="decode", key=key
  354. )
  355. ] == ["<Permission: test-perm (resource)>"]
  356. oid.client_id = orig_client_id
  357. oid.logout(refresh_token=token["refresh_token"])
  358. with pytest.raises(KeycloakInvalidTokenError):
  359. oid.get_permissions(token=token["access_token"])
  360. def test_uma_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]):
  361. """Test UMA permissions.
  362. :param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization
  363. server with client credentials
  364. :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]
  365. """
  366. oid, username, password = oid_with_credentials_authz
  367. token = oid.token(username=username, password=password)
  368. assert len(oid.uma_permissions(token=token["access_token"])) == 1
  369. assert oid.uma_permissions(token=token["access_token"])[0]["rsname"] == "Default Resource"
  370. def test_has_uma_access(
  371. oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin
  372. ):
  373. """Test has UMA access.
  374. :param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization
  375. server with client credentials
  376. :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]
  377. :param admin: Keycloak Admin client
  378. :type admin: KeycloakAdmin
  379. """
  380. oid, username, password = oid_with_credentials_authz
  381. token = oid.token(username=username, password=password)
  382. assert (
  383. str(oid.has_uma_access(token=token["access_token"], permissions=""))
  384. == "AuthStatus(is_authorized=True, is_logged_in=True, missing_permissions=set())"
  385. )
  386. assert (
  387. str(oid.has_uma_access(token=token["access_token"], permissions="Default Resource"))
  388. == "AuthStatus(is_authorized=True, is_logged_in=True, missing_permissions=set())"
  389. )
  390. with pytest.raises(KeycloakPostError):
  391. oid.has_uma_access(token=token["access_token"], permissions="Does not exist")
  392. oid.logout(refresh_token=token["refresh_token"])
  393. assert (
  394. str(oid.has_uma_access(token=token["access_token"], permissions=""))
  395. == "AuthStatus(is_authorized=False, is_logged_in=False, missing_permissions=set())"
  396. )
  397. assert (
  398. str(oid.has_uma_access(token=admin.token["access_token"], permissions="Default Resource"))
  399. == "AuthStatus(is_authorized=False, is_logged_in=False, missing_permissions="
  400. + "{'Default Resource'})"
  401. )