This is an automated email from the ASF dual-hosted git repository. smolnar82 pushed a commit to branch knox_idf in repository https://gitbox.apache.org/repos/asf/knox.git
commit ca62c98fcba29de3c8166c094db8945e36b91e1c Author: Sandor Molnar <[email protected]> AuthorDate: Tue Jul 21 15:42:37 2026 +0200 KnoxIDF - Fixed pylint and test issues in Docker-based tests --- .../build/conf/topologies/knoxidf-ldap.xml | 2 +- .github/workflows/tests/common_utils.py | 4 +- .github/workflows/tests/test_knoxidf.py | 184 +++++++++++---------- 3 files changed, 101 insertions(+), 89 deletions(-) diff --git a/.github/workflows/build/conf/topologies/knoxidf-ldap.xml b/.github/workflows/build/conf/topologies/knoxidf-ldap.xml index a82920bdb..d25bb8869 100644 --- a/.github/workflows/build/conf/topologies/knoxidf-ldap.xml +++ b/.github/workflows/build/conf/topologies/knoxidf-ldap.xml @@ -15,7 +15,7 @@ </param> <param> <name>main.ldapRealm.contextFactory.url</name> - <value>ldap://ldap:33389</value> + <value>ldaps://localhost:33390</value> </param> <param> <name>main.ldapRealm.contextFactory.authenticationMechanism</name> diff --git a/.github/workflows/tests/common_utils.py b/.github/workflows/tests/common_utils.py index e80193374..5d11de053 100644 --- a/.github/workflows/tests/common_utils.py +++ b/.github/workflows/tests/common_utils.py @@ -100,6 +100,6 @@ def get_token_claim(token, claim): payload_json = base64.urlsafe_b64decode(payload_b64).decode('utf-8') payload = json.loads(payload_json) return payload.get(claim) - except Exception as e: + except (ValueError, IndexError, json.JSONDecodeError) as e: print(f"Failed to decode token for claim '{claim}': {e}") - return None \ No newline at end of file + return None diff --git a/.github/workflows/tests/test_knoxidf.py b/.github/workflows/tests/test_knoxidf.py index 918cc37d2..cbbbacaf2 100644 --- a/.github/workflows/tests/test_knoxidf.py +++ b/.github/workflows/tests/test_knoxidf.py @@ -13,18 +13,29 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Integration tests for Knox as an OIDC Identity Federation (IDF) provider.""" + import unittest import hashlib import base64 from urllib.parse import urlparse, parse_qs from requests.auth import HTTPBasicAuth -from common_utils import gateway_base_url, knox_get, knox_post, get_token_claim, get_token_id_display_text +from common_utils import ( + gateway_base_url, + knox_get, + knox_post, + get_token_claim, + get_token_id_display_text, +) + class TestKnoxIDF(unittest.TestCase): + """OIDC provider tests covering discovery, client credentials, and auth code flows.""" + def setUp(self): # Get the Knox Gateway URL from environment variables - self.base_url = gateway_base_url() + self.base_url = gateway_base_url() self.knoxidf_ldap_url = f"{self.base_url}gateway/knoxidf-ldap/" self.knoxidf_token_url = f"{self.base_url}gateway/knoxidf-token/" self.username = "guest" @@ -39,7 +50,7 @@ class TestKnoxIDF(unittest.TestCase): response = knox_get(url) self.assertEqual(response.status_code, 200) config = response.json() - + # Construct expected values based on dynamic base_url expected_issuer = f"{self.knoxidf_ldap_url}knoxidf" expected_auth_endpoint = f"{self.knoxidf_ldap_url}knoxidf/api/v1/authorize" @@ -54,31 +65,22 @@ class TestKnoxIDF(unittest.TestCase): self.assertEqual(config.get("jwks_uri"), expected_jwks_uri) self.assertEqual(config.get("response_types_supported"), ["code"]) - self.assertEqual(config.get("grant_types_supported"), ["authorization_code", "refresh_token"]) + self.assertEqual( + config.get("grant_types_supported"), + ["authorization_code", "refresh_token"], + ) self.assertEqual(config.get("id_token_signing_alg_values_supported"), ["RS256"]) - self.assertEqual(config.get("scopes_supported"), ["openid", "email", "profile", "offline_access"]) + self.assertEqual( + config.get("scopes_supported"), + ["openid", "email", "profile", "offline_access"], + ) def test_client_credentials_flow(self): """ Test OIDC Client Credentials Flow. """ # 1. Register client - reg_url = f"{self.knoxidf_ldap_url}knoxidf/api/v1/client/register" - print(f"Registering client at: {reg_url}") - data = { - "redirect_uris": "http://localhost/callback", - "allowed_scopes": "openid,profile,email,offline_access" - } - response = knox_post( - reg_url, - data=data, - auth=HTTPBasicAuth(self.username, self.password), - ) - self.assertEqual(response.status_code, 200) - reg_info = response.json() - print(f"Registration response: {reg_info}") - client_id = reg_info["client_id"] - client_secret = reg_info["client_secret"] + client_id, client_secret = self._register_test_client() # 2. Get token via client_credentials token_url = f"{self.knoxidf_token_url}knoxtoken/api/v1/token" @@ -92,7 +94,7 @@ class TestKnoxIDF(unittest.TestCase): # ClientCredentialsResource uses Basic Auth for client authentication response = knox_post(token_url, data=data, verify=False) if response.status_code != 200: - print(f"Token error response: {response.text}") + print(f"Token error response: {response.text}") self.assertEqual(response.status_code, 200) tokens = response.json() self.assertIn("access_token", tokens) @@ -103,25 +105,9 @@ class TestKnoxIDF(unittest.TestCase): Test OIDC Authorization Code Flow with Refresh Token. """ # 1. Register client - reg_url = f"{self.knoxidf_ldap_url}knoxidf/api/v1/client/register" - print(f"Registering client at: {reg_url}") - data = { - "redirect_uris": "http://localhost/callback", - "allowed_scopes": "openid,profile,email,offline_access" - } - response = knox_post( - reg_url, - data=data, - auth=HTTPBasicAuth(self.username, self.password), - ) - self.assertEqual(response.status_code, 200) - reg_info = response.json() - print(f"Registration response: {reg_info}") - client_id = reg_info["client_id"] - client_secret = reg_info["client_secret"] + client_id, client_secret = self._register_test_client() # 2. Authorize (with Basic Auth for the user 'guest') - auth_url = f"{self.knoxidf_ldap_url}knoxidf/api/v1/authorize" params = { "response_type": "code", "client_id": client_id, @@ -130,22 +116,7 @@ class TestKnoxIDF(unittest.TestCase): "state": "test_state", "auto_consent": "true" } - print(f"Authorizing at: {auth_url}") - # allow_redirects=False to catch the redirect to redirect_uri - response = knox_get(auth_url, params=params, auth=(self.username, self.password), verify=False, allow_redirects=False) - - # Should be a redirect to the callback URL - self.assertEqual(response.status_code, 303) - location = response.headers.get("Location") - self.assertIsNotNone(location) - self.assertTrue(location.startswith("http://localhost/callback")) - - parsed_url = urlparse(location) - query_params = parse_qs(parsed_url.query) - self.assertIn("code", query_params) - self.assertIn("state", query_params) - self.assertEqual(query_params["state"][0], "test_state") - code = query_params["code"][0] + code = self._authorize_get_code(params, expect_state="test_state") # 3. Exchange code for tokens token_url = f"{self.knoxidf_token_url}knoxidf/api/v1/token" @@ -159,15 +130,14 @@ class TestKnoxIDF(unittest.TestCase): } response = knox_post(token_url, data=data, verify=False) if response.status_code != 200: - print(f"Code exchange error: {response.text}") + print(f"Code exchange error: {response.text}") self.assertEqual(response.status_code, 200) tokens = response.json() self.assertIn("access_token", tokens) self.assertIn("id_token", tokens) self.assertIn("refresh_token", tokens) - - refresh_token = tokens["refresh_token"] + refresh_token = tokens["refresh_token"] print(f"Refresh token: {refresh_token}") refresh_token_id = get_token_claim(refresh_token, 'knox.id') print(f"Refresh token knox.id: {refresh_token_id}") @@ -185,12 +155,12 @@ class TestKnoxIDF(unittest.TestCase): new_tokens = response.json() self.assertIn("access_token", new_tokens) self.assertIn("refresh_token", new_tokens) - + # Verify rotation: new refresh token should be different self.assertNotEqual(refresh_token, new_tokens["refresh_token"]) # 5. Verify old refresh token is invalidated - print(f"Verifying old refresh token is invalidated...") + print("Verifying old refresh token is invalidated...") # Use same data (with old refresh_token) data_old = { "grant_type": "refresh_token", @@ -198,7 +168,12 @@ class TestKnoxIDF(unittest.TestCase): "client_id": client_id, "client_secret": client_secret } - response = knox_post(token_url, data=data_old, verify=False, headers={"Accept": "application/json"}) + response = knox_post( + token_url, + data=data_old, + verify=False, + headers={"Accept": "application/json"}, + ) self.assertEqual(response.status_code, 401) error_info = response.json() display_id = get_token_id_display_text(refresh_token_id) @@ -214,10 +189,9 @@ class TestKnoxIDF(unittest.TestCase): # 2. PKCE Setup code_verifier = "thisshouldbealongandrandomstringthatissecure" - code_challenge = base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest()).decode().replace('=', '') + code_challenge = self._s256_challenge(code_verifier) # 3. Authorize - auth_url = f"{self.knoxidf_ldap_url}knoxidf/api/v1/authorize" params = { "response_type": "code", "client_id": client_id, @@ -228,10 +202,7 @@ class TestKnoxIDF(unittest.TestCase): "code_challenge": code_challenge, "code_challenge_method": "S256" } - response = knox_get(auth_url, params=params, auth=(self.username, self.password), allow_redirects=False) - self.assertEqual(response.status_code, 303) - location = response.headers.get("Location") - code = parse_qs(urlparse(location).query)["code"][0] + code = self._authorize_get_code(params) # 4. Token Exchange token_url = f"{self.knoxidf_token_url}knoxidf/api/v1/token" @@ -260,7 +231,6 @@ class TestKnoxIDF(unittest.TestCase): code_challenge = code_verifier # 3. Authorize - auth_url = f"{self.knoxidf_ldap_url}knoxidf/api/v1/authorize" params = { "response_type": "code", "client_id": client_id, @@ -271,10 +241,7 @@ class TestKnoxIDF(unittest.TestCase): "code_challenge": code_challenge, "code_challenge_method": "plain" } - response = knox_get(auth_url, params=params, auth=(self.username, self.password), allow_redirects=False) - self.assertEqual(response.status_code, 303) - location = response.headers.get("Location") - code = parse_qs(urlparse(location).query)["code"][0] + code = self._authorize_get_code(params) # 4. Token Exchange token_url = f"{self.knoxidf_token_url}knoxidf/api/v1/token" @@ -297,37 +264,46 @@ class TestKnoxIDF(unittest.TestCase): """ client_id, client_secret = self._register_test_client() code_verifier = "correct-verifier" - code_challenge = base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest()).decode().replace('=', '') + code_challenge = self._s256_challenge(code_verifier) # Authorize - auth_url = f"{self.knoxidf_ldap_url}knoxidf/api/v1/authorize" params = { - "response_type": "code", "client_id": client_id, "redirect_uri": "http://localhost/callback", - "scope": "openid", "state": "pkce_fail", "auto_consent": "true", - "code_challenge": code_challenge, "code_challenge_method": "S256" + "response_type": "code", + "client_id": client_id, + "redirect_uri": "http://localhost/callback", + "scope": "openid", + "state": "pkce_fail", + "auto_consent": "true", + "code_challenge": code_challenge, + "code_challenge_method": "S256" } - response = knox_get(auth_url, params=params, auth=(self.username, self.password), allow_redirects=False) - code = parse_qs(urlparse(response.headers.get("Location")).query)["code"][0] + code = self._authorize_get_code(params) token_url = f"{self.knoxidf_token_url}knoxidf/api/v1/token" # 1. Invalid verifier data = { - "grant_type": "authorization_code", "code": code, "redirect_uri": "http://localhost/callback", - "client_id": client_id, "client_secret": client_secret, "code_verifier": "wrong-verifier" + "grant_type": "authorization_code", + "code": code, + "redirect_uri": "http://localhost/callback", + "client_id": client_id, + "client_secret": client_secret, + "code_verifier": "wrong-verifier" } response = knox_post(token_url, data=data) self.assertEqual(response.status_code, 401) self.assertIn("Invalid code_verifier", response.json()["error_description"]) # Note: the code is revoked after first use, so we need a new one for the next test - response = knox_get(auth_url, params=params, auth=(self.username, self.password), allow_redirects=False) - code = parse_qs(urlparse(response.headers.get("Location")).query)["code"][0] + code = self._authorize_get_code(params) # 2. Missing verifier data = { - "grant_type": "authorization_code", "code": code, "redirect_uri": "http://localhost/callback", - "client_id": client_id, "client_secret": client_secret + "grant_type": "authorization_code", + "code": code, + "redirect_uri": "http://localhost/callback", + "client_id": client_id, + "client_secret": client_secret } response = knox_post(token_url, data=data) self.assertEqual(response.status_code, 401) @@ -335,10 +311,46 @@ class TestKnoxIDF(unittest.TestCase): def _register_test_client(self): reg_url = f"{self.knoxidf_ldap_url}knoxidf/api/v1/client/register" - data = {"redirect_uris": "http://localhost/callback", "allowed_scopes": "openid,profile,email,offline_access"} + print(f"Registering client at: {reg_url}") + data = { + "redirect_uris": "http://localhost/callback", + "allowed_scopes": "openid,profile,email,offline_access" + } response = knox_post(reg_url, data=data, auth=HTTPBasicAuth(self.username, self.password)) + self.assertEqual(response.status_code, 200) reg_info = response.json() + print(f"Registration response: {reg_info}") return reg_info["client_id"], reg_info["client_secret"] + def _authorize_get_code(self, params, expect_state=None): + """Hit the authorize endpoint and return the code from the redirect Location.""" + auth_url = f"{self.knoxidf_ldap_url}knoxidf/api/v1/authorize" + print(f"Authorizing at: {auth_url}") + # allow_redirects=False to catch the redirect to redirect_uri + response = knox_get( + auth_url, + params=params, + auth=(self.username, self.password), + verify=False, + allow_redirects=False, + ) + self.assertEqual(response.status_code, 303) + location = response.headers.get("Location") + self.assertIsNotNone(location) + self.assertTrue(location.startswith("http://localhost/callback")) + + query_params = parse_qs(urlparse(location).query) + self.assertIn("code", query_params) + if expect_state is not None: + self.assertIn("state", query_params) + self.assertEqual(query_params["state"][0], expect_state) + return query_params["code"][0] + + @staticmethod + def _s256_challenge(code_verifier): + digest = hashlib.sha256(code_verifier.encode()).digest() + return base64.urlsafe_b64encode(digest).decode().replace('=', '') + + if __name__ == '__main__': unittest.main()
