diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 242130d23b8b..56a1cb50be3a 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -6,8 +6,23 @@ > Only code written against a beta version such as 1.6.0b1 may be affected. - Renamed `CertificateCredential` keyword argument `certificate_bytes` to `certificate_data` - +- Credentials accepting keyword arguments `allow_unencrypted_cache` and + `enable_persistent_cache` to configure persistent caching accept a + `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_options = TokenCachePersistenceOptions(allow_unencrypted_storage=True) + DeviceCodeCredential(cache_persistence_options=cache_options) + ``` + + See the documentation and samples for more details. + ### Added +- New class `TokenCachePersistenceOptions` configures persistent caching - The `AuthenticationRequiredError.claims` property provides any additional claims required by a user credential's `authenticate()` method diff --git a/sdk/identity/azure-identity/azure/identity/__init__.py b/sdk/identity/azure-identity/azure/identity/__init__.py index 3819005a9e63..cb6f35ec35b1 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 TokenCachePersistenceOptions __all__ = [ @@ -41,6 +42,7 @@ "KnownAuthorities", "ManagedIdentityCredential", "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 f940cba40e3a..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 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 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 a517030cc5e5..2f28984d82f7 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 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 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 311a6f1ef3e8..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 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 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 f8acc94e34ee..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 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 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 c5469dfdbcc9..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,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 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): 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..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 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 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 da0c1ff1e20a..39f554bc47a7 100644 --- a/sdk/identity/azure-identity/azure/identity/_internal/__init__.py +++ b/sdk/identity/azure-identity/azure/identity/_internal/__init__.py @@ -3,6 +3,7 @@ # Licensed under the MIT License. # ------------------------------------ import os + from six.moves.urllib_parse import urlparse from .._constants import EnvironmentVariables, KnownAuthorities @@ -47,7 +48,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 @@ -71,7 +71,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 deleted file mode 100644 index 204b9a52ee51..000000000000 --- a/sdk/identity/azure-identity/azure/identity/_internal/client_secret_credential_base.py +++ /dev/null @@ -1,49 +0,0 @@ -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ -import abc -from typing import TYPE_CHECKING - -from msal import TokenCache - -from . import validate_tenant_id -from .persistent_cache import load_service_principal_cache - -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/_internal/msal_credentials.py b/sdk/identity/azure-identity/azure/identity/_internal/msal_credentials.py index b924c050d9ed..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,8 +7,8 @@ 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 .._persistent_cache import _load_persistent_cache, TokenCachePersistenceOptions try: ABC = abc.ABC @@ -29,7 +29,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,11 +38,11 @@ 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 + self._cache = kwargs.pop("_cache", None) 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) + options = kwargs.pop("cache_persistence_options", None) + if options: + self._cache = _load_persistent_cache(options) else: self._cache = msal.TokenCache() 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/_internal/shared_token_cache.py b/sdk/identity/azure-identity/azure/identity/_internal/shared_token_cache.py index 11d42936cd57..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 @@ -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 _load_persistent_cache, TokenCachePersistenceOptions try: ABC = abc.ABC @@ -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, **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 = _load_persistent_cache(TokenCachePersistenceOptions(allow_unencrypted_storage=True)) except Exception: # pylint:disable=broad-except pass @@ -124,7 +129,7 @@ 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 = [] @@ -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: @@ -190,7 +195,7 @@ def _get_cached_access_token(self, scopes, account): try: cache_entries = self._cache.find( - TokenCache.CredentialType.ACCESS_TOKEN, + msal.TokenCache.CredentialType.ACCESS_TOKEN, target=list(scopes), query={"home_account_id": account["home_account_id"]}, ) @@ -210,7 +215,7 @@ def _get_refresh_tokens(self, account): try: cache_entries = self._cache.find( - TokenCache.CredentialType.REFRESH_TOKEN, query={"home_account_id": account["home_account_id"]} + 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/_persistent_cache.py b/sdk/identity/azure-identity/azure/identity/_persistent_cache.py new file mode 100644 index 000000000000..f863b328740a --- /dev/null +++ b/sdk/identity/azure-identity/azure/identity/_persistent_cache.py @@ -0,0 +1,95 @@ +# ------------------------------------ +# 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 TokenCachePersistenceOptions(object): + """Options for persistent token caching. + + 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_persistent_cache.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. + """ + + def __init__(self, **kwargs): + # type: (**Any) -> None + 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): + # 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.") 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 6b3d5516ff79..97b6996ced6c 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,13 @@ # ------------------------------------ from typing import TYPE_CHECKING -from msal import TokenCache +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 AadClientCertificate, validate_tenant_id -from ..._internal.persistent_cache import load_service_principal_cache +from ..._persistent_cache import _load_persistent_cache, TokenCachePersistenceOptions if TYPE_CHECKING: from typing import Any, Optional @@ -34,6 +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 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): @@ -46,12 +49,11 @@ 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) + cache_options = kwargs.pop("cache_persistence_options", None) + if cache_options: + cache = _load_persistent_cache(cache_options) else: - cache = TokenCache() + 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 b83ac21dba5b..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 @@ -4,16 +4,19 @@ # ------------------------------------ from typing import TYPE_CHECKING +import msal + from .._internal import AadClient, AsyncContextManager from .._internal.decorators import log_get_token_async -from ..._internal import ClientSecretCredentialBase +from ..._internal import validate_tenant_id +from ..._persistent_cache import _load_persistent_cache, TokenCachePersistenceOptions 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. @@ -23,12 +26,33 @@ class ClientSecretCredential(AsyncContextManager, ClientSecretCredentialBase): :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 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): + # 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) + + 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 + self._secret = client_secret + async def __aenter__(self): await self._client.__aenter__() return self @@ -62,6 +86,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) 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..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,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 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/user_authentication.py b/sdk/identity/azure-identity/samples/user_authentication.py index 2c21c2a44973..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 +from azure.identity import AuthenticationRecord, InteractiveBrowserCredential, TokenCachePersistenceOptions from azure.keyvault.secrets import SecretClient @@ -18,7 +18,9 @@ # Persistent caching is optional. By default, interactive credentials cache in memory only. -credential = InteractiveBrowserCredential(enable_persistent_cache=True) +# 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. @@ -32,11 +34,13 @@ 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( + 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_browser_credential.py b/sdk/identity/azure-identity/tests/test_browser_credential.py index 4e49dce0170e..5d9af1025a60 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_certificate_credential.py b/sdk/identity/azure-identity/tests/test_certificate_credential.py index cfef3f421982..417e83792ebd 100644 --- a/sdk/identity/azure-identity/tests/test_certificate_credential.py +++ b/sdk/identity/azure-identity/tests/test_certificate_credential.py @@ -6,7 +6,7 @@ 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.user_agent import USER_AGENT from cryptography import x509 @@ -237,68 +237,26 @@ 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""" - - 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) +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: + credential = CertificateCredential("tenant", "client-id", cert_path, password=cert_password) + assert not mock_msal_extensions.PersistedTokenCache.called + assert isinstance(credential._cache, TokenCache) + + 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) -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" @@ -311,14 +269,25 @@ def test_persistent_cache_multiple_clients(cert_path, cert_password): ) cache = TokenCache() - with patch("azure.identity._internal.persistent_cache._load_persistent_cache") as mock_cache_loader: + 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, enable_persistent_cache=True, transport=transport_a + "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, enable_persistent_cache=True, transport=transport_b + "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" @@ -332,3 +301,5 @@ def test_persistent_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 38272c36281b..a4c357c2beb2 100644 --- a/sdk/identity/azure-identity/tests/test_certificate_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_certificate_credential_async.py @@ -6,6 +6,7 @@ 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.user_agent import USER_AGENT from azure.identity.aio import CertificateCredential @@ -187,69 +188,28 @@ async def mock_send(request, **kwargs): @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) +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_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" @@ -262,14 +222,25 @@ async def test_persistent_cache_multiple_clients(cert_path, cert_password): ) cache = TokenCache() - with patch("azure.identity._internal.persistent_cache._load_persistent_cache") as mock_cache_loader: + 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, enable_persistent_cache=True, transport=transport_a + "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, enable_persistent_cache=True, transport=transport_b + "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" @@ -283,3 +254,5 @@ async def test_persistent_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 2e651c1924a0..badd1b2d278b 100644 --- a/sdk/identity/azure-identity/tests/test_client_secret_credential.py +++ b/sdk/identity/azure-identity/tests/test_client_secret_credential.py @@ -3,7 +3,7 @@ # 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.user_agent import USER_AGENT from msal import TokenCache @@ -117,60 +117,21 @@ 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 default to an in memory cache, and optionally use a persistent cache""" - required_arguments = ("tenant-id", "client-id", "secret") - persistent_cache = "azure.identity._internal.persistent_cache" + 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) - # 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) + ClientSecretCredential( + "tenant", "client-id", "secret", cache_persistence_options=TokenCachePersistenceOptions(), + ) + assert mock_msal_extensions.PersistedTokenCache.call_count == 1 -def test_persistent_cache_multiple_clients(): +def test_cache_multiple_clients(): """the credential shouldn't use tokens issued to other service principals""" access_token_a = "token a" @@ -183,14 +144,23 @@ def test_persistent_cache_multiple_clients(): ) cache = TokenCache() - with patch("azure.identity._internal.persistent_cache._load_persistent_cache") as mock_cache_loader: + 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-id", "client-a", "...", enable_persistent_cache=True, transport=transport_a + "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-id", "client-b", "...", enable_persistent_cache=True, transport=transport_b + "tenant", + "client-b", + "secret", + transport=transport_b, + cache_persistence_options=TokenCachePersistenceOptions(), ) assert mock_cache_loader.call_count == 2, "credential should load the persistent cache" @@ -204,3 +174,5 @@ def test_persistent_cache_multiple_clients(): 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 3c0bdd231941..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 @@ -8,6 +8,7 @@ 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.user_agent import USER_AGENT from azure.identity.aio import ClientSecretCredential @@ -185,61 +186,23 @@ 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 default to an in memory cache, and optionally use a persistent cache""" - required_arguments = ("tenant-id", "client-id", "secret") - persistent_cache = "azure.identity._internal.persistent_cache" + 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 - # 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) + ClientSecretCredential( + "tenant", "client-id", "secret", cache_persistence_options=TokenCachePersistenceOptions(), + ) + assert mock_msal_extensions.PersistedTokenCache.call_count == 1 @pytest.mark.asyncio -async def test_persistent_cache_multiple_clients(): +async def test_cache_multiple_clients(): """the credential shouldn't use tokens issued to other service principals""" access_token_a = "token a" @@ -252,14 +215,23 @@ async def test_persistent_cache_multiple_clients(): ) cache = TokenCache() - with patch("azure.identity._internal.persistent_cache._load_persistent_cache") as mock_cache_loader: + with patch(ClientSecretCredential.__module__ + "._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 + "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-id", "client-b", "...", enable_persistent_cache=True, transport=transport_b + "tenant", + "client-b", + "secret", + transport=transport_b, + cache_persistence_options=TokenCachePersistenceOptions(), ) assert mock_cache_loader.call_count == 2, "credential should load the persistent cache" @@ -273,3 +245,5 @@ async def test_persistent_cache_multiple_clients(): 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_device_code_credential.py b/sdk/identity/azure-identity/tests/test_device_code_credential.py index d2c2a62a17c4..98c2647679f3 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_interactive_credential.py b/sdk/identity/azure-identity/tests/test_interactive_credential.py index 214fe9e5482f..d987bba2d4e8 100644 --- a/sdk/identity/azure-identity/tests/test_interactive_credential.py +++ b/sdk/identity/azure-identity/tests/test_interactive_credential.py @@ -8,9 +8,9 @@ AuthenticationRecord, KnownAuthorities, CredentialUnavailableError, + TokenCachePersistenceOptions, ) from azure.identity._internal import InteractiveCredential -from msal import TokenCache import pytest try: @@ -42,13 +42,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): @@ -217,8 +217,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 default to an in memory cache, and optionally use a persistent cache""" class TestCredential(InteractiveCredential): def __init__(self, **kwargs): @@ -227,63 +227,14 @@ def __init__(self, **kwargs): def _request_token(self, *_, **__): pass - in_memory_cache = Mock() + 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 - 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) + 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_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) 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..9ee2dcf1327f 100644 --- a/sdk/identity/azure-identity/tests/test_shared_cache_credential.py +++ b/sdk/identity/azure-identity/tests/test_shared_cache_credential.py @@ -642,32 +642,6 @@ def test_auth_record_multiple_accounts_for_username(): 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""" @@ -724,56 +698,17 @@ def test_writes_to_cache(): 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], - ) - ) - - 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._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 @@ -805,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, _cache=TokenCache() + ) with patch(SharedTokenCacheCredential.__module__ + ".PublicClientApplication") as PublicClientApplication: credential._initialize() @@ -829,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, _cache=TokenCache() + ) with patch(SharedTokenCacheCredential.__module__ + ".PublicClientApplication", lambda *_, **__: msal_app): credential.get_token("scope", claims=expected_claims) 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..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 @@ -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._persistent_cache._load_persistent_cache", Mock(side_effect=NotImplementedError)): credential = SharedTokenCacheCredential(transport=transport) async with credential: @@ -589,53 +589,17 @@ async def test_authority_environment_variable(): 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() 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 -