Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support oidc v2 #43

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
108 changes: 103 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand All @@ -38,7 +38,7 @@ alterations that may result in breaking change.

```sh

pip3 install conjur
pip3 install conjur-api

```

Expand All @@ -58,24 +58,94 @@ pip3 install .

### Configuring the client

#### Define Modules
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)
```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.
In order to login to conjur you need to have 5 parameters known in advance.
1. Authn/LDAP login parameters

```python
from conjur_api.models import SslVerificationMode

conjur_url = "https://my_conjur.com"
account = "my_account"
username = "user1"
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 = "[email protected]"
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
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion conjur_api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
20 changes: 17 additions & 3 deletions conjur_api/models/general/credentials_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
36 changes: 33 additions & 3 deletions conjur_api/providers/oidc_authentication_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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):

Expand Down Expand Up @@ -76,4 +80,19 @@ def test_remove_credentials(self):

def test_get_store_location(self):
provider = SimpleCredentialsProvider()
self.assertEqual("SimpleCredentialsProvider",provider.get_store_location())
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))
26 changes: 25 additions & 1 deletion tests/https/test_unit_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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)
Expand Down Expand Up @@ -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('nonce', kwargs['query'].get('nonce'))
mock_auth_invoke_endpoint.assert_called_once()