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.

471 lines
17 KiB

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