From a413c3e89525f202c5c5a43674c3fdefd30f267a Mon Sep 17 00:00:00 2001 From: Nirupma-Verma Date: Thu, 3 Aug 2023 18:32:06 +0530 Subject: [PATCH 1/2] support oidc v2, unit testcase, readme updated and vulnerability fix for cryptography package --- CHANGELOG.md | 4 + README.md | 106 +++++++++++++++++- conjur_api/models/__init__.py | 2 +- conjur_api/models/general/credentials_data.py | 20 +++- .../providers/oidc_authentication_strategy.py | 36 +++++- requirements.txt | 2 +- setup.cfg | 2 +- .../test_unit_simple_credentials_provider.py | 23 +++- tests/https/test_unit_client.py | 26 ++++- 9 files changed, 205 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcd6001..f49e7b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added +- Add support for OIDC Authenticator for Conjur UI and Conjur CLI. + [conjur-api-python#43](https://github.com/cyberark/conjur-api-python/pull/43) + ### Security - Upgrade ubuntu base image in Dockerfile.test to 23.04 [conjur-api-python#41](https://github.com/cyberark/conjur-api-python/pull/41) diff --git a/README.md b/README.md index 9b910cf..a0b93e1 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ It officially requires python 3.10 and above but can run with lower versions com It is assumed that Conjur (OSS or Enterprise) have already been installed in the environment and running in the background. If you haven't done so, follow these instructions for installation of -the [OSS](https://docs.conjur.org/Latest/en/Content/OSS/Installation/Install_methods.htm) and these for installation -of [Enterprise](https://docs.cyberark.com/Product-Doc/OnlineHelp/AAM-DAP/Latest/en/Content/HomeTilesLPs/LP-Tile2.htm). +the [OSS](https://docs.conjur.org/Latest/en/Content/HomeTilesLPs/LP-Tile2.htm) , these for installation +of [Enterprise](https://docs.cyberark.com/Product-Doc/OnlineHelp/AAM-DAP/Latest/en/Content/HomeTilesLPs/LP-Tile2.htm) and [Cloud](https://docs-er.cyberark.com/ConjurCloud/en/Content/ConjurCloud/ccl-manage-users.htm?tocpath=Get%20started%7CTutorial%7C_____1) Once Conjur is running in the background, you are ready to start setting up your python app to work with our Conjur python API! @@ -38,7 +38,7 @@ alterations that may result in breaking change. ```sh -pip3 install conjur +pip3 install conjur-api ``` @@ -58,11 +58,34 @@ pip3 install . ### Configuring the client +#### Define Modules +Authentication strategy supported by package are `authn`, `authn-ldap`, `authn-oidc` and based on authentication machanisam use the below module. + +* Authn authentication (Supported on Conjur OSS,Enterprise,Cloud) +```python +from conjur_api.providers.authn_authentication_strategy import AuthnAuthenticationStrategy +``` + +* OIDC authentication (Supported on Conjur OSS, Enterprise) +##### OIDC authenticator setup on [Conjur](https://docs.cyberark.com/AAM-DAP/13.0/en/Content/OIDC/OIDC.htm?tocpath=Integrations%7COpenID%20Connect%20(OIDC)%20Authenticator%7C_____0) +```python +from conjur_api.providers.oidc_authentication_strategy import OidcAuthenticationStrategy +``` + +* LDAP authentication (Supported on Conjur OSS, Enterprise) +##### LADP authenticator setup on [Conjur](https://docs.cyberark.com/AAM-DAP/13.0/en/Content/Integrations/ldap/ldap_authenticator.html?tocpath=Integrations%7CLDAP%20Authentication%7C_____0) +```python +from conjur_api.providers.ldap_authentication_strategy import LdapAuthenticationStrategy +``` + #### Define connection parameters In order to login to conjur you need to have 5 parameters known from advance. +1. Authn/LDAP login parameters ```python +from conjur_api.models import SslVerificationMode + conjur_url = "https://my_conjur.com" account = "my_account" username = "user1" @@ -70,12 +93,59 @@ password = "SomeStr@ngPassword!1" ssl_verification_mode = SslVerificationMode.TRUST_STORE ``` +2. Authn API key based login parameters + +```python +from conjur_api.models import SslVerificationMode + +conjur_url = "https://my_conjur.com" +account = "my_account" +username = "user1" +api_key = "asjfdsjcnk......" +ssl_verification_mode = SslVerificationMode.TRUST_STORE +``` + +3. OIDC Authenticator for Application Authentication + +```python +from conjur_api.models import SslVerificationMode + +conjur_url = "https://my_conjur.com" +account = "my_account" +username = "user1@xtz.com" +password = "xyz.asa.xyz" ## Provide valid ID token +ssl_verification_mode = SslVerificationMode.TRUST_STORE +``` + +4. OIDC Authenticator for Conjur UI and Conjur CLI Authentication + +```python +from conjur_api.models import SslVerificationMode + +conjur_url = "https://my_conjur.com" +account = "my_account" +code = 'dhdf...-fhd...' +nonce = 'cwq4.....' +code_verifier = 'ih0BJ.......' +ssl_verification_mode = SslVerificationMode.TRUST_STORE +``` + #### Define ConjurConnectionInfo ConjurConnectionInfo is a data class containing all the non-credentials connection details. +1. authn authentication ```python -connection_info = ConjurConnectionInfo(conjur_url=conjur_url,account=account,cert_file=None,service_id="ldap-service-id") +from conjur_api.models import ConjurConnectionInfo + +connection_info = ConjurConnectionInfo(conjur_url=conjur_url,account=account,cert_file=None) +``` +2. OIDC/LDAP authentication + +```python +from conjur_api.models import ConjurConnectionInfo + +connection_info = ConjurConnectionInfo(conjur_url=conjur_url,account=account,cert_file=None,service_id="service-id") ``` * conjur_url - url of conjur server @@ -95,12 +165,38 @@ fit (`keyring` usage for example) We also provide the user with a simple implementation of such provider called `SimpleCredentialsProvider`. Example of creating such provider + storing credentials: +1. Authn/LDAP/OIDC for application authentication + ```python +from conjur_api.models import CredentialsData + credentials = CredentialsData(username=username, password=password, machine=conjur_url) credentials_provider = SimpleCredentialsProvider() credentials_provider.save(credentials) del credentials ``` +2. authn API Key authentication + +```python +from conjur_api.models import CredentialsData + +credentials = CredentialsData(username=username, api_key="api key", machine=conjur_url) +credentials_provider = SimpleCredentialsProvider() +credentials_provider.save(credentials) +del credentials +``` + +3. OIDC authentication for Conjur UI and Conjur CLI Authentication + +```python +from conjur_api.models import CredentialsData,OidcCodeDetail + +oidc_detail = OidcCodeDetail(code=code, code_verifier=code_verifier, nonce=nonce) +credentials = CredentialsData(oidc_code_detail=oidc_detail, machine=conjur_url) +credentials_provider = SimpleCredentialsProvider() +credentials_provider.save(credentials) +del credentials +``` #### Create authentication strategy @@ -128,6 +224,8 @@ When using these strategies, make sure `connection_info` has a `service_id` spec Now that we have created `connection_info` and `authn_provider`, we can create our client: ```python +from conjur_api.client import Client + client = Client(connection_info, authn_strategy=authn_provider, ssl_verification_mode=ssl_verification_mode) diff --git a/conjur_api/models/__init__.py b/conjur_api/models/__init__.py index 0061077..155ac13 100644 --- a/conjur_api/models/__init__.py +++ b/conjur_api/models/__init__.py @@ -12,4 +12,4 @@ from conjur_api.models.list.list_members_of_data import ListMembersOfData from conjur_api.models.hostfactory.create_host_data import CreateHostData from conjur_api.models.ssl.ssl_verification_mode import SslVerificationMode -from conjur_api.models.general.credentials_data import CredentialsData +from conjur_api.models.general.credentials_data import * diff --git a/conjur_api/models/general/credentials_data.py b/conjur_api/models/general/credentials_data.py index 4ed0d9a..b49b88d 100644 --- a/conjur_api/models/general/credentials_data.py +++ b/conjur_api/models/general/credentials_data.py @@ -9,24 +9,38 @@ # pylint: disable=too-few-public-methods from datetime import datetime - EXPIRATION_FORMAT = "%Y-%m-%d %H:%M:%S" +class OidcCodeDetail: + """ + Used for setting user input data to login to Conjur using OIDC + Authenticator for Conjur UI and Conjur CLI uses OIDC interfaces + """ + + def __init__(self, code: str = None, code_verifier: str = None, + nonce: str = None): + self.code = code + self.code_verifier = code_verifier + self.nonce = nonce + + class CredentialsData: """ Used for setting user input data to login to Conjur """ # pylint: disable=too-many-arguments - def __init__(self, machine: str = None, username: str = None, password: str = None, api_key: str = None, - api_token: str = None, api_token_expiration: str = None): + def __init__(self, machine: str = None, username: str = None, password: str = None, + api_key: str = None, api_token: str = None, + api_token_expiration: str = None, oidc_code_detail: OidcCodeDetail = None): self.machine = machine self.username = username self.password = password self.api_key = api_key self.api_token = api_token self.api_token_expiration = api_token_expiration + self.oidc_code_detail = oidc_code_detail @classmethod def convert_dict_to_obj(cls, dic: dict): diff --git a/conjur_api/providers/oidc_authentication_strategy.py b/conjur_api/providers/oidc_authentication_strategy.py index 2d8f1ce..3dd2d4f 100644 --- a/conjur_api/providers/oidc_authentication_strategy.py +++ b/conjur_api/providers/oidc_authentication_strategy.py @@ -29,10 +29,40 @@ async def _send_authenticate_request(self, ssl_verification_data, connection_inf 'service_id': connection_info.service_id, 'account': connection_info.conjur_account, } - data = f"id_token={creds.password}" - response = await invoke_endpoint(HttpVerb.POST, ConjurEndpoint.AUTHENTICATE_OIDC, - params, data, ssl_verification_metadata=ssl_verification_data) + oidc = creds.oidc_code_detail + + if (not oidc) and (not creds.username or not creds.password): + raise MissingRequiredParameterException("code,code_verifier,nonce or username " + "and password are required for login") + + # The OIDC Authenticator for application authentication. + # OIDC v1 flow works if Username and ID Token provided. + + if not oidc and creds.username and creds.password: + data = f"id_token={creds.password}" + response = await invoke_endpoint( + HttpVerb.POST, + ConjurEndpoint.AUTHENTICATE_OIDC, params, data, + ssl_verification_metadata=ssl_verification_data + ) + + # The OIDC Authenticator for Conjur UI and Conjur CLI. + # OIDC V2 flow works if Code, Code_Verifier and Nonce provided + + if oidc: + if not oidc.code or not oidc.code_verifier or not oidc.nonce: + raise MissingRequiredParameterException("code,code_verifier,nonce") + query = { + 'code': oidc.code, + 'code_verifier': oidc.code_verifier, + 'nonce': oidc.nonce + } + response = await invoke_endpoint( + HttpVerb.GET, + ConjurEndpoint.AUTHENTICATE_OIDC, params, query=query, + ssl_verification_metadata=ssl_verification_data + ) return response.text async def _ensure_logged_in(self, connection_info, ssl_verification_data, creds): diff --git a/requirements.txt b/requirements.txt index 2a69ff9..5c483bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ nose2>=0.9.2 nose2[coverage_plugin]>=0.6.5 pylint>=2.6.0 -cryptography~=39.0.1 +cryptography~=41.0.3 keyring>=23.0.0 pyopenssl>=20.0.0 PyInstaller>=4.0 diff --git a/setup.cfg b/setup.cfg index 1354af5..c3d6e55 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,7 +33,7 @@ zip_safe = True include_package_data = False install_requires = - cryptography~=39.0.1 + cryptography~=41.0.3 keyring>=23.0.0 aiohttp>=3.8.1 asynctest >= 0.13.0; python_version<"3.8" diff --git a/tests/credentials_provider/test_unit_simple_credentials_provider.py b/tests/credentials_provider/test_unit_simple_credentials_provider.py index 97c7e54..b8e0a0f 100644 --- a/tests/credentials_provider/test_unit_simple_credentials_provider.py +++ b/tests/credentials_provider/test_unit_simple_credentials_provider.py @@ -1,12 +1,16 @@ from unittest import TestCase from conjur_api.providers import SimpleCredentialsProvider -from conjur_api.models import CredentialsData +from conjur_api.models import CredentialsData, OidcCodeDetail def create_credentials(machine: str = "machine", username: str = "some_username", password: str = "some_password", api_key: str = "some_api_key") -> CredentialsData: return CredentialsData(machine, username, password, api_key) +def oidc_v2_credential(machine: str="machine", code="code", code_verifier="coded_verifier", nonce="nonce") -> CredentialsData: + oidc_detail = OidcCodeDetail(code, code_verifier, nonce) + return CredentialsData(machine, oidc_detail) + class SimpleCredentialsProviderTest(TestCase): @@ -76,4 +80,19 @@ def test_remove_credentials(self): def test_get_store_location(self): provider = SimpleCredentialsProvider() - self.assertEqual("SimpleCredentialsProvider",provider.get_store_location()) \ No newline at end of file + self.assertEqual("SimpleCredentialsProvider",provider.get_store_location()) + + def test_cleanup_oidc_v2_credentials_if_exist(self): + provider = SimpleCredentialsProvider() + credentials_data = oidc_v2_credential() + provider.save(credentials_data) + provider.cleanup_if_exists(credentials_data.machine) + + self.assertFalse(provider.is_exists(credentials_data.machine)) + + def test_remove_oidc_v2_credentials(self): + provider = SimpleCredentialsProvider() + credentials_data = oidc_v2_credential() + provider.save(credentials_data) + provider.remove_credentials(credentials_data.machine) + self.assertFalse(provider.is_exists(credentials_data.machine)) \ No newline at end of file diff --git a/tests/https/test_unit_client.py b/tests/https/test_unit_client.py index 80016bb..22426b6 100644 --- a/tests/https/test_unit_client.py +++ b/tests/https/test_unit_client.py @@ -7,7 +7,8 @@ from conjur_api.client import Client from conjur_api.http.api import Api -from conjur_api.models import SslVerificationMode, CredentialsData +from conjur_api.models import SslVerificationMode, CredentialsData, \ + OidcCodeDetail from conjur_api.models.general.conjur_connection_info import ConjurConnectionInfo from conjur_api.models.general.resource import Resource from conjur_api.models.hostfactory.create_host_data import CreateHostData @@ -41,6 +42,10 @@ def __init__(self, testname): ) credential_provider = SimpleCredentialsProvider() credential_provider.save(CredentialsData(self.conjur_data.conjur_url, 'username', 'password', 'api_key')) + + oidc_credential_provider = SimpleCredentialsProvider() + oidc_credential_provider.save(CredentialsData(self.conjur_data.conjur_url, oidc_code_detail=OidcCodeDetail('code', 'code_verifier', 'nonce'))) + self.authn_provider = AuthnAuthenticationStrategy(credential_provider) self.oidc_provider = OidcAuthenticationStrategy(credential_provider) @@ -52,6 +57,11 @@ def __init__(self, testname): self.oidc_client = Client(self.conjur_oidc_data, authn_strategy=self.oidc_provider, ssl_verification_mode=self.ssl_verification_mode) + self.oidc_client_code = Client( + self.conjur_oidc_data, authn_strategy=OidcAuthenticationStrategy(oidc_credential_provider), + ssl_verification_mode=self.ssl_verification_mode + ) + # Shift the API token expiration ahead to avoid false negatives self.client._api.api_token_expiration = datetime.now() + timedelta(days=1) self.oidc_client._api.api_token_expiration = datetime.now() + timedelta(days=1) @@ -503,3 +513,17 @@ async def test_oidc_authentication(self, mock_regular_invoke_endpoint, mock_auth self.assertTrue(exists_in_args('account', args)) self.assertTrue(exists_in_args('id_token', args)) mock_auth_invoke_endpoint.assert_called_once() + + @patch('conjur_api.providers.oidc_authentication_strategy.invoke_endpoint') + @patch('conjur_api.http.api.invoke_endpoint') + async def test_oidc_authentication(self, mock_regular_invoke_endpoint, mock_auth_invoke_endpoint): + await self.oidc_client_code.authenticate() + + args, kwargs = mock_auth_invoke_endpoint.call_args + self.assertTrue(exists_in_args('url', args)) + self.assertTrue(exists_in_args('service_id', args)) + self.assertTrue(exists_in_args('account', args)) + self.assertEqual('code', kwargs['query'].get('code')) + self.assertEqual('code_verifier', kwargs['query'].get('code_verifier')) + self.assertEqual('code', kwargs['query'].get('code')) + mock_auth_invoke_endpoint.assert_called_once() From e0f0488f3c7030638f959a0c408cdc6ed4164241 Mon Sep 17 00:00:00 2001 From: Nirupma-Verma Date: Wed, 16 Aug 2023 16:29:19 +0530 Subject: [PATCH 2/2] unit testcase change and minor readme changes --- README.md | 6 +++--- tests/https/test_unit_client.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a0b93e1..699c640 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,9 @@ pip3 install . ### Configuring the client #### Define Modules -Authentication strategy supported by package are `authn`, `authn-ldap`, `authn-oidc` and based on authentication machanisam use the below module. +Authentication strategies supported by this package are `authn`, `authn-ldap`, `authn-oidc`. Based on authentication machanisam use the below module. -* Authn authentication (Supported on Conjur OSS,Enterprise,Cloud) +* Authn authentication (Supported on Conjur OSS, Enterprise, Cloud) ```python from conjur_api.providers.authn_authentication_strategy import AuthnAuthenticationStrategy ``` @@ -80,7 +80,7 @@ from conjur_api.providers.ldap_authentication_strategy import LdapAuthentication #### Define connection parameters -In order to login to conjur you need to have 5 parameters known from advance. +In order to login to conjur you need to have 5 parameters known in advance. 1. Authn/LDAP login parameters ```python diff --git a/tests/https/test_unit_client.py b/tests/https/test_unit_client.py index 22426b6..049811a 100644 --- a/tests/https/test_unit_client.py +++ b/tests/https/test_unit_client.py @@ -525,5 +525,5 @@ async def test_oidc_authentication(self, mock_regular_invoke_endpoint, mock_auth self.assertTrue(exists_in_args('account', args)) self.assertEqual('code', kwargs['query'].get('code')) self.assertEqual('code_verifier', kwargs['query'].get('code_verifier')) - self.assertEqual('code', kwargs['query'].get('code')) + self.assertEqual('nonce', kwargs['query'].get('nonce')) mock_auth_invoke_endpoint.assert_called_once()