Skip to content
Merged
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
45 changes: 44 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ from fds.sdk.utils.authentication import ConfidentialClient
import requests

# The ConfidentialClient instance should be reused in production environments.
client = ConfidentialClient('/path/to/config.json')
client = ConfidentialClient(
config_path='/path/to/config.json'
)
res = requests.get(
'https://api.factset.com/analytics/lookups/v3/currencies',
headers={
Expand All @@ -47,6 +49,47 @@ res = requests.get(
print(res.text)
```

### Configure a Proxy

You can pass proxy settings to the ConfidentialClient if necessary.
The `proxy` parameter takes a URL to tell the request library which proxy should be used.

If necessary it is possible to set custom `proxy_headers` as dictionary.

```python
from fds.sdk.utils.authentication import ConfidentialClient

client = ConfidentialClient(
config_path='/path/to/config.json',
proxy= "http://secret:password@localhost:5050",
proxy_headers={
"Custom-Proxy-Header": "Custom-Proxy-Header-Value"
}
)
```

### Custom SSL Certificate

If you have proxies or firewalls which are using custom TLS certificates,
you are able to pass a custom pem file (`ssl_ca_cert` parameter) so that the
request library is able to verify the validity of that certificate. If a
ca cert is passed it is validated regardless if `verify_ssl` is set to false.

With `verify_ssl` it is possible to disable the verifications of certificates.
Disabling the verification is not recommended, but it might be useful during
local development or testing


```python
from fds.sdk.utils.authentication import ConfidentialClient

client = ConfidentialClient(
config_path='/path/to/config.json',
verify_ssl=True,
ssl_ca_cert='/path/to/ca.pem'
)
```

## Modules

Information about the various utility modules contained in this library can be found below.
Expand Down
69 changes: 65 additions & 4 deletions src/fds/sdk/utils/authentication/confidential.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,15 @@ class ConfidentialClient(OAuth2Client):
the access token, caching it and refreshing it as needed.
"""

def __init__(self, config_path: str = "", config: dict = None) -> None:
def __init__(
self,
config_path: str = None,
config: dict = None,
proxy: str = None,
proxy_headers: dict = None,
verify_ssl: bool = True,
ssl_ca_cert: str = None,
) -> None:
"""
Creates a new ConfidentialClient.

Expand Down Expand Up @@ -70,7 +78,24 @@ def __init__(self, config_path: str = "", config: dict = None) -> None:
}
}

`NB`: Within the JWK parameters kty, alg, use, kid, n, e, d, p, q, dp, dq, qi are required for authorization.
`NB`: Within the JWK parameters kty, alg, use, kid, n, e, d, p, q, dp, dq, qi are
required for authorization.

`proxy` (str) : Proxy URL

`proxy_headers` (dict) : Sometimes it is necessary to add custom headers to http requests to be able to
use a proxy or firewall

`verify_ssl` (bool): Set this to ``False`` to skip verifying SSL certificate when calling API from
https server. When set to ``False``, requests will accept any TLS certificate presented by the server,
and will ignore hostname mismatches and/or expired certificates, which will make your application
vulnerable to man-in-the-middle (MitM) attacks. Setting verify to ``False`` may be useful during
local development or testing.

`ssl_ca_cert` (str): Set this to customize the certificate file to verify the peer. If ``ssl_ca_cert`` is
set, the ca_cert will be verified whether ``verify_ssl`` is enabled


Raises:
AuthServerMetadataError: Raised if there's an issue retrieving the authorization server metadata
AuthServerMetadataContentError: Raised if the authorization server metadata is incomplete
Expand All @@ -84,7 +109,7 @@ def __init__(self, config_path: str = "", config: dict = None) -> None:
raise ValueError("Either 'config_path' or 'config' must be set.")

if config_path and config:
raise ValueError("Either 'config_path' or 'config' must be set. Not Both.")
raise ValueError("Either 'config_path' or 'config' must be set. Not Both.")

if config_path:
try:
Expand All @@ -97,6 +122,15 @@ def __init__(self, config_path: str = "", config: dict = None) -> None:
if config:
self._config = config

if proxy:
self._proxy = {"http": proxy, "https": proxy}
else:
self._proxy = None

self._verify_ssl = verify_ssl
self._proxy_headers = proxy_headers
self._ssl_ca_cert = ssl_ca_cert

try:
self._oauth_session = OAuth2Session(
client=BackendApplicationClient(client_id=self._config[CONSTS.CONFIG_CLIENT_ID])
Expand Down Expand Up @@ -127,7 +161,18 @@ def _init_auth_server_metadata(self) -> None:
log.debug(
"Attempting metadata retrieval from well_known_uri: %s", self._config[CONSTS.CONFIG_WELL_KNOWN_URI]
)
res = requests.get(self._config[CONSTS.CONFIG_WELL_KNOWN_URI])

verify = self._verify_ssl

if self._ssl_ca_cert:
verify = self._ssl_ca_cert

res = requests.get(
url=self._config[CONSTS.CONFIG_WELL_KNOWN_URI],
proxies=self._proxy,
verify=verify,
headers=self._proxy_headers,
)
log.debug("Request from well_known_uri completed with status: %s", res.status_code)
log.debug("Response headers from well_known_uri were %s", res.headers)
self._well_known_uri_metadata = res.json()
Expand Down Expand Up @@ -208,11 +253,27 @@ def get_access_token(self) -> str:

try:
log.debug("Fetching new access token")

headers = {
"Accept": "application/json",
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
}

if self._proxy_headers:
headers |= self._proxy_headers

verify = self._verify_ssl
if self._ssl_ca_cert:
verify = self._ssl_ca_cert

token = self._oauth_session.fetch_token(
token_url=self._well_known_uri_metadata[CONSTS.META_TOKEN_ENDPOINT],
client_id=self._config[CONSTS.CONFIG_CLIENT_ID],
client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
client_assertion=self._get_client_assertion_jws(),
verify=verify,
proxies=self._proxy,
headers=headers,
)
self._cached_token = token
log.info("Caching token that expires at %s", token[CONSTS.TOKEN_EXPIRES_AT])
Expand Down
45 changes: 44 additions & 1 deletion tests/fds/sdk/utils/authentication/test_confidential.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,43 @@ def test_constructor_session_instantiation(mocker, example_config):
mock_oauth2_session.assert_called_with(client=backend_result)


def test_constructor_session_instantiation_with_additional_parameters(mocker, example_config):
test_client_id = "good_test"
backend_result = "good_mock_backend"
example_config["clientId"] = test_client_id
mock_oauth_backend = mocker.patch(
"fds.sdk.utils.authentication.confidential.BackendApplicationClient", return_value=backend_result
)

mock_oauth2_session = mocker.patch("fds.sdk.utils.authentication.confidential.OAuth2Session")

additional_parameters = {
"proxy": "http://my:pass@test.test.test",
"verify_ssl": False,
"proxy_headers": {},
}

class AuthServerMetadataRes:
status_code = 200
headers = {"header": "value"}

def json(self):
return {"issuer": "test", "token_endpoint": "http://test.test"}

get_mock = mocker.patch("requests.get", return_value=AuthServerMetadataRes())

ConfidentialClient(config=example_config, **additional_parameters)

mock_oauth_backend.assert_called_with(client_id=test_client_id)
mock_oauth2_session.assert_called_with(client=backend_result)
get_mock.assert_called_with(
url="https://auth.factset.com/.well-known/openid-configuration",
proxies={"http": "http://my:pass@test.test.test", "https": "http://my:pass@test.test.test"},
verify=False,
headers={},
)


def test_constructor_custom_well_known_uri(mocker, example_config, caplog):
caplog.set_level(logging.DEBUG)
mocker.patch("fds.sdk.utils.authentication.confidential.BackendApplicationClient")
Expand All @@ -188,7 +225,7 @@ def json(self):

client = ConfidentialClient(config=example_config)

get_mock.assert_called_with(auth_test)
get_mock.assert_called_with(url=auth_test, proxies=None, verify=True, headers=None)
assert client

assert "Attempting metadata retrieval from well_known_uri: https://auth.test" in caplog.text
Expand Down Expand Up @@ -325,6 +362,12 @@ def test_get_access_token_fetch(client, mocker):
client_id="test-clientid",
client_assertion_type="urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
client_assertion="jws",
proxies=None,
verify=True,
headers={
"Accept": "application/json",
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
},
)


Expand Down