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.

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