From 88a8ccc6d34a7b94c474d1d0e11ecb0c172b9086 Mon Sep 17 00:00:00 2001 From: Nehuen Gonzalez-Montoro Date: Thu, 1 Aug 2024 04:28:19 -0300 Subject: [PATCH] feat: allow the use of client certificates in all requests (#584) --- src/keycloak/connection.py | 30 ++++++++++++++++++++++++++++-- src/keycloak/keycloak_admin.py | 9 +++++++++ src/keycloak/keycloak_openid.py | 15 ++++++++++++++- src/keycloak/openid_connection.py | 8 ++++++++ tests/test_keycloak_admin.py | 10 ++-------- 5 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/keycloak/connection.py b/src/keycloak/connection.py index c4881f0..9d55c37 100644 --- a/src/keycloak/connection.py +++ b/src/keycloak/connection.py @@ -49,9 +49,13 @@ class ConnectionManager(object): :type verify: Union[bool,str] :param proxies: The proxies servers requests is sent by. :type proxies: dict + :param cert: An SSL certificate used by the requested host to authenticate the client. + Either a path to an SSL certificate file, or two-tuple of + (certificate file, key file). + :type cert: Union[str,Tuple[str,str]] """ - def __init__(self, base_url, headers={}, timeout=60, verify=True, proxies=None): + def __init__(self, base_url, headers={}, timeout=60, verify=True, proxies=None, cert=None): """Init method. :param base_url: The server URL. @@ -65,11 +69,16 @@ class ConnectionManager(object): :type verify: Union[bool,str] :param proxies: The proxies servers requests is sent by. :type proxies: dict + :param cert: An SSL certificate used by the requested host to authenticate the client. + Either a path to an SSL certificate file, or two-tuple of + (certificate file, key file). + :type cert: Union[str,Tuple[str,str]] """ self.base_url = base_url self.headers = headers self.timeout = timeout self.verify = verify + self.cert = cert self._s = requests.Session() self._s.auth = lambda x: x # don't let requests add auth headers @@ -87,7 +96,7 @@ class ConnectionManager(object): if proxies: self._s.proxies.update(proxies) - self.async_s = httpx.AsyncClient(verify=verify, proxies=proxies) + self.async_s = httpx.AsyncClient(verify=verify, proxies=proxies, cert=cert) self.async_s.auth = None # don't let requests add auth headers self.async_s.transport = httpx.AsyncHTTPTransport(retries=1) @@ -140,6 +149,19 @@ class ConnectionManager(object): def verify(self, value): self._verify = value + @property + def cert(self): + """Return client certificates in use for request to the server. + + :returns: Client certificate + :rtype: Union[str,Tuple[str,str]] + """ + return self._cert + + @cert.setter + def cert(self, value): + self._cert = value + @property def headers(self): """Return header request to the server. @@ -213,6 +235,7 @@ class ConnectionManager(object): headers=self.headers, timeout=self.timeout, verify=self.verify, + cert=self.cert, ) except Exception as e: raise KeycloakConnectionError("Can't connect to server (%s)" % e) @@ -238,6 +261,7 @@ class ConnectionManager(object): headers=self.headers, timeout=self.timeout, verify=self.verify, + cert=self.cert, ) except Exception as e: raise KeycloakConnectionError("Can't connect to server (%s)" % e) @@ -263,6 +287,7 @@ class ConnectionManager(object): headers=self.headers, timeout=self.timeout, verify=self.verify, + cert=self.cert, ) except Exception as e: raise KeycloakConnectionError("Can't connect to server (%s)" % e) @@ -288,6 +313,7 @@ class ConnectionManager(object): headers=self.headers, timeout=self.timeout, verify=self.verify, + cert=self.cert, ) return r except Exception as e: diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 41f2e46..8415d10 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -73,6 +73,10 @@ class KeycloakAdmin: :type user_realm_name: str :param timeout: connection timeout in seconds :type timeout: int + :param cert: An SSL certificate used by the requested host to authenticate the client. + Either a path to an SSL certificate file, or two-tuple of + (certificate file, key file). + :type cert: Union[str,Tuple[str,str]] :param connection: A KeycloakOpenIDConnection as an alternative to individual params. :type connection: KeycloakOpenIDConnection """ @@ -93,6 +97,7 @@ class KeycloakAdmin: custom_headers=None, user_realm_name=None, timeout=60, + cert=None, connection: Optional[KeycloakOpenIDConnection] = None, ): """Init method. @@ -123,6 +128,9 @@ class KeycloakAdmin: :type user_realm_name: str :param timeout: connection timeout in seconds :type timeout: int + :param cert: An SSL certificate used by the requested host to authenticate the client. + Either a path to an SSL certificate file, or two-tuple of (certificate file, key file). + :type cert: Union[str,Tuple[str,str]] :param connection: An OpenID Connection as an alternative to individual params. :type connection: KeycloakOpenIDConnection """ @@ -139,6 +147,7 @@ class KeycloakAdmin: user_realm_name=user_realm_name, custom_headers=custom_headers, timeout=timeout, + cert=cert, ) @property diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index 59ddabe..7c0a558 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -74,6 +74,9 @@ class KeycloakOpenID: :param custom_headers: dict of custom header to pass to each HTML request :param proxies: dict of proxies to sent the request by. :param timeout: connection timeout in seconds + :param cert: An SSL certificate used by the requested host to authenticate the client. + Either a path to an SSL certificate file, or two-tuple of + (certificate file, key file). """ def __init__( @@ -86,6 +89,7 @@ class KeycloakOpenID: custom_headers=None, proxies=None, timeout=60, + cert=None, ): """Init method. @@ -106,13 +110,22 @@ class KeycloakOpenID: :type proxies: dict :param timeout: connection timeout in seconds :type timeout: int + :param cert: An SSL certificate used by the requested host to authenticate the client. + Either a path to an SSL certificate file, or two-tuple of + (certificate file, key file). + :type cert: Union[str,Tuple[str,str]] """ self.client_id = client_id self.client_secret_key = client_secret_key self.realm_name = realm_name headers = custom_headers if custom_headers is not None else dict() self.connection = ConnectionManager( - base_url=server_url, headers=headers, timeout=timeout, verify=verify, proxies=proxies + base_url=server_url, + headers=headers, + timeout=timeout, + verify=verify, + proxies=proxies, + cert=cert, ) self.authorization = Authorization() diff --git a/src/keycloak/openid_connection.py b/src/keycloak/openid_connection.py index c604356..15d2cb9 100644 --- a/src/keycloak/openid_connection.py +++ b/src/keycloak/openid_connection.py @@ -70,6 +70,7 @@ class KeycloakOpenIDConnection(ConnectionManager): custom_headers=None, user_realm_name=None, timeout=60, + cert=None, ): """Init method. @@ -99,6 +100,10 @@ class KeycloakOpenIDConnection(ConnectionManager): :type user_realm_name: str :param timeout: connection timeout in seconds :type timeout: int + :param cert: An SSL certificate used by the requested host to authenticate the client. + Either a path to an SSL certificate file, or two-tuple of + (certificate file, key file). + :type cert: Union[str,Tuple[str,str]] """ # token is renewed when it hits 90% of its lifetime. This is to account for any possible # clock skew. @@ -117,12 +122,14 @@ class KeycloakOpenIDConnection(ConnectionManager): self.timeout = timeout self.custom_headers = custom_headers self.headers = {**self.headers, "Content-Type": "application/json"} + self.cert = cert super().__init__( base_url=self.server_url, headers=self.headers, timeout=self.timeout, verify=self.verify, + cert=cert, ) @property @@ -297,6 +304,7 @@ class KeycloakOpenIDConnection(ConnectionManager): client_secret_key=self.client_secret_key, timeout=self.timeout, custom_headers=self.custom_headers, + cert=self.cert, ) return self._keycloak_openid diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index c6e6448..b87f413 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -5198,10 +5198,7 @@ async def test_a_email_query_param_handling(admin: KeycloakAdmin, user: str): mock_put.assert_awaited_once_with( ANY, data='["UPDATE_PASSWORD"]', - params={ - "client_id": "update-account-client-id", - "redirect_uri": "https://example.com", - }, + params={"client_id": "update-account-client-id", "redirect_uri": "https://example.com"}, headers=ANY, timeout=60, ) @@ -5216,10 +5213,7 @@ async def test_a_email_query_param_handling(admin: KeycloakAdmin, user: str): mock_put.assert_awaited_once_with( ANY, data=ANY, - params={ - "client_id": "verify-client-id", - "redirect_uri": "https://example.com", - }, + params={"client_id": "verify-client-id", "redirect_uri": "https://example.com"}, headers=ANY, timeout=60, )