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.

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