From 9e6018cd75cf6d3d9fe1a35f711f2e89731f77fd Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 12 Aug 2020 10:29:21 -0700 Subject: [PATCH 1/2] Revert "Remove application authentication APIs from stable 1.4.0 release (#12788)" This reverts commit d8dda157 --- sdk/identity/azure-identity/CHANGELOG.md | 7 +-- sdk/identity/azure-identity/MANIFEST.in | 1 + .../azure-identity/azure/identity/__init__.py | 5 +- .../azure/identity/_credentials/browser.py | 7 +++ .../identity/_credentials/certificate.py | 4 ++ .../identity/_credentials/client_secret.py | 4 ++ .../identity/_credentials/device_code.py | 7 +++ .../identity/_credentials/managed_identity.py | 6 ++- .../identity/_credentials/shared_cache.py | 4 ++ .../identity/_credentials/user_password.py | 6 ++- .../_internal/certificate_credential_base.py | 4 +- .../client_secret_credential_base.py | 4 +- .../azure/identity/_internal/interactive.py | 8 +-- .../identity/_internal/msal_credentials.py | 4 +- .../identity/_internal/shared_token_cache.py | 6 +-- .../aio/_credentials/client_secret.py | 4 ++ .../aio/_credentials/managed_identity.py | 4 ++ .../identity/aio/_credentials/shared_cache.py | 4 ++ sdk/identity/azure-identity/samples/README.md | 37 ++++++++++++++ .../samples/control_interactive_prompts.py | 38 ++++++++++++++ .../samples/user_authentication.py | 43 ++++++++++++++++ .../azure-identity/tests/test_auth_record.py | 2 +- .../tests/test_browser_credential.py | 7 ++- .../tests/test_certificate_credential.py | 18 +++---- .../test_certificate_credential_async.py | 18 +++---- .../tests/test_client_secret_credential.py | 18 +++---- .../test_client_secret_credential_async.py | 18 +++---- .../tests/test_device_code_credential.py | 7 ++- .../tests/test_imds_credential.py | 2 +- .../tests/test_imds_credential_async.py | 2 +- .../tests/test_interactive_credential.py | 49 ++++++++++--------- .../tests/test_msi_credential.py | 4 +- .../tests/test_msi_credential_async.py | 4 +- .../tests/test_shared_cache_credential.py | 22 +++++---- .../test_shared_cache_credential_async.py | 22 ++++----- .../test_username_password_credential.py | 2 +- 36 files changed, 287 insertions(+), 115 deletions(-) create mode 100644 sdk/identity/azure-identity/samples/README.md create mode 100644 sdk/identity/azure-identity/samples/control_interactive_prompts.py create mode 100644 sdk/identity/azure-identity/samples/user_authentication.py diff --git a/sdk/identity/azure-identity/CHANGELOG.md b/sdk/identity/azure-identity/CHANGELOG.md index 81c7cb4d0336..44d140ac2d5d 100644 --- a/sdk/identity/azure-identity/CHANGELOG.md +++ b/sdk/identity/azure-identity/CHANGELOG.md @@ -1,8 +1,9 @@ # Release History -## 1.4.1 (Unreleased) - - +## 1.5.0b1 (Unreleased) +### Added +- Application authentication APIs from 1.4.0b7 + ## 1.4.0 (2020-08-10) ### Added - `DefaultAzureCredential` uses the value of environment variable diff --git a/sdk/identity/azure-identity/MANIFEST.in b/sdk/identity/azure-identity/MANIFEST.in index 3eee8c39c7a7..07b576fc0eb3 100644 --- a/sdk/identity/azure-identity/MANIFEST.in +++ b/sdk/identity/azure-identity/MANIFEST.in @@ -1,3 +1,4 @@ +recursive-include samples *.py recursive-include tests *.py include *.md include azure/__init__.py diff --git a/sdk/identity/azure-identity/azure/identity/__init__.py b/sdk/identity/azure-identity/azure/identity/__init__.py index a749d3510112..3819005a9e63 100644 --- a/sdk/identity/azure-identity/azure/identity/__init__.py +++ b/sdk/identity/azure-identity/azure/identity/__init__.py @@ -4,7 +4,8 @@ # ------------------------------------ """Credentials for Azure SDK clients.""" -from ._exceptions import CredentialUnavailableError +from ._auth_record import AuthenticationRecord +from ._exceptions import AuthenticationRequiredError, CredentialUnavailableError from ._constants import AzureAuthorityHosts, KnownAuthorities from ._credentials import ( AzureCliCredential, @@ -24,6 +25,8 @@ __all__ = [ + "AuthenticationRecord", + "AuthenticationRequiredError", "AuthorizationCodeCredential", "AzureAuthorityHosts", "AzureCliCredential", diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/browser.py b/sdk/identity/azure-identity/azure/identity/_credentials/browser.py index 2726c425074a..cf860f5b39f1 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/browser.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/browser.py @@ -36,6 +36,13 @@ class InteractiveBrowserCredential(InteractiveCredential): authenticate work or school accounts. :keyword str client_id: Client ID of the Azure Active Directory application users will sign in to. If unspecified, the Azure CLI's ID will be used. + :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 int timeout: seconds to wait for the user to complete authentication. Defaults to 300 (5 minutes). """ diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py b/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py index 8ae461c8ef78..35c81b2e3da5 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py @@ -25,6 +25,10 @@ class CertificateCredential(CertificateCredentialBase): :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 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. """ @log_get_token("CertificateCredential") 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 54bce366781a..a327416cd731 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/client_secret.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/client_secret.py @@ -26,6 +26,10 @@ class ClientSecretCredential(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. """ @log_get_token("ClientSecretCredential") 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 37278c2a7a16..87fc9e738a31 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/device_code.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/device_code.py @@ -46,6 +46,13 @@ class DeviceCodeCredential(InteractiveCredential): - ``expires_on`` (datetime.datetime) the UTC time at which the code will expire If this argument isn't provided, the credential will print instructions to stdout. :paramtype prompt_callback: Callable[str, str, ~datetime.datetime] + :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. """ def __init__(self, client_id, **kwargs): diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/managed_identity.py b/sdk/identity/azure-identity/azure/identity/_credentials/managed_identity.py index 1205b1a083db..8bdd46f5cba9 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/managed_identity.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/managed_identity.py @@ -44,6 +44,10 @@ class ManagedIdentityCredential(object): the keyword arguments. :keyword str client_id: a user-assigned identity's client ID. This is supported in all hosting environments. + :keyword identity_config: a mapping ``{parameter_name: value}`` specifying a user-assigned identity by its object + or resource ID, for example ``{"object_id": "..."}``. Check the documentation for your hosting environment to + learn what values it expects. + :paramtype identity_config: Mapping[str, str] """ def __init__(self, **kwargs): @@ -76,7 +80,7 @@ def get_token(self, *scopes, **kwargs): class _ManagedIdentityBase(object): def __init__(self, endpoint, client_cls, config=None, client_id=None, **kwargs): # type: (str, Type, Optional[Configuration], Optional[str], **Any) -> None - self._identity_config = kwargs.pop("_identity_config", None) or {} + self._identity_config = kwargs.pop("identity_config", None) or {} if client_id: if os.environ.get(EnvironmentVariables.MSI_ENDPOINT) and os.environ.get(EnvironmentVariables.MSI_SECRET): # App Service: version 2017-09-1 accepts client ID as parameter "clientid" 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 59aa7728a8a2..741dcc30bf03 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/shared_cache.py @@ -31,6 +31,10 @@ class SharedTokenCacheCredential(SharedTokenCacheBase): 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 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. """ @log_get_token("SharedTokenCacheCredential") 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 a5636855a6e1..1c6c1b3561d6 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/user_password.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/user_password.py @@ -33,6 +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. """ def __init__(self, client_id, username, password, **kwargs): @@ -42,7 +46,7 @@ def __init__(self, client_id, username, password, **kwargs): # first time it's asked for a token. However, we want to ensure this first authentication is not silent, to # validate the given password. This class therefore doesn't document the authentication_record argument, and we # discard it here. - kwargs.pop("_authentication_record", None) + kwargs.pop("authentication_record", None) super(UsernamePasswordCredential, self).__init__(client_id=client_id, **kwargs) self._username = username self._password = password diff --git a/sdk/identity/azure-identity/azure/identity/_internal/certificate_credential_base.py b/sdk/identity/azure-identity/azure/identity/_internal/certificate_credential_base.py index e799bce87adf..c13fe86d7a29 100644 --- a/sdk/identity/azure-identity/azure/identity/_internal/certificate_credential_base.py +++ b/sdk/identity/azure-identity/azure/identity/_internal/certificate_credential_base.py @@ -44,9 +44,9 @@ def __init__(self, tenant_id, client_id, certificate_path, **kwargs): self._certificate = AadClientCertificate(pem_bytes, password=password) - enable_persistent_cache = kwargs.pop("_enable_persistent_cache", False) + enable_persistent_cache = kwargs.pop("enable_persistent_cache", False) if enable_persistent_cache: - allow_unencrypted = kwargs.pop("_allow_unencrypted_cache", False) + allow_unencrypted = kwargs.pop("allow_unencrypted_cache", False) cache = load_service_principal_cache(allow_unencrypted) else: cache = TokenCache() 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 a8b0a1fdc4ce..4854a396e84f 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 @@ -31,9 +31,9 @@ def __init__(self, tenant_id, client_id, client_secret, **kwargs): "tenant_id should be an Azure Active Directory tenant's id (also called its 'directory id')" ) - enable_persistent_cache = kwargs.pop("_enable_persistent_cache", False) + enable_persistent_cache = kwargs.pop("enable_persistent_cache", False) if enable_persistent_cache: - allow_unencrypted = kwargs.pop("_allow_unencrypted_cache", False) + allow_unencrypted = kwargs.pop("allow_unencrypted_cache", False) cache = load_service_principal_cache(allow_unencrypted) else: cache = TokenCache() diff --git a/sdk/identity/azure-identity/azure/identity/_internal/interactive.py b/sdk/identity/azure-identity/azure/identity/_internal/interactive.py index 466c849f069b..4e226bc0c357 100644 --- a/sdk/identity/azure-identity/azure/identity/_internal/interactive.py +++ b/sdk/identity/azure-identity/azure/identity/_internal/interactive.py @@ -71,8 +71,8 @@ def _build_auth_record(response): class InteractiveCredential(MsalCredential): def __init__(self, **kwargs): - self._disable_automatic_authentication = kwargs.pop("_disable_automatic_authentication", False) - self._auth_record = kwargs.pop("_authentication_record", None) # type: Optional[AuthenticationRecord] + self._disable_automatic_authentication = kwargs.pop("disable_automatic_authentication", False) + self._auth_record = kwargs.pop("authentication_record", None) # type: Optional[AuthenticationRecord] if self._auth_record: kwargs.pop("client_id", None) # authentication_record overrides client_id argument tenant_id = kwargs.pop("tenant_id", None) or self._auth_record.tenant_id @@ -97,6 +97,8 @@ def get_token(self, *scopes, **kwargs): required data, state, or platform support :raises ~azure.core.exceptions.ClientAuthenticationError: authentication failed. The error's ``message`` attribute gives a reason. + :raises AuthenticationRequiredError: user interaction is necessary to acquire a token, and the credential is + configured not to begin this automatically. Call :func:`authenticate` to begin interactive authentication. """ if not scopes: message = "'get_token' requires at least one scope" @@ -138,7 +140,7 @@ def get_token(self, *scopes, **kwargs): _LOGGER.info("%s.get_token succeeded", self.__class__.__name__) return AccessToken(result["access_token"], now + int(result["expires_in"])) - def _authenticate(self, **kwargs): + def authenticate(self, **kwargs): # type: (**Any) -> AuthenticationRecord """Interactively authenticate a user. 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 65b8e6efaf83..fd5034acd4bb 100644 --- a/sdk/identity/azure-identity/azure/identity/_internal/msal_credentials.py +++ b/sdk/identity/azure-identity/azure/identity/_internal/msal_credentials.py @@ -40,8 +40,8 @@ def __init__(self, client_id, client_credential=None, **kwargs): 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) + 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() 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 865ccf5e2273..d28a7602fd5e 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 @@ -30,7 +30,7 @@ # pylint:disable=unused-import,ungrouped-imports from typing import Any, Iterable, List, Mapping, Optional from .._internal import AadClientBase - from azure.identity._auth_record import AuthenticationRecord + from azure.identity import AuthenticationRecord CacheItem = Mapping[str, str] @@ -90,7 +90,7 @@ class SharedTokenCacheBase(ABC): def __init__(self, username=None, **kwargs): # pylint:disable=unused-argument # type: (Optional[str], **Any) -> None - self._auth_record = kwargs.pop("_authentication_record", None) # type: Optional[AuthenticationRecord] + self._auth_record = kwargs.pop("authentication_record", None) # type: Optional[AuthenticationRecord] if self._auth_record: # authenticate in the tenant that produced the record unless 'tenant_id' specifies another authenticating_tenant = kwargs.pop("tenant_id", None) or self._auth_record.tenant_id @@ -118,7 +118,7 @@ def _initialize(self): return if not self._cache and self.supported(): - allow_unencrypted = self._client_kwargs.get("_allow_unencrypted_cache", True) + allow_unencrypted = self._client_kwargs.get("allow_unencrypted_cache", False) try: self._cache = load_user_cache(allow_unencrypted) except Exception: # pylint:disable=broad-except 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 0f8c40cd1db4..bbc0aa98e472 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 @@ -24,6 +24,10 @@ class ClientSecretCredential(AsyncCredentialBase, 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. """ async def __aenter__(self): diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/managed_identity.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/managed_identity.py index c3b5a9520908..7def20143526 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/managed_identity.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/managed_identity.py @@ -32,6 +32,10 @@ class ManagedIdentityCredential(AsyncCredentialBase): the keyword arguments. :keyword str client_id: a user-assigned identity's client ID. This is supported in all hosting environments. + :keyword identity_config: a mapping ``{parameter_name: value}`` specifying a user-assigned identity by its object + or resource ID, for example ``{"object_id": "..."}``. Check the documentation for your hosting environment to + learn what values it expects. + :paramtype identity_config: Mapping[str, str] """ def __init__(self, **kwargs: "Any") -> None: 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 de2fd404fc77..a737b8bcecfc 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,6 +29,10 @@ class SharedTokenCacheCredential(SharedTokenCacheBase, AsyncCredentialBase): 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 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. """ async def __aenter__(self): diff --git a/sdk/identity/azure-identity/samples/README.md b/sdk/identity/azure-identity/samples/README.md new file mode 100644 index 000000000000..35a9cd502650 --- /dev/null +++ b/sdk/identity/azure-identity/samples/README.md @@ -0,0 +1,37 @@ +--- +page_type: sample +languages: + - python +products: + - azure + - azure-identity +urlFragment: identity-samples +--- + +# Azure Identity Library Python Samples + +## Prerequisites + +You must have an [Azure subscription](https://azure.microsoft.com/free) and an +[Azure Key Vault](https://azure.microsoft.com/en-us/services/key-vault/) to run +these samples. You can create a Key Vault in the +[Azure Portal](https://portal.azure.com/#create/Microsoft.KeyVault) or with the +[Azure CLI](https://docs.microsoft.com/en-us/azure/key-vault/secrets/quick-create-cli). + +Azure Key Vault is used only to demonstrate authentication. Azure Identity has +the same API for all compatible client libraries. + +## Setup + +To run these samples, first install the Azure Identity and Key Vault Secrets +client libraries: + +```commandline +pip install azure-identity azure-keyvault-secrets +``` + +## Contents +| File | Description | +|-------------|-------------| +| control_interactive_prompts.py | demonstrates controlling when interactive credentials prompt for user interaction | +| user_authentication.py | demonstrates user authentication API for applications | diff --git a/sdk/identity/azure-identity/samples/control_interactive_prompts.py b/sdk/identity/azure-identity/samples/control_interactive_prompts.py new file mode 100644 index 000000000000..10dabf65e9d2 --- /dev/null +++ b/sdk/identity/azure-identity/samples/control_interactive_prompts.py @@ -0,0 +1,38 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +"""Demonstrates controlling the timing of interactive authentication using InteractiveBrowserCredential. + +DeviceCodeCredential supports the same API. +""" + +import os +import sys +from azure.identity import AuthenticationRequiredError, InteractiveBrowserCredential +from azure.keyvault.secrets import SecretClient + + +# This sample uses Key Vault only for demonstration. Any client accepting azure-identity credentials will work the same. +VAULT_URL = os.environ.get("VAULT_URL") +if not VAULT_URL: + print("This sample expects environment variable 'VAULT_URL' to be set with the URL of a Key Vault.") + sys.exit(1) + + +# If it's important for your application to prompt for authentication only at certain times, +# create the credential with disable_automatic_authentication=True. This configures the credential to raise +# when interactive authentication is required, instead of immediately beginning that authentication. +credential = InteractiveBrowserCredential(disable_automatic_authentication=True) +client = SecretClient(VAULT_URL, credential) + +try: + secret_names = [s.name for s in client.list_properties_of_secrets()] +except AuthenticationRequiredError as ex: + # Interactive authentication is necessary to authorize the client's request. The exception carries the + # requested authentication scopes. If you pass these to 'authenticate', it will cache an access token + # for those scopes. + credential.authenticate(scopes=ex.scopes) + +# the client operation should now succeed +secret_names = [s.name for s in client.list_properties_of_secrets()] diff --git a/sdk/identity/azure-identity/samples/user_authentication.py b/sdk/identity/azure-identity/samples/user_authentication.py new file mode 100644 index 000000000000..2c21c2a44973 --- /dev/null +++ b/sdk/identity/azure-identity/samples/user_authentication.py @@ -0,0 +1,43 @@ +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ +"""Demonstrates user authentication using InteractiveBrowserCredential. DeviceCodeCredential supports the same API.""" + +import os +import sys +from azure.identity import AuthenticationRecord, InteractiveBrowserCredential +from azure.keyvault.secrets import SecretClient + + +# This sample uses Key Vault only for demonstration. Any client accepting azure-identity credentials will work the same. +VAULT_URL = os.environ.get("VAULT_URL") +if not VAULT_URL: + print("This sample expects environment variable 'VAULT_URL' to be set with the URL of a Key Vault.") + sys.exit(1) + + +# Persistent caching is optional. By default, interactive credentials cache in memory only. +credential = InteractiveBrowserCredential(enable_persistent_cache=True) + +# 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. +record = credential.authenticate() + +# The record contains no authentication secrets. You can serialize it to JSON for storage. +record_json = record.serialize() + +# An authenticated credential is ready for use with a client. This request should succeed +# without prompting for authentication again. +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. +deserialized_record = AuthenticationRecord.deserialize(record_json) +new_credential = InteractiveBrowserCredential(enable_persistent_cache=True, authentication_record=deserialized_record) + +# This request should also succeed without prompting for authentication. +client = SecretClient(VAULT_URL, new_credential) +secret_names = [s.name for s in client.list_properties_of_secrets()] diff --git a/sdk/identity/azure-identity/tests/test_auth_record.py b/sdk/identity/azure-identity/tests/test_auth_record.py index 5b4ec83d6da2..5daef9c5dec4 100644 --- a/sdk/identity/azure-identity/tests/test_auth_record.py +++ b/sdk/identity/azure-identity/tests/test_auth_record.py @@ -4,7 +4,7 @@ # ------------------------------------ import json -from azure.identity._auth_record import AuthenticationRecord +from azure.identity import AuthenticationRecord def test_serialization(): diff --git a/sdk/identity/azure-identity/tests/test_browser_credential.py b/sdk/identity/azure-identity/tests/test_browser_credential.py index c8d4e9e7fef2..e07c69f30396 100644 --- a/sdk/identity/azure-identity/tests/test_browser_credential.py +++ b/sdk/identity/azure-identity/tests/test_browser_credential.py @@ -10,8 +10,7 @@ from azure.core.exceptions import ClientAuthenticationError from azure.core.pipeline.policies import SansIOHTTPPolicy -from azure.identity import CredentialUnavailableError, InteractiveBrowserCredential -from azure.identity._exceptions import AuthenticationRequiredError +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 @@ -82,7 +81,7 @@ def test_authenticate(): tenant_id=tenant_id, transport=transport, ) - record = credential._authenticate(scopes=(scope,)) + record = credential.authenticate(scopes=(scope,)) assert record.authority == environment assert record.home_account_id == object_id + "." + home_tenant @@ -101,7 +100,7 @@ def test_disable_automatic_authentication(): 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 + disable_automatic_authentication=True, transport=transport, _cache=empty_cache ) with patch(WEBBROWSER_OPEN, Mock(side_effect=Exception("credential shouldn't try interactive authentication"))): diff --git a/sdk/identity/azure-identity/tests/test_certificate_credential.py b/sdk/identity/azure-identity/tests/test_certificate_credential.py index 1d0c332cf7f6..af0eee63c580 100644 --- a/sdk/identity/azure-identity/tests/test_certificate_credential.py +++ b/sdk/identity/azure-identity/tests/test_certificate_credential.py @@ -157,20 +157,20 @@ def test_enable_persistent_cache(cert_path, cert_password): 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) + 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) + 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) + 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 + *required_arguments, password=cert_password, enable_persistent_cache=True, allow_unencrypted_cache=True ) @@ -187,7 +187,7 @@ def test_persistent_cache_linux(mock_extensions, cert_path, cert_password): # 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 + *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() @@ -197,10 +197,10 @@ def test_persistent_cache_linux(mock_extensions, cert_path, cert_password): # 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) CertificateCredential( - *required_arguments, password=cert_password, _enable_persistent_cache=True, _allow_unencrypted_cache=True + *required_arguments, password=cert_password, enable_persistent_cache=True, allow_unencrypted_cache=True ) assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.FilePersistence) @@ -222,11 +222,11 @@ def test_persistent_cache_multiple_clients(cert_path, cert_password): 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 + "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 + "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" 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 9d0f5a2e08cb..01d2839fc2cc 100644 --- a/sdk/identity/azure-identity/tests/test_certificate_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_certificate_credential_async.py @@ -149,20 +149,20 @@ def test_enable_persistent_cache(cert_path, cert_password): 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) + 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) + 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) + 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 + *required_arguments, password=cert_password, enable_persistent_cache=True, allow_unencrypted_cache=True ) @@ -179,7 +179,7 @@ def test_persistent_cache_linux(mock_extensions, cert_path, cert_password): # 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 + *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() @@ -189,10 +189,10 @@ def test_persistent_cache_linux(mock_extensions, cert_path, cert_password): # 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) CertificateCredential( - *required_arguments, password=cert_password, _enable_persistent_cache=True, _allow_unencrypted_cache=True + *required_arguments, password=cert_password, enable_persistent_cache=True, allow_unencrypted_cache=True ) assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.FilePersistence) @@ -215,11 +215,11 @@ async def test_persistent_cache_multiple_clients(cert_path, cert_password): 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 + "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 + "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" 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 542ea203fe10..ea3362a3f0ff 100644 --- a/sdk/identity/azure-identity/tests/test_client_secret_credential.py +++ b/sdk/identity/azure-identity/tests/test_client_secret_credential.py @@ -158,19 +158,19 @@ def test_enable_persistent_cache(): ClientSecretCredential(*required_arguments) # allowing an unencrypted cache doesn't count as opting in to the persistent cache - ClientSecretCredential(*required_arguments, _allow_unencrypted_cache=True) + 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) + 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) + ClientSecretCredential(*required_arguments, enable_persistent_cache=True) with pytest.raises(NotImplementedError): - ClientSecretCredential(*required_arguments, _enable_persistent_cache=True, _allow_unencrypted_cache=True) + ClientSecretCredential(*required_arguments, enable_persistent_cache=True, allow_unencrypted_cache=True) @patch("azure.identity._internal.persistent_cache.sys.platform", "linux2") @@ -184,7 +184,7 @@ def test_persistent_cache_linux(mock_extensions): 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) + 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() @@ -193,9 +193,9 @@ def test_persistent_cache_linux(mock_extensions): # 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) - ClientSecretCredential(*required_arguments, _enable_persistent_cache=True, _allow_unencrypted_cache=True) + ClientSecretCredential(*required_arguments, enable_persistent_cache=True, allow_unencrypted_cache=True) assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.FilePersistence) @@ -215,11 +215,11 @@ def test_persistent_cache_multiple_clients(): 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 + "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 + "tenant-id", "client-b", "...", enable_persistent_cache=True, transport=transport_b ) assert mock_cache_loader.call_count == 2, "credential should load the persistent 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 24ff4f3dc224..4731f1cb7bc2 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 @@ -184,19 +184,19 @@ def test_enable_persistent_cache(): ClientSecretCredential(*required_arguments) # allowing an unencrypted cache doesn't count as opting in to the persistent cache - ClientSecretCredential(*required_arguments, _allow_unencrypted_cache=True) + 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) + 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) + ClientSecretCredential(*required_arguments, enable_persistent_cache=True) with pytest.raises(NotImplementedError): - ClientSecretCredential(*required_arguments, _enable_persistent_cache=True, _allow_unencrypted_cache=True) + ClientSecretCredential(*required_arguments, enable_persistent_cache=True, allow_unencrypted_cache=True) @patch("azure.identity._internal.persistent_cache.sys.platform", "linux2") @@ -210,7 +210,7 @@ def test_persistent_cache_linux(mock_extensions): 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) + 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() @@ -219,9 +219,9 @@ def test_persistent_cache_linux(mock_extensions): # 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) - ClientSecretCredential(*required_arguments, _enable_persistent_cache=True, _allow_unencrypted_cache=True) + ClientSecretCredential(*required_arguments, enable_persistent_cache=True, allow_unencrypted_cache=True) assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.FilePersistence) @@ -242,11 +242,11 @@ async def test_persistent_cache_multiple_clients(): 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 + "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 + "tenant-id", "client-b", "...", enable_persistent_cache=True, transport=transport_b ) assert mock_cache_loader.call_count == 2, "credential should load the persistent cache" 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 896b3ef4e1b9..f33553dcd6e3 100644 --- a/sdk/identity/azure-identity/tests/test_device_code_credential.py +++ b/sdk/identity/azure-identity/tests/test_device_code_credential.py @@ -6,8 +6,7 @@ from azure.core.exceptions import ClientAuthenticationError from azure.core.pipeline.policies import SansIOHTTPPolicy -from azure.identity import DeviceCodeCredential -from azure.identity._exceptions import AuthenticationRequiredError +from azure.identity import AuthenticationRequiredError, DeviceCodeCredential from azure.identity._internal.user_agent import USER_AGENT from msal import TokenCache import pytest @@ -78,7 +77,7 @@ def test_authenticate(): tenant_id=tenant_id, _cache=TokenCache(), ) - record = credential._authenticate(scopes=(scope,)) + record = credential.authenticate(scopes=(scope,)) assert record.authority == environment assert record.home_account_id == object_id + "." + home_tenant assert record.tenant_id == home_tenant @@ -94,7 +93,7 @@ def test_disable_automatic_authentication(): 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, _cache=empty_cache) with pytest.raises(AuthenticationRequiredError): credential.get_token("scope") diff --git a/sdk/identity/azure-identity/tests/test_imds_credential.py b/sdk/identity/azure-identity/tests/test_imds_credential.py index 8b040b769f55..95f53088b11a 100644 --- a/sdk/identity/azure-identity/tests/test_imds_credential.py +++ b/sdk/identity/azure-identity/tests/test_imds_credential.py @@ -169,7 +169,7 @@ def test_identity_config(): ], ) - credential = ImdsCredential(_identity_config={param_name: param_value}, transport=transport) + credential = ImdsCredential(identity_config={param_name: param_value}, transport=transport) token = credential.get_token(scope) assert token == expected_token diff --git a/sdk/identity/azure-identity/tests/test_imds_credential_async.py b/sdk/identity/azure-identity/tests/test_imds_credential_async.py index 8c42e643a855..a4d056f399fc 100644 --- a/sdk/identity/azure-identity/tests/test_imds_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_imds_credential_async.py @@ -198,7 +198,7 @@ async def test_identity_config(): ], ) - credential = ImdsCredential(client_id=client_id, _identity_config={param_name: param_value}, transport=transport) + credential = ImdsCredential(client_id=client_id, identity_config={param_name: param_value}, transport=transport) token = await credential.get_token(scope) assert token == expected_token diff --git a/sdk/identity/azure-identity/tests/test_interactive_credential.py b/sdk/identity/azure-identity/tests/test_interactive_credential.py index 9573c7623632..645e74f21bd0 100644 --- a/sdk/identity/azure-identity/tests/test_interactive_credential.py +++ b/sdk/identity/azure-identity/tests/test_interactive_credential.py @@ -3,9 +3,12 @@ # Licensed under the MIT License. # ------------------------------------ from azure.core.exceptions import ClientAuthenticationError -from azure.identity import KnownAuthorities, CredentialUnavailableError -from azure.identity._auth_record import AuthenticationRecord -from azure.identity._exceptions import AuthenticationRequiredError +from azure.identity import ( + AuthenticationRequiredError, + AuthenticationRecord, + KnownAuthorities, + CredentialUnavailableError, +) from azure.identity._internal import InteractiveCredential from msal import TokenCache import pytest @@ -64,7 +67,7 @@ def validate_app_parameters(authority, client_id, **_): app_factory = Mock(wraps=validate_app_parameters) credential = MockCredential( - _authentication_record=record, _disable_automatic_authentication=True, msal_app_factory=app_factory, + authentication_record=record, disable_automatic_authentication=True, msal_app_factory=app_factory, ) with pytest.raises(AuthenticationRequiredError): credential.get_token("scope") @@ -87,9 +90,9 @@ def validate_authority(authority, **_): return Mock(get_accounts=Mock(return_value=[])) credential = MockCredential( - _authentication_record=record, + authentication_record=record, tenant_id=expected_tenant, - _disable_automatic_authentication=True, + disable_automatic_authentication=True, msal_app_factory=validate_authority, ) with pytest.raises(AuthenticationRequiredError): @@ -107,8 +110,8 @@ def test_disable_automatic_authentication(): ) credential = MockCredential( - _authentication_record=record, - _disable_automatic_authentication=True, + authentication_record=record, + disable_automatic_authentication=True, msal_app_factory=lambda *_, **__: msal_app, request_token=Mock(side_effect=Exception("credential shouldn't begin interactive authentication")), ) @@ -132,11 +135,11 @@ def validate_scopes(*scopes, **_): return {"access_token": "**", "expires_in": 42} request_token = Mock(wraps=validate_scopes) - credential = MockCredential(_disable_automatic_authentication=True, request_token=request_token) + credential = MockCredential(disable_automatic_authentication=True, request_token=request_token) with pytest.raises(AuthenticationRequiredError) as ex: credential.get_token(scope) - credential._authenticate(scopes=ex.value.scopes) + credential.authenticate(scopes=ex.value.scopes) assert request_token.call_count == 1, "validation method wasn't called" @@ -158,7 +161,7 @@ def validate_scopes(*scopes): return {"access_token": "**", "expires_in": 42} request_token = Mock(wraps=validate_scopes) - MockCredential(authority=authority, request_token=request_token)._authenticate() + MockCredential(authority=authority, request_token=request_token).authenticate() assert request_token.call_count == 1 @@ -166,7 +169,7 @@ def test_authenticate_unknown_cloud(): """authenticate should raise when given no scopes in an unknown cloud""" with pytest.raises(CredentialUnavailableError): - MockCredential(authority="localhost")._authenticate() + MockCredential(authority="localhost").authenticate() @pytest.mark.parametrize("option", (True, False)) @@ -174,7 +177,7 @@ def test_authenticate_ignores_disable_automatic_authentication(option): """authenticate should prompt for authentication regardless of the credential's configuration""" request_token = Mock(return_value={"access_token": "**", "expires_in": 42}) - MockCredential(request_token=request_token, _disable_automatic_authentication=option)._authenticate() + MockCredential(request_token=request_token, disable_automatic_authentication=option).authenticate() assert request_token.call_count == 1, "credential didn't begin interactive authentication" @@ -190,7 +193,7 @@ class CustomException(Exception): acquire_token_silent_with_error=Mock(side_effect=CustomException(expected_message)), get_accounts=Mock(return_value=[{"home_account_id": record.home_account_id}]), ) - credential = MockCredential(msal_app_factory=lambda *_, **__: msal_app, _authentication_record=record) + credential = MockCredential(msal_app_factory=lambda *_, **__: msal_app, authentication_record=record) with pytest.raises(ClientAuthenticationError) as ex: credential.get_token("scope") @@ -220,20 +223,20 @@ def _request_token(self, *_, **__): 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) + 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) + 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) + TestCredential(enable_persistent_cache=True) with pytest.raises(NotImplementedError): - TestCredential(_enable_persistent_cache=True, _allow_unencrypted_cache=True) + TestCredential(enable_persistent_cache=True, allow_unencrypted_cache=True) @patch("azure.identity._internal.persistent_cache.sys.platform", "linux2") @@ -252,7 +255,7 @@ 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) + TestCredential(enable_persistent_cache=True, allow_unencrypted_cache=True) assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.LibsecretPersistence) mock_extensions.PersistedTokenCache.reset_mock() @@ -261,9 +264,9 @@ def _request_token(self, *_, **__): # 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) - TestCredential(_enable_persistent_cache=True, _allow_unencrypted_cache=True) + TestCredential(enable_persistent_cache=True, allow_unencrypted_cache=True) assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.FilePersistence) @@ -289,7 +292,7 @@ def __init__(self, **kwargs): def _request_token(self, *_, **__): return msal_response - record = TestCredential()._authenticate() + record = TestCredential().authenticate() assert record.home_account_id == "{}.{}".format(object_id, home_tenant) @@ -314,5 +317,5 @@ def __init__(self, **kwargs): def _request_token(self, *_, **__): return msal_response - record = TestCredential()._authenticate() + record = TestCredential().authenticate() assert record.home_account_id == subject diff --git a/sdk/identity/azure-identity/tests/test_msi_credential.py b/sdk/identity/azure-identity/tests/test_msi_credential.py index 28f67a357790..7536688774e2 100644 --- a/sdk/identity/azure-identity/tests/test_msi_credential.py +++ b/sdk/identity/azure-identity/tests/test_msi_credential.py @@ -68,7 +68,7 @@ def test_identity_config_app_service(): {EnvironmentVariables.MSI_ENDPOINT: endpoint, EnvironmentVariables.MSI_SECRET: secret}, clear=True, ): - credential = MsiCredential(_identity_config={param_name: param_value}, transport=transport) + credential = MsiCredential(identity_config={param_name: param_value}, transport=transport) token = credential.get_token(scope) assert token == expected_token @@ -107,7 +107,7 @@ def test_identity_config_cloud_shell(): with mock.patch.dict( MsiCredential.__module__ + ".os.environ", {EnvironmentVariables.MSI_ENDPOINT: endpoint}, clear=True ): - credential = MsiCredential(_identity_config={param_name: param_value}, transport=transport) + credential = MsiCredential(identity_config={param_name: param_value}, transport=transport) token = credential.get_token(scope) assert token == expected_token diff --git a/sdk/identity/azure-identity/tests/test_msi_credential_async.py b/sdk/identity/azure-identity/tests/test_msi_credential_async.py index b9ff2bf114f8..dfa038963536 100644 --- a/sdk/identity/azure-identity/tests/test_msi_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_msi_credential_async.py @@ -96,7 +96,7 @@ async def test_identity_config_app_service(): {EnvironmentVariables.MSI_ENDPOINT: endpoint, EnvironmentVariables.MSI_SECRET: secret}, clear=True, ): - credential = MsiCredential(_identity_config={param_name: param_value}, transport=transport) + credential = MsiCredential(identity_config={param_name: param_value}, transport=transport) token = await credential.get_token(scope) assert token == expected_token @@ -135,7 +135,7 @@ async def test_identity_config_cloud_shell(): with mock.patch.dict( MsiCredential.__module__ + ".os.environ", {EnvironmentVariables.MSI_ENDPOINT: endpoint}, clear=True ): - credential = MsiCredential(_identity_config={param_name: param_value}, transport=transport) + credential = MsiCredential(identity_config={param_name: param_value}, transport=transport) token = await credential.get_token(scope) assert token == expected_token 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 e2b1713c9d07..efb5bcc668af 100644 --- a/sdk/identity/azure-identity/tests/test_shared_cache_credential.py +++ b/sdk/identity/azure-identity/tests/test_shared_cache_credential.py @@ -4,8 +4,11 @@ # ------------------------------------ from azure.core.exceptions import ClientAuthenticationError from azure.core.pipeline.policies import SansIOHTTPPolicy -from azure.identity import CredentialUnavailableError, SharedTokenCacheCredential -from azure.identity._auth_record import AuthenticationRecord +from azure.identity import ( + AuthenticationRecord, + CredentialUnavailableError, + SharedTokenCacheCredential, +) from azure.identity._constants import AZURE_CLI_CLIENT_ID, EnvironmentVariables from azure.identity._internal.shared_token_cache import ( KNOWN_ALIASES, @@ -511,7 +514,7 @@ def test_authority_environment_variable(): def test_authentication_record_empty_cache(): record = AuthenticationRecord("tenant_id", "client_id", "authority", "home_account_id", "username") transport = Mock(side_effect=Exception("the credential shouldn't send a request")) - credential = SharedTokenCacheCredential(_authentication_record=record, transport=transport, _cache=TokenCache()) + credential = SharedTokenCacheCredential(authentication_record=record, transport=transport, _cache=TokenCache()) with pytest.raises(CredentialUnavailableError): credential.get_token("scope") @@ -532,7 +535,7 @@ def test_authentication_record_no_match(): "not-" + username, "not-" + object_id, "different-" + tenant_id, client_id="not-" + client_id, ), ) - credential = SharedTokenCacheCredential(_authentication_record=record, transport=transport, _cache=cache) + credential = SharedTokenCacheCredential(authentication_record=record, transport=transport, _cache=cache) with pytest.raises(CredentialUnavailableError): credential.get_token("scope") @@ -558,7 +561,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, _cache=cache) token = credential.get_token("scope") assert token.token == expected_access_token @@ -594,13 +597,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, _cache=cache) token = credential.get_token("scope") assert token.token == expected_access_token -@pytest.mark.skip("in 1.4.0 allow_unencrypted_cache is private and defaults to True") @patch("azure.identity._internal.persistent_cache.sys.platform", "linux2") @patch("azure.identity._internal.persistent_cache.msal_extensions") def test_allow_unencrypted_cache(mock_extensions): @@ -610,7 +612,7 @@ def test_allow_unencrypted_cache(mock_extensions): """ # the credential should prefer an encrypted cache even when the user allows an unencrypted one - SharedTokenCacheCredential(_allow_unencrypted_cache=True) + SharedTokenCacheCredential(allow_unencrypted_cache=True) assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.LibsecretPersistence) mock_extensions.PersistedTokenCache.reset_mock() @@ -623,7 +625,7 @@ def test_allow_unencrypted_cache(mock_extensions): assert mock_extensions.PersistedTokenCache.call_count == 0 # still no encryption, but now we allow the unencrypted fallback - SharedTokenCacheCredential(_allow_unencrypted_cache=True) + SharedTokenCacheCredential(allow_unencrypted_cache=True) assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.FilePersistence) @@ -743,7 +745,7 @@ def test_authentication_record_authenticating_tenant(): with patch.object(SharedTokenCacheCredential, "_get_auth_client") as get_auth_client: credential = SharedTokenCacheCredential( - _authentication_record=record, _cache=TokenCache(), tenant_id=expected_tenant_id + authentication_record=record, _cache=TokenCache(), tenant_id=expected_tenant_id ) with pytest.raises(CredentialUnavailableError): # this raises because the cache is empty 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 518913db4eeb..389ba606d482 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 @@ -7,8 +7,8 @@ from azure.core.exceptions import ClientAuthenticationError from azure.core.pipeline.policies import SansIOHTTPPolicy -from azure.identity import CredentialUnavailableError -from azure.identity._auth_record import AuthenticationRecord +from azure.identity import AuthenticationRecord, CredentialUnavailableError +from azure.identity.aio import SharedTokenCacheCredential from azure.identity._constants import EnvironmentVariables from azure.identity._internal.shared_token_cache import ( KNOWN_ALIASES, @@ -18,7 +18,6 @@ NO_MATCHING_ACCOUNTS, ) from azure.identity._internal.user_agent import USER_AGENT -from azure.identity.aio import SharedTokenCacheCredential from msal import TokenCache import pytest @@ -594,7 +593,7 @@ async def test_authority_environment_variable(): async def test_authentication_record_empty_cache(): record = AuthenticationRecord("tenant_id", "client_id", "authority", "home_account_id", "username") transport = Mock(side_effect=Exception("the credential shouldn't send a request")) - credential = SharedTokenCacheCredential(_authentication_record=record, transport=transport, _cache=TokenCache()) + credential = SharedTokenCacheCredential(authentication_record=record, transport=transport, _cache=TokenCache()) with pytest.raises(CredentialUnavailableError): await credential.get_token("scope") @@ -616,7 +615,7 @@ async def test_authentication_record_no_match(): "not-" + username, "not-" + object_id, "different-" + tenant_id, client_id="not-" + client_id, ), ) - credential = SharedTokenCacheCredential(_authentication_record=record, transport=transport, _cache=cache) + credential = SharedTokenCacheCredential(authentication_record=record, transport=transport, _cache=cache) with pytest.raises(CredentialUnavailableError): await credential.get_token("scope") @@ -643,7 +642,7 @@ async 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, _cache=cache) token = await credential.get_token("scope") assert token.token == expected_access_token @@ -680,7 +679,7 @@ async 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, _cache=cache) token = await credential.get_token("scope") assert token.token == expected_access_token @@ -695,7 +694,7 @@ async def test_authentication_record_authenticating_tenant(): with patch.object(SharedTokenCacheCredential, "_get_auth_client") as get_auth_client: credential = SharedTokenCacheCredential( - _authentication_record=record, _cache=TokenCache(), tenant_id=expected_tenant_id + authentication_record=record, _cache=TokenCache(), tenant_id=expected_tenant_id ) with pytest.raises(CredentialUnavailableError): # this raises because the cache is empty @@ -706,7 +705,6 @@ async def test_authentication_record_authenticating_tenant(): assert kwargs["tenant_id"] == expected_tenant_id -@pytest.mark.skip("in 1.4.0 allow_unencrypted_cache is private and defaults to True") @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. @@ -721,7 +719,7 @@ async def test_allow_unencrypted_cache(): 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) + SharedTokenCacheCredential(allow_unencrypted_cache=True) assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.LibsecretPersistence) mock_extensions.PersistedTokenCache.reset_mock() @@ -730,12 +728,12 @@ async def test_allow_unencrypted_cache(): # 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") - assert mock_extensions.PersistedTokenCache.call_count == 0 # still no encryption, but now we allow the unencrypted fallback - SharedTokenCacheCredential(_allow_unencrypted_cache=True) + SharedTokenCacheCredential(allow_unencrypted_cache=True) assert mock_extensions.PersistedTokenCache.called_with(mock_extensions.FilePersistence) msal_extensions_patch.stop() diff --git a/sdk/identity/azure-identity/tests/test_username_password_credential.py b/sdk/identity/azure-identity/tests/test_username_password_credential.py index 6a93b79d7827..f82d251090b0 100644 --- a/sdk/identity/azure-identity/tests/test_username_password_credential.py +++ b/sdk/identity/azure-identity/tests/test_username_password_credential.py @@ -125,7 +125,7 @@ def test_authenticate(): tenant_id=tenant_id, transport=transport, ) - record = credential._authenticate(scopes=(scope,)) + record = credential.authenticate(scopes=(scope,)) assert record.authority == environment assert record.home_account_id == object_id + "." + home_tenant assert record.tenant_id == home_tenant From b493cac6005b476a41114b70e79a857342891e2e Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 12 Aug 2020 10:30:10 -0700 Subject: [PATCH 2/2] update package metadata --- sdk/identity/azure-identity/azure/identity/_version.py | 2 +- sdk/identity/azure-identity/setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_version.py b/sdk/identity/azure-identity/azure/identity/_version.py index a5b3f7bf1cb2..f336d90e0fae 100644 --- a/sdk/identity/azure-identity/azure/identity/_version.py +++ b/sdk/identity/azure-identity/azure/identity/_version.py @@ -2,4 +2,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -VERSION = "1.4.1" +VERSION = "1.5.0b1" diff --git a/sdk/identity/azure-identity/setup.py b/sdk/identity/azure-identity/setup.py index 4b209061a771..2e6f1ea22af6 100644 --- a/sdk/identity/azure-identity/setup.py +++ b/sdk/identity/azure-identity/setup.py @@ -51,7 +51,7 @@ author_email="azpysdkhelp@microsoft.com", url="https://github.com/Azure/azure-sdk-for-python/tree/master/sdk/identity/azure-identity", classifiers=[ - "Development Status :: 5 - Production/Stable", + "Development Status :: 4 - Beta", "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7",