Skip to content

Commit

Permalink
Restore user authentication API from 1.4.0b7 (#13070)
Browse files Browse the repository at this point in the history
  • Loading branch information
chlowell authored Aug 13, 2020
1 parent 58cdc8a commit 805dd17
Show file tree
Hide file tree
Showing 38 changed files with 289 additions and 117 deletions.
7 changes: 4 additions & 3 deletions sdk/identity/azure-identity/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions sdk/identity/azure-identity/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
recursive-include samples *.py
recursive-include tests *.py
include *.md
include azure/__init__.py
5 changes: 4 additions & 1 deletion sdk/identity/azure-identity/azure/identity/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -24,6 +25,8 @@


__all__ = [
"AuthenticationRecord",
"AuthenticationRequiredError",
"AuthorizationCodeCredential",
"AzureAuthorityHosts",
"AzureCliCredential",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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).
"""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion sdk/identity/azure-identity/azure/identity/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# ------------------------------------
VERSION = "1.4.1"
VERSION = "1.5.0b1"
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
37 changes: 37 additions & 0 deletions sdk/identity/azure-identity/samples/README.md
Original file line number Diff line number Diff line change
@@ -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 |
38 changes: 38 additions & 0 deletions sdk/identity/azure-identity/samples/control_interactive_prompts.py
Original file line number Diff line number Diff line change
@@ -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()]
Loading

0 comments on commit 805dd17

Please sign in to comment.