From 7bf8b4905d6d2bbb0834ddfe77715f606cf5ef98 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Fri, 11 Sep 2020 10:39:34 -0700 Subject: [PATCH 01/12] new token cache classes --- .../azure-identity/azure/identity/__init__.py | 2 + .../azure/identity/_internal/__init__.py | 9 ++ .../identity/_internal/persistent_cache.py | 65 --------------- .../azure/identity/_persistent_cache.py | 82 +++++++++++++++++++ 4 files changed, 93 insertions(+), 65 deletions(-) delete mode 100644 sdk/identity/azure-identity/azure/identity/_internal/persistent_cache.py create mode 100644 sdk/identity/azure-identity/azure/identity/_persistent_cache.py diff --git a/sdk/identity/azure-identity/azure/identity/__init__.py b/sdk/identity/azure-identity/azure/identity/__init__.py index 3819005a9e63..8469d70430ae 100644 --- a/sdk/identity/azure-identity/azure/identity/__init__.py +++ b/sdk/identity/azure-identity/azure/identity/__init__.py @@ -22,6 +22,7 @@ UsernamePasswordCredential, VisualStudioCodeCredential, ) +from ._persistent_cache import PersistentTokenCache __all__ = [ @@ -40,6 +41,7 @@ "InteractiveBrowserCredential", "KnownAuthorities", "ManagedIdentityCredential", + "PersistentTokenCache", "SharedTokenCacheCredential", "UsernamePasswordCredential", "VisualStudioCodeCredential", diff --git a/sdk/identity/azure-identity/azure/identity/_internal/__init__.py b/sdk/identity/azure-identity/azure/identity/_internal/__init__.py index da0c1ff1e20a..3dc779295986 100644 --- a/sdk/identity/azure-identity/azure/identity/_internal/__init__.py +++ b/sdk/identity/azure-identity/azure/identity/_internal/__init__.py @@ -3,11 +3,19 @@ # Licensed under the MIT License. # ------------------------------------ import os + +import msal from six.moves.urllib_parse import urlparse from .._constants import EnvironmentVariables, KnownAuthorities +class _TokenCache(object): + """Wrapper for in memory token caches""" + def __init__(self): + self._cache = msal.TokenCache() + + def normalize_authority(authority): # type: (str) -> str """Ensure authority uses https, strip trailing spaces and /""" @@ -67,6 +75,7 @@ def _scopes_to_resource(*scopes): __all__ = [ "_scopes_to_resource", + "_TokenCache", "AadClient", "AadClientBase", "AuthCodeRedirectServer", diff --git a/sdk/identity/azure-identity/azure/identity/_internal/persistent_cache.py b/sdk/identity/azure-identity/azure/identity/_internal/persistent_cache.py deleted file mode 100644 index 4887cd296d7a..000000000000 --- a/sdk/identity/azure-identity/azure/identity/_internal/persistent_cache.py +++ /dev/null @@ -1,65 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import os -import sys -from typing import TYPE_CHECKING - -import msal_extensions - -if TYPE_CHECKING: - from typing import Optional - import msal - - -def load_service_principal_cache(allow_unencrypted): - # type: (Optional[bool]) -> msal.TokenCache - return _load_persistent_cache(allow_unencrypted, "MSALConfidentialCache", "msal.confidential.cache") - - -def load_user_cache(allow_unencrypted): - # type: (Optional[bool]) -> msal.TokenCache - return _load_persistent_cache(allow_unencrypted, "MSALCache", "msal.cache") - - -def _load_persistent_cache(allow_unencrypted, account_name, cache_name): - # type: (Optional[bool], str, str) -> msal.TokenCache - """Load the persistent cache using msal_extensions. - - On Windows the cache is a file protected by the Data Protection API. On Linux and macOS the cache is stored by - libsecret and Keychain, respectively. On those platforms the cache uses the modified timestamp of a file on disk to - decide whether to reload the cache. - - :param bool allow_unencrypted: when True, the cache will be kept in plaintext should encryption be impossible in the - current environment - """ - - if sys.platform.startswith("win") and "LOCALAPPDATA" in os.environ: - cache_location = os.path.join(os.environ["LOCALAPPDATA"], ".IdentityService", cache_name) - persistence = msal_extensions.FilePersistenceWithDataProtection(cache_location) - elif sys.platform.startswith("darwin"): - # the cache uses this file's modified timestamp to decide whether to reload - file_path = os.path.expanduser(os.path.join("~", ".IdentityService", cache_name)) - persistence = msal_extensions.KeychainPersistence( - file_path, "Microsoft.Developer.IdentityService", account_name - ) - elif sys.platform.startswith("linux"): - # The cache uses this file's modified timestamp to decide whether to reload. Note this path is the same - # as that of the plaintext fallback: a new encrypted cache will stomp an unencrypted cache. - file_path = os.path.expanduser(os.path.join("~", ".IdentityService", cache_name)) - try: - persistence = msal_extensions.LibsecretPersistence( - file_path, cache_name, {"MsalClientID": "Microsoft.Developer.IdentityService"}, label=account_name - ) - except ImportError: - if not allow_unencrypted: - raise ValueError( - "PyGObject is required to encrypt the persistent cache. Please install that library or ", - "specify 'allow_unencrypted_cache=True' to store the cache without encryption.", - ) - persistence = msal_extensions.FilePersistence(file_path) - else: - raise NotImplementedError("A persistent cache is not available in this environment.") - - return msal_extensions.PersistedTokenCache(persistence) diff --git a/sdk/identity/azure-identity/azure/identity/_persistent_cache.py b/sdk/identity/azure-identity/azure/identity/_persistent_cache.py new file mode 100644 index 000000000000..fb9c73cc195c --- /dev/null +++ b/sdk/identity/azure-identity/azure/identity/_persistent_cache.py @@ -0,0 +1,82 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +import os +import sys +from typing import TYPE_CHECKING + +import msal_extensions + +if TYPE_CHECKING: + from typing import Any + + +class PersistentTokenCache(object): + """Token cache backed by persistent storage. + + This class encrypts its data by default. On Linux, libsecret and pygobject are required for encryption. On macOS, + Keychain protects the cache. On Windows, the cache is protected by the data protection API (DPAPI). + + :keyword str name: name of the cache, used to isolate its data from other applications. Defaults to the name of the + cache shared by Microsoft dev tools and :class:`~azure.identity.SharedTokenCacheCredential`. + :keyword bool allow_unencrypted_storage: whether the cache should fall back to storing its data in plain text when + encryption isn't possible. False by default. Setting this to True does not disable encryption. The cache will + always try to encrypt its data. + + :raises NotImplementedError: persistent token caching isn't supported on the current platform + :raises ValueError: encryption isn't available on the current platform, and `allow_unencrypted_storage` is False. + Specify `allow_unencrypted_storage=True` to work around this, if it's acceptable for the cache to store data + without encryption. + """ + + def __init__(self, **kwargs): + # type: (**Any) -> None + persistence = kwargs.get("_persistence") + if not persistence: + persistence = _get_persistence( + allow_unencrypted=kwargs.get("allow_unencrypted_storage", False), + account_name="MSALCache", + cache_name=kwargs.get("name", "msal.cache"), + ) + self._cache = msal_extensions.PersistedTokenCache(persistence) + + +def _get_persistence(allow_unencrypted, account_name, cache_name): + # type: (bool, str, str) -> msal_extensions.persistence.BasePersistence + """Get an msal_extensions persistence instance for the current platform. + + On Windows the cache is a file protected by the Data Protection API. On Linux and macOS the cache is stored by + libsecret and Keychain, respectively. On those platforms the cache uses the modified timestamp of a file on disk to + decide whether to reload the cache. + + :param bool allow_unencrypted: when True, the cache will be kept in plaintext should encryption be impossible in the + current environment + """ + + if sys.platform.startswith("win") and "LOCALAPPDATA" in os.environ: + cache_location = os.path.join(os.environ["LOCALAPPDATA"], ".IdentityService", cache_name) + return msal_extensions.FilePersistenceWithDataProtection(cache_location) + + if sys.platform.startswith("darwin"): + # the cache uses this file's modified timestamp to decide whether to reload + file_path = os.path.expanduser(os.path.join("~", ".IdentityService", cache_name)) + return msal_extensions.KeychainPersistence(file_path, "Microsoft.Developer.IdentityService", account_name) + + if sys.platform.startswith("linux"): + # The cache uses this file's modified timestamp to decide whether to reload. Note this path is the same + # as that of the plaintext fallback: a new encrypted cache will stomp an unencrypted cache. + file_path = os.path.expanduser(os.path.join("~", ".IdentityService", cache_name)) + try: + return msal_extensions.LibsecretPersistence( + file_path, cache_name, {"MsalClientID": "Microsoft.Developer.IdentityService"}, label=account_name + ) + except ImportError: + if not allow_unencrypted: + raise ValueError( + "PyGObject is required to encrypt the persistent cache. Please install that library or ", + "specify 'allow_unencrypted_cache=True' to store the cache without encryption.", + ) + return msal_extensions.FilePersistence(file_path) + + raise NotImplementedError("A persistent cache is not available in this environment.") From 4a69f97de8b4804fb45fc9edba32649551f1e252 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 13 Jan 2021 14:31:36 -0800 Subject: [PATCH 02/12] update credentials to use them --- .../identity/_credentials/shared_cache.py | 4 +-- .../azure/identity/_internal/__init__.py | 2 -- .../_internal/client_credential_base.py | 9 ----- .../client_secret_credential_base.py | 3 +- .../identity/_internal/msal_credentials.py | 16 +++------ .../identity/_internal/shared_token_cache.py | 33 +++++++++++-------- .../identity/aio/_credentials/certificate.py | 15 ++------- .../aio/_credentials/client_secret.py | 21 ++++++++++-- 8 files changed, 48 insertions(+), 55 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py b/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py index c5469dfdbcc9..091e9d5e59f7 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py @@ -54,7 +54,7 @@ def __init__(self, username=None, **kwargs): # authenticate in the tenant that produced the record unless "tenant_id" specifies another self._tenant_id = kwargs.pop("tenant_id", None) or self._auth_record.tenant_id validate_tenant_id(self._tenant_id) - self._cache = kwargs.pop("_cache", None) + self._cache = kwargs.pop("token_cache", None) self._app = None self._client_kwargs = kwargs self._initialized = False @@ -121,7 +121,7 @@ def _initialize(self): self._app = PublicClientApplication( client_id=self._auth_record.client_id, authority="https://{}/{}".format(self._auth_record.authority, self._tenant_id), - token_cache=self._cache, + token_cache=self._cache._cache, # pylint:disable=protected-access http_client=MsalClient(**self._client_kwargs), client_capabilities=["CP1"] ) diff --git a/sdk/identity/azure-identity/azure/identity/_internal/__init__.py b/sdk/identity/azure-identity/azure/identity/_internal/__init__.py index 3dc779295986..e00047afb7e3 100644 --- a/sdk/identity/azure-identity/azure/identity/_internal/__init__.py +++ b/sdk/identity/azure-identity/azure/identity/_internal/__init__.py @@ -55,7 +55,6 @@ def validate_tenant_id(tenant_id): from .aad_client_base import AadClientBase from .auth_code_redirect_handler import AuthCodeRedirectServer from .aadclient_certificate import AadClientCertificate -from .client_secret_credential_base import ClientSecretCredentialBase from .decorators import wrap_exceptions from .interactive import InteractiveCredential @@ -80,7 +79,6 @@ def _scopes_to_resource(*scopes): "AadClientBase", "AuthCodeRedirectServer", "AadClientCertificate", - "ClientSecretCredentialBase", "get_default_authority", "InteractiveCredential", "normalize_authority", diff --git a/sdk/identity/azure-identity/azure/identity/_internal/client_credential_base.py b/sdk/identity/azure-identity/azure/identity/_internal/client_credential_base.py index 68fc0df801ea..9b6d9186f49b 100644 --- a/sdk/identity/azure-identity/azure/identity/_internal/client_credential_base.py +++ b/sdk/identity/azure-identity/azure/identity/_internal/client_credential_base.py @@ -10,7 +10,6 @@ from azure.core.credentials import AccessToken from azure.core.exceptions import ClientAuthenticationError from .get_token_mixin import GetTokenMixin -from .persistent_cache import load_service_principal_cache from . import wrap_exceptions from .msal_credentials import MsalCredential @@ -22,14 +21,6 @@ class ClientCredentialBase(MsalCredential, GetTokenMixin): """Base class for credentials authenticating a service principal with a certificate or secret""" - def __init__(self, **kwargs): - if kwargs.pop("enable_persistent_cache", False): - allow_unencrypted = kwargs.pop("allow_unencrypted_cache", False) - cache = load_service_principal_cache(allow_unencrypted) - else: - cache = msal.TokenCache() - super(ClientCredentialBase, self).__init__(_cache=cache, **kwargs) - @wrap_exceptions def _acquire_token_silently(self, *scopes, **kwargs): # type: (*str, **Any) -> Optional[AccessToken] diff --git a/sdk/identity/azure-identity/azure/identity/_internal/client_secret_credential_base.py b/sdk/identity/azure-identity/azure/identity/_internal/client_secret_credential_base.py index 204b9a52ee51..77bb4a27b4aa 100644 --- a/sdk/identity/azure-identity/azure/identity/_internal/client_secret_credential_base.py +++ b/sdk/identity/azure-identity/azure/identity/_internal/client_secret_credential_base.py @@ -5,10 +5,9 @@ import abc from typing import TYPE_CHECKING -from msal import TokenCache - from . import validate_tenant_id from .persistent_cache import load_service_principal_cache +from .._token_cache import TokenCache try: ABC = abc.ABC diff --git a/sdk/identity/azure-identity/azure/identity/_internal/msal_credentials.py b/sdk/identity/azure-identity/azure/identity/_internal/msal_credentials.py index b924c050d9ed..e0506dd191bb 100644 --- a/sdk/identity/azure-identity/azure/identity/_internal/msal_credentials.py +++ b/sdk/identity/azure-identity/azure/identity/_internal/msal_credentials.py @@ -7,8 +7,7 @@ import msal from .msal_client import MsalClient -from .persistent_cache import load_user_cache -from .._internal import get_default_authority, normalize_authority, validate_tenant_id +from .._internal import _TokenCache, get_default_authority, normalize_authority, validate_tenant_id try: ABC = abc.ABC @@ -29,7 +28,7 @@ class MsalCredential(ABC): """Base class for credentials wrapping MSAL applications""" def __init__(self, client_id, client_credential=None, **kwargs): - # type: (str, Optional[Union[str, Mapping[str, str]]], **Any) -> None + # type: (str, Optional[Union[str, dict]], **Any) -> None authority = kwargs.pop("authority", None) self._authority = normalize_authority(authority) if authority else get_default_authority() self._tenant_id = kwargs.pop("tenant_id", None) or "organizations" @@ -38,14 +37,7 @@ def __init__(self, client_id, client_credential=None, **kwargs): self._client_credential = client_credential self._client_id = client_id - self._cache = kwargs.pop("_cache", None) # internal, for use in tests - if not self._cache: - if kwargs.pop("enable_persistent_cache", False): - allow_unencrypted = kwargs.pop("allow_unencrypted_cache", False) - self._cache = load_user_cache(allow_unencrypted) - else: - self._cache = msal.TokenCache() - + self._cache = kwargs.pop("token_cache", None) or _TokenCache() self._client = MsalClient(**kwargs) # postpone creating the wrapped application because its initializer uses the network @@ -63,7 +55,7 @@ def _create_app(self, cls, **kwargs): client_id=self._client_id, client_credential=self._client_credential, authority="{}/{}".format(self._authority, self._tenant_id), - token_cache=self._cache, + token_cache=self._cache._cache, # pylint:disable=protected-access http_client=self._client, **kwargs ) diff --git a/sdk/identity/azure-identity/azure/identity/_internal/shared_token_cache.py b/sdk/identity/azure-identity/azure/identity/_internal/shared_token_cache.py index 11d42936cd57..fd5cea04d32f 100644 --- a/sdk/identity/azure-identity/azure/identity/_internal/shared_token_cache.py +++ b/sdk/identity/azure-identity/azure/identity/_internal/shared_token_cache.py @@ -6,7 +6,7 @@ import platform import time -from msal import TokenCache +import msal import six from six.moves.urllib_parse import urlparse @@ -14,7 +14,7 @@ from .. import CredentialUnavailableError from .._constants import KnownAuthorities from .._internal import get_default_authority, normalize_authority, wrap_exceptions -from .._internal.persistent_cache import load_user_cache +from .._persistent_cache import PersistentTokenCache # importing this from azure.identity creates a cycle try: ABC = abc.ABC @@ -94,7 +94,7 @@ def __init__(self, username=None, **kwargs): # pylint:disable=unused-argument self._environment_aliases = KNOWN_ALIASES.get(environment) or frozenset((environment,)) self._username = username self._tenant_id = kwargs.pop("tenant_id", None) - self._cache = kwargs.pop("_cache", None) + self._cache = kwargs.pop("token_cache", None) self._client = None # type: Optional[AadClientBase] self._client_kwargs = kwargs self._client_kwargs["tenant_id"] = "organizations" @@ -106,15 +106,20 @@ def _initialize(self): self._load_cache() if self._cache: - self._client = self._get_auth_client(authority=self._authority, cache=self._cache, **self._client_kwargs) + # pylint:disable=protected-access + self._client = self._get_auth_client( + authority=self._authority, cache=self._cache._cache, **self._client_kwargs + ) self._initialized = True def _load_cache(self): if not self._cache and self.supported(): - allow_unencrypted = self._client_kwargs.get("allow_unencrypted_cache", False) try: - self._cache = load_user_cache(allow_unencrypted) + # This credential accepts the user's default cache regardless of whether it's encrypted. It doesn't + # create a new cache. If the default cache exists, the user must have created it earlier. If it's + # unencrypted, the user must have allowed that. + self._cache = PersistentTokenCache(allow_unencrypted=True) except Exception: # pylint:disable=broad-except pass @@ -124,11 +129,11 @@ def _get_auth_client(self, **kwargs): pass def _get_cache_items_for_authority(self, credential_type): - # type: (TokenCache.CredentialType) -> List[CacheItem] + # type: (msal.TokenCache.CredentialType) -> List[CacheItem] """yield cache items matching this credential's authority or one of its aliases""" items = [] - for item in self._cache.find(credential_type): + for item in self._cache._cache.find(credential_type): # pylint:disable=protected-access environment = item.get("environment") if environment in self._environment_aliases: items.append(item) @@ -138,8 +143,8 @@ def _get_accounts_having_matching_refresh_tokens(self): # type: () -> Iterable[CacheItem] """returns an iterable of cached accounts which have a matching refresh token""" - refresh_tokens = self._get_cache_items_for_authority(TokenCache.CredentialType.REFRESH_TOKEN) - all_accounts = self._get_cache_items_for_authority(TokenCache.CredentialType.ACCOUNT) + refresh_tokens = self._get_cache_items_for_authority(msal.TokenCache.CredentialType.REFRESH_TOKEN) + all_accounts = self._get_cache_items_for_authority(msal.TokenCache.CredentialType.ACCOUNT) accounts = {} for refresh_token in refresh_tokens: @@ -189,8 +194,8 @@ def _get_cached_access_token(self, scopes, account): return None try: - cache_entries = self._cache.find( - TokenCache.CredentialType.ACCESS_TOKEN, + cache_entries = self._cache._cache.find( # pylint:disable=protected-access + msal.TokenCache.CredentialType.ACCESS_TOKEN, target=list(scopes), query={"home_account_id": account["home_account_id"]}, ) @@ -209,8 +214,8 @@ def _get_refresh_tokens(self, account): return None try: - cache_entries = self._cache.find( - TokenCache.CredentialType.REFRESH_TOKEN, query={"home_account_id": account["home_account_id"]} + cache_entries = self._cache._cache.find( # pylint:disable=protected-access + msal.TokenCache.CredentialType.REFRESH_TOKEN, query={"home_account_id": account["home_account_id"]} ) return [token["secret"] for token in cache_entries if "secret" in token] except Exception as ex: # pylint:disable=broad-except diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py index 6400fd793408..4c7ecd825272 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py @@ -4,13 +4,10 @@ # ------------------------------------ from typing import TYPE_CHECKING -from msal import TokenCache - from .._internal import AadClient, AsyncContextManager from .._internal.decorators import log_get_token_async from ..._credentials.certificate import get_client_credential -from ..._internal import AadClientCertificate, validate_tenant_id -from ..._internal.persistent_cache import load_service_principal_cache +from ..._internal import _TokenCache, AadClientCertificate, validate_tenant_id if TYPE_CHECKING: from typing import Any, Optional @@ -46,14 +43,8 @@ def __init__(self, tenant_id, client_id, certificate_path=None, **kwargs): client_credential["private_key"], password=client_credential.get("passphrase") ) - enable_persistent_cache = kwargs.pop("enable_persistent_cache", False) - if enable_persistent_cache: - allow_unencrypted = kwargs.pop("allow_unencrypted_cache", False) - cache = load_service_principal_cache(allow_unencrypted) - else: - cache = TokenCache() - - self._client = AadClient(tenant_id, client_id, cache=cache, **kwargs) + cache = kwargs.pop("token_cache", None) or _TokenCache() + self._client = AadClient(tenant_id, client_id, cache=cache._cache, **kwargs) self._client_id = client_id async def __aenter__(self): diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py index b83ac21dba5b..82e8556f6875 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py @@ -6,14 +6,14 @@ from .._internal import AadClient, AsyncContextManager from .._internal.decorators import log_get_token_async -from ..._internal import ClientSecretCredentialBase +from ..._internal import _TokenCache, validate_tenant_id if TYPE_CHECKING: from typing import Any from azure.core.credentials import AccessToken -class ClientSecretCredential(AsyncContextManager, ClientSecretCredentialBase): +class ClientSecretCredential(AsyncContextManager): """Authenticates as a service principal using a client ID and client secret. :param str tenant_id: ID of the service principal's tenant. Also called its 'directory' ID. @@ -29,6 +29,23 @@ class ClientSecretCredential(AsyncContextManager, ClientSecretCredentialBase): is unavailable. Default to False. Has no effect when `enable_persistent_cache` is False. """ + def __init__(self, tenant_id, client_id, client_secret, **kwargs): + # type: (str, str, str, **Any) -> None + if not client_id: + raise ValueError("client_id should be the id of an Azure Active Directory application") + if not client_secret: + raise ValueError("secret should be an Azure Active Directory application's client secret") + if not tenant_id: + raise ValueError( + "tenant_id should be an Azure Active Directory tenant's id (also called its 'directory id')" + ) + validate_tenant_id(tenant_id) + + self._cache = kwargs.pop("token_cache", None) or _TokenCache() + self._client = AadClient(tenant_id, client_id, cache=self._cache._cache, **kwargs) + self._client_id = client_id + self._secret = client_secret + async def __aenter__(self): await self._client.__aenter__() return self From 23a1c21bc70bce11e94a2441fc952cc45c685449 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 6 Jan 2021 10:12:15 -0800 Subject: [PATCH 03/12] remove unused base class --- .../client_secret_credential_base.py | 48 ------------------- .../aio/_credentials/client_secret.py | 3 -- 2 files changed, 51 deletions(-) delete mode 100644 sdk/identity/azure-identity/azure/identity/_internal/client_secret_credential_base.py diff --git a/sdk/identity/azure-identity/azure/identity/_internal/client_secret_credential_base.py b/sdk/identity/azure-identity/azure/identity/_internal/client_secret_credential_base.py deleted file mode 100644 index 77bb4a27b4aa..000000000000 --- a/sdk/identity/azure-identity/azure/identity/_internal/client_secret_credential_base.py +++ /dev/null @@ -1,48 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import abc -from typing import TYPE_CHECKING - -from . import validate_tenant_id -from .persistent_cache import load_service_principal_cache -from .._token_cache import TokenCache - -try: - ABC = abc.ABC -except AttributeError: # Python 2.7 - ABC = abc.ABCMeta("ABC", (object,), {"__slots__": ()}) # type: ignore - -if TYPE_CHECKING: - # pylint:disable=unused-import,ungrouped-imports - from typing import Any - - -class ClientSecretCredentialBase(ABC): - def __init__(self, tenant_id, client_id, client_secret, **kwargs): - # type: (str, str, str, **Any) -> None - if not client_id: - raise ValueError("client_id should be the id of an Azure Active Directory application") - if not client_secret: - raise ValueError("secret should be an Azure Active Directory application's client secret") - if not tenant_id: - raise ValueError( - "tenant_id should be an Azure Active Directory tenant's id (also called its 'directory id')" - ) - validate_tenant_id(tenant_id) - - enable_persistent_cache = kwargs.pop("enable_persistent_cache", False) - if enable_persistent_cache: - allow_unencrypted = kwargs.pop("allow_unencrypted_cache", False) - cache = load_service_principal_cache(allow_unencrypted) - else: - cache = TokenCache() - - self._client = self._get_auth_client(tenant_id, client_id, cache=cache, **kwargs) - self._client_id = client_id - self._secret = client_secret - - @abc.abstractmethod - def _get_auth_client(self, tenant_id, client_id, **kwargs): - pass diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py index 82e8556f6875..aa49a67f438f 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py @@ -79,6 +79,3 @@ async def get_token(self, *scopes: str, **kwargs: "Any") -> "AccessToken": except Exception: # pylint: disable=broad-except pass return token - - def _get_auth_client(self, tenant_id, client_id, **kwargs): - return AadClient(tenant_id, client_id, **kwargs) From 45ca797ef0395a662d921d562927f4da164b5dc7 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 13 Jan 2021 14:30:38 -0800 Subject: [PATCH 04/12] update tests --- .../tests/test_certificate_credential.py | 85 ++------- .../test_certificate_credential_async.py | 83 +-------- .../tests/test_client_secret_credential.py | 94 +--------- .../test_client_secret_credential_async.py | 95 +--------- .../azure-identity/tests/test_default.py | 2 +- .../tests/test_default_async.py | 2 +- .../tests/test_interactive_credential.py | 72 +------- .../tests/test_shared_cache_credential.py | 171 ++++++------------ .../test_shared_cache_credential_async.py | 113 ++++-------- 9 files changed, 146 insertions(+), 571 deletions(-) diff --git a/sdk/identity/azure-identity/tests/test_certificate_credential.py b/sdk/identity/azure-identity/tests/test_certificate_credential.py index 0837fd822090..5206078e6bca 100644 --- a/sdk/identity/azure-identity/tests/test_certificate_credential.py +++ b/sdk/identity/azure-identity/tests/test_certificate_credential.py @@ -8,12 +8,12 @@ from azure.core.pipeline.policies import ContentDecodePolicy, SansIOHTTPPolicy from azure.identity import CertificateCredential from azure.identity._constants import EnvironmentVariables +from azure.identity._internal import _TokenCache from azure.identity._internal.user_agent import USER_AGENT from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding -from msal import TokenCache import pytest import six from six.moves.urllib_parse import urlparse @@ -237,68 +237,21 @@ def validate_jwt(request, client_id, pem_bytes, expect_x5c=False): @pytest.mark.parametrize("cert_path,cert_password", BOTH_CERTS) -def test_enable_persistent_cache(cert_path, cert_password): - """the credential should use the persistent cache only when given enable_persistent_cache=True""" +def test_token_cache(cert_path, cert_password): + """the credential should use the cache it's given, and default to an in memory cache otherwise""" - persistent_cache = "azure.identity._internal.persistent_cache" - required_arguments = ("tenant-id", "client-id", cert_path) + credential = CertificateCredential("tenant", "client-id", cert_path, password=cert_password) + assert isinstance(credential._cache, _TokenCache) - # credential should default to an in memory cache - raise_when_called = Mock(side_effect=Exception("credential shouldn't attempt to load a persistent cache")) - with patch(persistent_cache + "._load_persistent_cache", raise_when_called): - CertificateCredential(*required_arguments, password=cert_password) - - # allowing an unencrypted cache doesn't count as opting in to the persistent cache - CertificateCredential(*required_arguments, password=cert_password, allow_unencrypted_cache=True) - - # keyword argument opts in to persistent cache - with patch(persistent_cache + ".msal_extensions") as mock_extensions: - CertificateCredential(*required_arguments, password=cert_password, enable_persistent_cache=True) - assert mock_extensions.PersistedTokenCache.call_count == 1 - - # opting in on an unsupported platform raises an exception - with patch(persistent_cache + ".sys.platform", "commodore64"): - with pytest.raises(NotImplementedError): - CertificateCredential(*required_arguments, password=cert_password, enable_persistent_cache=True) - with pytest.raises(NotImplementedError): - CertificateCredential( - *required_arguments, password=cert_password, enable_persistent_cache=True, allow_unencrypted_cache=True - ) - - -@patch("azure.identity._internal.persistent_cache.sys.platform", "linux2") -@patch("azure.identity._internal.persistent_cache.msal_extensions") -@pytest.mark.parametrize("cert_path,cert_password", BOTH_CERTS) -def test_persistent_cache_linux(mock_extensions, cert_path, cert_password): - """The credential should use an unencrypted cache when encryption is unavailable and the user explicitly opts in. - - This test was written when Linux was the only platform on which encryption may not be available. - """ - - required_arguments = ("tenant-id", "client-id", cert_path) - - # the credential should prefer an encrypted cache even when the user allows an unencrypted one - CertificateCredential( - *required_arguments, password=cert_password, enable_persistent_cache=True, allow_unencrypted_cache=True - ) - assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.LibsecretPersistence) - mock_extensions.PersistedTokenCache.reset_mock() - - # (when LibsecretPersistence's dependencies aren't available, constructing it raises ImportError) - mock_extensions.LibsecretPersistence = Mock(side_effect=ImportError) - - # encryption unavailable, no opt in to unencrypted cache -> credential should raise - with pytest.raises(ValueError): - CertificateCredential(*required_arguments, password=cert_password, enable_persistent_cache=True) - - CertificateCredential( - *required_arguments, password=cert_password, enable_persistent_cache=True, allow_unencrypted_cache=True + expected_cache = _TokenCache() + credential = CertificateCredential( + "tenant", "client-id", cert_path, password=cert_password, token_cache=expected_cache ) - assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.FilePersistence) + assert credential._cache is expected_cache @pytest.mark.parametrize("cert_path,cert_password", BOTH_CERTS) -def test_persistent_cache_multiple_clients(cert_path, cert_password): +def test_cache_multiple_clients(cert_path, cert_password): """the credential shouldn't use tokens issued to other service principals""" access_token_a = "token a" @@ -310,17 +263,13 @@ def test_persistent_cache_multiple_clients(cert_path, cert_password): requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_b))] ) - cache = TokenCache() - with patch("azure.identity._internal.persistent_cache._load_persistent_cache") as mock_cache_loader: - mock_cache_loader.return_value = Mock(wraps=cache) - credential_a = CertificateCredential( - "tenant", "client-a", cert_path, password=cert_password, enable_persistent_cache=True, transport=transport_a - ) - assert mock_cache_loader.call_count == 1, "credential should load the persistent cache" - credential_b = CertificateCredential( - "tenant", "client-b", cert_path, password=cert_password, enable_persistent_cache=True, transport=transport_b - ) - assert mock_cache_loader.call_count == 2, "credential should load the persistent cache" + cache = _TokenCache() + credential_a = CertificateCredential( + "tenant", "client-a", cert_path, password=cert_password, transport=transport_a, token_cache=cache + ) + credential_b = CertificateCredential( + "tenant", "client-b", cert_path, password=cert_password, transport=transport_b, token_cache=cache + ) # A caches a token scope = "scope" diff --git a/sdk/identity/azure-identity/tests/test_certificate_credential_async.py b/sdk/identity/azure-identity/tests/test_certificate_credential_async.py index 0432e6c898d0..063771176970 100644 --- a/sdk/identity/azure-identity/tests/test_certificate_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_certificate_credential_async.py @@ -7,10 +7,10 @@ from azure.core.pipeline.policies import ContentDecodePolicy, SansIOHTTPPolicy from azure.identity._constants import EnvironmentVariables +from azure.identity._internal import _TokenCache from azure.identity._internal.user_agent import USER_AGENT from azure.identity.aio import CertificateCredential -from msal import TokenCache import pytest from helpers import build_aad_response, urlsafeb64_decode, mock_response, Request @@ -186,70 +186,9 @@ async def mock_send(request, **kwargs): assert token.token == access_token -@pytest.mark.parametrize("cert_path,cert_password", BOTH_CERTS) -def test_enable_persistent_cache(cert_path, cert_password): - """the credential should use the persistent cache only when given enable_persistent_cache=True""" - - persistent_cache = "azure.identity._internal.persistent_cache" - required_arguments = ("tenant-id", "client-id", cert_path) - - # credential should default to an in memory cache - raise_when_called = Mock(side_effect=Exception("credential shouldn't attempt to load a persistent cache")) - with patch(persistent_cache + "._load_persistent_cache", raise_when_called): - CertificateCredential(*required_arguments, password=cert_password) - - # allowing an unencrypted cache doesn't count as opting in to the persistent cache - CertificateCredential(*required_arguments, password=cert_password, allow_unencrypted_cache=True) - - # keyword argument opts in to persistent cache - with patch(persistent_cache + ".msal_extensions") as mock_extensions: - CertificateCredential(*required_arguments, password=cert_password, enable_persistent_cache=True) - assert mock_extensions.PersistedTokenCache.call_count == 1 - - # opting in on an unsupported platform raises an exception - with patch(persistent_cache + ".sys.platform", "commodore64"): - with pytest.raises(NotImplementedError): - CertificateCredential(*required_arguments, password=cert_password, enable_persistent_cache=True) - with pytest.raises(NotImplementedError): - CertificateCredential( - *required_arguments, password=cert_password, enable_persistent_cache=True, allow_unencrypted_cache=True - ) - - -@patch("azure.identity._internal.persistent_cache.sys.platform", "linux2") -@patch("azure.identity._internal.persistent_cache.msal_extensions") -@pytest.mark.parametrize("cert_path,cert_password", BOTH_CERTS) -def test_persistent_cache_linux(mock_extensions, cert_path, cert_password): - """The credential should use an unencrypted cache when encryption is unavailable and the user explicitly opts in. - - This test was written when Linux was the only platform on which encryption may not be available. - """ - - required_arguments = ("tenant-id", "client-id", cert_path) - - # the credential should prefer an encrypted cache even when the user allows an unencrypted one - CertificateCredential( - *required_arguments, password=cert_password, enable_persistent_cache=True, allow_unencrypted_cache=True - ) - assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.LibsecretPersistence) - mock_extensions.PersistedTokenCache.reset_mock() - - # (when LibsecretPersistence's dependencies aren't available, constructing it raises ImportError) - mock_extensions.LibsecretPersistence = Mock(side_effect=ImportError) - - # encryption unavailable, no opt in to unencrypted cache -> credential should raise - with pytest.raises(ValueError): - CertificateCredential(*required_arguments, password=cert_password, enable_persistent_cache=True) - - CertificateCredential( - *required_arguments, password=cert_password, enable_persistent_cache=True, allow_unencrypted_cache=True - ) - assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.FilePersistence) - - @pytest.mark.asyncio @pytest.mark.parametrize("cert_path,cert_password", BOTH_CERTS) -async def test_persistent_cache_multiple_clients(cert_path, cert_password): +async def test_cache_multiple_clients(cert_path, cert_password): """the credential shouldn't use tokens issued to other service principals""" access_token_a = "token a" @@ -261,17 +200,13 @@ async def test_persistent_cache_multiple_clients(cert_path, cert_password): requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_b))] ) - cache = TokenCache() - with patch("azure.identity._internal.persistent_cache._load_persistent_cache") as mock_cache_loader: - mock_cache_loader.return_value = Mock(wraps=cache) - credential_a = CertificateCredential( - "tenant", "client-a", cert_path, password=cert_password, enable_persistent_cache=True, transport=transport_a - ) - assert mock_cache_loader.call_count == 1, "credential should load the persistent cache" - credential_b = CertificateCredential( - "tenant", "client-b", cert_path, password=cert_password, enable_persistent_cache=True, transport=transport_b - ) - assert mock_cache_loader.call_count == 2, "credential should load the persistent cache" + cache = _TokenCache() + credential_a = CertificateCredential( + "tenant", "client-a", cert_path, password=cert_password, transport=transport_a, token_cache=cache + ) + credential_b = CertificateCredential( + "tenant", "client-b", cert_path, password=cert_password, transport=transport_b, token_cache=cache + ) # A caches a token scope = "scope" diff --git a/sdk/identity/azure-identity/tests/test_client_secret_credential.py b/sdk/identity/azure-identity/tests/test_client_secret_credential.py index 2e651c1924a0..9ba5041953d5 100644 --- a/sdk/identity/azure-identity/tests/test_client_secret_credential.py +++ b/sdk/identity/azure-identity/tests/test_client_secret_credential.py @@ -5,8 +5,8 @@ from azure.core.pipeline.policies import ContentDecodePolicy, SansIOHTTPPolicy from azure.identity import ClientSecretCredential from azure.identity._constants import EnvironmentVariables +from azure.identity._internal import _TokenCache from azure.identity._internal.user_agent import USER_AGENT -from msal import TokenCache import pytest from six.moves.urllib_parse import urlparse @@ -117,90 +117,12 @@ def test_authority(authority): assert kwargs["authority"] == expected_authority -def test_enable_persistent_cache(): - """the credential should use the persistent cache only when given enable_persistent_cache=True""" +def test_token_cache(): + """the credential should use the cache it's given, and default to an in memory cache otherwise""" - required_arguments = ("tenant-id", "client-id", "secret") - persistent_cache = "azure.identity._internal.persistent_cache" + credential = ClientSecretCredential("tenant", "client-id", "secret") + assert isinstance(credential._cache, _TokenCache) - # credential should default to an in memory cache - raise_when_called = Mock(side_effect=Exception("credential shouldn't attempt to load a persistent cache")) - with patch(persistent_cache + "._load_persistent_cache", raise_when_called): - ClientSecretCredential(*required_arguments) - - # allowing an unencrypted cache doesn't count as opting in to the persistent cache - ClientSecretCredential(*required_arguments, allow_unencrypted_cache=True) - - # keyword argument opts in to persistent cache - with patch(persistent_cache + ".msal_extensions") as mock_extensions: - ClientSecretCredential(*required_arguments, enable_persistent_cache=True) - assert mock_extensions.PersistedTokenCache.call_count == 1 - - # opting in on an unsupported platform raises an exception - with patch(persistent_cache + ".sys.platform", "commodore64"): - with pytest.raises(NotImplementedError): - ClientSecretCredential(*required_arguments, enable_persistent_cache=True) - with pytest.raises(NotImplementedError): - ClientSecretCredential(*required_arguments, enable_persistent_cache=True, allow_unencrypted_cache=True) - - -@patch("azure.identity._internal.persistent_cache.sys.platform", "linux2") -@patch("azure.identity._internal.persistent_cache.msal_extensions") -def test_persistent_cache_linux(mock_extensions): - """The credential should use an unencrypted cache when encryption is unavailable and the user explicitly opts in. - - This test was written when Linux was the only platform on which encryption may not be available. - """ - - required_arguments = ("tenant-id", "client-id", "secret") - - # the credential should prefer an encrypted cache even when the user allows an unencrypted one - ClientSecretCredential(*required_arguments, enable_persistent_cache=True, allow_unencrypted_cache=True) - assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.LibsecretPersistence) - mock_extensions.PersistedTokenCache.reset_mock() - - # (when LibsecretPersistence's dependencies aren't available, constructing it raises ImportError) - mock_extensions.LibsecretPersistence = Mock(side_effect=ImportError) - - # encryption unavailable, no opt in to unencrypted cache -> credential should raise - with pytest.raises(ValueError): - ClientSecretCredential(*required_arguments, enable_persistent_cache=True) - - ClientSecretCredential(*required_arguments, enable_persistent_cache=True, allow_unencrypted_cache=True) - assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.FilePersistence) - - -def test_persistent_cache_multiple_clients(): - """the credential shouldn't use tokens issued to other service principals""" - - access_token_a = "token a" - access_token_b = "not " + access_token_a - transport_a = msal_validating_transport( - requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))] - ) - transport_b = msal_validating_transport( - requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_b))] - ) - - cache = TokenCache() - with patch("azure.identity._internal.persistent_cache._load_persistent_cache") as mock_cache_loader: - mock_cache_loader.return_value = Mock(wraps=cache) - credential_a = ClientSecretCredential( - "tenant-id", "client-a", "...", enable_persistent_cache=True, transport=transport_a - ) - assert mock_cache_loader.call_count == 1, "credential should load the persistent cache" - credential_b = ClientSecretCredential( - "tenant-id", "client-b", "...", enable_persistent_cache=True, transport=transport_b - ) - assert mock_cache_loader.call_count == 2, "credential should load the persistent cache" - - # A caches a token - scope = "scope" - token_a = credential_a.get_token(scope) - assert token_a.token == access_token_a - assert transport_a.send.call_count == 3 # two MSAL discovery requests, one token request - - # B should get a different token for the same scope - token_b = credential_b.get_token(scope) - assert token_b.token == access_token_b - assert transport_b.send.call_count == 3 + expected_cache = _TokenCache() + credential = ClientSecretCredential("tenant", "client-id", "secret", token_cache=expected_cache) + assert credential._cache is expected_cache diff --git a/sdk/identity/azure-identity/tests/test_client_secret_credential_async.py b/sdk/identity/azure-identity/tests/test_client_secret_credential_async.py index 3c0bdd231941..e6e3cd265bd7 100644 --- a/sdk/identity/azure-identity/tests/test_client_secret_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_client_secret_credential_async.py @@ -9,9 +9,9 @@ from azure.core.credentials import AccessToken from azure.core.pipeline.policies import ContentDecodePolicy, SansIOHTTPPolicy from azure.identity._constants import EnvironmentVariables +from azure.identity._internal import _TokenCache from azure.identity._internal.user_agent import USER_AGENT from azure.identity.aio import ClientSecretCredential -from msal import TokenCache import pytest from helpers import build_aad_response, mock_response, Request @@ -185,91 +185,12 @@ async def test_cache(): assert mock_send.call_count == 2 -def test_enable_persistent_cache(): - """the credential should use the persistent cache only when given enable_persistent_cache=True""" +def test_token_cache(): + """the credential should use the cache it's given, and default to an in memory cache otherwise""" - required_arguments = ("tenant-id", "client-id", "secret") - persistent_cache = "azure.identity._internal.persistent_cache" + credential = ClientSecretCredential("tenant", "client-id", "secret") + assert isinstance(credential._cache, _TokenCache) - # credential should default to an in memory cache - raise_when_called = Mock(side_effect=Exception("credential shouldn't attempt to load a persistent cache")) - with patch(persistent_cache + "._load_persistent_cache", raise_when_called): - ClientSecretCredential(*required_arguments) - - # allowing an unencrypted cache doesn't count as opting in to the persistent cache - ClientSecretCredential(*required_arguments, allow_unencrypted_cache=True) - - # keyword argument opts in to persistent cache - with patch(persistent_cache + ".msal_extensions") as mock_extensions: - ClientSecretCredential(*required_arguments, enable_persistent_cache=True) - assert mock_extensions.PersistedTokenCache.call_count == 1 - - # opting in on an unsupported platform raises an exception - with patch(persistent_cache + ".sys.platform", "commodore64"): - with pytest.raises(NotImplementedError): - ClientSecretCredential(*required_arguments, enable_persistent_cache=True) - with pytest.raises(NotImplementedError): - ClientSecretCredential(*required_arguments, enable_persistent_cache=True, allow_unencrypted_cache=True) - - -@patch("azure.identity._internal.persistent_cache.sys.platform", "linux2") -@patch("azure.identity._internal.persistent_cache.msal_extensions") -def test_persistent_cache_linux(mock_extensions): - """The credential should use an unencrypted cache when encryption is unavailable and the user explicitly opts in. - - This test was written when Linux was the only platform on which encryption may not be available. - """ - - required_arguments = ("tenant-id", "client-id", "secret") - - # the credential should prefer an encrypted cache even when the user allows an unencrypted one - ClientSecretCredential(*required_arguments, enable_persistent_cache=True, allow_unencrypted_cache=True) - assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.LibsecretPersistence) - mock_extensions.PersistedTokenCache.reset_mock() - - # (when LibsecretPersistence's dependencies aren't available, constructing it raises ImportError) - mock_extensions.LibsecretPersistence = Mock(side_effect=ImportError) - - # encryption unavailable, no opt in to unencrypted cache -> credential should raise - with pytest.raises(ValueError): - ClientSecretCredential(*required_arguments, enable_persistent_cache=True) - - ClientSecretCredential(*required_arguments, enable_persistent_cache=True, allow_unencrypted_cache=True) - assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.FilePersistence) - - -@pytest.mark.asyncio -async def test_persistent_cache_multiple_clients(): - """the credential shouldn't use tokens issued to other service principals""" - - access_token_a = "token a" - access_token_b = "not " + access_token_a - transport_a = async_validating_transport( - requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))] - ) - transport_b = async_validating_transport( - requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_b))] - ) - - cache = TokenCache() - with patch("azure.identity._internal.persistent_cache._load_persistent_cache") as mock_cache_loader: - mock_cache_loader.return_value = Mock(wraps=cache) - credential_a = ClientSecretCredential( - "tenant-id", "client-a", "...", enable_persistent_cache=True, transport=transport_a - ) - assert mock_cache_loader.call_count == 1, "credential should load the persistent cache" - credential_b = ClientSecretCredential( - "tenant-id", "client-b", "...", enable_persistent_cache=True, transport=transport_b - ) - assert mock_cache_loader.call_count == 2, "credential should load the persistent cache" - - # A caches a token - scope = "scope" - token_a = await credential_a.get_token(scope) - assert token_a.token == access_token_a - assert transport_a.send.call_count == 1 - - # B should get a different token for the same scope - token_b = await credential_b.get_token(scope) - assert token_b.token == access_token_b - assert transport_b.send.call_count == 1 + expected_cache = _TokenCache() + credential = ClientSecretCredential("tenant", "client-id", "secret", token_cache=expected_cache) + assert credential._cache is expected_cache diff --git a/sdk/identity/azure-identity/tests/test_default.py b/sdk/identity/azure-identity/tests/test_default.py index 2f8564da1957..d3cf51eec5b1 100644 --- a/sdk/identity/azure-identity/tests/test_default.py +++ b/sdk/identity/azure-identity/tests/test_default.py @@ -302,7 +302,7 @@ def get_credential_for_shared_cache_test(expected_refresh_token, expected_access # this credential uses a mock shared cache, so it works on all platforms with patch.object(SharedTokenCacheCredential, "supported"): - return DefaultAzureCredential(_cache=cache, transport=transport, **options) + return DefaultAzureCredential(token_cache=cache, transport=transport, **options) def test_interactive_browser_tenant_id(): diff --git a/sdk/identity/azure-identity/tests/test_default_async.py b/sdk/identity/azure-identity/tests/test_default_async.py index 4e74e55e745a..be9cb5a9d8b0 100644 --- a/sdk/identity/azure-identity/tests/test_default_async.py +++ b/sdk/identity/azure-identity/tests/test_default_async.py @@ -289,4 +289,4 @@ def get_credential_for_shared_cache_test(expected_refresh_token, expected_access # this credential uses a mock shared cache, so it works on all platforms with patch.object(SharedTokenCacheCredential, "supported", lambda: True): - return DefaultAzureCredential(_cache=cache, transport=transport, **exclude_other_credentials, **kwargs) + return DefaultAzureCredential(token_cache=cache, transport=transport, **exclude_other_credentials, **kwargs) diff --git a/sdk/identity/azure-identity/tests/test_interactive_credential.py b/sdk/identity/azure-identity/tests/test_interactive_credential.py index a708b7c2c9fc..27da20ce0527 100644 --- a/sdk/identity/azure-identity/tests/test_interactive_credential.py +++ b/sdk/identity/azure-identity/tests/test_interactive_credential.py @@ -9,8 +9,7 @@ KnownAuthorities, CredentialUnavailableError, ) -from azure.identity._internal import InteractiveCredential -from msal import TokenCache +from azure.identity._internal import _TokenCache, InteractiveCredential import pytest try: @@ -42,13 +41,13 @@ class MockCredential(InteractiveCredential): """ def __init__( - self, client_id="...", request_token=None, cache=None, msal_app_factory=None, transport=None, **kwargs + self, client_id="...", request_token=None, msal_app_factory=None, transport=None, **kwargs ): self._msal_app_factory = msal_app_factory self._request_token_impl = request_token or Mock() transport = transport or Mock(send=Mock(side_effect=Exception("credential shouldn't send a request"))) super(MockCredential, self).__init__( - client_id=client_id, _cache=cache or TokenCache(), transport=transport, **kwargs + client_id=client_id, transport=transport, **kwargs ) def _request_token(self, *scopes, **kwargs): @@ -215,8 +214,8 @@ class CustomException(Exception): assert msal_app.acquire_token_silent_with_error.call_count == 1, "credential didn't attempt silent auth" -def test_enable_persistent_cache(): - """the credential should use the persistent cache only when given enable_persistent_cache=True""" +def test_token_cache(): + """the credential should use the cache it's given, and default to an in memory cache otherwise""" class TestCredential(InteractiveCredential): def __init__(self, **kwargs): @@ -225,63 +224,12 @@ def __init__(self, **kwargs): def _request_token(self, *_, **__): pass - in_memory_cache = Mock() + credential = TestCredential() + assert isinstance(credential._cache, _TokenCache) - persistent_cache = "azure.identity._internal.persistent_cache" - - # credential should default to an in memory cache - raise_when_called = Mock(side_effect=Exception("credential shouldn't attempt to load a persistent cache")) - with patch(persistent_cache + "._load_persistent_cache", raise_when_called): - with patch(InteractiveCredential.__module__ + ".msal.TokenCache", lambda: in_memory_cache): - credential = TestCredential() - assert credential._cache is in_memory_cache - - # allowing an unencrypted cache doesn't count as opting in to the persistent cache - credential = TestCredential(allow_unencrypted_cache=True) - assert credential._cache is in_memory_cache - - # keyword argument opts in to persistent cache - with patch(persistent_cache + ".msal_extensions") as mock_extensions: - TestCredential(enable_persistent_cache=True) - assert mock_extensions.PersistedTokenCache.call_count == 1 - - # opting in on an unsupported platform raises an exception - with patch(persistent_cache + ".sys.platform", "commodore64"): - with pytest.raises(NotImplementedError): - TestCredential(enable_persistent_cache=True) - with pytest.raises(NotImplementedError): - TestCredential(enable_persistent_cache=True, allow_unencrypted_cache=True) - - -@patch("azure.identity._internal.persistent_cache.sys.platform", "linux2") -@patch("azure.identity._internal.persistent_cache.msal_extensions") -def test_persistent_cache_linux(mock_extensions): - """The credential should use an unencrypted cache when encryption is unavailable and the user explicitly opts in. - - This test was written when Linux was the only platform on which encryption may not be available. - """ - - class TestCredential(InteractiveCredential): - def __init__(self, **kwargs): - super(TestCredential, self).__init__(client_id="...", **kwargs) - - def _request_token(self, *_, **__): - pass - - # the credential should prefer an encrypted cache even when the user allows an unencrypted one - TestCredential(enable_persistent_cache=True, allow_unencrypted_cache=True) - assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.LibsecretPersistence) - mock_extensions.PersistedTokenCache.reset_mock() - - # (when LibsecretPersistence's dependencies aren't available, constructing it raises ImportError) - mock_extensions.LibsecretPersistence = Mock(side_effect=ImportError) - - # encryption unavailable, no opt in to unencrypted cache -> credential should raise - with pytest.raises(ValueError): - TestCredential(enable_persistent_cache=True) - - TestCredential(enable_persistent_cache=True, allow_unencrypted_cache=True) - assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.FilePersistence) + expected_cache = _TokenCache() + credential = TestCredential(token_cache=expected_cache) + assert credential._cache is expected_cache def test_home_account_id_client_info(): diff --git a/sdk/identity/azure-identity/tests/test_shared_cache_credential.py b/sdk/identity/azure-identity/tests/test_shared_cache_credential.py index 593eb560273b..be2e0190c232 100644 --- a/sdk/identity/azure-identity/tests/test_shared_cache_credential.py +++ b/sdk/identity/azure-identity/tests/test_shared_cache_credential.py @@ -17,9 +17,9 @@ NO_ACCOUNTS, NO_MATCHING_ACCOUNTS, ) -from azure.identity._internal import get_default_authority +from azure.identity._internal import _TokenCache, get_default_authority from azure.identity._internal.user_agent import USER_AGENT -from msal import TokenCache +import msal import pytest from six.moves.urllib_parse import urlparse @@ -65,7 +65,7 @@ def test_supported(): def test_no_scopes(): """The credential should raise when get_token is called with no scopes""" - credential = SharedTokenCacheCredential(_cache=TokenCache()) + credential = SharedTokenCacheCredential(token_cache=_TokenCache()) with pytest.raises(ValueError): credential.get_token() @@ -77,7 +77,7 @@ def send(*_, **__): return mock_response(json_payload=build_aad_response(access_token="**")) credential = SharedTokenCacheCredential( - _cache=populated_cache(get_account_event("test@user", "uid", "utid")), + token_cache=populated_cache(get_account_event("test@user", "uid", "utid")), policies=[policy], transport=Mock(send=send), ) @@ -94,7 +94,7 @@ def test_user_agent(): ) credential = SharedTokenCacheCredential( - _cache=populated_cache(get_account_event("test@user", "uid", "utid")), transport=transport + token_cache=populated_cache(get_account_event("test@user", "uid", "utid")), transport=transport ) credential.get_token("scope") @@ -114,23 +114,23 @@ def _get_auth_client(self, authority=None, **kwargs): assert actual.netloc == expected_netloc transport = Mock(send=Mock(side_effect=Exception("credential shouldn't send a request"))) - MockCredential(_cache=TokenCache(), authority=authority, transport=transport) + MockCredential(token_cache=_TokenCache(), authority=authority, transport=transport) with patch.dict("os.environ", {EnvironmentVariables.AZURE_AUTHORITY_HOST: authority}, clear=True): - MockCredential(_cache=TokenCache(), authority=authority, transport=transport) + MockCredential(token_cache=_TokenCache(), authority=authority, transport=transport) def test_empty_cache(): """the credential should raise CredentialUnavailableError when the cache is empty""" with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): - SharedTokenCacheCredential(_cache=TokenCache()).get_token("scope") + SharedTokenCacheCredential(token_cache=_TokenCache()).get_token("scope") with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): - SharedTokenCacheCredential(_cache=TokenCache(), username="not@cache").get_token("scope") + SharedTokenCacheCredential(token_cache=_TokenCache(), username="not@cache").get_token("scope") with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): - SharedTokenCacheCredential(_cache=TokenCache(), tenant_id="not-cached").get_token("scope") + SharedTokenCacheCredential(token_cache=_TokenCache(), tenant_id="not-cached").get_token("scope") with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): - SharedTokenCacheCredential(_cache=TokenCache(), tenant_id="not-cached", username="not@cache").get_token("scope") + SharedTokenCacheCredential(token_cache=_TokenCache(), tenant_id="not-cached", username="not@cache").get_token("scope") def test_no_matching_account_for_username(): @@ -142,7 +142,7 @@ def test_no_matching_account_for_username(): cache = populated_cache(account) with pytest.raises(CredentialUnavailableError) as ex: - SharedTokenCacheCredential(_cache=cache, username="not" + upn).get_token("scope") + SharedTokenCacheCredential(token_cache=cache, username="not" + upn).get_token("scope") assert ex.value.message.startswith(NO_MATCHING_ACCOUNTS[: NO_MATCHING_ACCOUNTS.index("{")]) assert "not" + upn in ex.value.message @@ -157,7 +157,7 @@ def test_no_matching_account_for_tenant(): cache = populated_cache(account) with pytest.raises(CredentialUnavailableError) as ex: - SharedTokenCacheCredential(_cache=cache, tenant_id="not-" + tenant).get_token("scope") + SharedTokenCacheCredential(token_cache=cache, tenant_id="not-" + tenant).get_token("scope") assert ex.value.message.startswith(NO_MATCHING_ACCOUNTS[: NO_MATCHING_ACCOUNTS.index("{")]) assert "not-" + tenant in ex.value.message @@ -172,7 +172,7 @@ def test_no_matching_account_for_tenant_and_username(): cache = populated_cache(account) with pytest.raises(CredentialUnavailableError) as ex: - SharedTokenCacheCredential(_cache=cache, tenant_id="not-" + tenant, username="not" + upn).get_token("scope") + SharedTokenCacheCredential(token_cache=cache, tenant_id="not-" + tenant, username="not" + upn).get_token("scope") assert ex.value.message.startswith(NO_MATCHING_ACCOUNTS[: NO_MATCHING_ACCOUNTS.index("{")]) assert "not" + upn in ex.value.message and "not-" + tenant in ex.value.message @@ -193,13 +193,13 @@ def test_no_matching_account_for_tenant_or_username(): transport = Mock(side_effect=Exception()) # credential shouldn't use the network - credential = SharedTokenCacheCredential(username=upn_a, tenant_id=tenant_b, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn_a, tenant_id=tenant_b, token_cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError) as ex: credential.get_token("scope") assert ex.value.message.startswith(NO_MATCHING_ACCOUNTS[: NO_MATCHING_ACCOUNTS.index("{")]) assert upn_a in ex.value.message and tenant_b in ex.value.message - credential = SharedTokenCacheCredential(username=upn_b, tenant_id=tenant_a, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn_b, tenant_id=tenant_a, token_cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError) as ex: credential.get_token("scope") assert ex.value.message.startswith(NO_MATCHING_ACCOUNTS[: NO_MATCHING_ACCOUNTS.index("{")]) @@ -220,7 +220,7 @@ def test_single_account_matching_username(): requests=[Request(required_data={"refresh_token": refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(_cache=cache, transport=transport, username=upn) + credential = SharedTokenCacheCredential(token_cache=cache, transport=transport, username=upn) token = credential.get_token(scope) assert token.token == expected_token @@ -239,7 +239,7 @@ def test_single_account_matching_tenant(): requests=[Request(required_data={"refresh_token": refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(_cache=cache, transport=transport, tenant_id=tenant_id) + credential = SharedTokenCacheCredential(token_cache=cache, transport=transport, tenant_id=tenant_id) token = credential.get_token(scope) assert token.token == expected_token @@ -259,7 +259,7 @@ def test_single_account_matching_tenant_and_username(): requests=[Request(required_data={"refresh_token": refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(_cache=cache, transport=transport, tenant_id=tenant_id, username=upn) + credential = SharedTokenCacheCredential(token_cache=cache, transport=transport, tenant_id=tenant_id, username=upn) token = credential.get_token(scope) assert token.token == expected_token @@ -277,7 +277,7 @@ def test_single_account(): requests=[Request(required_data={"refresh_token": refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(token_cache=cache, transport=transport) token = credential.get_token(scope) assert token.token == expected_token @@ -291,11 +291,11 @@ def test_no_refresh_token(): transport = Mock(side_effect=Exception()) # credential shouldn't use the network - credential = SharedTokenCacheCredential(_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(token_cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): credential.get_token("scope") - credential = SharedTokenCacheCredential(_cache=cache, transport=transport, username="not@cache") + credential = SharedTokenCacheCredential(token_cache=cache, transport=transport, username="not@cache") with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): credential.get_token("scope") @@ -313,7 +313,7 @@ def test_two_accounts_no_username_or_tenant(): transport = Mock(side_effect=Exception()) # two users in the cache, no username specified -> CredentialUnavailableError - credential = SharedTokenCacheCredential(_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(token_cache=cache, transport=transport) with pytest.raises(ClientAuthenticationError, match=MULTIPLE_ACCOUNTS) as ex: credential.get_token("scope") @@ -334,7 +334,7 @@ def test_two_accounts_username_specified(): requests=[Request(required_data={"refresh_token": expected_refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(username=upn_a, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn_a, token_cache=cache, transport=transport) token = credential.get_token(scope) assert token.token == expected_token @@ -356,7 +356,7 @@ def test_two_accounts_tenant_specified(): requests=[Request(required_data={"refresh_token": expected_refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(tenant_id=tenant_id, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(tenant_id=tenant_id, token_cache=cache, transport=transport) token = credential.get_token(scope) assert token.token == expected_token @@ -378,7 +378,7 @@ def test_two_accounts_tenant_and_username_specified(): requests=[Request(required_data={"refresh_token": expected_refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(tenant_id=tenant_id, username=upn_a, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(tenant_id=tenant_id, username=upn_a, token_cache=cache, transport=transport) token = credential.get_token(scope) assert token.token == expected_token @@ -400,7 +400,7 @@ def test_same_username_different_tenants(): # with no tenant specified the credential can't select an identity transport = Mock(side_effect=Exception()) # (so it shouldn't use the network) - credential = SharedTokenCacheCredential(username=upn, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn, token_cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError) as ex: credential.get_token("scope") @@ -413,7 +413,7 @@ def test_same_username_different_tenants(): requests=[Request(required_data={"refresh_token": refresh_token_a, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))], ) - credential = SharedTokenCacheCredential(tenant_id=tenant_a, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(tenant_id=tenant_a, token_cache=cache, transport=transport) token = credential.get_token(scope) assert token.token == access_token_a @@ -421,7 +421,7 @@ def test_same_username_different_tenants(): requests=[Request(required_data={"refresh_token": refresh_token_b, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_b))], ) - credential = SharedTokenCacheCredential(tenant_id=tenant_b, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(tenant_id=tenant_b, token_cache=cache, transport=transport) token = credential.get_token(scope) assert token.token == access_token_b @@ -443,7 +443,7 @@ def test_same_tenant_different_usernames(): # with no username specified the credential can't select an identity transport = Mock(side_effect=Exception()) # (so it shouldn't use the network) - credential = SharedTokenCacheCredential(tenant_id=tenant_id, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(tenant_id=tenant_id, token_cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError) as ex: credential.get_token("scope") @@ -456,7 +456,7 @@ def test_same_tenant_different_usernames(): requests=[Request(required_data={"refresh_token": refresh_token_b, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))], ) - credential = SharedTokenCacheCredential(username=upn_b, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn_b, token_cache=cache, transport=transport) token = credential.get_token(scope) assert token.token == access_token_a @@ -464,7 +464,7 @@ def test_same_tenant_different_usernames(): requests=[Request(required_data={"refresh_token": refresh_token_a, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))], ) - credential = SharedTokenCacheCredential(username=upn_a, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn_a, token_cache=cache, transport=transport) token = credential.get_token(scope) assert token.token == access_token_a @@ -487,7 +487,7 @@ def test_authority_aliases(): requests=[Request(authority=authority, required_data={"refresh_token": expected_refresh_token})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) - credential = SharedTokenCacheCredential(authority=authority, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(authority=authority, token_cache=cache, transport=transport) token = credential.get_token("scope") assert token.token == expected_access_token @@ -497,7 +497,7 @@ def test_authority_aliases(): requests=[Request(authority=alias, required_data={"refresh_token": expected_refresh_token})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) - credential = SharedTokenCacheCredential(authority=alias, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(authority=alias, token_cache=cache, transport=transport) token = credential.get_token("scope") assert token.token == expected_access_token @@ -514,7 +514,7 @@ def test_authority_with_no_known_alias(): requests=[Request(authority=authority, required_data={"refresh_token": expected_refresh_token})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) - credential = SharedTokenCacheCredential(authority=authority, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(authority=authority, token_cache=cache, transport=transport) token = credential.get_token("scope") assert token.token == expected_access_token @@ -532,7 +532,7 @@ def test_authority_environment_variable(): responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) with patch.dict("os.environ", {EnvironmentVariables.AZURE_AUTHORITY_HOST: authority}, clear=True): - credential = SharedTokenCacheCredential(transport=transport, _cache=cache) + credential = SharedTokenCacheCredential(transport=transport, token_cache=cache) token = credential.get_token("scope") assert token.token == expected_access_token @@ -546,7 +546,7 @@ def send(request, **_): return get_discovery_response() credential = SharedTokenCacheCredential( - authentication_record=record, transport=Mock(send=send), _cache=TokenCache() + authentication_record=record, transport=Mock(send=send), token_cache=_TokenCache() ) with pytest.raises(CredentialUnavailableError): @@ -572,7 +572,7 @@ def send(request, **_): "not-" + username, "not-" + object_id, "different-" + tenant_id, client_id="not-" + client_id, ), ) - credential = SharedTokenCacheCredential(authentication_record=record, transport=Mock(send=send), _cache=cache) + credential = SharedTokenCacheCredential(authentication_record=record, transport=Mock(send=send), token_cache=cache) with pytest.raises(CredentialUnavailableError): credential.get_token("scope") @@ -599,7 +599,7 @@ def test_authentication_record(): requests=[Request(authority=authority, required_data={"refresh_token": expected_refresh_token})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) - credential = SharedTokenCacheCredential(authentication_record=record, transport=transport, _cache=cache) + credential = SharedTokenCacheCredential(authentication_record=record, transport=transport, token_cache=cache) token = credential.get_token("scope") assert token.token == expected_access_token @@ -636,38 +636,12 @@ def test_auth_record_multiple_accounts_for_username(): requests=[Request(authority=authority, required_data={"refresh_token": expected_refresh_token})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) - credential = SharedTokenCacheCredential(authentication_record=record, transport=transport, _cache=cache) + credential = SharedTokenCacheCredential(authentication_record=record, transport=transport, token_cache=cache) token = credential.get_token("scope") assert token.token == expected_access_token -@patch("azure.identity._internal.persistent_cache.sys.platform", "linux2") -@patch("azure.identity._internal.persistent_cache.msal_extensions") -def test_allow_unencrypted_cache(mock_extensions): - """The credential should use an unencrypted cache when encryption is unavailable and the user explicitly allows it. - - This test was written when Linux was the only platform on which encryption may not be available. - """ - - # the credential should prefer an encrypted cache even when the user allows an unencrypted one - SharedTokenCacheCredential(allow_unencrypted_cache=True) - assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.LibsecretPersistence) - mock_extensions.PersistedTokenCache.reset_mock() - - # (when LibsecretPersistence's dependencies aren't available, constructing it raises ImportError) - mock_extensions.LibsecretPersistence = Mock(side_effect=ImportError) - - # encryption unavailable, no opt in to unencrypted cache -> credential should be unavailable - with pytest.raises(CredentialUnavailableError): - SharedTokenCacheCredential().get_token("scope") - assert mock_extensions.PersistedTokenCache.call_count == 0 - - # still no encryption, but now we allow the unencrypted fallback - SharedTokenCacheCredential(allow_unencrypted_cache=True) - assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.FilePersistence) - - def test_writes_to_cache(): """the credential should write tokens it acquires to the cache""" @@ -680,8 +654,8 @@ def test_writes_to_cache(): uid = "uid" utid = "utid" account = get_account_event(username=username, uid=uid, utid=utid, refresh_token=first_refresh_token) - cache = TokenCache() - cache.add(account) + cache = _TokenCache() + cache._cache.add(account) transport = validating_transport( requests=[Request(required_data={"refresh_token": first_refresh_token})], # credential redeems refresh token @@ -699,13 +673,13 @@ def test_writes_to_cache(): ) ], ) - credential = SharedTokenCacheCredential(_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(token_cache=cache, transport=transport) token = credential.get_token(scope) assert token.token == expected_access_token # access token should be in the cache, and another instance should retrieve it credential = SharedTokenCacheCredential( - _cache=cache, transport=Mock(send=Mock(side_effect=Exception("the credential should return a cached token"))) + token_cache=cache, transport=Mock(send=Mock(side_effect=Exception("the credential should return a cached token"))) ) token = credential.get_token(scope) assert token.token == expected_access_token @@ -716,57 +690,18 @@ def test_writes_to_cache(): requests=[Request(required_data={"refresh_token": second_refresh_token})], responses=[mock_response(json_payload=build_aad_response(access_token=second_access_token))], ) - credential = SharedTokenCacheCredential(_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(token_cache=cache, transport=transport) token = credential.get_token("some other " + scope) assert token.token == second_access_token # verify the credential didn't add a new cache entry - assert len(cache.find(TokenCache.CredentialType.REFRESH_TOKEN)) == 1 - - -def test_access_token_caching(): - """'get_token' shouldn't return other users' access tokens""" - - scope = "scope" - forbidden_access_token = "don't use me" - expected_access_token = "access token" - my_refresh_token = "my refresh token" - your_refresh_token = "your refresh token" - - me = "me" - uid = "uidme" - utid = "utidme" - cache = TokenCache() - cache.add( - get_account_event( - username=me, - uid=uid, - utid=utid, - refresh_token=my_refresh_token, - access_token=forbidden_access_token, - scopes=[scope], - ) - ) - - you = "you" - uid = "uidyou" - utid = "utidyou" - cache.add( - get_account_event( - username=you, - uid=uid, - utid=utid, - refresh_token=your_refresh_token, - access_token=expected_access_token, - scopes=[scope], - ) - ) + assert len(cache._cache.find(msal.TokenCache.CredentialType.REFRESH_TOKEN)) == 1 def test_initialization(): """the credential should attempt to load the cache only once, when it's first needed""" - with patch("azure.identity._internal.persistent_cache._load_persistent_cache") as mock_cache_loader: + with patch("azure.identity._persistent_cache._get_persistence") as mock_cache_loader: mock_cache_loader.side_effect = Exception("it didn't work") credential = SharedTokenCacheCredential() @@ -792,7 +727,7 @@ def mock_send(request, **_): transport = Mock(send=Mock(wraps=mock_send)) credential = SharedTokenCacheCredential( - authentication_record=record, _cache=TokenCache(), tenant_id=expected_tenant_id, transport=transport + authentication_record=record, token_cache=_TokenCache(), tenant_id=expected_tenant_id, transport=transport ) with pytest.raises(CredentialUnavailableError): credential.get_token("scope") # this raises because the cache is empty @@ -805,7 +740,7 @@ def test_client_capabilities(): record = AuthenticationRecord("tenant-id", "client_id", "authority", "home_account_id", "username") transport = Mock(send=Mock(side_effect=Exception("this test mocks MSAL, so no request should be sent"))) - credential = SharedTokenCacheCredential(transport=transport, authentication_record=record, _cache=TokenCache()) + credential = SharedTokenCacheCredential(transport=transport, authentication_record=record, _cache=_TokenCache()) with patch(SharedTokenCacheCredential.__module__ + ".PublicClientApplication") as PublicClientApplication: credential._initialize() @@ -829,7 +764,7 @@ def test_claims_challenge(): ) transport = Mock(send=Mock(side_effect=Exception("this test mocks MSAL, so no request should be sent"))) - credential = SharedTokenCacheCredential(transport=transport, authentication_record=record, _cache=TokenCache()) + credential = SharedTokenCacheCredential(transport=transport, authentication_record=record, _cache=_TokenCache()) with patch(SharedTokenCacheCredential.__module__ + ".PublicClientApplication", lambda *_, **__: msal_app): credential.get_token("scope", claims=expected_claims) @@ -862,8 +797,8 @@ def get_account_event( def populated_cache(*accounts): - cache = TokenCache() + cache = _TokenCache() for account in accounts: - cache.add(account) - cache.add = lambda *_, **__: None # prevent anything being added to the cache + cache._cache.add(account) + cache._cache.add = lambda *_, **__: None # prevent anything being added to the cache return cache diff --git a/sdk/identity/azure-identity/tests/test_shared_cache_credential_async.py b/sdk/identity/azure-identity/tests/test_shared_cache_credential_async.py index 7613200e97ee..b17712d4d642 100644 --- a/sdk/identity/azure-identity/tests/test_shared_cache_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_shared_cache_credential_async.py @@ -10,6 +10,7 @@ from azure.identity import CredentialUnavailableError from azure.identity.aio import SharedTokenCacheCredential from azure.identity._constants import EnvironmentVariables +from azure.identity._internal import _TokenCache from azure.identity._internal.shared_token_cache import ( KNOWN_ALIASES, MULTIPLE_ACCOUNTS, @@ -18,7 +19,6 @@ NO_MATCHING_ACCOUNTS, ) from azure.identity._internal.user_agent import USER_AGENT -from msal import TokenCache import pytest from helpers import build_aad_response, build_id_token, mock_response, Request @@ -35,7 +35,7 @@ def test_supported(): async def test_no_scopes(): """The credential should raise when get_token is called with no scopes""" - credential = SharedTokenCacheCredential(_cache=TokenCache()) + credential = SharedTokenCacheCredential(token_cache=_TokenCache()) with pytest.raises(ValueError): await credential.get_token() @@ -47,7 +47,7 @@ async def send(*_, **__): transport = AsyncMockTransport(send=send) credential = SharedTokenCacheCredential( - _cache=populated_cache(get_account_event("test@user", "uid", "utid")), transport=transport + token_cache=populated_cache(get_account_event("test@user", "uid", "utid")), transport=transport ) # the credential doesn't open a transport session before one is needed, so we send a request @@ -65,7 +65,7 @@ async def send(*_, **__): transport = AsyncMockTransport(send=send) credential = SharedTokenCacheCredential( - _cache=populated_cache(get_account_event("test@user", "uid", "utid")), transport=transport + token_cache=populated_cache(get_account_event("test@user", "uid", "utid")), transport=transport ) # async with before initialization: credential should call aexit but not aenter @@ -88,7 +88,7 @@ async def test_context_manager_no_cache(): transport = AsyncMockTransport() - with patch("azure.identity._internal.shared_token_cache.load_user_cache", Mock(side_effect=NotImplementedError)): + with patch("azure.identity._internal.shared_token_cache.PersistentTokenCache", Mock(side_effect=NotImplementedError)): credential = SharedTokenCacheCredential(transport=transport) async with credential: @@ -106,7 +106,7 @@ async def send(*_, **__): return mock_response(json_payload=build_aad_response(access_token="**")) credential = SharedTokenCacheCredential( - _cache=populated_cache(get_account_event("test@user", "uid", "utid")), + token_cache=populated_cache(get_account_event("test@user", "uid", "utid")), policies=[policy], transport=Mock(send=send), ) @@ -124,7 +124,7 @@ async def test_user_agent(): ) credential = SharedTokenCacheCredential( - _cache=populated_cache(get_account_event("test@user", "uid", "utid")), transport=transport + token_cache=populated_cache(get_account_event("test@user", "uid", "utid")), transport=transport ) await credential.get_token("scope") @@ -144,10 +144,10 @@ def _get_auth_client(self, authority=None, **kwargs): assert actual.netloc == expected_netloc transport = Mock(send=Mock(side_effect=Exception("credential shouldn't send a request"))) - MockCredential(_cache=TokenCache(), authority=authority, transport=transport) + MockCredential(token_cache=_TokenCache(), authority=authority, transport=transport) with patch.dict("os.environ", {EnvironmentVariables.AZURE_AUTHORITY_HOST: authority}, clear=True): - MockCredential(_cache=TokenCache(), authority=authority, transport=transport) + MockCredential(token_cache=_TokenCache(), authority=authority, transport=transport) @pytest.mark.asyncio @@ -155,13 +155,13 @@ async def test_empty_cache(): """the credential should raise CredentialUnavailableError when the cache is empty""" with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): - await SharedTokenCacheCredential(_cache=TokenCache()).get_token("scope") + await SharedTokenCacheCredential(token_cache=_TokenCache()).get_token("scope") with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): - await SharedTokenCacheCredential(_cache=TokenCache(), username="not@cache").get_token("scope") + await SharedTokenCacheCredential(token_cache=_TokenCache(), username="not@cache").get_token("scope") with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): - await SharedTokenCacheCredential(_cache=TokenCache(), tenant_id="not-cached").get_token("scope") + await SharedTokenCacheCredential(token_cache=_TokenCache(), tenant_id="not-cached").get_token("scope") with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): - credential = SharedTokenCacheCredential(_cache=TokenCache(), tenant_id="not-cached", username="not@cache") + credential = SharedTokenCacheCredential(token_cache=_TokenCache(), tenant_id="not-cached", username="not@cache") await credential.get_token("scope") @@ -175,7 +175,7 @@ async def test_no_matching_account_for_username(): cache = populated_cache(account) with pytest.raises(CredentialUnavailableError) as ex: - await SharedTokenCacheCredential(_cache=cache, username="not" + upn).get_token("scope") + await SharedTokenCacheCredential(token_cache=cache, username="not" + upn).get_token("scope") assert ex.value.message.startswith(NO_MATCHING_ACCOUNTS[: NO_MATCHING_ACCOUNTS.index("{")]) assert "not" + upn in ex.value.message @@ -191,7 +191,7 @@ async def test_no_matching_account_for_tenant(): cache = populated_cache(account) with pytest.raises(CredentialUnavailableError) as ex: - await SharedTokenCacheCredential(_cache=cache, tenant_id="not-" + tenant).get_token("scope") + await SharedTokenCacheCredential(token_cache=cache, tenant_id="not-" + tenant).get_token("scope") assert ex.value.message.startswith(NO_MATCHING_ACCOUNTS[: NO_MATCHING_ACCOUNTS.index("{")]) assert "not-" + tenant in ex.value.message @@ -207,7 +207,7 @@ async def test_no_matching_account_for_tenant_and_username(): cache = populated_cache(account) with pytest.raises(CredentialUnavailableError) as ex: - await SharedTokenCacheCredential(_cache=cache, tenant_id="not-" + tenant, username="not" + upn).get_token( + await SharedTokenCacheCredential(token_cache=cache, tenant_id="not-" + tenant, username="not" + upn).get_token( "scope" ) @@ -231,13 +231,13 @@ async def test_no_matching_account_for_tenant_or_username(): transport = Mock(side_effect=Exception()) # credential shouldn't use the network - credential = SharedTokenCacheCredential(username=upn_a, tenant_id=tenant_b, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn_a, tenant_id=tenant_b, token_cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError) as ex: await credential.get_token("scope") assert ex.value.message.startswith(NO_MATCHING_ACCOUNTS[: NO_MATCHING_ACCOUNTS.index("{")]) assert upn_a in ex.value.message and tenant_b in ex.value.message - credential = SharedTokenCacheCredential(username=upn_b, tenant_id=tenant_a, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn_b, tenant_id=tenant_a, token_cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError) as ex: await credential.get_token("scope") assert ex.value.message.startswith(NO_MATCHING_ACCOUNTS[: NO_MATCHING_ACCOUNTS.index("{")]) @@ -259,7 +259,7 @@ async def test_single_account_matching_username(): requests=[Request(required_data={"refresh_token": refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(_cache=cache, transport=transport, username=upn) + credential = SharedTokenCacheCredential(token_cache=cache, transport=transport, username=upn) token = await credential.get_token(scope) assert token.token == expected_token @@ -279,7 +279,7 @@ async def test_single_account_matching_tenant(): requests=[Request(required_data={"refresh_token": refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(_cache=cache, transport=transport, tenant_id=tenant_id) + credential = SharedTokenCacheCredential(token_cache=cache, transport=transport, tenant_id=tenant_id) token = await credential.get_token(scope) assert token.token == expected_token @@ -300,7 +300,7 @@ async def test_single_account_matching_tenant_and_username(): requests=[Request(required_data={"refresh_token": refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(_cache=cache, transport=transport, tenant_id=tenant_id, username=upn) + credential = SharedTokenCacheCredential(token_cache=cache, transport=transport, tenant_id=tenant_id, username=upn) token = await credential.get_token(scope) assert token.token == expected_token @@ -319,7 +319,7 @@ async def test_single_account(): requests=[Request(required_data={"refresh_token": refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(token_cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == expected_token @@ -334,11 +334,11 @@ async def test_no_refresh_token(): transport = Mock(side_effect=Exception()) # credential shouldn't use the network - credential = SharedTokenCacheCredential(_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(token_cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): await credential.get_token("scope") - credential = SharedTokenCacheCredential(_cache=cache, transport=transport, username="not@cache") + credential = SharedTokenCacheCredential(token_cache=cache, transport=transport, username="not@cache") with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): await credential.get_token("scope") @@ -357,7 +357,7 @@ async def test_two_accounts_no_username_or_tenant(): transport = Mock(side_effect=Exception()) # two users in the cache, no username specified -> CredentialUnavailableError - credential = SharedTokenCacheCredential(_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(token_cache=cache, transport=transport) with pytest.raises(ClientAuthenticationError, match=MULTIPLE_ACCOUNTS) as ex: await credential.get_token("scope") @@ -379,7 +379,7 @@ async def test_two_accounts_username_specified(): requests=[Request(required_data={"refresh_token": expected_refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(username=upn_a, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn_a, token_cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == expected_token @@ -402,7 +402,7 @@ async def test_two_accounts_tenant_specified(): requests=[Request(required_data={"refresh_token": expected_refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(tenant_id=tenant_id, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(tenant_id=tenant_id, token_cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == expected_token @@ -425,7 +425,7 @@ async def test_two_accounts_tenant_and_username_specified(): requests=[Request(required_data={"refresh_token": expected_refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(tenant_id=tenant_id, username=upn_a, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(tenant_id=tenant_id, username=upn_a, token_cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == expected_token @@ -448,7 +448,7 @@ async def test_same_username_different_tenants(): # with no tenant specified the credential can't select an identity transport = Mock(side_effect=Exception()) # (so it shouldn't use the network) - credential = SharedTokenCacheCredential(username=upn, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn, token_cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError) as ex: await credential.get_token("scope") @@ -461,7 +461,7 @@ async def test_same_username_different_tenants(): requests=[Request(required_data={"refresh_token": refresh_token_a, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))], ) - credential = SharedTokenCacheCredential(tenant_id=tenant_a, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(tenant_id=tenant_a, token_cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == access_token_a @@ -469,7 +469,7 @@ async def test_same_username_different_tenants(): requests=[Request(required_data={"refresh_token": refresh_token_b, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_b))], ) - credential = SharedTokenCacheCredential(tenant_id=tenant_b, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(tenant_id=tenant_b, token_cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == access_token_b @@ -492,7 +492,7 @@ async def test_same_tenant_different_usernames(): # with no username specified the credential can't select an identity transport = Mock(side_effect=Exception()) # (so it shouldn't use the network) - credential = SharedTokenCacheCredential(tenant_id=tenant_id, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(tenant_id=tenant_id, token_cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError) as ex: await credential.get_token("scope") @@ -505,7 +505,7 @@ async def test_same_tenant_different_usernames(): requests=[Request(required_data={"refresh_token": refresh_token_b, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))], ) - credential = SharedTokenCacheCredential(username=upn_b, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn_b, token_cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == access_token_a @@ -513,7 +513,7 @@ async def test_same_tenant_different_usernames(): requests=[Request(required_data={"refresh_token": refresh_token_a, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))], ) - credential = SharedTokenCacheCredential(username=upn_a, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn_a, token_cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == access_token_a @@ -537,7 +537,7 @@ async def test_authority_aliases(): requests=[Request(authority=authority, required_data={"refresh_token": expected_refresh_token})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) - credential = SharedTokenCacheCredential(authority=authority, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(authority=authority, token_cache=cache, transport=transport) token = await credential.get_token("scope") assert token.token == expected_access_token @@ -547,7 +547,7 @@ async def test_authority_aliases(): requests=[Request(authority=alias, required_data={"refresh_token": expected_refresh_token})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) - credential = SharedTokenCacheCredential(authority=alias, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(authority=alias, token_cache=cache, transport=transport) token = await credential.get_token("scope") assert token.token == expected_access_token @@ -565,7 +565,7 @@ async def test_authority_with_no_known_alias(): requests=[Request(authority=authority, required_data={"refresh_token": expected_refresh_token})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) - credential = SharedTokenCacheCredential(authority=authority, _cache=cache, transport=transport) + credential = SharedTokenCacheCredential(authority=authority, token_cache=cache, transport=transport) token = await credential.get_token("scope") assert token.token == expected_access_token @@ -584,51 +584,16 @@ async def test_authority_environment_variable(): responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) with patch.dict("os.environ", {EnvironmentVariables.AZURE_AUTHORITY_HOST: authority}, clear=True): - credential = SharedTokenCacheCredential(transport=transport, _cache=cache) + credential = SharedTokenCacheCredential(transport=transport, token_cache=cache) token = await credential.get_token("scope") assert token.token == expected_access_token -@pytest.mark.asyncio -async def test_allow_unencrypted_cache(): - """The credential should use an unencrypted cache when encryption is unavailable and the user explicitly allows it. - - This test was written when Linux was the only platform on which encryption may not be available. - """ - - platform_patch = patch("azure.identity._internal.persistent_cache.sys.platform", "linux2") - platform_patch.start() - - msal_extensions_patch = patch("azure.identity._internal.persistent_cache.msal_extensions") - mock_extensions = msal_extensions_patch.start() - - # the credential should prefer an encrypted cache even when the user allows an unencrypted one - SharedTokenCacheCredential(allow_unencrypted_cache=True) - assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.LibsecretPersistence) - mock_extensions.PersistedTokenCache.reset_mock() - - # (when LibsecretPersistence's dependencies aren't available, constructing it raises ImportError) - mock_extensions.LibsecretPersistence = Mock(side_effect=ImportError) - - # encryption unavailable, no opt in to unencrypted cache -> credential should be unavailable - credential = SharedTokenCacheCredential() - assert mock_extensions.PersistedTokenCache.call_count == 0 - with pytest.raises(CredentialUnavailableError): - await credential.get_token("scope") - - # still no encryption, but now we allow the unencrypted fallback - SharedTokenCacheCredential(allow_unencrypted_cache=True) - assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.FilePersistence) - - msal_extensions_patch.stop() - platform_patch.stop() - - @pytest.mark.asyncio async def test_initialization(): """the credential should attempt to load the cache only once, when it's first needed""" - with patch("azure.identity._internal.persistent_cache._load_persistent_cache") as mock_cache_loader: + with patch("azure.identity._persistent_cache._get_persistence") as mock_cache_loader: mock_cache_loader.side_effect = Exception("it didn't work") credential = SharedTokenCacheCredential() From 130af124defc91326b1e96cdcb4d38a5683444f9 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 6 Jan 2021 12:23:55 -0800 Subject: [PATCH 05/12] update docstrings --- .../azure-identity/azure/identity/_credentials/browser.py | 8 ++++---- .../azure/identity/_credentials/certificate.py | 8 ++++---- .../azure/identity/_credentials/client_secret.py | 8 ++++---- .../azure/identity/_credentials/device_code.py | 8 ++++---- .../azure/identity/_credentials/shared_cache.py | 5 +++-- .../azure/identity/_credentials/user_password.py | 8 ++++---- .../azure/identity/aio/_credentials/certificate.py | 4 ++++ .../azure/identity/aio/_credentials/client_secret.py | 8 ++++---- .../azure/identity/aio/_credentials/shared_cache.py | 5 +++-- 9 files changed, 34 insertions(+), 28 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/browser.py b/sdk/identity/azure-identity/azure/identity/_credentials/browser.py index f940cba40e3a..d6639f3912d0 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/browser.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/browser.py @@ -45,10 +45,10 @@ class InteractiveBrowserCredential(InteractiveCredential): :keyword AuthenticationRecord authentication_record: :class:`AuthenticationRecord` returned by :func:`authenticate` :keyword bool disable_automatic_authentication: if True, :func:`get_token` will raise :class:`AuthenticationRequiredError` when user interaction is required to acquire a token. Defaults to False. - :keyword bool enable_persistent_cache: if True, the credential will store tokens in a persistent cache shared by - other user credentials. Defaults to False. - :keyword bool allow_unencrypted_cache: if True, the credential will fall back to a plaintext cache on platforms - where encryption is unavailable. Default to False. Has no effect when `enable_persistent_cache` is False. + :keyword token_cache: token cache the credential should use. Defaults to an in memory + cache not shared with other credential instances. To enable persistent caching, provide a + :class:`~azure.identity.PersistentTokenCache` instance. + :paramtype token_cache: ~azure.identity.PersistentTokenCache :keyword int timeout: seconds to wait for the user to complete authentication. Defaults to 300 (5 minutes). :raises ValueError: invalid `redirect_uri` """ diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py b/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py index 20de6f5a79d5..fdd3a80311a1 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py @@ -38,10 +38,10 @@ class CertificateCredential(ClientCredentialBase): :keyword bool send_certificate_chain: if True, the credential will send the public certificate chain in the x5c header of each token request's JWT. This is required for Subject Name/Issuer (SNI) authentication. Defaults to False. - :keyword bool enable_persistent_cache: if True, the credential will store tokens in a persistent cache. Defaults to - False. - :keyword bool allow_unencrypted_cache: if True, the credential will fall back to a plaintext cache when encryption - is unavailable. Default to False. Has no effect when `enable_persistent_cache` is False. + :keyword token_cache: token cache the credential should use. Defaults to an in memory + cache not shared with other credential instances. To enable persistent caching, provide a + :class:`~azure.identity.PersistentTokenCache` instance. + :paramtype token_cache: ~azure.identity.PersistentTokenCache """ def __init__(self, tenant_id, client_id, certificate_path=None, **kwargs): diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/client_secret.py b/sdk/identity/azure-identity/azure/identity/_credentials/client_secret.py index 311a6f1ef3e8..cc2548e05bd1 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/client_secret.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/client_secret.py @@ -21,10 +21,10 @@ class ClientSecretCredential(ClientCredentialBase): :keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com', the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.AzureAuthorityHosts` defines authorities for other clouds. - :keyword bool enable_persistent_cache: if True, the credential will store tokens in a persistent cache. Defaults to - False. - :keyword bool allow_unencrypted_cache: if True, the credential will fall back to a plaintext cache when encryption - is unavailable. Default to False. Has no effect when `enable_persistent_cache` is False. + :keyword token_cache: token cache the credential should use. Defaults to an in memory + cache not shared with other credential instances. To enable persistent caching, provide a + :class:`~azure.identity.PersistentTokenCache` instance. + :paramtype token_cache: ~azure.identity.PersistentTokenCache """ def __init__(self, tenant_id, client_id, client_secret, **kwargs): diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/device_code.py b/sdk/identity/azure-identity/azure/identity/_credentials/device_code.py index f8acc94e34ee..dea1c271f7cf 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/device_code.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/device_code.py @@ -51,10 +51,10 @@ class DeviceCodeCredential(InteractiveCredential): :keyword AuthenticationRecord authentication_record: :class:`AuthenticationRecord` returned by :func:`authenticate` :keyword bool disable_automatic_authentication: if True, :func:`get_token` will raise :class:`AuthenticationRequiredError` when user interaction is required to acquire a token. Defaults to False. - :keyword bool enable_persistent_cache: if True, the credential will store tokens in a persistent cache shared by - other user credentials. Defaults to False. - :keyword bool allow_unencrypted_cache: if True, the credential will fall back to a plaintext cache on platforms - where encryption is unavailable. Default to False. Has no effect when `enable_persistent_cache` is False. + :keyword token_cache: token cache the credential should use. Defaults to an in memory + cache not shared with other credential instances. To enable persistent caching, provide a + :class:`~azure.identity.PersistentTokenCache` instance. + :paramtype token_cache: ~azure.identity.PersistentTokenCache """ def __init__(self, client_id=DEVELOPER_SIGN_ON_CLIENT_ID, **kwargs): diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py b/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py index 091e9d5e59f7..9fc75fee3b24 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py @@ -42,8 +42,9 @@ class SharedTokenCacheCredential(SharedTokenCacheBase): tokens for multiple identities. :keyword AuthenticationRecord authentication_record: an authentication record returned by a user credential such as :class:`DeviceCodeCredential` or :class:`InteractiveBrowserCredential` - :keyword bool allow_unencrypted_cache: if True, the credential will fall back to a plaintext cache when encryption - is unavailable. Defaults to False. + :keyword token_cache: token cache the credential should use. Defaults to a persistent cache shared by Microsoft + development applications. + :paramtype token_cache: ~azure.identity.PersistentTokenCache """ def __init__(self, username=None, **kwargs): diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/user_password.py b/sdk/identity/azure-identity/azure/identity/_credentials/user_password.py index a7bb9975d60c..c4a0b677dc24 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/user_password.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/user_password.py @@ -33,10 +33,10 @@ class UsernamePasswordCredential(InteractiveCredential): defines authorities for other clouds. :keyword str tenant_id: tenant ID or a domain associated with a tenant. If not provided, defaults to the 'organizations' tenant, which supports only Azure Active Directory work or school accounts. - :keyword bool enable_persistent_cache: if True, the credential will store tokens in a persistent cache shared by - other user credentials. Defaults to False. - :keyword bool allow_unencrypted_cache: if True, the credential will fall back to a plaintext cache on platforms - where encryption is unavailable. Default to False. Has no effect when `enable_persistent_cache` is False. + :keyword token_cache: token cache the credential should use. Defaults to an in memory + cache not shared with other credential instances. To enable persistent caching, provide a + :class:`~azure.identity.PersistentTokenCache` instance. + :paramtype token_cache: ~azure.identity.PersistentTokenCache """ def __init__(self, client_id, username, password, **kwargs): diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py index 4c7ecd825272..d93880b1638d 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py @@ -31,6 +31,10 @@ class CertificateCredential(AsyncContextManager): :keyword password: The certificate's password. If a unicode string, it will be encoded as UTF-8. If the certificate requires a different encoding, pass appropriately encoded bytes instead. :paramtype password: str or bytes + :keyword token_cache: token cache the credential should use. Defaults to an in memory + cache not shared with other credential instances. To enable persistent caching, provide a + :class:`~azure.identity.PersistentTokenCache` instance. + :paramtype token_cache: ~azure.identity.PersistentTokenCache """ def __init__(self, tenant_id, client_id, certificate_path=None, **kwargs): diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py index aa49a67f438f..0a8fece49597 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py @@ -23,10 +23,10 @@ class ClientSecretCredential(AsyncContextManager): :keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com', the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.AzureAuthorityHosts` defines authorities for other clouds. - :keyword bool enable_persistent_cache: if True, the credential will store tokens in a persistent cache. Defaults to - False. - :keyword bool allow_unencrypted_cache: if True, the credential will fall back to a plaintext cache when encryption - is unavailable. Default to False. Has no effect when `enable_persistent_cache` is False. + :keyword token_cache: token cache the credential should use. Defaults to an in memory + cache not shared with other credential instances. To enable persistent caching, provide a + :class:`~azure.identity.PersistentTokenCache` instance. + :paramtype token_cache: ~azure.identity.PersistentTokenCache """ def __init__(self, tenant_id, client_id, client_secret, **kwargs): diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/shared_cache.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/shared_cache.py index a34d56042970..c41f7b9ff946 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/shared_cache.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/shared_cache.py @@ -29,8 +29,9 @@ class SharedTokenCacheCredential(SharedTokenCacheBase, AsyncContextManager): defines authorities for other clouds. :keyword str tenant_id: an Azure Active Directory tenant ID. Used to select an account when the cache contains tokens for multiple identities. - :keyword bool allow_unencrypted_cache: if True, the credential will fall back to a plaintext cache when encryption - is unavailable. Defaults to False. + :keyword token_cache: token cache the credential should use. Defaults to a persistent cache shared by Microsoft + development applications. + :paramtype token_cache: ~azure.identity.PersistentTokenCache """ async def __aenter__(self): From c01f1b696a551518290b41b5919dfe9757ebfa42 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Thu, 7 Jan 2021 16:41:30 -0800 Subject: [PATCH 06/12] add sample --- sdk/identity/azure-identity/samples/README.md | 1 + .../samples/token_cache_persistence.py | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 sdk/identity/azure-identity/samples/token_cache_persistence.py diff --git a/sdk/identity/azure-identity/samples/README.md b/sdk/identity/azure-identity/samples/README.md index ab61f28e7cc6..f2f179504118 100644 --- a/sdk/identity/azure-identity/samples/README.md +++ b/sdk/identity/azure-identity/samples/README.md @@ -35,4 +35,5 @@ pip install azure-identity azure-keyvault-secrets |-------------|-------------| | control_interactive_prompts.py | demonstrates controlling when interactive credentials prompt for user interaction | | custom_credentials.py | demonstrates custom credential implementation | +| token_cache_persistence.py | demonstrates configuring and customizing token cache persistence | user_authentication.py | demonstrates user authentication API for applications | diff --git a/sdk/identity/azure-identity/samples/token_cache_persistence.py b/sdk/identity/azure-identity/samples/token_cache_persistence.py new file mode 100644 index 000000000000..e5c3c2d18bce --- /dev/null +++ b/sdk/identity/azure-identity/samples/token_cache_persistence.py @@ -0,0 +1,31 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +"""Demonstrates configuring token cache persistence. + +Many credential implementations in azure-identity have an underlying token cache holding sensitive authentication +data such as account information, access tokens, and refresh tokens. By default this is an in memory cache not shared +with other credential instances. Some applications need to share a token cache among credentials, and persist it across +executions. This file shows how to do this with the PeristentTokenCache class. +""" + +from azure.identity import DeviceCodeCredential, InteractiveBrowserCredential, PersistentTokenCache + +# PersistentTokenCache represents a persistent token cache managed by the Azure SDK. It defaults to +# the cache shared by Microsoft development applications, which SharedTokenCacheCredential also uses. +cache = PersistentTokenCache() +credential = InteractiveBrowserCredential(token_cache=cache) + +# Multiple credentials can share a PersistentTokenCache instance +device_code_credential = DeviceCodeCredential(token_cache=cache) + +# An application can isolate its authentication data from other applications by naming its cache +cache = PersistentTokenCache(name="my_application") +credential = InteractiveBrowserCredential(token_cache=cache) + +# By default, PersistentTokenCache encrypts its data with the current platform's user data protection +# APIs, and will raise an error when it isn't able to do so. Applications can configure it to instead +# fall back to storing data in clear text. This does not disable encryption. PersistentTokenCache will +# always attempt to encrypt its data. +cache = PersistentTokenCache(allow_unencrypted_storage=True) From f28a1a57b60a37b816d4436606e3b8f12086e540 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Fri, 8 Jan 2021 15:31:46 -0800 Subject: [PATCH 07/12] update user auth sample --- .../azure-identity/samples/user_authentication.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/sdk/identity/azure-identity/samples/user_authentication.py b/sdk/identity/azure-identity/samples/user_authentication.py index 2c21c2a44973..1a787f50e0dc 100644 --- a/sdk/identity/azure-identity/samples/user_authentication.py +++ b/sdk/identity/azure-identity/samples/user_authentication.py @@ -6,7 +6,7 @@ import os import sys -from azure.identity import AuthenticationRecord, InteractiveBrowserCredential +from azure.identity import AuthenticationRecord, InteractiveBrowserCredential, PersistentTokenCache from azure.keyvault.secrets import SecretClient @@ -18,7 +18,10 @@ # Persistent caching is optional. By default, interactive credentials cache in memory only. -credential = InteractiveBrowserCredential(enable_persistent_cache=True) +# To enable persistent caching, create the credential with a PersistentTokenCache instance +# (see the token_cache_persistence.py sample for more details about PersistentTokenCache) +cache = PersistentTokenCache() +credential = InteractiveBrowserCredential(token_cache=cache) # The 'authenticate' method begins interactive authentication. Call it whenever it's convenient # for your application to authenticate a user. It returns a record of the authentication. @@ -32,11 +35,11 @@ client = SecretClient(VAULT_URL, credential) secret_names = [s.name for s in client.list_properties_of_secrets()] -# With persistent caching enabled, an authentication record stored by your application enables -# credentials to access data from past authentications. If the cache contains sufficient data, -# this eliminates the need for your application to prompt for authentication every time it runs. +# An authentication record stored by your application enables other credentials to access data from +# past authentications. If the cache contains sufficient data, this eliminates the need for your +# application to prompt for authentication every time it runs. deserialized_record = AuthenticationRecord.deserialize(record_json) -new_credential = InteractiveBrowserCredential(enable_persistent_cache=True, authentication_record=deserialized_record) +new_credential = InteractiveBrowserCredential(token_cache=cache, authentication_record=deserialized_record) # This request should also succeed without prompting for authentication. client = SecretClient(VAULT_URL, new_credential) From 28cb11b9382f6ddbf169d07ce78d0b6a3c5ef6c2 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Fri, 26 Feb 2021 11:25:46 -0800 Subject: [PATCH 08/12] update changelog --- sdk/identity/azure-identity/CHANGELOG.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index df5aa2ef760d..4adf0e0d811c 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -1,7 +1,26 @@ # Release History ## 1.6.0b2 (Unreleased) - +### Breaking Changes +> These changes do not impact the API of stable versions such as 1.5.0. +> Only code written against a beta version such as 1.6.0b1 may be affected. +- Credentials accepting keyword arguments `allow_unencrypted_cache` and + `enable_persistent_cache` to configure persistent caching accept a + `token_cache` argument instead whose value should be an instance of + `PersistentTokenCache`. For example: + ``` + # before (e.g. in 1.6.0b1): + DeviceCodeCredential(enable_persistent_cache=True, allow_unencrypted_cache=True) + + # after: + cache = PersistentTokenCache(allow_unencrypted_storage=True) + DeviceCodeCredential(token_cache=cache) + ``` + + See the documentation and samples for more details. + +### Added +- New class `PersistentTokenCache` configures persistent caching ## 1.6.0b1 (2021-02-09) ### Changed From c3762af04536d5c4a290a8015294c1c566680540 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Fri, 26 Feb 2021 13:45:28 -0800 Subject: [PATCH 09/12] fix tests needlessly passing cache arguments --- .../tests/test_browser_credential.py | 14 +++------- .../tests/test_device_code_credential.py | 26 ++++--------------- .../tests/test_shared_cache_credential.py | 8 ++++-- 3 files changed, 14 insertions(+), 34 deletions(-) diff --git a/sdk/identity/azure-identity/tests/test_browser_credential.py b/sdk/identity/azure-identity/tests/test_browser_credential.py index af0f9983e37b..30161629cbe0 100644 --- a/sdk/identity/azure-identity/tests/test_browser_credential.py +++ b/sdk/identity/azure-identity/tests/test_browser_credential.py @@ -13,7 +13,6 @@ from azure.identity import AuthenticationRequiredError, CredentialUnavailableError, InteractiveBrowserCredential from azure.identity._internal import AuthCodeRedirectServer from azure.identity._internal.user_agent import USER_AGENT -from msal import TokenCache import pytest from six.moves import urllib @@ -91,11 +90,8 @@ def test_no_scopes(): def test_disable_automatic_authentication(): """When configured for strict silent auth, the credential should raise when silent auth fails""" - empty_cache = TokenCache() # empty cache makes silent auth impossible transport = Mock(send=Mock(side_effect=Exception("no request should be sent"))) - credential = InteractiveBrowserCredential( - disable_automatic_authentication=True, transport=transport, _cache=empty_cache - ) + credential = InteractiveBrowserCredential(disable_automatic_authentication=True, transport=transport) with patch(WEBBROWSER_OPEN, Mock(side_effect=Exception("credential shouldn't try interactive authentication"))): with pytest.raises(AuthenticationRequiredError): @@ -133,9 +129,7 @@ def handle_request(self): ) ) - credential = InteractiveBrowserCredential( - timeout=timeout, transport=transport, _cache=TokenCache(), _server_class=GuaranteedTimeout - ) + credential = InteractiveBrowserCredential(timeout=timeout, transport=transport, _server_class=GuaranteedTimeout) with patch(WEBBROWSER_OPEN, lambda _: True): with pytest.raises(ClientAuthenticationError) as ex: @@ -175,9 +169,7 @@ def test_redirect_server(): def test_no_browser(): transport = validating_transport(requests=[Request()] * 2, responses=[get_discovery_response()] * 2) - credential = InteractiveBrowserCredential( - client_id="client-id", _server_class=Mock(), transport=transport, _cache=TokenCache() - ) + credential = InteractiveBrowserCredential(client_id="client-id", _server_class=Mock(), transport=transport) with pytest.raises(ClientAuthenticationError, match=r".*browser.*"): with patch(WEBBROWSER_OPEN, lambda _: False): credential.get_token("scope") diff --git a/sdk/identity/azure-identity/tests/test_device_code_credential.py b/sdk/identity/azure-identity/tests/test_device_code_credential.py index 3924ae6036b8..128c646dfa30 100644 --- a/sdk/identity/azure-identity/tests/test_device_code_credential.py +++ b/sdk/identity/azure-identity/tests/test_device_code_credential.py @@ -8,7 +8,6 @@ from azure.core.pipeline.policies import SansIOHTTPPolicy from azure.identity import AuthenticationRequiredError, DeviceCodeCredential from azure.identity._internal.user_agent import USER_AGENT -from msal import TokenCache import pytest from helpers import ( @@ -89,7 +88,6 @@ def test_authenticate(): transport=transport, authority=environment, tenant_id=tenant_id, - _cache=TokenCache(), ) record = credential.authenticate(scopes=(scope,)) assert record.authority == environment @@ -105,11 +103,8 @@ def test_authenticate(): def test_disable_automatic_authentication(): """When configured for strict silent auth, the credential should raise when silent auth fails""" - empty_cache = TokenCache() # empty cache makes silent auth impossible transport = Mock(send=Mock(side_effect=Exception("no request should be sent"))) - credential = DeviceCodeCredential( - "client-id", disable_automatic_authentication=True, transport=transport, _cache=empty_cache - ) + credential = DeviceCodeCredential("client-id", disable_automatic_authentication=True, transport=transport) with pytest.raises(AuthenticationRequiredError): credential.get_token("scope") @@ -141,7 +136,7 @@ def test_policies_configurable(): ) credential = DeviceCodeCredential( - client_id=client_id, prompt_callback=Mock(), policies=[policy], transport=transport, _cache=TokenCache() + client_id=client_id, prompt_callback=Mock(), policies=[policy], transport=transport ) credential.get_token("scope") @@ -171,9 +166,7 @@ def test_user_agent(): ], ) - credential = DeviceCodeCredential( - client_id=client_id, prompt_callback=Mock(), transport=transport, _cache=TokenCache() - ) + credential = DeviceCodeCredential(client_id=client_id, prompt_callback=Mock(), transport=transport) credential.get_token("scope") @@ -214,11 +207,7 @@ def test_device_code_credential(): callback = Mock() credential = DeviceCodeCredential( - client_id=client_id, - prompt_callback=callback, - transport=transport, - instance_discovery=False, - _cache=TokenCache(), + client_id=client_id, prompt_callback=callback, transport=transport, instance_discovery=False, ) now = datetime.datetime.utcnow() @@ -250,12 +239,7 @@ def test_timeout(): ) credential = DeviceCodeCredential( - client_id="_", - prompt_callback=Mock(), - transport=transport, - timeout=0.01, - instance_discovery=False, - _cache=TokenCache(), + client_id="_", prompt_callback=Mock(), transport=transport, timeout=0.01, instance_discovery=False, ) with pytest.raises(ClientAuthenticationError) as ex: diff --git a/sdk/identity/azure-identity/tests/test_shared_cache_credential.py b/sdk/identity/azure-identity/tests/test_shared_cache_credential.py index be2e0190c232..0916f432e30d 100644 --- a/sdk/identity/azure-identity/tests/test_shared_cache_credential.py +++ b/sdk/identity/azure-identity/tests/test_shared_cache_credential.py @@ -740,7 +740,9 @@ def test_client_capabilities(): record = AuthenticationRecord("tenant-id", "client_id", "authority", "home_account_id", "username") transport = Mock(send=Mock(side_effect=Exception("this test mocks MSAL, so no request should be sent"))) - credential = SharedTokenCacheCredential(transport=transport, authentication_record=record, _cache=_TokenCache()) + credential = SharedTokenCacheCredential( + transport=transport, authentication_record=record, token_cache=_TokenCache() + ) with patch(SharedTokenCacheCredential.__module__ + ".PublicClientApplication") as PublicClientApplication: credential._initialize() @@ -764,7 +766,9 @@ def test_claims_challenge(): ) transport = Mock(send=Mock(side_effect=Exception("this test mocks MSAL, so no request should be sent"))) - credential = SharedTokenCacheCredential(transport=transport, authentication_record=record, _cache=_TokenCache()) + credential = SharedTokenCacheCredential( + transport=transport, authentication_record=record, token_cache=_TokenCache() + ) with patch(SharedTokenCacheCredential.__module__ + ".PublicClientApplication", lambda *_, **__: msal_app): credential.get_token("scope", claims=expected_claims) From 2894a74037a5b5211b17dd11ba5b7fcf286ed0a1 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Mon, 1 Mar 2021 17:51:54 -0800 Subject: [PATCH 10/12] replace PersistentTokenCache with TokenCachePersistenceOptions --- sdk/identity/azure-identity/CHANGELOG.md | 10 +- .../azure-identity/azure/identity/__init__.py | 4 +- .../azure/identity/_credentials/browser.py | 7 +- .../identity/_credentials/certificate.py | 7 +- .../identity/_credentials/client_secret.py | 7 +- .../identity/_credentials/device_code.py | 7 +- .../identity/_credentials/shared_cache.py | 10 +- .../identity/_credentials/user_password.py | 7 +- .../azure/identity/_internal/__init__.py | 8 -- .../identity/_internal/msal_credentials.py | 14 ++- .../identity/_internal/shared_token_cache.py | 14 +-- .../azure/identity/_persistent_cache.py | 47 +++++--- .../identity/aio/_credentials/certificate.py | 23 ++-- .../aio/_credentials/client_secret.py | 23 ++-- .../identity/aio/_credentials/shared_cache.py | 6 +- sdk/identity/azure-identity/samples/README.md | 1 - .../samples/token_cache_persistence.py | 31 ----- .../samples/user_authentication.py | 13 ++- .../tests/test_cache_options_samples.py | 21 ++++ .../tests/test_certificate_credential.py | 33 +++--- .../test_certificate_credential_async.py | 31 ++++- .../tests/test_client_secret_credential.py | 49 ++++++-- .../test_client_secret_credential_async.py | 50 ++++++-- .../azure-identity/tests/test_default.py | 2 +- .../tests/test_default_async.py | 2 +- .../tests/test_interactive_credential.py | 17 +-- .../tests/test_shared_cache_credential.py | 108 +++++++++--------- .../test_shared_cache_credential_async.py | 79 +++++++------ 28 files changed, 374 insertions(+), 257 deletions(-) delete mode 100644 sdk/identity/azure-identity/samples/token_cache_persistence.py create mode 100644 sdk/identity/azure-identity/tests/test_cache_options_samples.py diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 4adf0e0d811c..ce5dcfc84c92 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -6,21 +6,21 @@ > Only code written against a beta version such as 1.6.0b1 may be affected. - Credentials accepting keyword arguments `allow_unencrypted_cache` and `enable_persistent_cache` to configure persistent caching accept a - `token_cache` argument instead whose value should be an instance of - `PersistentTokenCache`. For example: + `cache_persistence_options` argument instead whose value should be an + instance of `TokenCachePersistenceOptions`. For example: ``` # before (e.g. in 1.6.0b1): DeviceCodeCredential(enable_persistent_cache=True, allow_unencrypted_cache=True) # after: - cache = PersistentTokenCache(allow_unencrypted_storage=True) - DeviceCodeCredential(token_cache=cache) + cache_options = TokenCachePersistenceOptions(allow_unencrypted_storage=True) + DeviceCodeCredential(cache_persistence_options=cache_options) ``` See the documentation and samples for more details. ### Added -- New class `PersistentTokenCache` configures persistent caching +- New class `TokenCachePersistenceOptions` configures persistent caching ## 1.6.0b1 (2021-02-09) ### Changed diff --git a/sdk/identity/azure-identity/azure/identity/__init__.py b/sdk/identity/azure-identity/azure/identity/__init__.py index 8469d70430ae..cb6f35ec35b1 100644 --- a/sdk/identity/azure-identity/azure/identity/__init__.py +++ b/sdk/identity/azure-identity/azure/identity/__init__.py @@ -22,7 +22,7 @@ UsernamePasswordCredential, VisualStudioCodeCredential, ) -from ._persistent_cache import PersistentTokenCache +from ._persistent_cache import TokenCachePersistenceOptions __all__ = [ @@ -41,8 +41,8 @@ "InteractiveBrowserCredential", "KnownAuthorities", "ManagedIdentityCredential", - "PersistentTokenCache", "SharedTokenCacheCredential", + "TokenCachePersistenceOptions", "UsernamePasswordCredential", "VisualStudioCodeCredential", ] diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/browser.py b/sdk/identity/azure-identity/azure/identity/_credentials/browser.py index d6639f3912d0..5e4d76480ba9 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/browser.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/browser.py @@ -45,10 +45,9 @@ class InteractiveBrowserCredential(InteractiveCredential): :keyword AuthenticationRecord authentication_record: :class:`AuthenticationRecord` returned by :func:`authenticate` :keyword bool disable_automatic_authentication: if True, :func:`get_token` will raise :class:`AuthenticationRequiredError` when user interaction is required to acquire a token. Defaults to False. - :keyword token_cache: token cache the credential should use. Defaults to an in memory - cache not shared with other credential instances. To enable persistent caching, provide a - :class:`~azure.identity.PersistentTokenCache` instance. - :paramtype token_cache: ~azure.identity.PersistentTokenCache + :keyword cache_persistence_options: configuration for persistent token caching. If unspecified, the credential + will cache tokens in memory. + :paramtype cache_persistence_options: ~azure.identity.TokenCachePersistenceOptions :keyword int timeout: seconds to wait for the user to complete authentication. Defaults to 300 (5 minutes). :raises ValueError: invalid `redirect_uri` """ diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py b/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py index fdd3a80311a1..d22378694fbd 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py @@ -38,10 +38,9 @@ class CertificateCredential(ClientCredentialBase): :keyword bool send_certificate_chain: if True, the credential will send the public certificate chain in the x5c header of each token request's JWT. This is required for Subject Name/Issuer (SNI) authentication. Defaults to False. - :keyword token_cache: token cache the credential should use. Defaults to an in memory - cache not shared with other credential instances. To enable persistent caching, provide a - :class:`~azure.identity.PersistentTokenCache` instance. - :paramtype token_cache: ~azure.identity.PersistentTokenCache + :keyword cache_persistence_options: configuration for persistent token caching. If unspecified, the credential + will cache tokens in memory. + :paramtype cache_persistence_options: ~azure.identity.TokenCachePersistenceOptions """ def __init__(self, tenant_id, client_id, certificate_path=None, **kwargs): diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/client_secret.py b/sdk/identity/azure-identity/azure/identity/_credentials/client_secret.py index cc2548e05bd1..1eef4f8abd78 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/client_secret.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/client_secret.py @@ -21,10 +21,9 @@ class ClientSecretCredential(ClientCredentialBase): :keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com', the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.AzureAuthorityHosts` defines authorities for other clouds. - :keyword token_cache: token cache the credential should use. Defaults to an in memory - cache not shared with other credential instances. To enable persistent caching, provide a - :class:`~azure.identity.PersistentTokenCache` instance. - :paramtype token_cache: ~azure.identity.PersistentTokenCache + :keyword cache_persistence_options: configuration for persistent token caching. If unspecified, the credential + will cache tokens in memory. + :paramtype cache_persistence_options: ~azure.identity.TokenCachePersistenceOptions """ def __init__(self, tenant_id, client_id, client_secret, **kwargs): diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/device_code.py b/sdk/identity/azure-identity/azure/identity/_credentials/device_code.py index dea1c271f7cf..e12cc121b724 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/device_code.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/device_code.py @@ -51,10 +51,9 @@ class DeviceCodeCredential(InteractiveCredential): :keyword AuthenticationRecord authentication_record: :class:`AuthenticationRecord` returned by :func:`authenticate` :keyword bool disable_automatic_authentication: if True, :func:`get_token` will raise :class:`AuthenticationRequiredError` when user interaction is required to acquire a token. Defaults to False. - :keyword token_cache: token cache the credential should use. Defaults to an in memory - cache not shared with other credential instances. To enable persistent caching, provide a - :class:`~azure.identity.PersistentTokenCache` instance. - :paramtype token_cache: ~azure.identity.PersistentTokenCache + :keyword cache_persistence_options: configuration for persistent token caching. If unspecified, the credential + will cache tokens in memory. + :paramtype cache_persistence_options: ~azure.identity.TokenCachePersistenceOptions """ def __init__(self, client_id=DEVELOPER_SIGN_ON_CLIENT_ID, **kwargs): diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py b/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py index 9fc75fee3b24..edddc8a8974c 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py @@ -42,9 +42,9 @@ class SharedTokenCacheCredential(SharedTokenCacheBase): tokens for multiple identities. :keyword AuthenticationRecord authentication_record: an authentication record returned by a user credential such as :class:`DeviceCodeCredential` or :class:`InteractiveBrowserCredential` - :keyword token_cache: token cache the credential should use. Defaults to a persistent cache shared by Microsoft - development applications. - :paramtype token_cache: ~azure.identity.PersistentTokenCache + :keyword cache_persistence_options: configuration for persistent token caching. If not provided, the credential + will use the persistent cache shared by Microsoft development applications + :paramtype cache_persistence_options: ~azure.identity.TokenCachePersistenceOptions """ def __init__(self, username=None, **kwargs): @@ -55,7 +55,7 @@ def __init__(self, username=None, **kwargs): # authenticate in the tenant that produced the record unless "tenant_id" specifies another self._tenant_id = kwargs.pop("tenant_id", None) or self._auth_record.tenant_id validate_tenant_id(self._tenant_id) - self._cache = kwargs.pop("token_cache", None) + self._cache = kwargs.pop("_cache", None) self._app = None self._client_kwargs = kwargs self._initialized = False @@ -122,7 +122,7 @@ def _initialize(self): self._app = PublicClientApplication( client_id=self._auth_record.client_id, authority="https://{}/{}".format(self._auth_record.authority, self._tenant_id), - token_cache=self._cache._cache, # pylint:disable=protected-access + token_cache=self._cache, http_client=MsalClient(**self._client_kwargs), client_capabilities=["CP1"] ) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/user_password.py b/sdk/identity/azure-identity/azure/identity/_credentials/user_password.py index c4a0b677dc24..99e5c9b1955a 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/user_password.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/user_password.py @@ -33,10 +33,9 @@ class UsernamePasswordCredential(InteractiveCredential): defines authorities for other clouds. :keyword str tenant_id: tenant ID or a domain associated with a tenant. If not provided, defaults to the 'organizations' tenant, which supports only Azure Active Directory work or school accounts. - :keyword token_cache: token cache the credential should use. Defaults to an in memory - cache not shared with other credential instances. To enable persistent caching, provide a - :class:`~azure.identity.PersistentTokenCache` instance. - :paramtype token_cache: ~azure.identity.PersistentTokenCache + :keyword cache_persistence_options: configuration for persistent token caching. If unspecified, the credential + will cache tokens in memory. + :paramtype cache_persistence_options: ~azure.identity.TokenCachePersistenceOptions """ def __init__(self, client_id, username, password, **kwargs): diff --git a/sdk/identity/azure-identity/azure/identity/_internal/__init__.py b/sdk/identity/azure-identity/azure/identity/_internal/__init__.py index e00047afb7e3..39f554bc47a7 100644 --- a/sdk/identity/azure-identity/azure/identity/_internal/__init__.py +++ b/sdk/identity/azure-identity/azure/identity/_internal/__init__.py @@ -4,18 +4,11 @@ # ------------------------------------ import os -import msal from six.moves.urllib_parse import urlparse from .._constants import EnvironmentVariables, KnownAuthorities -class _TokenCache(object): - """Wrapper for in memory token caches""" - def __init__(self): - self._cache = msal.TokenCache() - - def normalize_authority(authority): # type: (str) -> str """Ensure authority uses https, strip trailing spaces and /""" @@ -74,7 +67,6 @@ def _scopes_to_resource(*scopes): __all__ = [ "_scopes_to_resource", - "_TokenCache", "AadClient", "AadClientBase", "AuthCodeRedirectServer", diff --git a/sdk/identity/azure-identity/azure/identity/_internal/msal_credentials.py b/sdk/identity/azure-identity/azure/identity/_internal/msal_credentials.py index e0506dd191bb..6a260ae8474b 100644 --- a/sdk/identity/azure-identity/azure/identity/_internal/msal_credentials.py +++ b/sdk/identity/azure-identity/azure/identity/_internal/msal_credentials.py @@ -7,7 +7,8 @@ import msal from .msal_client import MsalClient -from .._internal import _TokenCache, get_default_authority, normalize_authority, validate_tenant_id +from .._internal import get_default_authority, normalize_authority, validate_tenant_id +from .._persistent_cache import _load_persistent_cache, TokenCachePersistenceOptions try: ABC = abc.ABC @@ -37,7 +38,14 @@ def __init__(self, client_id, client_credential=None, **kwargs): self._client_credential = client_credential self._client_id = client_id - self._cache = kwargs.pop("token_cache", None) or _TokenCache() + self._cache = kwargs.pop("_cache", None) + if not self._cache: + options = kwargs.pop("cache_persistence_options", None) + if options: + self._cache = _load_persistent_cache(options) + else: + self._cache = msal.TokenCache() + self._client = MsalClient(**kwargs) # postpone creating the wrapped application because its initializer uses the network @@ -55,7 +63,7 @@ def _create_app(self, cls, **kwargs): client_id=self._client_id, client_credential=self._client_credential, authority="{}/{}".format(self._authority, self._tenant_id), - token_cache=self._cache._cache, # pylint:disable=protected-access + token_cache=self._cache, http_client=self._client, **kwargs ) diff --git a/sdk/identity/azure-identity/azure/identity/_internal/shared_token_cache.py b/sdk/identity/azure-identity/azure/identity/_internal/shared_token_cache.py index fd5cea04d32f..7228e2a01496 100644 --- a/sdk/identity/azure-identity/azure/identity/_internal/shared_token_cache.py +++ b/sdk/identity/azure-identity/azure/identity/_internal/shared_token_cache.py @@ -14,7 +14,7 @@ from .. import CredentialUnavailableError from .._constants import KnownAuthorities from .._internal import get_default_authority, normalize_authority, wrap_exceptions -from .._persistent_cache import PersistentTokenCache # importing this from azure.identity creates a cycle +from .._persistent_cache import _load_persistent_cache, TokenCachePersistenceOptions try: ABC = abc.ABC @@ -94,7 +94,7 @@ def __init__(self, username=None, **kwargs): # pylint:disable=unused-argument self._environment_aliases = KNOWN_ALIASES.get(environment) or frozenset((environment,)) self._username = username self._tenant_id = kwargs.pop("tenant_id", None) - self._cache = kwargs.pop("token_cache", None) + self._cache = kwargs.pop("_cache", None) self._client = None # type: Optional[AadClientBase] self._client_kwargs = kwargs self._client_kwargs["tenant_id"] = "organizations" @@ -108,7 +108,7 @@ def _initialize(self): if self._cache: # pylint:disable=protected-access self._client = self._get_auth_client( - authority=self._authority, cache=self._cache._cache, **self._client_kwargs + authority=self._authority, cache=self._cache, **self._client_kwargs ) self._initialized = True @@ -119,7 +119,7 @@ def _load_cache(self): # This credential accepts the user's default cache regardless of whether it's encrypted. It doesn't # create a new cache. If the default cache exists, the user must have created it earlier. If it's # unencrypted, the user must have allowed that. - self._cache = PersistentTokenCache(allow_unencrypted=True) + self._cache = _load_persistent_cache(TokenCachePersistenceOptions(allow_unencrypted_storage=True)) except Exception: # pylint:disable=broad-except pass @@ -133,7 +133,7 @@ def _get_cache_items_for_authority(self, credential_type): """yield cache items matching this credential's authority or one of its aliases""" items = [] - for item in self._cache._cache.find(credential_type): # pylint:disable=protected-access + for item in self._cache.find(credential_type): environment = item.get("environment") if environment in self._environment_aliases: items.append(item) @@ -194,7 +194,7 @@ def _get_cached_access_token(self, scopes, account): return None try: - cache_entries = self._cache._cache.find( # pylint:disable=protected-access + cache_entries = self._cache.find( msal.TokenCache.CredentialType.ACCESS_TOKEN, target=list(scopes), query={"home_account_id": account["home_account_id"]}, @@ -214,7 +214,7 @@ def _get_refresh_tokens(self, account): return None try: - cache_entries = self._cache._cache.find( # pylint:disable=protected-access + cache_entries = self._cache.find( msal.TokenCache.CredentialType.REFRESH_TOKEN, query={"home_account_id": account["home_account_id"]} ) return [token["secret"] for token in cache_entries if "secret" in token] diff --git a/sdk/identity/azure-identity/azure/identity/_persistent_cache.py b/sdk/identity/azure-identity/azure/identity/_persistent_cache.py index fb9c73cc195c..90e343157222 100644 --- a/sdk/identity/azure-identity/azure/identity/_persistent_cache.py +++ b/sdk/identity/azure-identity/azure/identity/_persistent_cache.py @@ -12,34 +12,47 @@ from typing import Any -class PersistentTokenCache(object): - """Token cache backed by persistent storage. +class TokenCachePersistenceOptions(object): + """Options for persistent token caching. - This class encrypts its data by default. On Linux, libsecret and pygobject are required for encryption. On macOS, - Keychain protects the cache. On Windows, the cache is protected by the data protection API (DPAPI). + Most credentials accept an instance of this class to configure persistent token caching. The default values + configure a credential to use a cache shared with Microsoft developer tools and + :class:`~azure.identity.SharedTokenCacheCredential`. To isolate a credential's data from other applications, + specify a `name` for the cache. + + By default, the cache is encrypted with the current platform's user data protection API, and will raise an error + when this is not available. To configure the cache to fall back to an unencrypted file instead of raising an + error, specify `allow_unencrypted_storage=True`. + + .. warning:: The cache contains authentication secrets. If the cache is not encrypted, protecting it is the + application's responsibility. A breach of its contents will fully compromise accounts. + + .. literalinclude:: ../tests/test_cache_options_samples.py + :start-after: [START snippet] + :end-before: [END snippet] + :language: python + :caption: Configuring a credential for persistent caching + :dedent: 8 :keyword str name: name of the cache, used to isolate its data from other applications. Defaults to the name of the cache shared by Microsoft dev tools and :class:`~azure.identity.SharedTokenCacheCredential`. :keyword bool allow_unencrypted_storage: whether the cache should fall back to storing its data in plain text when encryption isn't possible. False by default. Setting this to True does not disable encryption. The cache will always try to encrypt its data. - - :raises NotImplementedError: persistent token caching isn't supported on the current platform - :raises ValueError: encryption isn't available on the current platform, and `allow_unencrypted_storage` is False. - Specify `allow_unencrypted_storage=True` to work around this, if it's acceptable for the cache to store data - without encryption. """ def __init__(self, **kwargs): # type: (**Any) -> None - persistence = kwargs.get("_persistence") - if not persistence: - persistence = _get_persistence( - allow_unencrypted=kwargs.get("allow_unencrypted_storage", False), - account_name="MSALCache", - cache_name=kwargs.get("name", "msal.cache"), - ) - self._cache = msal_extensions.PersistedTokenCache(persistence) + self.allow_unencrypted_storage = kwargs.get("allow_unencrypted_storage", False) + self.name = kwargs.get("name", "msal.cache") + + +def _load_persistent_cache(options): + # type: (TokenCachePersistenceOptions) -> msal_extensions.PersistedTokenCache + persistence = _get_persistence( + allow_unencrypted=options.allow_unencrypted_storage, account_name="MSALCache", cache_name=options.name, + ) + return msal_extensions.PersistedTokenCache(persistence) def _get_persistence(allow_unencrypted, account_name, cache_name): diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py index d93880b1638d..eaaa487edf19 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py @@ -4,10 +4,13 @@ # ------------------------------------ from typing import TYPE_CHECKING +import msal + from .._internal import AadClient, AsyncContextManager from .._internal.decorators import log_get_token_async from ..._credentials.certificate import get_client_credential -from ..._internal import _TokenCache, AadClientCertificate, validate_tenant_id +from ..._internal import AadClientCertificate, validate_tenant_id +from ..._persistent_cache import _load_persistent_cache, TokenCachePersistenceOptions if TYPE_CHECKING: from typing import Any, Optional @@ -31,10 +34,9 @@ class CertificateCredential(AsyncContextManager): :keyword password: The certificate's password. If a unicode string, it will be encoded as UTF-8. If the certificate requires a different encoding, pass appropriately encoded bytes instead. :paramtype password: str or bytes - :keyword token_cache: token cache the credential should use. Defaults to an in memory - cache not shared with other credential instances. To enable persistent caching, provide a - :class:`~azure.identity.PersistentTokenCache` instance. - :paramtype token_cache: ~azure.identity.PersistentTokenCache + :keyword cache_persistence_options: configuration for persistent token caching. If unspecified, the credential + will cache tokens in memory. + :paramtype cache_persistence_options: ~azure.identity.TokenCachePersistenceOptions """ def __init__(self, tenant_id, client_id, certificate_path=None, **kwargs): @@ -47,8 +49,15 @@ def __init__(self, tenant_id, client_id, certificate_path=None, **kwargs): client_credential["private_key"], password=client_credential.get("passphrase") ) - cache = kwargs.pop("token_cache", None) or _TokenCache() - self._client = AadClient(tenant_id, client_id, cache=cache._cache, **kwargs) + cache = kwargs.pop("_cache", None) + if not cache: + options = kwargs.pop("cache_persistence_options", None) + if options: + cache = _load_persistent_cache(options) + else: + cache = msal.TokenCache() + + self._client = AadClient(tenant_id, client_id, cache=cache, **kwargs) self._client_id = client_id async def __aenter__(self): diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py index 0a8fece49597..10eb903b37d3 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py @@ -4,9 +4,12 @@ # ------------------------------------ from typing import TYPE_CHECKING +import msal + from .._internal import AadClient, AsyncContextManager from .._internal.decorators import log_get_token_async -from ..._internal import _TokenCache, validate_tenant_id +from ..._internal import validate_tenant_id +from ..._persistent_cache import _load_persistent_cache, TokenCachePersistenceOptions if TYPE_CHECKING: from typing import Any @@ -23,10 +26,9 @@ class ClientSecretCredential(AsyncContextManager): :keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com', the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.AzureAuthorityHosts` defines authorities for other clouds. - :keyword token_cache: token cache the credential should use. Defaults to an in memory - cache not shared with other credential instances. To enable persistent caching, provide a - :class:`~azure.identity.PersistentTokenCache` instance. - :paramtype token_cache: ~azure.identity.PersistentTokenCache + :keyword cache_persistence_options: configuration for persistent token caching. If unspecified, the credential + will cache tokens in memory. + :paramtype cache_persistence_options: ~azure.identity.TokenCachePersistenceOptions """ def __init__(self, tenant_id, client_id, client_secret, **kwargs): @@ -41,8 +43,15 @@ def __init__(self, tenant_id, client_id, client_secret, **kwargs): ) validate_tenant_id(tenant_id) - self._cache = kwargs.pop("token_cache", None) or _TokenCache() - self._client = AadClient(tenant_id, client_id, cache=self._cache._cache, **kwargs) + cache = kwargs.pop("_cache", None) + if not cache: + options = kwargs.pop("cache_persistence_options", None) + if options: + cache = _load_persistent_cache(options) + else: + cache = msal.TokenCache() + + self._client = AadClient(tenant_id, client_id, cache=cache, **kwargs) self._client_id = client_id self._secret = client_secret diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/shared_cache.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/shared_cache.py index c41f7b9ff946..30f50b937d44 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/shared_cache.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/shared_cache.py @@ -29,9 +29,9 @@ class SharedTokenCacheCredential(SharedTokenCacheBase, AsyncContextManager): defines authorities for other clouds. :keyword str tenant_id: an Azure Active Directory tenant ID. Used to select an account when the cache contains tokens for multiple identities. - :keyword token_cache: token cache the credential should use. Defaults to a persistent cache shared by Microsoft - development applications. - :paramtype token_cache: ~azure.identity.PersistentTokenCache + :keyword cache_persistence_options: configuration for persistent token caching. If not provided, the credential + will use the persistent cache shared by Microsoft development applications + :paramtype cache_persistence_options: ~azure.identity.TokenCachePersistenceOptions """ async def __aenter__(self): diff --git a/sdk/identity/azure-identity/samples/README.md b/sdk/identity/azure-identity/samples/README.md index f2f179504118..ab61f28e7cc6 100644 --- a/sdk/identity/azure-identity/samples/README.md +++ b/sdk/identity/azure-identity/samples/README.md @@ -35,5 +35,4 @@ pip install azure-identity azure-keyvault-secrets |-------------|-------------| | control_interactive_prompts.py | demonstrates controlling when interactive credentials prompt for user interaction | | custom_credentials.py | demonstrates custom credential implementation | -| token_cache_persistence.py | demonstrates configuring and customizing token cache persistence | user_authentication.py | demonstrates user authentication API for applications | diff --git a/sdk/identity/azure-identity/samples/token_cache_persistence.py b/sdk/identity/azure-identity/samples/token_cache_persistence.py deleted file mode 100644 index e5c3c2d18bce..000000000000 --- a/sdk/identity/azure-identity/samples/token_cache_persistence.py +++ /dev/null @@ -1,31 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -"""Demonstrates configuring token cache persistence. - -Many credential implementations in azure-identity have an underlying token cache holding sensitive authentication -data such as account information, access tokens, and refresh tokens. By default this is an in memory cache not shared -with other credential instances. Some applications need to share a token cache among credentials, and persist it across -executions. This file shows how to do this with the PeristentTokenCache class. -""" - -from azure.identity import DeviceCodeCredential, InteractiveBrowserCredential, PersistentTokenCache - -# PersistentTokenCache represents a persistent token cache managed by the Azure SDK. It defaults to -# the cache shared by Microsoft development applications, which SharedTokenCacheCredential also uses. -cache = PersistentTokenCache() -credential = InteractiveBrowserCredential(token_cache=cache) - -# Multiple credentials can share a PersistentTokenCache instance -device_code_credential = DeviceCodeCredential(token_cache=cache) - -# An application can isolate its authentication data from other applications by naming its cache -cache = PersistentTokenCache(name="my_application") -credential = InteractiveBrowserCredential(token_cache=cache) - -# By default, PersistentTokenCache encrypts its data with the current platform's user data protection -# APIs, and will raise an error when it isn't able to do so. Applications can configure it to instead -# fall back to storing data in clear text. This does not disable encryption. PersistentTokenCache will -# always attempt to encrypt its data. -cache = PersistentTokenCache(allow_unencrypted_storage=True) diff --git a/sdk/identity/azure-identity/samples/user_authentication.py b/sdk/identity/azure-identity/samples/user_authentication.py index 1a787f50e0dc..9a461c53a439 100644 --- a/sdk/identity/azure-identity/samples/user_authentication.py +++ b/sdk/identity/azure-identity/samples/user_authentication.py @@ -6,7 +6,7 @@ import os import sys -from azure.identity import AuthenticationRecord, InteractiveBrowserCredential, PersistentTokenCache +from azure.identity import AuthenticationRecord, InteractiveBrowserCredential, TokenCachePersistenceOptions from azure.keyvault.secrets import SecretClient @@ -18,10 +18,9 @@ # Persistent caching is optional. By default, interactive credentials cache in memory only. -# To enable persistent caching, create the credential with a PersistentTokenCache instance -# (see the token_cache_persistence.py sample for more details about PersistentTokenCache) -cache = PersistentTokenCache() -credential = InteractiveBrowserCredential(token_cache=cache) +# To enable persistent caching, give the credential an instance of TokenCachePersistenceOptions. +# (see the API documentation for more information about TokenCachePersistenceOptions) +credential = InteractiveBrowserCredential(cache_persistence_options=TokenCachePersistenceOptions()) # The 'authenticate' method begins interactive authentication. Call it whenever it's convenient # for your application to authenticate a user. It returns a record of the authentication. @@ -39,7 +38,9 @@ # past authentications. If the cache contains sufficient data, this eliminates the need for your # application to prompt for authentication every time it runs. deserialized_record = AuthenticationRecord.deserialize(record_json) -new_credential = InteractiveBrowserCredential(token_cache=cache, authentication_record=deserialized_record) +new_credential = InteractiveBrowserCredential( + cache_persistence_options=TokenCachePersistenceOptions(), authentication_record=deserialized_record +) # This request should also succeed without prompting for authentication. client = SecretClient(VAULT_URL, new_credential) diff --git a/sdk/identity/azure-identity/tests/test_cache_options_samples.py b/sdk/identity/azure-identity/tests/test_cache_options_samples.py new file mode 100644 index 000000000000..22582d002368 --- /dev/null +++ b/sdk/identity/azure-identity/tests/test_cache_options_samples.py @@ -0,0 +1,21 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from helpers import mock + +from azure.identity import InteractiveBrowserCredential, TokenCachePersistenceOptions + + +def test_token_cache_persistence_options(): + with mock.patch("azure.identity._persistent_cache.msal_extensions"): + # [START snippet] + cache_options = TokenCachePersistenceOptions() + credential = InteractiveBrowserCredential(cache_persistence_options=cache_options) + + # specify a cache name to isolate the cache from other applications + TokenCachePersistenceOptions(name="my_application") + + # configure the cache to fall back to unencrypted storage when encryption isn't available + TokenCachePersistenceOptions(allow_unencrypted_storage=True) + # [END snippet] diff --git a/sdk/identity/azure-identity/tests/test_certificate_credential.py b/sdk/identity/azure-identity/tests/test_certificate_credential.py index 5206078e6bca..cb39bd880491 100644 --- a/sdk/identity/azure-identity/tests/test_certificate_credential.py +++ b/sdk/identity/azure-identity/tests/test_certificate_credential.py @@ -6,14 +6,14 @@ import os from azure.core.pipeline.policies import ContentDecodePolicy, SansIOHTTPPolicy -from azure.identity import CertificateCredential +from azure.identity import CertificateCredential, TokenCachePersistenceOptions from azure.identity._constants import EnvironmentVariables -from azure.identity._internal import _TokenCache from azure.identity._internal.user_agent import USER_AGENT from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding +from msal import TokenCache import pytest import six from six.moves.urllib_parse import urlparse @@ -238,16 +238,21 @@ def validate_jwt(request, client_id, pem_bytes, expect_x5c=False): @pytest.mark.parametrize("cert_path,cert_password", BOTH_CERTS) def test_token_cache(cert_path, cert_password): - """the credential should use the cache it's given, and default to an in memory cache otherwise""" + """the credential should optionally use a persistent cache, and default to an in memory cache""" - credential = CertificateCredential("tenant", "client-id", cert_path, password=cert_password) - assert isinstance(credential._cache, _TokenCache) + with patch("azure.identity._persistent_cache.msal_extensions") as mock_msal_extensions: + credential = CertificateCredential("tenant", "client-id", cert_path, password=cert_password) + assert not mock_msal_extensions.PersistedTokenCache.called + assert isinstance(credential._cache, TokenCache) - expected_cache = _TokenCache() - credential = CertificateCredential( - "tenant", "client-id", cert_path, password=cert_password, token_cache=expected_cache - ) - assert credential._cache is expected_cache + CertificateCredential( + "tenant", + "client-id", + cert_path, + password=cert_password, + cache_persistence_options=TokenCachePersistenceOptions(), + ) + assert mock_msal_extensions.PersistedTokenCache.call_count == 1 @pytest.mark.parametrize("cert_path,cert_password", BOTH_CERTS) @@ -263,12 +268,12 @@ def test_cache_multiple_clients(cert_path, cert_password): requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_b))] ) - cache = _TokenCache() + cache = TokenCache() credential_a = CertificateCredential( - "tenant", "client-a", cert_path, password=cert_password, transport=transport_a, token_cache=cache + "tenant", "client-a", cert_path, password=cert_password, transport=transport_a, _cache=cache ) credential_b = CertificateCredential( - "tenant", "client-b", cert_path, password=cert_password, transport=transport_b, token_cache=cache + "tenant", "client-b", cert_path, password=cert_password, transport=transport_b, _cache=cache ) # A caches a token @@ -281,3 +286,5 @@ def test_cache_multiple_clients(cert_path, cert_password): token_b = credential_b.get_token(scope) assert token_b.token == access_token_b assert transport_b.send.call_count == 3 + + assert len(cache.find(TokenCache.CredentialType.ACCESS_TOKEN)) == 2 diff --git a/sdk/identity/azure-identity/tests/test_certificate_credential_async.py b/sdk/identity/azure-identity/tests/test_certificate_credential_async.py index 063771176970..69dda6c045ff 100644 --- a/sdk/identity/azure-identity/tests/test_certificate_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_certificate_credential_async.py @@ -6,11 +6,12 @@ from urllib.parse import urlparse from azure.core.pipeline.policies import ContentDecodePolicy, SansIOHTTPPolicy +from azure.identity import TokenCachePersistenceOptions from azure.identity._constants import EnvironmentVariables -from azure.identity._internal import _TokenCache from azure.identity._internal.user_agent import USER_AGENT from azure.identity.aio import CertificateCredential +from msal import TokenCache import pytest from helpers import build_aad_response, urlsafeb64_decode, mock_response, Request @@ -186,6 +187,26 @@ async def mock_send(request, **kwargs): assert token.token == access_token +@pytest.mark.parametrize("cert_path,cert_password", BOTH_CERTS) +def test_token_cache(cert_path, cert_password): + """the credential should optionally use a persistent cache, and default to an in memory cache""" + + with patch("azure.identity._persistent_cache.msal_extensions") as mock_msal_extensions: + with patch(CertificateCredential.__module__ + ".msal") as mock_msal: + CertificateCredential("tenant", "client-id", cert_path, password=cert_password) + assert mock_msal.TokenCache.call_count == 1 + assert not mock_msal_extensions.PersistedTokenCache.called + + CertificateCredential( + "tenant", + "client-id", + cert_path, + password=cert_password, + cache_persistence_options=TokenCachePersistenceOptions(), + ) + assert mock_msal_extensions.PersistedTokenCache.call_count == 1 + + @pytest.mark.asyncio @pytest.mark.parametrize("cert_path,cert_password", BOTH_CERTS) async def test_cache_multiple_clients(cert_path, cert_password): @@ -200,12 +221,12 @@ async def test_cache_multiple_clients(cert_path, cert_password): requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_b))] ) - cache = _TokenCache() + cache = TokenCache() credential_a = CertificateCredential( - "tenant", "client-a", cert_path, password=cert_password, transport=transport_a, token_cache=cache + "tenant", "client-a", cert_path, password=cert_password, transport=transport_a, _cache=cache ) credential_b = CertificateCredential( - "tenant", "client-b", cert_path, password=cert_password, transport=transport_b, token_cache=cache + "tenant", "client-b", cert_path, password=cert_password, transport=transport_b, _cache=cache ) # A caches a token @@ -218,3 +239,5 @@ async def test_cache_multiple_clients(cert_path, cert_password): token_b = await credential_b.get_token(scope) assert token_b.token == access_token_b assert transport_b.send.call_count == 1 + + assert len(cache.find(TokenCache.CredentialType.ACCESS_TOKEN)) == 2 diff --git a/sdk/identity/azure-identity/tests/test_client_secret_credential.py b/sdk/identity/azure-identity/tests/test_client_secret_credential.py index 9ba5041953d5..7fea1949340e 100644 --- a/sdk/identity/azure-identity/tests/test_client_secret_credential.py +++ b/sdk/identity/azure-identity/tests/test_client_secret_credential.py @@ -3,10 +3,10 @@ # Licensed under the MIT License. # ------------------------------------ from azure.core.pipeline.policies import ContentDecodePolicy, SansIOHTTPPolicy -from azure.identity import ClientSecretCredential +from azure.identity import ClientSecretCredential, TokenCachePersistenceOptions from azure.identity._constants import EnvironmentVariables -from azure.identity._internal import _TokenCache from azure.identity._internal.user_agent import USER_AGENT +from msal import TokenCache import pytest from six.moves.urllib_parse import urlparse @@ -118,11 +118,44 @@ def test_authority(authority): def test_token_cache(): - """the credential should use the cache it's given, and default to an in memory cache otherwise""" + """the credential should default to an in memory cache, and optionally use a persistent cache""" - credential = ClientSecretCredential("tenant", "client-id", "secret") - assert isinstance(credential._cache, _TokenCache) + with patch("azure.identity._persistent_cache.msal_extensions") as mock_msal_extensions: + credential = ClientSecretCredential("tenant", "client-id", "secret") + assert not mock_msal_extensions.PersistedTokenCache.called + assert isinstance(credential._cache, TokenCache) - expected_cache = _TokenCache() - credential = ClientSecretCredential("tenant", "client-id", "secret", token_cache=expected_cache) - assert credential._cache is expected_cache + ClientSecretCredential( + "tenant", "client-id", "secret", cache_persistence_options=TokenCachePersistenceOptions(), + ) + assert mock_msal_extensions.PersistedTokenCache.call_count == 1 + + +def test_cache_multiple_clients(): + """the credential shouldn't use tokens issued to other service principals""" + + access_token_a = "token a" + access_token_b = "not " + access_token_a + transport_a = msal_validating_transport( + requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))] + ) + transport_b = msal_validating_transport( + requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_b))] + ) + + cache = TokenCache() + credential_a = ClientSecretCredential("tenant", "client-a", "secret", transport=transport_a, _cache=cache) + credential_b = ClientSecretCredential("tenant", "client-b", "secret", transport=transport_b, _cache=cache) + + # A caches a token + scope = "scope" + token_a = credential_a.get_token(scope) + assert token_a.token == access_token_a + assert transport_a.send.call_count == 3 # two MSAL discovery requests, one token request + + # B should get a different token for the same scope + token_b = credential_b.get_token(scope) + assert token_b.token == access_token_b + assert transport_b.send.call_count == 3 + + assert len(cache.find(TokenCache.CredentialType.ACCESS_TOKEN)) == 2 diff --git a/sdk/identity/azure-identity/tests/test_client_secret_credential_async.py b/sdk/identity/azure-identity/tests/test_client_secret_credential_async.py index e6e3cd265bd7..c449ff70187d 100644 --- a/sdk/identity/azure-identity/tests/test_client_secret_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_client_secret_credential_async.py @@ -8,10 +8,11 @@ from azure.core.credentials import AccessToken from azure.core.pipeline.policies import ContentDecodePolicy, SansIOHTTPPolicy +from azure.identity import TokenCachePersistenceOptions from azure.identity._constants import EnvironmentVariables -from azure.identity._internal import _TokenCache from azure.identity._internal.user_agent import USER_AGENT from azure.identity.aio import ClientSecretCredential +from msal import TokenCache import pytest from helpers import build_aad_response, mock_response, Request @@ -186,11 +187,46 @@ async def test_cache(): def test_token_cache(): - """the credential should use the cache it's given, and default to an in memory cache otherwise""" + """the credential should default to an in memory cache, and optionally use a persistent cache""" - credential = ClientSecretCredential("tenant", "client-id", "secret") - assert isinstance(credential._cache, _TokenCache) + with patch("azure.identity._persistent_cache.msal_extensions") as mock_msal_extensions: + with patch(ClientSecretCredential.__module__ + ".msal") as mock_msal: + ClientSecretCredential("tenant", "client-id", "secret") + assert mock_msal.TokenCache.call_count == 1 + assert not mock_msal_extensions.PersistedTokenCache.called - expected_cache = _TokenCache() - credential = ClientSecretCredential("tenant", "client-id", "secret", token_cache=expected_cache) - assert credential._cache is expected_cache + ClientSecretCredential( + "tenant", "client-id", "secret", cache_persistence_options=TokenCachePersistenceOptions(), + ) + assert mock_msal_extensions.PersistedTokenCache.call_count == 1 + + +@pytest.mark.asyncio +async def test_cache_multiple_clients(): + """the credential shouldn't use tokens issued to other service principals""" + + access_token_a = "token a" + access_token_b = "not " + access_token_a + transport_a = async_validating_transport( + requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))] + ) + transport_b = async_validating_transport( + requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_b))] + ) + + cache = TokenCache() + credential_a = ClientSecretCredential("tenant", "client-a", "secret", transport=transport_a, _cache=cache) + credential_b = ClientSecretCredential("tenant", "client-b", "secret", transport=transport_b, _cache=cache) + + # A caches a token + scope = "scope" + token_a = await credential_a.get_token(scope) + assert token_a.token == access_token_a + assert transport_a.send.call_count == 1 + + # B should get a different token for the same scope + token_b = await credential_b.get_token(scope) + assert token_b.token == access_token_b + assert transport_b.send.call_count == 1 + + assert len(cache.find(TokenCache.CredentialType.ACCESS_TOKEN)) == 2 diff --git a/sdk/identity/azure-identity/tests/test_default.py b/sdk/identity/azure-identity/tests/test_default.py index d3cf51eec5b1..2f8564da1957 100644 --- a/sdk/identity/azure-identity/tests/test_default.py +++ b/sdk/identity/azure-identity/tests/test_default.py @@ -302,7 +302,7 @@ def get_credential_for_shared_cache_test(expected_refresh_token, expected_access # this credential uses a mock shared cache, so it works on all platforms with patch.object(SharedTokenCacheCredential, "supported"): - return DefaultAzureCredential(token_cache=cache, transport=transport, **options) + return DefaultAzureCredential(_cache=cache, transport=transport, **options) def test_interactive_browser_tenant_id(): diff --git a/sdk/identity/azure-identity/tests/test_default_async.py b/sdk/identity/azure-identity/tests/test_default_async.py index be9cb5a9d8b0..4e74e55e745a 100644 --- a/sdk/identity/azure-identity/tests/test_default_async.py +++ b/sdk/identity/azure-identity/tests/test_default_async.py @@ -289,4 +289,4 @@ def get_credential_for_shared_cache_test(expected_refresh_token, expected_access # this credential uses a mock shared cache, so it works on all platforms with patch.object(SharedTokenCacheCredential, "supported", lambda: True): - return DefaultAzureCredential(token_cache=cache, transport=transport, **exclude_other_credentials, **kwargs) + return DefaultAzureCredential(_cache=cache, transport=transport, **exclude_other_credentials, **kwargs) diff --git a/sdk/identity/azure-identity/tests/test_interactive_credential.py b/sdk/identity/azure-identity/tests/test_interactive_credential.py index 27da20ce0527..253577976a49 100644 --- a/sdk/identity/azure-identity/tests/test_interactive_credential.py +++ b/sdk/identity/azure-identity/tests/test_interactive_credential.py @@ -8,8 +8,9 @@ AuthenticationRecord, KnownAuthorities, CredentialUnavailableError, + TokenCachePersistenceOptions, ) -from azure.identity._internal import _TokenCache, InteractiveCredential +from azure.identity._internal import InteractiveCredential import pytest try: @@ -215,7 +216,7 @@ class CustomException(Exception): def test_token_cache(): - """the credential should use the cache it's given, and default to an in memory cache otherwise""" + """the credential should default to an in memory cache, and optionally use a persistent cache""" class TestCredential(InteractiveCredential): def __init__(self, **kwargs): @@ -224,12 +225,14 @@ def __init__(self, **kwargs): def _request_token(self, *_, **__): pass - credential = TestCredential() - assert isinstance(credential._cache, _TokenCache) + with patch("azure.identity._persistent_cache.msal_extensions") as mock_msal_extensions: + with patch("azure.identity._internal.msal_credentials.msal") as mock_msal: + TestCredential() + assert not mock_msal_extensions.PersistedTokenCache.called + assert mock_msal.TokenCache.call_count == 1 - expected_cache = _TokenCache() - credential = TestCredential(token_cache=expected_cache) - assert credential._cache is expected_cache + TestCredential(cache_persistence_options=TokenCachePersistenceOptions()) + assert mock_msal_extensions.PersistedTokenCache.call_count == 1 def test_home_account_id_client_info(): diff --git a/sdk/identity/azure-identity/tests/test_shared_cache_credential.py b/sdk/identity/azure-identity/tests/test_shared_cache_credential.py index 0916f432e30d..9ee2dcf1327f 100644 --- a/sdk/identity/azure-identity/tests/test_shared_cache_credential.py +++ b/sdk/identity/azure-identity/tests/test_shared_cache_credential.py @@ -17,9 +17,9 @@ NO_ACCOUNTS, NO_MATCHING_ACCOUNTS, ) -from azure.identity._internal import _TokenCache, get_default_authority +from azure.identity._internal import get_default_authority from azure.identity._internal.user_agent import USER_AGENT -import msal +from msal import TokenCache import pytest from six.moves.urllib_parse import urlparse @@ -65,7 +65,7 @@ def test_supported(): def test_no_scopes(): """The credential should raise when get_token is called with no scopes""" - credential = SharedTokenCacheCredential(token_cache=_TokenCache()) + credential = SharedTokenCacheCredential(_cache=TokenCache()) with pytest.raises(ValueError): credential.get_token() @@ -77,7 +77,7 @@ def send(*_, **__): return mock_response(json_payload=build_aad_response(access_token="**")) credential = SharedTokenCacheCredential( - token_cache=populated_cache(get_account_event("test@user", "uid", "utid")), + _cache=populated_cache(get_account_event("test@user", "uid", "utid")), policies=[policy], transport=Mock(send=send), ) @@ -94,7 +94,7 @@ def test_user_agent(): ) credential = SharedTokenCacheCredential( - token_cache=populated_cache(get_account_event("test@user", "uid", "utid")), transport=transport + _cache=populated_cache(get_account_event("test@user", "uid", "utid")), transport=transport ) credential.get_token("scope") @@ -114,23 +114,23 @@ def _get_auth_client(self, authority=None, **kwargs): assert actual.netloc == expected_netloc transport = Mock(send=Mock(side_effect=Exception("credential shouldn't send a request"))) - MockCredential(token_cache=_TokenCache(), authority=authority, transport=transport) + MockCredential(_cache=TokenCache(), authority=authority, transport=transport) with patch.dict("os.environ", {EnvironmentVariables.AZURE_AUTHORITY_HOST: authority}, clear=True): - MockCredential(token_cache=_TokenCache(), authority=authority, transport=transport) + MockCredential(_cache=TokenCache(), authority=authority, transport=transport) def test_empty_cache(): """the credential should raise CredentialUnavailableError when the cache is empty""" with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): - SharedTokenCacheCredential(token_cache=_TokenCache()).get_token("scope") + SharedTokenCacheCredential(_cache=TokenCache()).get_token("scope") with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): - SharedTokenCacheCredential(token_cache=_TokenCache(), username="not@cache").get_token("scope") + SharedTokenCacheCredential(_cache=TokenCache(), username="not@cache").get_token("scope") with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): - SharedTokenCacheCredential(token_cache=_TokenCache(), tenant_id="not-cached").get_token("scope") + SharedTokenCacheCredential(_cache=TokenCache(), tenant_id="not-cached").get_token("scope") with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): - SharedTokenCacheCredential(token_cache=_TokenCache(), tenant_id="not-cached", username="not@cache").get_token("scope") + SharedTokenCacheCredential(_cache=TokenCache(), tenant_id="not-cached", username="not@cache").get_token("scope") def test_no_matching_account_for_username(): @@ -142,7 +142,7 @@ def test_no_matching_account_for_username(): cache = populated_cache(account) with pytest.raises(CredentialUnavailableError) as ex: - SharedTokenCacheCredential(token_cache=cache, username="not" + upn).get_token("scope") + SharedTokenCacheCredential(_cache=cache, username="not" + upn).get_token("scope") assert ex.value.message.startswith(NO_MATCHING_ACCOUNTS[: NO_MATCHING_ACCOUNTS.index("{")]) assert "not" + upn in ex.value.message @@ -157,7 +157,7 @@ def test_no_matching_account_for_tenant(): cache = populated_cache(account) with pytest.raises(CredentialUnavailableError) as ex: - SharedTokenCacheCredential(token_cache=cache, tenant_id="not-" + tenant).get_token("scope") + SharedTokenCacheCredential(_cache=cache, tenant_id="not-" + tenant).get_token("scope") assert ex.value.message.startswith(NO_MATCHING_ACCOUNTS[: NO_MATCHING_ACCOUNTS.index("{")]) assert "not-" + tenant in ex.value.message @@ -172,7 +172,7 @@ def test_no_matching_account_for_tenant_and_username(): cache = populated_cache(account) with pytest.raises(CredentialUnavailableError) as ex: - SharedTokenCacheCredential(token_cache=cache, tenant_id="not-" + tenant, username="not" + upn).get_token("scope") + SharedTokenCacheCredential(_cache=cache, tenant_id="not-" + tenant, username="not" + upn).get_token("scope") assert ex.value.message.startswith(NO_MATCHING_ACCOUNTS[: NO_MATCHING_ACCOUNTS.index("{")]) assert "not" + upn in ex.value.message and "not-" + tenant in ex.value.message @@ -193,13 +193,13 @@ def test_no_matching_account_for_tenant_or_username(): transport = Mock(side_effect=Exception()) # credential shouldn't use the network - credential = SharedTokenCacheCredential(username=upn_a, tenant_id=tenant_b, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn_a, tenant_id=tenant_b, _cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError) as ex: credential.get_token("scope") assert ex.value.message.startswith(NO_MATCHING_ACCOUNTS[: NO_MATCHING_ACCOUNTS.index("{")]) assert upn_a in ex.value.message and tenant_b in ex.value.message - credential = SharedTokenCacheCredential(username=upn_b, tenant_id=tenant_a, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn_b, tenant_id=tenant_a, _cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError) as ex: credential.get_token("scope") assert ex.value.message.startswith(NO_MATCHING_ACCOUNTS[: NO_MATCHING_ACCOUNTS.index("{")]) @@ -220,7 +220,7 @@ def test_single_account_matching_username(): requests=[Request(required_data={"refresh_token": refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(token_cache=cache, transport=transport, username=upn) + credential = SharedTokenCacheCredential(_cache=cache, transport=transport, username=upn) token = credential.get_token(scope) assert token.token == expected_token @@ -239,7 +239,7 @@ def test_single_account_matching_tenant(): requests=[Request(required_data={"refresh_token": refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(token_cache=cache, transport=transport, tenant_id=tenant_id) + credential = SharedTokenCacheCredential(_cache=cache, transport=transport, tenant_id=tenant_id) token = credential.get_token(scope) assert token.token == expected_token @@ -259,7 +259,7 @@ def test_single_account_matching_tenant_and_username(): requests=[Request(required_data={"refresh_token": refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(token_cache=cache, transport=transport, tenant_id=tenant_id, username=upn) + credential = SharedTokenCacheCredential(_cache=cache, transport=transport, tenant_id=tenant_id, username=upn) token = credential.get_token(scope) assert token.token == expected_token @@ -277,7 +277,7 @@ def test_single_account(): requests=[Request(required_data={"refresh_token": refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(_cache=cache, transport=transport) token = credential.get_token(scope) assert token.token == expected_token @@ -291,11 +291,11 @@ def test_no_refresh_token(): transport = Mock(side_effect=Exception()) # credential shouldn't use the network - credential = SharedTokenCacheCredential(token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(_cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): credential.get_token("scope") - credential = SharedTokenCacheCredential(token_cache=cache, transport=transport, username="not@cache") + credential = SharedTokenCacheCredential(_cache=cache, transport=transport, username="not@cache") with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): credential.get_token("scope") @@ -313,7 +313,7 @@ def test_two_accounts_no_username_or_tenant(): transport = Mock(side_effect=Exception()) # two users in the cache, no username specified -> CredentialUnavailableError - credential = SharedTokenCacheCredential(token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(_cache=cache, transport=transport) with pytest.raises(ClientAuthenticationError, match=MULTIPLE_ACCOUNTS) as ex: credential.get_token("scope") @@ -334,7 +334,7 @@ def test_two_accounts_username_specified(): requests=[Request(required_data={"refresh_token": expected_refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(username=upn_a, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn_a, _cache=cache, transport=transport) token = credential.get_token(scope) assert token.token == expected_token @@ -356,7 +356,7 @@ def test_two_accounts_tenant_specified(): requests=[Request(required_data={"refresh_token": expected_refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(tenant_id=tenant_id, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(tenant_id=tenant_id, _cache=cache, transport=transport) token = credential.get_token(scope) assert token.token == expected_token @@ -378,7 +378,7 @@ def test_two_accounts_tenant_and_username_specified(): requests=[Request(required_data={"refresh_token": expected_refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(tenant_id=tenant_id, username=upn_a, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(tenant_id=tenant_id, username=upn_a, _cache=cache, transport=transport) token = credential.get_token(scope) assert token.token == expected_token @@ -400,7 +400,7 @@ def test_same_username_different_tenants(): # with no tenant specified the credential can't select an identity transport = Mock(side_effect=Exception()) # (so it shouldn't use the network) - credential = SharedTokenCacheCredential(username=upn, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn, _cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError) as ex: credential.get_token("scope") @@ -413,7 +413,7 @@ def test_same_username_different_tenants(): requests=[Request(required_data={"refresh_token": refresh_token_a, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))], ) - credential = SharedTokenCacheCredential(tenant_id=tenant_a, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(tenant_id=tenant_a, _cache=cache, transport=transport) token = credential.get_token(scope) assert token.token == access_token_a @@ -421,7 +421,7 @@ def test_same_username_different_tenants(): requests=[Request(required_data={"refresh_token": refresh_token_b, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_b))], ) - credential = SharedTokenCacheCredential(tenant_id=tenant_b, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(tenant_id=tenant_b, _cache=cache, transport=transport) token = credential.get_token(scope) assert token.token == access_token_b @@ -443,7 +443,7 @@ def test_same_tenant_different_usernames(): # with no username specified the credential can't select an identity transport = Mock(side_effect=Exception()) # (so it shouldn't use the network) - credential = SharedTokenCacheCredential(tenant_id=tenant_id, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(tenant_id=tenant_id, _cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError) as ex: credential.get_token("scope") @@ -456,7 +456,7 @@ def test_same_tenant_different_usernames(): requests=[Request(required_data={"refresh_token": refresh_token_b, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))], ) - credential = SharedTokenCacheCredential(username=upn_b, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn_b, _cache=cache, transport=transport) token = credential.get_token(scope) assert token.token == access_token_a @@ -464,7 +464,7 @@ def test_same_tenant_different_usernames(): requests=[Request(required_data={"refresh_token": refresh_token_a, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))], ) - credential = SharedTokenCacheCredential(username=upn_a, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn_a, _cache=cache, transport=transport) token = credential.get_token(scope) assert token.token == access_token_a @@ -487,7 +487,7 @@ def test_authority_aliases(): requests=[Request(authority=authority, required_data={"refresh_token": expected_refresh_token})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) - credential = SharedTokenCacheCredential(authority=authority, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(authority=authority, _cache=cache, transport=transport) token = credential.get_token("scope") assert token.token == expected_access_token @@ -497,7 +497,7 @@ def test_authority_aliases(): requests=[Request(authority=alias, required_data={"refresh_token": expected_refresh_token})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) - credential = SharedTokenCacheCredential(authority=alias, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(authority=alias, _cache=cache, transport=transport) token = credential.get_token("scope") assert token.token == expected_access_token @@ -514,7 +514,7 @@ def test_authority_with_no_known_alias(): requests=[Request(authority=authority, required_data={"refresh_token": expected_refresh_token})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) - credential = SharedTokenCacheCredential(authority=authority, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(authority=authority, _cache=cache, transport=transport) token = credential.get_token("scope") assert token.token == expected_access_token @@ -532,7 +532,7 @@ def test_authority_environment_variable(): responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) with patch.dict("os.environ", {EnvironmentVariables.AZURE_AUTHORITY_HOST: authority}, clear=True): - credential = SharedTokenCacheCredential(transport=transport, token_cache=cache) + credential = SharedTokenCacheCredential(transport=transport, _cache=cache) token = credential.get_token("scope") assert token.token == expected_access_token @@ -546,7 +546,7 @@ def send(request, **_): return get_discovery_response() credential = SharedTokenCacheCredential( - authentication_record=record, transport=Mock(send=send), token_cache=_TokenCache() + authentication_record=record, transport=Mock(send=send), _cache=TokenCache() ) with pytest.raises(CredentialUnavailableError): @@ -572,7 +572,7 @@ def send(request, **_): "not-" + username, "not-" + object_id, "different-" + tenant_id, client_id="not-" + client_id, ), ) - credential = SharedTokenCacheCredential(authentication_record=record, transport=Mock(send=send), token_cache=cache) + credential = SharedTokenCacheCredential(authentication_record=record, transport=Mock(send=send), _cache=cache) with pytest.raises(CredentialUnavailableError): credential.get_token("scope") @@ -599,7 +599,7 @@ def test_authentication_record(): requests=[Request(authority=authority, required_data={"refresh_token": expected_refresh_token})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) - credential = SharedTokenCacheCredential(authentication_record=record, transport=transport, token_cache=cache) + credential = SharedTokenCacheCredential(authentication_record=record, transport=transport, _cache=cache) token = credential.get_token("scope") assert token.token == expected_access_token @@ -636,7 +636,7 @@ def test_auth_record_multiple_accounts_for_username(): requests=[Request(authority=authority, required_data={"refresh_token": expected_refresh_token})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) - credential = SharedTokenCacheCredential(authentication_record=record, transport=transport, token_cache=cache) + credential = SharedTokenCacheCredential(authentication_record=record, transport=transport, _cache=cache) token = credential.get_token("scope") assert token.token == expected_access_token @@ -654,8 +654,8 @@ def test_writes_to_cache(): uid = "uid" utid = "utid" account = get_account_event(username=username, uid=uid, utid=utid, refresh_token=first_refresh_token) - cache = _TokenCache() - cache._cache.add(account) + cache = TokenCache() + cache.add(account) transport = validating_transport( requests=[Request(required_data={"refresh_token": first_refresh_token})], # credential redeems refresh token @@ -673,13 +673,13 @@ def test_writes_to_cache(): ) ], ) - credential = SharedTokenCacheCredential(token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(_cache=cache, transport=transport) token = credential.get_token(scope) assert token.token == expected_access_token # access token should be in the cache, and another instance should retrieve it credential = SharedTokenCacheCredential( - token_cache=cache, transport=Mock(send=Mock(side_effect=Exception("the credential should return a cached token"))) + _cache=cache, transport=Mock(send=Mock(side_effect=Exception("the credential should return a cached token"))) ) token = credential.get_token(scope) assert token.token == expected_access_token @@ -690,25 +690,25 @@ def test_writes_to_cache(): requests=[Request(required_data={"refresh_token": second_refresh_token})], responses=[mock_response(json_payload=build_aad_response(access_token=second_access_token))], ) - credential = SharedTokenCacheCredential(token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(_cache=cache, transport=transport) token = credential.get_token("some other " + scope) assert token.token == second_access_token # verify the credential didn't add a new cache entry - assert len(cache._cache.find(msal.TokenCache.CredentialType.REFRESH_TOKEN)) == 1 + assert len(cache.find(TokenCache.CredentialType.REFRESH_TOKEN)) == 1 def test_initialization(): """the credential should attempt to load the cache only once, when it's first needed""" - with patch("azure.identity._persistent_cache._get_persistence") as mock_cache_loader: + with patch("azure.identity._internal.shared_token_cache._load_persistent_cache") as mock_cache_loader: mock_cache_loader.side_effect = Exception("it didn't work") credential = SharedTokenCacheCredential() assert mock_cache_loader.call_count == 0 for _ in range(2): - with pytest.raises(CredentialUnavailableError): + with pytest.raises(CredentialUnavailableError, match="Shared token cache unavailable"): credential.get_token("scope") assert mock_cache_loader.call_count == 1 @@ -727,7 +727,7 @@ def mock_send(request, **_): transport = Mock(send=Mock(wraps=mock_send)) credential = SharedTokenCacheCredential( - authentication_record=record, token_cache=_TokenCache(), tenant_id=expected_tenant_id, transport=transport + authentication_record=record, _cache=TokenCache(), tenant_id=expected_tenant_id, transport=transport ) with pytest.raises(CredentialUnavailableError): credential.get_token("scope") # this raises because the cache is empty @@ -741,7 +741,7 @@ def test_client_capabilities(): record = AuthenticationRecord("tenant-id", "client_id", "authority", "home_account_id", "username") transport = Mock(send=Mock(side_effect=Exception("this test mocks MSAL, so no request should be sent"))) credential = SharedTokenCacheCredential( - transport=transport, authentication_record=record, token_cache=_TokenCache() + transport=transport, authentication_record=record, _cache=TokenCache() ) with patch(SharedTokenCacheCredential.__module__ + ".PublicClientApplication") as PublicClientApplication: @@ -767,7 +767,7 @@ def test_claims_challenge(): transport = Mock(send=Mock(side_effect=Exception("this test mocks MSAL, so no request should be sent"))) credential = SharedTokenCacheCredential( - transport=transport, authentication_record=record, token_cache=_TokenCache() + transport=transport, authentication_record=record, _cache=TokenCache() ) with patch(SharedTokenCacheCredential.__module__ + ".PublicClientApplication", lambda *_, **__: msal_app): credential.get_token("scope", claims=expected_claims) @@ -801,8 +801,8 @@ def get_account_event( def populated_cache(*accounts): - cache = _TokenCache() + cache = TokenCache() for account in accounts: - cache._cache.add(account) - cache._cache.add = lambda *_, **__: None # prevent anything being added to the cache + cache.add(account) + cache.add = lambda *_, **__: None # prevent anything being added to the cache return cache diff --git a/sdk/identity/azure-identity/tests/test_shared_cache_credential_async.py b/sdk/identity/azure-identity/tests/test_shared_cache_credential_async.py index b17712d4d642..ec4ca4c29153 100644 --- a/sdk/identity/azure-identity/tests/test_shared_cache_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_shared_cache_credential_async.py @@ -10,7 +10,6 @@ from azure.identity import CredentialUnavailableError from azure.identity.aio import SharedTokenCacheCredential from azure.identity._constants import EnvironmentVariables -from azure.identity._internal import _TokenCache from azure.identity._internal.shared_token_cache import ( KNOWN_ALIASES, MULTIPLE_ACCOUNTS, @@ -19,6 +18,7 @@ NO_MATCHING_ACCOUNTS, ) from azure.identity._internal.user_agent import USER_AGENT +from msal import TokenCache import pytest from helpers import build_aad_response, build_id_token, mock_response, Request @@ -35,7 +35,7 @@ def test_supported(): async def test_no_scopes(): """The credential should raise when get_token is called with no scopes""" - credential = SharedTokenCacheCredential(token_cache=_TokenCache()) + credential = SharedTokenCacheCredential(_cache=TokenCache()) with pytest.raises(ValueError): await credential.get_token() @@ -47,7 +47,7 @@ async def send(*_, **__): transport = AsyncMockTransport(send=send) credential = SharedTokenCacheCredential( - token_cache=populated_cache(get_account_event("test@user", "uid", "utid")), transport=transport + _cache=populated_cache(get_account_event("test@user", "uid", "utid")), transport=transport ) # the credential doesn't open a transport session before one is needed, so we send a request @@ -65,7 +65,7 @@ async def send(*_, **__): transport = AsyncMockTransport(send=send) credential = SharedTokenCacheCredential( - token_cache=populated_cache(get_account_event("test@user", "uid", "utid")), transport=transport + _cache=populated_cache(get_account_event("test@user", "uid", "utid")), transport=transport ) # async with before initialization: credential should call aexit but not aenter @@ -88,7 +88,7 @@ async def test_context_manager_no_cache(): transport = AsyncMockTransport() - with patch("azure.identity._internal.shared_token_cache.PersistentTokenCache", Mock(side_effect=NotImplementedError)): + with patch("azure.identity._persistent_cache._load_persistent_cache", Mock(side_effect=NotImplementedError)): credential = SharedTokenCacheCredential(transport=transport) async with credential: @@ -106,7 +106,7 @@ async def send(*_, **__): return mock_response(json_payload=build_aad_response(access_token="**")) credential = SharedTokenCacheCredential( - token_cache=populated_cache(get_account_event("test@user", "uid", "utid")), + _cache=populated_cache(get_account_event("test@user", "uid", "utid")), policies=[policy], transport=Mock(send=send), ) @@ -124,7 +124,7 @@ async def test_user_agent(): ) credential = SharedTokenCacheCredential( - token_cache=populated_cache(get_account_event("test@user", "uid", "utid")), transport=transport + _cache=populated_cache(get_account_event("test@user", "uid", "utid")), transport=transport ) await credential.get_token("scope") @@ -144,10 +144,10 @@ def _get_auth_client(self, authority=None, **kwargs): assert actual.netloc == expected_netloc transport = Mock(send=Mock(side_effect=Exception("credential shouldn't send a request"))) - MockCredential(token_cache=_TokenCache(), authority=authority, transport=transport) + MockCredential(_cache=TokenCache(), authority=authority, transport=transport) with patch.dict("os.environ", {EnvironmentVariables.AZURE_AUTHORITY_HOST: authority}, clear=True): - MockCredential(token_cache=_TokenCache(), authority=authority, transport=transport) + MockCredential(_cache=TokenCache(), authority=authority, transport=transport) @pytest.mark.asyncio @@ -155,13 +155,13 @@ async def test_empty_cache(): """the credential should raise CredentialUnavailableError when the cache is empty""" with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): - await SharedTokenCacheCredential(token_cache=_TokenCache()).get_token("scope") + await SharedTokenCacheCredential(_cache=TokenCache()).get_token("scope") with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): - await SharedTokenCacheCredential(token_cache=_TokenCache(), username="not@cache").get_token("scope") + await SharedTokenCacheCredential(_cache=TokenCache(), username="not@cache").get_token("scope") with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): - await SharedTokenCacheCredential(token_cache=_TokenCache(), tenant_id="not-cached").get_token("scope") + await SharedTokenCacheCredential(_cache=TokenCache(), tenant_id="not-cached").get_token("scope") with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): - credential = SharedTokenCacheCredential(token_cache=_TokenCache(), tenant_id="not-cached", username="not@cache") + credential = SharedTokenCacheCredential(_cache=TokenCache(), tenant_id="not-cached", username="not@cache") await credential.get_token("scope") @@ -175,7 +175,7 @@ async def test_no_matching_account_for_username(): cache = populated_cache(account) with pytest.raises(CredentialUnavailableError) as ex: - await SharedTokenCacheCredential(token_cache=cache, username="not" + upn).get_token("scope") + await SharedTokenCacheCredential(_cache=cache, username="not" + upn).get_token("scope") assert ex.value.message.startswith(NO_MATCHING_ACCOUNTS[: NO_MATCHING_ACCOUNTS.index("{")]) assert "not" + upn in ex.value.message @@ -191,7 +191,7 @@ async def test_no_matching_account_for_tenant(): cache = populated_cache(account) with pytest.raises(CredentialUnavailableError) as ex: - await SharedTokenCacheCredential(token_cache=cache, tenant_id="not-" + tenant).get_token("scope") + await SharedTokenCacheCredential(_cache=cache, tenant_id="not-" + tenant).get_token("scope") assert ex.value.message.startswith(NO_MATCHING_ACCOUNTS[: NO_MATCHING_ACCOUNTS.index("{")]) assert "not-" + tenant in ex.value.message @@ -207,7 +207,7 @@ async def test_no_matching_account_for_tenant_and_username(): cache = populated_cache(account) with pytest.raises(CredentialUnavailableError) as ex: - await SharedTokenCacheCredential(token_cache=cache, tenant_id="not-" + tenant, username="not" + upn).get_token( + await SharedTokenCacheCredential(_cache=cache, tenant_id="not-" + tenant, username="not" + upn).get_token( "scope" ) @@ -231,13 +231,13 @@ async def test_no_matching_account_for_tenant_or_username(): transport = Mock(side_effect=Exception()) # credential shouldn't use the network - credential = SharedTokenCacheCredential(username=upn_a, tenant_id=tenant_b, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn_a, tenant_id=tenant_b, _cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError) as ex: await credential.get_token("scope") assert ex.value.message.startswith(NO_MATCHING_ACCOUNTS[: NO_MATCHING_ACCOUNTS.index("{")]) assert upn_a in ex.value.message and tenant_b in ex.value.message - credential = SharedTokenCacheCredential(username=upn_b, tenant_id=tenant_a, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn_b, tenant_id=tenant_a, _cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError) as ex: await credential.get_token("scope") assert ex.value.message.startswith(NO_MATCHING_ACCOUNTS[: NO_MATCHING_ACCOUNTS.index("{")]) @@ -259,7 +259,7 @@ async def test_single_account_matching_username(): requests=[Request(required_data={"refresh_token": refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(token_cache=cache, transport=transport, username=upn) + credential = SharedTokenCacheCredential(_cache=cache, transport=transport, username=upn) token = await credential.get_token(scope) assert token.token == expected_token @@ -279,7 +279,7 @@ async def test_single_account_matching_tenant(): requests=[Request(required_data={"refresh_token": refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(token_cache=cache, transport=transport, tenant_id=tenant_id) + credential = SharedTokenCacheCredential(_cache=cache, transport=transport, tenant_id=tenant_id) token = await credential.get_token(scope) assert token.token == expected_token @@ -300,7 +300,7 @@ async def test_single_account_matching_tenant_and_username(): requests=[Request(required_data={"refresh_token": refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(token_cache=cache, transport=transport, tenant_id=tenant_id, username=upn) + credential = SharedTokenCacheCredential(_cache=cache, transport=transport, tenant_id=tenant_id, username=upn) token = await credential.get_token(scope) assert token.token == expected_token @@ -319,7 +319,7 @@ async def test_single_account(): requests=[Request(required_data={"refresh_token": refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(_cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == expected_token @@ -334,11 +334,11 @@ async def test_no_refresh_token(): transport = Mock(side_effect=Exception()) # credential shouldn't use the network - credential = SharedTokenCacheCredential(token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(_cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): await credential.get_token("scope") - credential = SharedTokenCacheCredential(token_cache=cache, transport=transport, username="not@cache") + credential = SharedTokenCacheCredential(_cache=cache, transport=transport, username="not@cache") with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): await credential.get_token("scope") @@ -357,7 +357,7 @@ async def test_two_accounts_no_username_or_tenant(): transport = Mock(side_effect=Exception()) # two users in the cache, no username specified -> CredentialUnavailableError - credential = SharedTokenCacheCredential(token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(_cache=cache, transport=transport) with pytest.raises(ClientAuthenticationError, match=MULTIPLE_ACCOUNTS) as ex: await credential.get_token("scope") @@ -379,7 +379,7 @@ async def test_two_accounts_username_specified(): requests=[Request(required_data={"refresh_token": expected_refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(username=upn_a, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn_a, _cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == expected_token @@ -402,7 +402,7 @@ async def test_two_accounts_tenant_specified(): requests=[Request(required_data={"refresh_token": expected_refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(tenant_id=tenant_id, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(tenant_id=tenant_id, _cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == expected_token @@ -425,7 +425,7 @@ async def test_two_accounts_tenant_and_username_specified(): requests=[Request(required_data={"refresh_token": expected_refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) - credential = SharedTokenCacheCredential(tenant_id=tenant_id, username=upn_a, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(tenant_id=tenant_id, username=upn_a, _cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == expected_token @@ -448,7 +448,7 @@ async def test_same_username_different_tenants(): # with no tenant specified the credential can't select an identity transport = Mock(side_effect=Exception()) # (so it shouldn't use the network) - credential = SharedTokenCacheCredential(username=upn, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn, _cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError) as ex: await credential.get_token("scope") @@ -461,7 +461,7 @@ async def test_same_username_different_tenants(): requests=[Request(required_data={"refresh_token": refresh_token_a, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))], ) - credential = SharedTokenCacheCredential(tenant_id=tenant_a, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(tenant_id=tenant_a, _cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == access_token_a @@ -469,7 +469,7 @@ async def test_same_username_different_tenants(): requests=[Request(required_data={"refresh_token": refresh_token_b, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_b))], ) - credential = SharedTokenCacheCredential(tenant_id=tenant_b, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(tenant_id=tenant_b, _cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == access_token_b @@ -492,7 +492,7 @@ async def test_same_tenant_different_usernames(): # with no username specified the credential can't select an identity transport = Mock(side_effect=Exception()) # (so it shouldn't use the network) - credential = SharedTokenCacheCredential(tenant_id=tenant_id, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(tenant_id=tenant_id, _cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError) as ex: await credential.get_token("scope") @@ -505,7 +505,7 @@ async def test_same_tenant_different_usernames(): requests=[Request(required_data={"refresh_token": refresh_token_b, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))], ) - credential = SharedTokenCacheCredential(username=upn_b, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn_b, _cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == access_token_a @@ -513,7 +513,7 @@ async def test_same_tenant_different_usernames(): requests=[Request(required_data={"refresh_token": refresh_token_a, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))], ) - credential = SharedTokenCacheCredential(username=upn_a, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(username=upn_a, _cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == access_token_a @@ -537,7 +537,7 @@ async def test_authority_aliases(): requests=[Request(authority=authority, required_data={"refresh_token": expected_refresh_token})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) - credential = SharedTokenCacheCredential(authority=authority, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(authority=authority, _cache=cache, transport=transport) token = await credential.get_token("scope") assert token.token == expected_access_token @@ -547,7 +547,7 @@ async def test_authority_aliases(): requests=[Request(authority=alias, required_data={"refresh_token": expected_refresh_token})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) - credential = SharedTokenCacheCredential(authority=alias, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(authority=alias, _cache=cache, transport=transport) token = await credential.get_token("scope") assert token.token == expected_access_token @@ -565,7 +565,7 @@ async def test_authority_with_no_known_alias(): requests=[Request(authority=authority, required_data={"refresh_token": expected_refresh_token})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) - credential = SharedTokenCacheCredential(authority=authority, token_cache=cache, transport=transport) + credential = SharedTokenCacheCredential(authority=authority, _cache=cache, transport=transport) token = await credential.get_token("scope") assert token.token == expected_access_token @@ -584,7 +584,7 @@ async def test_authority_environment_variable(): responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) with patch.dict("os.environ", {EnvironmentVariables.AZURE_AUTHORITY_HOST: authority}, clear=True): - credential = SharedTokenCacheCredential(transport=transport, token_cache=cache) + credential = SharedTokenCacheCredential(transport=transport, _cache=cache) token = await credential.get_token("scope") assert token.token == expected_access_token @@ -600,7 +600,6 @@ async def test_initialization(): assert mock_cache_loader.call_count == 0 for _ in range(2): - with pytest.raises(CredentialUnavailableError): + with pytest.raises(CredentialUnavailableError, match="Shared token cache unavailable"): await credential.get_token("scope") assert mock_cache_loader.call_count == 1 - From d7ba3826b99fca4a9552acbc3417b85e9bf7124c Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Tue, 2 Mar 2021 12:28:26 -0800 Subject: [PATCH 11/12] remove unnecessary test hook --- .../identity/aio/_credentials/certificate.py | 12 ++++----- .../aio/_credentials/client_secret.py | 12 ++++----- .../tests/test_certificate_credential.py | 27 ++++++++++++++----- .../test_certificate_credential_async.py | 27 ++++++++++++++----- .../tests/test_client_secret_credential.py | 21 +++++++++++++-- .../test_client_secret_credential_async.py | 21 +++++++++++++-- 6 files changed, 90 insertions(+), 30 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py index eaaa487edf19..5ebefcc7e0e7 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/certificate.py @@ -49,13 +49,11 @@ def __init__(self, tenant_id, client_id, certificate_path=None, **kwargs): client_credential["private_key"], password=client_credential.get("passphrase") ) - cache = kwargs.pop("_cache", None) - if not cache: - options = kwargs.pop("cache_persistence_options", None) - if options: - cache = _load_persistent_cache(options) - else: - cache = msal.TokenCache() + cache_options = kwargs.pop("cache_persistence_options", None) + if cache_options: + cache = _load_persistent_cache(cache_options) + else: + cache = msal.TokenCache() self._client = AadClient(tenant_id, client_id, cache=cache, **kwargs) self._client_id = client_id diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py index 10eb903b37d3..335e61c989cb 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_secret.py @@ -43,13 +43,11 @@ def __init__(self, tenant_id, client_id, client_secret, **kwargs): ) validate_tenant_id(tenant_id) - cache = kwargs.pop("_cache", None) - if not cache: - options = kwargs.pop("cache_persistence_options", None) - if options: - cache = _load_persistent_cache(options) - else: - cache = msal.TokenCache() + cache_options = kwargs.pop("cache_persistence_options", None) + if cache_options: + cache = _load_persistent_cache(cache_options) + else: + cache = msal.TokenCache() self._client = AadClient(tenant_id, client_id, cache=cache, **kwargs) self._client_id = client_id diff --git a/sdk/identity/azure-identity/tests/test_certificate_credential.py b/sdk/identity/azure-identity/tests/test_certificate_credential.py index cb39bd880491..6fcef8b12975 100644 --- a/sdk/identity/azure-identity/tests/test_certificate_credential.py +++ b/sdk/identity/azure-identity/tests/test_certificate_credential.py @@ -269,12 +269,27 @@ def test_cache_multiple_clients(cert_path, cert_password): ) cache = TokenCache() - credential_a = CertificateCredential( - "tenant", "client-a", cert_path, password=cert_password, transport=transport_a, _cache=cache - ) - credential_b = CertificateCredential( - "tenant", "client-b", cert_path, password=cert_password, transport=transport_b, _cache=cache - ) + with patch("azure.identity._internal.msal_credentials._load_persistent_cache") as mock_cache_loader: + mock_cache_loader.return_value = Mock(wraps=cache) + credential_a = CertificateCredential( + "tenant", + "client-a", + cert_path, + password=cert_password, + transport=transport_a, + cache_persistence_options=TokenCachePersistenceOptions(), + ) + assert mock_cache_loader.call_count == 1, "credential should load the persistent cache" + + credential_b = CertificateCredential( + "tenant", + "client-b", + cert_path, + password=cert_password, + transport=transport_b, + cache_persistence_options=TokenCachePersistenceOptions(), + ) + assert mock_cache_loader.call_count == 2, "credential should load the persistent cache" # A caches a token scope = "scope" diff --git a/sdk/identity/azure-identity/tests/test_certificate_credential_async.py b/sdk/identity/azure-identity/tests/test_certificate_credential_async.py index 69dda6c045ff..6601266950c0 100644 --- a/sdk/identity/azure-identity/tests/test_certificate_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_certificate_credential_async.py @@ -222,12 +222,27 @@ async def test_cache_multiple_clients(cert_path, cert_password): ) cache = TokenCache() - credential_a = CertificateCredential( - "tenant", "client-a", cert_path, password=cert_password, transport=transport_a, _cache=cache - ) - credential_b = CertificateCredential( - "tenant", "client-b", cert_path, password=cert_password, transport=transport_b, _cache=cache - ) + with patch(CertificateCredential.__module__ + "._load_persistent_cache") as mock_cache_loader: + mock_cache_loader.return_value = Mock(wraps=cache) + credential_a = CertificateCredential( + "tenant", + "client-a", + cert_path, + password=cert_password, + transport=transport_a, + cache_persistence_options=TokenCachePersistenceOptions(), + ) + assert mock_cache_loader.call_count == 1, "credential should load the persistent cache" + + credential_b = CertificateCredential( + "tenant", + "client-b", + cert_path, + password=cert_password, + transport=transport_b, + cache_persistence_options=TokenCachePersistenceOptions(), + ) + assert mock_cache_loader.call_count == 2, "credential should load the persistent cache" # A caches a token scope = "scope" diff --git a/sdk/identity/azure-identity/tests/test_client_secret_credential.py b/sdk/identity/azure-identity/tests/test_client_secret_credential.py index 7fea1949340e..badd1b2d278b 100644 --- a/sdk/identity/azure-identity/tests/test_client_secret_credential.py +++ b/sdk/identity/azure-identity/tests/test_client_secret_credential.py @@ -144,8 +144,25 @@ def test_cache_multiple_clients(): ) cache = TokenCache() - credential_a = ClientSecretCredential("tenant", "client-a", "secret", transport=transport_a, _cache=cache) - credential_b = ClientSecretCredential("tenant", "client-b", "secret", transport=transport_b, _cache=cache) + with patch("azure.identity._internal.msal_credentials._load_persistent_cache") as mock_cache_loader: + mock_cache_loader.return_value = Mock(wraps=cache) + credential_a = ClientSecretCredential( + "tenant", + "client-a", + "secret", + transport=transport_a, + cache_persistence_options=TokenCachePersistenceOptions(), + ) + assert mock_cache_loader.call_count == 1, "credential should load the persistent cache" + + credential_b = ClientSecretCredential( + "tenant", + "client-b", + "secret", + transport=transport_b, + cache_persistence_options=TokenCachePersistenceOptions(), + ) + assert mock_cache_loader.call_count == 2, "credential should load the persistent cache" # A caches a token scope = "scope" diff --git a/sdk/identity/azure-identity/tests/test_client_secret_credential_async.py b/sdk/identity/azure-identity/tests/test_client_secret_credential_async.py index c449ff70187d..6ec006b1f1c7 100644 --- a/sdk/identity/azure-identity/tests/test_client_secret_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_client_secret_credential_async.py @@ -215,8 +215,25 @@ async def test_cache_multiple_clients(): ) cache = TokenCache() - credential_a = ClientSecretCredential("tenant", "client-a", "secret", transport=transport_a, _cache=cache) - credential_b = ClientSecretCredential("tenant", "client-b", "secret", transport=transport_b, _cache=cache) + with patch(ClientSecretCredential.__module__ + "._load_persistent_cache") as mock_cache_loader: + mock_cache_loader.return_value = Mock(wraps=cache) + credential_a = ClientSecretCredential( + "tenant", + "client-a", + "secret", + transport=transport_a, + cache_persistence_options=TokenCachePersistenceOptions(), + ) + assert mock_cache_loader.call_count == 1, "credential should load the persistent cache" + + credential_b = ClientSecretCredential( + "tenant", + "client-b", + "secret", + transport=transport_b, + cache_persistence_options=TokenCachePersistenceOptions(), + ) + assert mock_cache_loader.call_count == 2, "credential should load the persistent cache" # A caches a token scope = "scope" From 8d590088c37f501a2520f06634e98c5ee931b590 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Tue, 2 Mar 2021 12:47:41 -0800 Subject: [PATCH 12/12] test unencrypted fallback --- .../azure/identity/_persistent_cache.py | 2 +- .../tests/test_cache_options_samples.py | 21 --------- .../tests/test_persistent_cache.py | 47 +++++++++++++++++++ 3 files changed, 48 insertions(+), 22 deletions(-) delete mode 100644 sdk/identity/azure-identity/tests/test_cache_options_samples.py create mode 100644 sdk/identity/azure-identity/tests/test_persistent_cache.py diff --git a/sdk/identity/azure-identity/azure/identity/_persistent_cache.py b/sdk/identity/azure-identity/azure/identity/_persistent_cache.py index 90e343157222..f863b328740a 100644 --- a/sdk/identity/azure-identity/azure/identity/_persistent_cache.py +++ b/sdk/identity/azure-identity/azure/identity/_persistent_cache.py @@ -27,7 +27,7 @@ class TokenCachePersistenceOptions(object): .. warning:: The cache contains authentication secrets. If the cache is not encrypted, protecting it is the application's responsibility. A breach of its contents will fully compromise accounts. - .. literalinclude:: ../tests/test_cache_options_samples.py + .. literalinclude:: ../tests/test_persistent_cache.py :start-after: [START snippet] :end-before: [END snippet] :language: python diff --git a/sdk/identity/azure-identity/tests/test_cache_options_samples.py b/sdk/identity/azure-identity/tests/test_cache_options_samples.py deleted file mode 100644 index 22582d002368..000000000000 --- a/sdk/identity/azure-identity/tests/test_cache_options_samples.py +++ /dev/null @@ -1,21 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -from helpers import mock - -from azure.identity import InteractiveBrowserCredential, TokenCachePersistenceOptions - - -def test_token_cache_persistence_options(): - with mock.patch("azure.identity._persistent_cache.msal_extensions"): - # [START snippet] - cache_options = TokenCachePersistenceOptions() - credential = InteractiveBrowserCredential(cache_persistence_options=cache_options) - - # specify a cache name to isolate the cache from other applications - TokenCachePersistenceOptions(name="my_application") - - # configure the cache to fall back to unencrypted storage when encryption isn't available - TokenCachePersistenceOptions(allow_unencrypted_storage=True) - # [END snippet] diff --git a/sdk/identity/azure-identity/tests/test_persistent_cache.py b/sdk/identity/azure-identity/tests/test_persistent_cache.py new file mode 100644 index 000000000000..93b4cbacb809 --- /dev/null +++ b/sdk/identity/azure-identity/tests/test_persistent_cache.py @@ -0,0 +1,47 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +from azure.identity import InteractiveBrowserCredential, TokenCachePersistenceOptions +import pytest + +from helpers import mock + + +def test_token_cache_persistence_options(): + with mock.patch("azure.identity._persistent_cache.msal_extensions"): + # [START snippet] + cache_options = TokenCachePersistenceOptions() + credential = InteractiveBrowserCredential(cache_persistence_options=cache_options) + + # specify a cache name to isolate the cache from other applications + TokenCachePersistenceOptions(name="my_application") + + # configure the cache to fall back to unencrypted storage when encryption isn't available + TokenCachePersistenceOptions(allow_unencrypted_storage=True) + # [END snippet] + + +@mock.patch("azure.identity._persistent_cache.sys.platform", "linux2") +@mock.patch("azure.identity._persistent_cache.msal_extensions") +def test_persistent_cache_linux(mock_extensions): + """Credentials should use an unencrypted cache when encryption is unavailable and the user explicitly opts in. + + This test was written when Linux was the only platform on which encryption may not be available. + """ + from azure.identity._persistent_cache import _load_persistent_cache + + _load_persistent_cache(TokenCachePersistenceOptions()) + assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.LibsecretPersistence) + mock_extensions.PersistedTokenCache.reset_mock() + + # when LibsecretPersistence's dependencies aren't available, constructing it raises ImportError + mock_extensions.LibsecretPersistence = mock.Mock(side_effect=ImportError) + + # encryption unavailable, no unencrypted storage not allowed + with pytest.raises(ValueError): + _load_persistent_cache(TokenCachePersistenceOptions()) + + # encryption unavailable, unencrypted storage allowed + _load_persistent_cache(TokenCachePersistenceOptions(allow_unencrypted_storage=True)) + mock_extensions.PersistedTokenCache.called_with(mock_extensions.FilePersistence)