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.

473 lines
17 KiB

  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": 300,
  122. "id_token": mock.ANY,
  123. "not-before-policy": 0,
  124. "refresh_expires_in": 1800,
  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": 300,
  135. "id_token": mock.ANY,
  136. "not-before-policy": 0,
  137. "refresh_expires_in": 1800,
  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": 300,
  148. "id_token": mock.ANY,
  149. "not-before-policy": 0,
  150. "refresh_expires_in": 1800,
  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.realm_name = 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": False,
  183. "preferred_username": username,
  184. "sub": mock.ANY,
  185. }
  186. # Exchange token with the new user
  187. new_token = oid.exchange_token(
  188. token=token["access_token"],
  189. client_id=oid.client_id,
  190. audience=oid.client_id,
  191. subject=username,
  192. )
  193. assert oid.userinfo(token=new_token["access_token"]) == {
  194. "email": f"{username}@test.test",
  195. "email_verified": False,
  196. "preferred_username": username,
  197. "sub": mock.ANY,
  198. }
  199. assert token != new_token
  200. def test_logout(oid_with_credentials):
  201. """Test logout.
  202. :param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials
  203. :type oid_with_credentials: Tuple[KeycloakOpenID, str, str]
  204. """
  205. oid, username, password = oid_with_credentials
  206. token = oid.token(username=username, password=password)
  207. assert oid.userinfo(token=token["access_token"]) != dict()
  208. assert oid.logout(refresh_token=token["refresh_token"]) == dict()
  209. with pytest.raises(KeycloakAuthenticationError):
  210. oid.userinfo(token=token["access_token"])
  211. def test_certs(oid: KeycloakOpenID):
  212. """Test certificates.
  213. :param oid: Keycloak OpenID client
  214. :type oid: KeycloakOpenID
  215. """
  216. assert len(oid.certs()["keys"]) == 2
  217. def test_public_key(oid: KeycloakOpenID):
  218. """Test public key.
  219. :param oid: Keycloak OpenID client
  220. :type oid: KeycloakOpenID
  221. """
  222. assert oid.public_key() is not None
  223. def test_entitlement(
  224. oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin
  225. ):
  226. """Test entitlement.
  227. :param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization
  228. server with client credentials
  229. :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]
  230. :param admin: Keycloak Admin client
  231. :type admin: KeycloakAdmin
  232. """
  233. oid, username, password = oid_with_credentials_authz
  234. token = oid.token(username=username, password=password)
  235. resource_server_id = admin.get_client_authz_resources(
  236. client_id=admin.get_client_id(oid.client_id)
  237. )[0]["_id"]
  238. with pytest.raises(KeycloakDeprecationError):
  239. oid.entitlement(token=token["access_token"], resource_server_id=resource_server_id)
  240. def test_introspect(oid_with_credentials: Tuple[KeycloakOpenID, str, str]):
  241. """Test introspect.
  242. :param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials
  243. :type oid_with_credentials: Tuple[KeycloakOpenID, str, str]
  244. """
  245. oid, username, password = oid_with_credentials
  246. token = oid.token(username=username, password=password)
  247. assert oid.introspect(token=token["access_token"])["active"]
  248. assert oid.introspect(
  249. token=token["access_token"], rpt="some", token_type_hint="requesting_party_token"
  250. ) == {"active": False}
  251. with pytest.raises(KeycloakRPTNotFound):
  252. oid.introspect(token=token["access_token"], token_type_hint="requesting_party_token")
  253. def test_decode_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]):
  254. """Test decode token.
  255. :param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials
  256. :type oid_with_credentials: Tuple[KeycloakOpenID, str, str]
  257. """
  258. oid, username, password = oid_with_credentials
  259. token = oid.token(username=username, password=password)
  260. assert (
  261. oid.decode_token(
  262. token=token["access_token"],
  263. key="-----BEGIN PUBLIC KEY-----\n" + oid.public_key() + "\n-----END PUBLIC KEY-----",
  264. options={"verify_aud": False},
  265. )["preferred_username"]
  266. == username
  267. )
  268. def test_load_authorization_config(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]):
  269. """Test load authorization config.
  270. :param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization
  271. server with client credentials
  272. :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]
  273. """
  274. oid, username, password = oid_with_credentials_authz
  275. oid.load_authorization_config(path="tests/data/authz_settings.json")
  276. assert "test-authz-rb-policy" in oid.authorization.policies
  277. assert isinstance(oid.authorization.policies["test-authz-rb-policy"], Policy)
  278. assert len(oid.authorization.policies["test-authz-rb-policy"].roles) == 1
  279. assert isinstance(oid.authorization.policies["test-authz-rb-policy"].roles[0], Role)
  280. assert len(oid.authorization.policies["test-authz-rb-policy"].permissions) == 2
  281. assert isinstance(
  282. oid.authorization.policies["test-authz-rb-policy"].permissions[0], Permission
  283. )
  284. def test_get_policies(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]):
  285. """Test get policies.
  286. :param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization
  287. server with client credentials
  288. :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]
  289. """
  290. oid, username, password = oid_with_credentials_authz
  291. token = oid.token(username=username, password=password)
  292. with pytest.raises(KeycloakAuthorizationConfigError):
  293. oid.get_policies(token=token["access_token"])
  294. oid.load_authorization_config(path="tests/data/authz_settings.json")
  295. assert oid.get_policies(token=token["access_token"]) is None
  296. key = "-----BEGIN PUBLIC KEY-----\n" + oid.public_key() + "\n-----END PUBLIC KEY-----"
  297. orig_client_id = oid.client_id
  298. oid.client_id = "account"
  299. assert oid.get_policies(token=token["access_token"], method_token_info="decode", key=key) == []
  300. policy = Policy(name="test", type="role", logic="POSITIVE", decision_strategy="UNANIMOUS")
  301. policy.add_role(role="account/view-profile")
  302. oid.authorization.policies["test"] = policy
  303. assert [
  304. str(x)
  305. for x in oid.get_policies(token=token["access_token"], method_token_info="decode", key=key)
  306. ] == ["Policy: test (role)"]
  307. assert [
  308. repr(x)
  309. for x in oid.get_policies(token=token["access_token"], method_token_info="decode", key=key)
  310. ] == ["<Policy: test (role)>"]
  311. oid.client_id = orig_client_id
  312. oid.logout(refresh_token=token["refresh_token"])
  313. with pytest.raises(KeycloakInvalidTokenError):
  314. oid.get_policies(token=token["access_token"])
  315. def test_get_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]):
  316. """Test get policies.
  317. :param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization
  318. server with client credentials
  319. :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]
  320. """
  321. oid, username, password = oid_with_credentials_authz
  322. token = oid.token(username=username, password=password)
  323. with pytest.raises(KeycloakAuthorizationConfigError):
  324. oid.get_permissions(token=token["access_token"])
  325. oid.load_authorization_config(path="tests/data/authz_settings.json")
  326. assert oid.get_permissions(token=token["access_token"]) is None
  327. key = "-----BEGIN PUBLIC KEY-----\n" + oid.public_key() + "\n-----END PUBLIC KEY-----"
  328. orig_client_id = oid.client_id
  329. oid.client_id = "account"
  330. assert (
  331. oid.get_permissions(token=token["access_token"], method_token_info="decode", key=key) == []
  332. )
  333. policy = Policy(name="test", type="role", logic="POSITIVE", decision_strategy="UNANIMOUS")
  334. policy.add_role(role="account/view-profile")
  335. policy.add_permission(
  336. permission=Permission(
  337. name="test-perm", type="resource", logic="POSITIVE", decision_strategy="UNANIMOUS"
  338. )
  339. )
  340. oid.authorization.policies["test"] = policy
  341. assert [
  342. str(x)
  343. for x in oid.get_permissions(
  344. token=token["access_token"], method_token_info="decode", key=key
  345. )
  346. ] == ["Permission: test-perm (resource)"]
  347. assert [
  348. repr(x)
  349. for x in oid.get_permissions(
  350. token=token["access_token"], method_token_info="decode", key=key
  351. )
  352. ] == ["<Permission: test-perm (resource)>"]
  353. oid.client_id = orig_client_id
  354. oid.logout(refresh_token=token["refresh_token"])
  355. with pytest.raises(KeycloakInvalidTokenError):
  356. oid.get_permissions(token=token["access_token"])
  357. def test_uma_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]):
  358. """Test UMA permissions.
  359. :param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization
  360. server with client credentials
  361. :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]
  362. """
  363. oid, username, password = oid_with_credentials_authz
  364. token = oid.token(username=username, password=password)
  365. assert len(oid.uma_permissions(token=token["access_token"])) == 1
  366. assert oid.uma_permissions(token=token["access_token"])[0]["rsname"] == "Default Resource"
  367. def test_has_uma_access(
  368. oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin
  369. ):
  370. """Test has UMA access.
  371. :param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization
  372. server with client credentials
  373. :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]
  374. :param admin: Keycloak Admin client
  375. :type admin: KeycloakAdmin
  376. """
  377. oid, username, password = oid_with_credentials_authz
  378. token = oid.token(username=username, password=password)
  379. assert (
  380. str(oid.has_uma_access(token=token["access_token"], permissions=""))
  381. == "AuthStatus(is_authorized=True, is_logged_in=True, missing_permissions=set())"
  382. )
  383. assert (
  384. str(oid.has_uma_access(token=token["access_token"], permissions="Default Resource"))
  385. == "AuthStatus(is_authorized=True, is_logged_in=True, missing_permissions=set())"
  386. )
  387. with pytest.raises(KeycloakPostError):
  388. oid.has_uma_access(token=token["access_token"], permissions="Does not exist")
  389. oid.logout(refresh_token=token["refresh_token"])
  390. assert (
  391. str(oid.has_uma_access(token=token["access_token"], permissions=""))
  392. == "AuthStatus(is_authorized=False, is_logged_in=False, missing_permissions=set())"
  393. )
  394. assert (
  395. str(oid.has_uma_access(token=admin.token["access_token"], permissions="Default Resource"))
  396. == "AuthStatus(is_authorized=False, is_logged_in=False, missing_permissions="
  397. + "{'Default Resource'})"
  398. )