From e7ff3a6e471f13464824497037072ecacbe6033e Mon Sep 17 00:00:00 2001 From: Erik Anderson Date: Thu, 15 Jan 2026 13:48:30 -0500 Subject: [PATCH 1/5] * Adding oauth2 and 2.1 support to trino-python-client * moved set_session to a base class. * added default keyring backend keyrings.cryptfile.cryptfile.CryptFileKeyring to securely store JWT tokens. --- README.md | 119 ++++++++++++++++- setup.py | 4 +- tests/unit/oauth_test_utils.py | 51 ++++++++ tests/unit/test_client.py | 45 +------ tests/unit/test_dbapi.py | 15 ++- trino/__init__.py | 2 + trino/auth.py | 230 ++++++++++++++++++++++++++++++++- 7 files changed, 414 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 68a84706..f251c8e6 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,10 @@ the [`JWT` authentication type](https://trino.io/docs/current/security/jwt.html) ### OAuth2 authentication +Make sure that the OAuth2 support is installed using `pip install trino[oauth]`. + +#### Interactive Browser authentication + The `OAuth2Authentication` class can be used to connect to a Trino cluster configured with the [OAuth2 authentication type](https://trino.io/docs/current/security/oauth2.html). @@ -248,7 +252,7 @@ The OAuth2 token will be cached either per `trino.auth.OAuth2Authentication` ins from trino.auth import OAuth2Authentication engine = create_engine( - "trino://@:/", + "trino://@:/", connect_args={ "auth": OAuth2Authentication(), "http_scheme": "https", @@ -256,6 +260,119 @@ The OAuth2 token will be cached either per `trino.auth.OAuth2Authentication` ins ) ``` +#### Client Credentials authentication + +```python +from trino.dbapi import connect +from trino.auth import ClientCredentials +from trino.oauth2.models import OidcConfig + +auth = ClientCredentials( + client_id="", + client_secret="", + url_config=OidcConfig( + token_endpoint="", + # other endpoints if needed + ), + scope="", # optional + audience="", # optional +) + +conn = connect( + user="", + auth=auth, + http_scheme="https", + ... +) +``` + +#### Device Code authentication + +```python +from trino.dbapi import connect +from trino.auth import DeviceCode +from trino.oauth2.models import OidcConfig + +auth = DeviceCode( + client_id="", + url_config=OidcConfig( + token_endpoint="", + device_authorization_endpoint="", + ), + scope="", # optional + audience="", # optional +) + +conn = connect( + user="", + auth=auth, + http_scheme="https", + ... +) +``` + +#### Authorization Code authentication + +```python +from trino.dbapi import connect +from trino.auth import AuthorizationCode +from trino.oauth2.models import OidcConfig + +auth = AuthorizationCode( + client_id="", + client_secret="", # optional + url_config=OidcConfig( + token_endpoint="", + authorization_endpoint="", + ), + scope="", # optional + audience="", # optional +) + +conn = connect( + user="", + auth=auth, + http_scheme="https", + ... +) +``` + +### Reference + +For further details, please consult [Trino documentation](https://trino.io/docs/current). + +### Secure Token Storage + +By default all ClientCredentials, DeviceCode, AuthorizationCode JWT tokens are securely storaged +using the keyrings.cryptfile feature of [keyring library](https://pypi.org/project/keyring/). + +Tokens are stored encrypted at ~/.local/share/python_keyring/cryptfile_pass.cfg + +You can optionally use different keyring backends by supplying the `PYTHON_KEYRING_BACKEND` environment variable. + +To use an encrypted file backend for credentials: + +```bash +export KEYRING_CRYPTFILE_PASSWORD=your_secure_password +``` + +Or you can pass the password directly (less secure): + +```python +conn = connect( + host="trino.example.com", + port=443, + auth=DeviceCode( + client_id="", + client_secret="", + url_config=OidcConfig(oidc_discovery_url="https://sso.example.com/.well-known/openid-configuration"), + token_storage_password="your_secure_password" # less secure + ), + http_scheme="https" +) +``` + + ### Certificate authentication `CertificateAuthentication` class can be used to connect to Trino cluster configured with [certificate based authentication](https://trino.io/docs/current/security/certificate.html). `CertificateAuthentication` requires paths to a valid client certificate and private key. diff --git a/setup.py b/setup.py index 0bd7102a..4a4cd6f3 100755 --- a/setup.py +++ b/setup.py @@ -33,9 +33,10 @@ "krb5 == 0.5.1"] sqlalchemy_require = ["sqlalchemy >= 1.3"] external_authentication_token_cache_require = ["keyring"] +oauth_require = ["trino.oauth2 @ git+https://github.com/dprophet/trino-python-oauth2"] # We don't add localstorage_require to all_require as users must explicitly opt in to use keyring. -all_require = kerberos_require + sqlalchemy_require +all_require = kerberos_require + sqlalchemy_require + oauth_require tests_require = all_require + gssapi_require + [ # httpretty >= 1.1 duplicates requests in `httpretty.latest_requests` @@ -96,6 +97,7 @@ "all": all_require, "kerberos": kerberos_require, "gssapi": gssapi_require, + "oauth": oauth_require, "sqlalchemy": sqlalchemy_require, "tests": tests_require, "external-authentication-token-cache": external_authentication_token_cache_require, diff --git a/tests/unit/oauth_test_utils.py b/tests/unit/oauth_test_utils.py index 956fee78..0d0a4130 100644 --- a/tests/unit/oauth_test_utils.py +++ b/tests/unit/oauth_test_utils.py @@ -150,3 +150,54 @@ def get_token_callback(self, request, uri, response_headers): if challenge.attempts == 0: return [200, response_headers, f'{{"token": "{challenge.token}"}}'] return [200, response_headers, f'{{"nextUri": "{uri}"}}'] + + +import keyring.backend + + +class MockKeyring(keyring.backend.KeyringBackend): + priority = 1 + + def __init__(self): + self.file_location = self._generate_test_root_dir() + + @staticmethod + def _generate_test_root_dir(): + import tempfile + + return tempfile.mkdtemp(prefix="trino-python-client-unit-test-") + + def _get_file_path(self, servicename, username): + from os.path import join + + file_location = self.file_location + file_name = f"{servicename}_{username}.txt" + return join(file_location, file_name) + + def set_password(self, servicename, username, password): + file_path = self._get_file_path(servicename, username) + + with open(file_path, "w") as file: + file.write(password) + + def get_password(self, servicename, username): + import os + + file_path = self._get_file_path(servicename, username) + if not os.path.exists(file_path): + return None + + with open(file_path, "r") as file: + password = file.read() + + return password + + def delete_password(self, servicename, username): + import os + + file_path = self._get_file_path(servicename, username) + if not os.path.exists(file_path): + return None + + os.remove(file_path) + diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index ba5f7f28..244cf200 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -47,6 +47,7 @@ from tests.unit.oauth_test_utils import RedirectHandlerWithException from tests.unit.oauth_test_utils import SERVER_ADDRESS from tests.unit.oauth_test_utils import TOKEN_RESOURCE +from tests.unit.oauth_test_utils import MockKeyring from trino import __version__ from trino import constants from trino.auth import _OAuth2KeyRingTokenCache @@ -1406,47 +1407,3 @@ def test_store_long_password(self): retrieved_password = cache.get_token_from_cache(host) self.assertEqual(long_password, retrieved_password) - -class MockKeyring(keyring.backend.KeyringBackend): - def __init__(self): - self.file_location = self._generate_test_root_dir() - - @staticmethod - def _generate_test_root_dir(): - import tempfile - - return tempfile.mkdtemp(prefix="trino-python-client-unit-test-") - - def file_path(self, servicename, username): - from os.path import join - - file_location = self.file_location - file_name = f"{servicename}_{username}.txt" - return join(file_location, file_name) - - def set_password(self, servicename, username, password): - file_path = self.file_path(servicename, username) - - with open(file_path, "w") as file: - file.write(password) - - def get_password(self, servicename, username): - import os - - file_path = self.file_path(servicename, username) - if not os.path.exists(file_path): - return None - - with open(file_path, "r") as file: - password = file.read() - - return password - - def delete_password(self, servicename, username): - import os - - file_path = self.file_path(servicename, username) - if not os.path.exists(file_path): - return None - - os.remove(file_path) diff --git a/tests/unit/test_dbapi.py b/tests/unit/test_dbapi.py index 080a3904..5ab3ab89 100644 --- a/tests/unit/test_dbapi.py +++ b/tests/unit/test_dbapi.py @@ -15,6 +15,7 @@ import httpretty import pytest +import keyring from httpretty import httprettified from requests import Session @@ -26,6 +27,7 @@ from tests.unit.oauth_test_utils import RedirectHandler from tests.unit.oauth_test_utils import SERVER_ADDRESS from tests.unit.oauth_test_utils import TOKEN_RESOURCE +from tests.unit.oauth_test_utils import MockKeyring from trino import constants from trino.auth import OAuth2Authentication from trino.dbapi import connect @@ -58,8 +60,15 @@ def test_http_session_is_defaulted_when_not_specified(mock_client): assert mock_client.TrinoRequest.http.Session.return_value in request_args +@pytest.fixture +def mock_keyring(): + mk = MockKeyring() + keyring.set_keyring(mk) + return mk + + @httprettified -def test_token_retrieved_once_per_auth_instance(sample_post_response_data, sample_get_response_data): +def test_token_retrieved_once_per_auth_instance(mock_keyring, sample_post_response_data, sample_get_response_data): token = str(uuid.uuid4()) challenge_id = str(uuid.uuid4()) @@ -123,7 +132,7 @@ def test_token_retrieved_once_per_auth_instance(sample_post_response_data, sampl @httprettified -def test_token_retrieved_once_when_authentication_instance_is_shared(sample_post_response_data, +def test_token_retrieved_once_when_authentication_instance_is_shared(mock_keyring, sample_post_response_data, sample_get_response_data): token = str(uuid.uuid4()) challenge_id = str(uuid.uuid4()) @@ -189,7 +198,7 @@ def test_token_retrieved_once_when_authentication_instance_is_shared(sample_post @httprettified -def test_token_retrieved_once_when_multithreaded(sample_post_response_data, sample_get_response_data): +def test_token_retrieved_once_when_multithreaded(mock_keyring, sample_post_response_data, sample_get_response_data): token = str(uuid.uuid4()) challenge_id = str(uuid.uuid4()) diff --git a/trino/__init__.py b/trino/__init__.py index 3db819cc..63e79c4d 100644 --- a/trino/__init__.py +++ b/trino/__init__.py @@ -9,6 +9,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +__path__ = __import__("pkgutil").extend_path(__path__, __name__) + from . import auth from . import client from . import constants diff --git a/trino/auth.py b/trino/auth.py index 783d0f5e..730cd987 100644 --- a/trino/auth.py +++ b/trino/auth.py @@ -23,6 +23,7 @@ from typing import List from typing import Optional from typing import Tuple +from typing import Union from urllib.parse import urlparse from requests import PreparedRequest @@ -37,6 +38,12 @@ from trino.constants import HEADER_ORIGINAL_USER from trino.constants import HEADER_USER from trino.constants import MAX_NT_PASSWORD_SIZE +from trino.oauth2 import OAuth2Client +from trino.oauth2.models import AuthorizationCodeConfig +from trino.oauth2.models import ClientCredentialsConfig +from trino.oauth2.models import DeviceCodeConfig +from trino.oauth2.models import ManualUrlsConfig +from trino.oauth2.models import OidcConfig logger = trino.logging.get_logger(__name__) @@ -50,6 +57,23 @@ def get_exceptions(self) -> Tuple[Any, ...]: return tuple() +class OAuth2TokenAuthentication(Authentication): + """Shared base for OAuth2 strategies that authenticate with a bearer token.""" + + def __init__(self) -> None: + self._oauth2: Optional[OAuth2Client] = None + + @property + def oauth2(self) -> OAuth2Client: + if self._oauth2 is None: + raise RuntimeError("OAuth2 client not initialized") + return self._oauth2 + + def set_http_session(self, http_session: Session) -> Session: + http_session.auth = _BearerAuth(self.oauth2.token()) + return http_session + + class KerberosAuthentication(Authentication): MUTUAL_REQUIRED = 1 MUTUAL_OPTIONAL = 2 @@ -276,6 +300,142 @@ def __eq__(self, other: object) -> bool: return self.token == other.token +class ClientCredentials(Authentication): + def __init__(self, + client_id: str, + client_secret: str, + url_config: Union[OidcConfig, ManualUrlsConfig], + scope: Optional[str] = None, + audience: Optional[str] = None, + token_storage_password: Optional[str] = None): + super().__init__() + self.client_id = client_id + self.client_secret = client_secret + self.url_config = url_config + self.scope = scope + self.audience = audience + self.token_storage_password = token_storage_password + + config_args = { + "client_id": self.client_id, + "client_secret": self.client_secret, + "url_config": self.url_config, + } + if self.scope is not None: + config_args["scope"] = self.scope + if self.audience is not None: + config_args["audience"] = self.audience + + self._oauth2 = OAuth2Client( + config=ClientCredentialsConfig(**config_args), + token_storage_password=self.token_storage_password + ) + + def get_exceptions(self) -> Tuple[Any, ...]: + return () + + def __eq__(self, other: object) -> bool: + if not isinstance(other, ClientCredentials): + return False + return ( + self.client_id == other.client_id + and self.client_secret == other.client_secret + and self.url_config == other.url_config + ) + + +class DeviceCode(Authentication): + def __init__(self, + client_id: str, + url_config: Union[OidcConfig, ManualUrlsConfig], + client_secret: Optional[str] = None, + scope: Optional[str] = None, + audience: Optional[str] = None, + token_storage_password: Optional[str] = None): + + super().__init__() + self.client_id = client_id + self.client_secret = client_secret + self.url_config = url_config + self.scope = scope + self.audience = audience + self.token_storage_password = token_storage_password + + config_args = { + "client_id": self.client_id, + "url_config": self.url_config, + } + if self.client_secret is not None: + config_args["client_secret"] = self.client_secret + if self.scope is not None: + config_args["scope"] = self.scope + if self.audience is not None: + config_args["audience"] = self.audience + + self._oauth2 = OAuth2Client( + config=DeviceCodeConfig(**config_args), + token_storage_password=self.token_storage_password + ) + + def get_exceptions(self) -> Tuple[Any, ...]: + return () + + def __eq__(self, other: object) -> bool: + if not isinstance(other, DeviceCode): + return False + return ( + self.client_id == other.client_id + and self.client_secret == other.client_secret + and self.url_config == other.url_config + ) + + +class AuthorizationCode(Authentication): + def __init__(self, + client_id: str, + url_config: Union[OidcConfig, ManualUrlsConfig], + client_secret: Optional[str] = None, + scope: Optional[str] = None, + audience: Optional[str] = None, + token_storage_password: Optional[str] = None): + + super().__init__() + self.client_id = client_id + self.client_secret = client_secret + self.url_config = url_config + self.scope = scope + self.audience = audience + self.token_storage_password = token_storage_password + + config_args = { + "client_id": self.client_id, + "url_config": self.url_config, + } + if self.client_secret is not None: + config_args["client_secret"] = self.client_secret + if self.scope is not None: + config_args["scope"] = self.scope + if self.audience is not None: + config_args["audience"] = self.audience + + self._oauth2 = OAuth2Client( + config=AuthorizationCodeConfig(**config_args), + token_storage_password=self.token_storage_password + ) + + def get_exceptions(self) -> Tuple[Any, ...]: + return () + + def __eq__(self, other: object) -> bool: + if not isinstance(other, DeviceCode): + return False + return ( + self.client_id == other.client_id + and self.client_secret == other.client_secret + and self.url_config == other.url_config + ) + + class RedirectHandler(metaclass=abc.ABCMeta): """ Abstract class for OAuth redirect handlers, inherit from this class to implement your own redirect handler. @@ -292,7 +452,10 @@ class ConsoleRedirectHandler(RedirectHandler): """ def __call__(self, url: str) -> None: - print(f"Open the following URL in browser for the external authentication:\n{url}", flush=True) + print( + f"Open the following URL in browser for the external authentication:\n{url}", + flush=True, + ) class WebBrowserRedirectHandler(RedirectHandler): @@ -361,8 +524,69 @@ def __init__(self) -> None: logger.info("keyring module not found. OAuth2 token will not be stored in keyring.") def is_keyring_available(self) -> bool: - return self._keyring is not None \ - and not isinstance(self._keyring.get_keyring(), self._keyring.backends.fail.Keyring) + if self._keyring is None: + return False + + try: + backend = self._keyring.get_keyring() + except Exception: + return False + + # 1. Check for "fail" backend + try: + fail_keyring_cls = self._keyring.backends.fail.Keyring + if isinstance(backend, fail_keyring_cls): + return False + except AttributeError: + pass + + # 2. Helper to check env vars for a specific backend instance + def has_env_config(backend_obj: Any) -> bool: + # Get class name: e.g., 'CryptFileKeyring' + cls_name = backend_obj.__class__.__name__.upper() + + # Generate potential config names: ['CRYPTFILEKEYRING', 'CRYPTFILE'] + possible_names = [cls_name] + if cls_name.endswith("KEYRING"): + possible_names.append(cls_name[:-7]) # Strip 'KEYRING' + + # check both KEYRING_PROPERTY_{NAME} and KEYRING_{NAME} for all variants + for name in possible_names: + prefixes = (f"KEYRING_PROPERTY_{name}", f"KEYRING_{name}") + if any(k.upper().startswith(prefixes) for k in os.environ): + return True + return False + + # 3. Handle ChainerBackend + if hasattr(backend, 'backends'): + for sub_backend in backend.backends: + # A. File-based backends + if hasattr(sub_backend, 'file_path'): + # Case 1: File exists + if os.path.exists(sub_backend.file_path): + return True + + # Case 2: Env var exists (using the helper) + if has_env_config(sub_backend): + return True + + # Neither exists; skip + continue + + # B. System backends (Priority check) + if sub_backend.priority >= 1: + return True + + return False + + # 4. Handle direct File-based backends via ~/.config/python_keyring/keyringrc.cfg + if hasattr(backend, 'file_path'): + if os.path.exists(backend.file_path): + return True + return has_env_config(backend) + + # 5. Fallback + return backend.priority >= 1 def get_token_from_cache(self, key: Optional[str]) -> Optional[str]: password = self._keyring.get_password(key, "token") From 693830485bde8c0afa7950b4b9f50e152c66d795 Mon Sep 17 00:00:00 2001 From: Erik Anderson Date: Mon, 26 Jan 2026 15:56:40 -0500 Subject: [PATCH 2/5] Debug PR process. --- trino/auth.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/trino/auth.py b/trino/auth.py index 730cd987..326b6916 100644 --- a/trino/auth.py +++ b/trino/auth.py @@ -554,6 +554,7 @@ def has_env_config(backend_obj: Any) -> bool: for name in possible_names: prefixes = (f"KEYRING_PROPERTY_{name}", f"KEYRING_{name}") if any(k.upper().startswith(prefixes) for k in os.environ): + print("Environment variable-based keyring backend found for %s", name) return True return False @@ -564,6 +565,7 @@ def has_env_config(backend_obj: Any) -> bool: if hasattr(sub_backend, 'file_path'): # Case 1: File exists if os.path.exists(sub_backend.file_path): + print("File-based keyring backend found at %s", sub_backend.file_path) return True # Case 2: Env var exists (using the helper) @@ -575,6 +577,7 @@ def has_env_config(backend_obj: Any) -> bool: # B. System backends (Priority check) if sub_backend.priority >= 1: + print("File-based keyring backend with sufficient priority found: {}, priority={}".format(str(sub_backend), str(sub_backend.priority))) return True return False From bdd4372177981249c5fc29979390358090e6a691 Mon Sep 17 00:00:00 2001 From: Erik Anderson Date: Mon, 26 Jan 2026 16:01:45 -0500 Subject: [PATCH 3/5] Fix reorderings. --- tests/unit/oauth_test_utils.py | 5 +---- tests/unit/test_client.py | 1 - tests/unit/test_dbapi.py | 4 ++-- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/unit/oauth_test_utils.py b/tests/unit/oauth_test_utils.py index 0d0a4130..d3e95919 100644 --- a/tests/unit/oauth_test_utils.py +++ b/tests/unit/oauth_test_utils.py @@ -15,6 +15,7 @@ from collections import namedtuple import httpretty +import keyring.backend from trino import constants @@ -152,9 +153,6 @@ def get_token_callback(self, request, uri, response_headers): return [200, response_headers, f'{{"nextUri": "{uri}"}}'] -import keyring.backend - - class MockKeyring(keyring.backend.KeyringBackend): priority = 1 @@ -200,4 +198,3 @@ def delete_password(self, servicename, username): return None os.remove(file_path) - diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 244cf200..65e3d7a2 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -1406,4 +1406,3 @@ def test_store_long_password(self): retrieved_password = cache.get_token_from_cache(host) self.assertEqual(long_password, retrieved_password) - diff --git a/tests/unit/test_dbapi.py b/tests/unit/test_dbapi.py index 5ab3ab89..1866d41a 100644 --- a/tests/unit/test_dbapi.py +++ b/tests/unit/test_dbapi.py @@ -14,20 +14,20 @@ from unittest.mock import patch import httpretty -import pytest import keyring +import pytest from httpretty import httprettified from requests import Session from tests.unit.oauth_test_utils import _get_token_requests from tests.unit.oauth_test_utils import _post_statement_requests from tests.unit.oauth_test_utils import GetTokenCallback +from tests.unit.oauth_test_utils import MockKeyring from tests.unit.oauth_test_utils import PostStatementCallback from tests.unit.oauth_test_utils import REDIRECT_RESOURCE from tests.unit.oauth_test_utils import RedirectHandler from tests.unit.oauth_test_utils import SERVER_ADDRESS from tests.unit.oauth_test_utils import TOKEN_RESOURCE -from tests.unit.oauth_test_utils import MockKeyring from trino import constants from trino.auth import OAuth2Authentication from trino.dbapi import connect From 49e825ea5cae20b0870b793311f426c1c9e91ff1 Mon Sep 17 00:00:00 2001 From: Erik Anderson Date: Mon, 26 Jan 2026 16:57:09 -0500 Subject: [PATCH 4/5] Fix tests and long line. --- tests/unit/conftest.py | 10 ++++++++++ trino/auth.py | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 39c4eaa0..ea803942 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -12,8 +12,18 @@ from unittest.mock import MagicMock from unittest.mock import patch +import keyring import pytest +from tests.unit.oauth_test_utils import MockKeyring + + +@pytest.fixture(autouse=True, scope="session") +def setup_test_keyring(): + mk = MockKeyring() + keyring.set_keyring(mk) + yield mk + @pytest.fixture(scope="session") def sample_post_response_data(): diff --git a/trino/auth.py b/trino/auth.py index 326b6916..c868ecb9 100644 --- a/trino/auth.py +++ b/trino/auth.py @@ -577,7 +577,8 @@ def has_env_config(backend_obj: Any) -> bool: # B. System backends (Priority check) if sub_backend.priority >= 1: - print("File-based keyring backend with sufficient priority found: {}, priority={}".format(str(sub_backend), str(sub_backend.priority))) + print("File-based keyring backend with sufficient priority found: {}, priority={}" + .format(str(sub_backend), str(sub_backend.priority))) return True return False From 1b3adf64c5cebf0e4c284b3db6bb7abce401b97c Mon Sep 17 00:00:00 2001 From: Erik Anderson Date: Wed, 4 Feb 2026 17:51:02 -0500 Subject: [PATCH 5/5] Fix --- trino/auth.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/trino/auth.py b/trino/auth.py index c868ecb9..56f4a842 100644 --- a/trino/auth.py +++ b/trino/auth.py @@ -300,7 +300,7 @@ def __eq__(self, other: object) -> bool: return self.token == other.token -class ClientCredentials(Authentication): +class ClientCredentials(OAuth2TokenAuthentication): def __init__(self, client_id: str, client_secret: str, @@ -344,7 +344,7 @@ def __eq__(self, other: object) -> bool: ) -class DeviceCode(Authentication): +class DeviceCode(OAuth2TokenAuthentication): def __init__(self, client_id: str, url_config: Union[OidcConfig, ManualUrlsConfig], @@ -390,7 +390,7 @@ def __eq__(self, other: object) -> bool: ) -class AuthorizationCode(Authentication): +class AuthorizationCode(OAuth2TokenAuthentication): def __init__(self, client_id: str, url_config: Union[OidcConfig, ManualUrlsConfig],