Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 57 additions & 66 deletions src/azure-cli-core/azure/cli/core/_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from knack.log import get_logger

from azure.identity import (
AuthProfile,
AuthenticationRecord,
InteractiveBrowserCredential,
DeviceCodeCredential,
UsernamePasswordCredential,
Expand Down Expand Up @@ -54,12 +54,27 @@ class Identity:
"""Class to interact with Azure Identity.
"""

def __init__(self, authority=None, tenant_id=None, **kwargs):
def __init__(self, authority=None, tenant_id=None, client_id=_CLIENT_ID, **kwargs):
self.authority = authority
self.tenant_id = tenant_id
self.client_id = client_id
self._cred_cache = kwargs.pop('cred_cache', None)
# todo: MSAL support force encryption
self._msal_store = MSALSecretStore(True)
self.allow_unencrypted = True

# Initialize _msal_app for logout, since Azure Identity doesn't provide the functionality for logout
from msal import PublicClientApplication
# sdk/identity/azure-identity/azure/identity/_internal/msal_credentials.py:95
from azure.identity._internal.persistent_cache import load_persistent_cache

# Store for user token persistence
cache = load_persistent_cache(self.allow_unencrypted)
authority = "https://{}/organizations".format(self.authority)
self._msal_app = PublicClientApplication(authority=authority, client_id=_CLIENT_ID, token_cache=cache)

# Store for Service principal credential persistence
self._msal_store = MSALSecretStore(fallback_to_plaintext=self.allow_unencrypted)

# TODO: Allow disabling SSL verification
# The underlying requests lib of MSAL has been patched with Azure Core by MsalTransportAdapter
# connection_verify will be received by azure.core.configuration.ConnectionConfiguration
Expand All @@ -70,51 +85,44 @@ def __init__(self, authority=None, tenant_id=None, **kwargs):

def login_with_interactive_browser(self):
# Use InteractiveBrowserCredential
if self.tenant_id:
credential, auth_profile = InteractiveBrowserCredential.authenticate(
client_id=_CLIENT_ID,
authority=self.authority,
tenant_id=self.tenant_id
)
else:
credential, auth_profile = InteractiveBrowserCredential.authenticate(
authority=self.authority,
client_id=_CLIENT_ID
)
credential = InteractiveBrowserCredential(authority=self.authority,
tenant_id=self.tenant_id,
client_id=self.client_id,
enable_persistent_cache=True)
auth_record = credential.authenticate()
# todo: remove after ADAL token deprecation
self._cred_cache.add_credential(credential)
return credential, auth_profile
return credential, auth_record

def login_with_device_code(self):
# Use DeviceCodeCredential
message = 'To sign in, use a web browser to open the page {} and enter the code {} to authenticate.'
prompt_callback = lambda verification_uri, user_code, expires_on: \
def prompt_callback(verification_uri, user_code, _):
# expires_on is discarded
message = 'To sign in, use a web browser to open the page {} and enter the code {} to authenticate.'
logger.warning(message.format(verification_uri, user_code))
if self.tenant_id:
cred, auth_profile = DeviceCodeCredential.authenticate(client_id=_CLIENT_ID,
authority=self.authority,
tenant_id=self.tenant_id,
prompt_callback=prompt_callback)
else:
cred, auth_profile = DeviceCodeCredential.authenticate(client_id=_CLIENT_ID,
authority=self.authority,
prompt_callback=prompt_callback)

credential = DeviceCodeCredential(authority=self.authority,
tenant_id=self.tenant_id,
client_id=self.client_id,
enable_persistent_cache=True,
prompt_callback=prompt_callback)
auth_record = credential.authenticate()
# todo: remove after ADAL token deprecation
self._cred_cache.add_credential(cred)
return cred, auth_profile
self._cred_cache.add_credential(credential)
return credential, auth_record

def login_with_username_password(self, username, password):
# Use UsernamePasswordCredential
if self.tenant_id:
credential, auth_profile = UsernamePasswordCredential.authenticate(
_CLIENT_ID, username, password, authority=self.authority, tenant_id=self.tenant_id,
**self.ssl_kwargs)
else:
credential, auth_profile = UsernamePasswordCredential.authenticate(
_CLIENT_ID, username, password, authority=self.authority, **self.ssl_kwargs)
credential = UsernamePasswordCredential(authority=self.authority,
tenant_id=self.tenant_id,
client_id=self.client_id,
username=username,
password=password)
auth_record = credential.au

# todo: remove after ADAL token deprecation
self._cred_cache.add_credential(credential)
return credential, auth_profile
return credential, auth_record

def login_with_service_principal_secret(self, client_id, client_secret):
# Use ClientSecretCredential
Expand Down Expand Up @@ -222,59 +230,41 @@ def login_with_managed_identity(self, identity_id, resource):
return credential, managed_identity_info

def get_user(self, user_or_sp=None):
from msal import PublicClientApplication
# sdk/identity/azure-identity/azure/identity/_internal/msal_credentials.py:122
from azure.identity._internal.msal_credentials import _load_cache
cache = _load_cache()
authority = "https://{}/organizations".format(self.authority)
app = PublicClientApplication(authority=authority, client_id=_CLIENT_ID, token_cache=cache)
return app.get_accounts(user_or_sp)
return self._msal_app.get_accounts(user_or_sp)

def logout_user(self, user_or_sp):
from msal import PublicClientApplication
# sdk/identity/azure-identity/azure/identity/_internal/msal_credentials.py:122
from azure.identity._internal.msal_credentials import _load_cache
cache = _load_cache()
authority = "https://{}/organizations".format(self.authority)
app = PublicClientApplication(authority=authority, client_id=_CLIENT_ID, token_cache=cache)

accounts = app.get_accounts(user_or_sp)
accounts = self._msal_app.get_accounts(user_or_sp)
logger.info('Before account removal:')
logger.info(json.dumps(accounts))

for account in accounts:
app.remove_account(account)
self._msal_app.remove_account(account)

accounts = app.get_accounts(user_or_sp)
accounts = self._msal_app.get_accounts(user_or_sp)
logger.info('After account removal:')
logger.info(json.dumps(accounts))
# remove service principal secrets
self._msal_store.remove_cached_creds(user_or_sp)

def logout_all(self):
from msal import PublicClientApplication
from azure.identity._internal.msal_credentials import _load_cache
cache = _load_cache()
# TODO: Support multi-authority logout
authority = "https://{}/organizations".format(self.authority)
app = PublicClientApplication(authority=authority, client_id=_CLIENT_ID, token_cache=cache)

accounts = app.get_accounts()
accounts = self._msal_app.get_accounts()
logger.info('Before account removal:')
logger.info(json.dumps(accounts))

for account in accounts:
app.remove_account(account)
self._msal_app.remove_account(account)

accounts = app.get_accounts()
accounts = self._msal_app.get_accounts()
logger.info('After account removal:')
logger.info(json.dumps(accounts))
# remove service principal secrets
self._msal_store.remove_all_cached_creds()

def get_user_credential(self, home_account_id, username):
auth_profile = AuthProfile(self.authority, home_account_id, self.tenant_id, username)
return InteractiveBrowserCredential(profile=auth_profile, silent_auth_only=True)
auth_record = AuthenticationRecord(self.tenant_id, self.client_id, self.authority,
home_account_id, username)
return InteractiveBrowserCredential(authentication_record=auth_record, disable_automatic_authentication=True,
Copy link
Member Author

@jiasli jiasli May 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InteractiveCredential/PublicClientCredential which is the base class of InteractiveBrowserCredential and DeviceCodeCredential should be exposed in azure/identity/__init__.py.

CLI won't know which type the user used to login before. Choosing any one of InteractiveBrowserCredential and DeviceCodeCredential works, yes, but this is not a good programming practice as we are using the base class from sub class. See Teams thread: InteractiveCredential should be exposed

image

enable_persistent_cache=True)

def get_service_principal_credential(self, client_id, use_cert_sn_issuer):
client_secret, certificate_path = self._msal_store.retrieve_secret_of_service_principal(client_id, self.tenant_id)
Expand Down Expand Up @@ -494,7 +484,7 @@ def get_entry_to_persist(self):

class MSALSecretStore:
"""Caches secrets in MSAL custom secret store
"""
"""
def __init__(self, fallback_to_plaintext=True):
self._token_file = (os.environ.get('AZURE_MSAL_TOKEN_FILE', None) or
os.path.join(get_config_dir(), 'msalCustomToken.bin'))
Expand Down Expand Up @@ -574,6 +564,7 @@ def _load_cached_creds(self):
pass

def _build_persistence(self):
# https://github.com/AzureAD/microsoft-authentication-extensions-for-python/blob/0.2.2/sample/persistence_sample.py
from msal_extensions import FilePersistenceWithDataProtection,\
KeychainPersistence,\
LibsecretPersistence,\
Expand Down
24 changes: 12 additions & 12 deletions src/azure-cli-core/azure/cli/core/_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def login(self,
find_subscriptions=True):

credential=None
auth_profile=None
auth_record=None
identity = Identity(self._authority, tenant, cred_cache=self._adal_cache)

if not subscription_finder:
Expand All @@ -139,13 +139,13 @@ def login(self,
if not use_device_code:
from azure.identity import CredentialUnavailableError
try:
credential, auth_profile = identity.login_with_interactive_browser()
credential, auth_record = identity.login_with_interactive_browser()
except CredentialUnavailableError:
use_device_code = True
logger.warning('Not able to launch a browser to log you in, falling back to device code...')

if use_device_code:
credential, auth_profile = identity.login_with_device_code()
credential, auth_record = identity.login_with_device_code()
else:
if is_service_principal:
if not tenant:
Expand All @@ -155,15 +155,15 @@ def login(self,
else:
credential = identity.login_with_service_principal_secret(username, password)
else:
credential, auth_profile = identity.login_with_username_password(username, password)
credential, auth_record = identity.login_with_username_password(username, password)

# List tenants and find subscriptions by calling ARM
subscriptions = []
if find_subscriptions:
if tenant and credential:
subscriptions = subscription_finder.find_using_specific_tenant(tenant, credential)
elif credential and auth_profile:
subscriptions = subscription_finder.find_using_common_tenant(auth_profile, credential)
elif credential and auth_record:
subscriptions = subscription_finder.find_using_common_tenant(auth_record, credential)
if not allow_no_subscriptions and not subscriptions:
if username:
msg = "No subscriptions found for {}.".format(username)
Expand All @@ -181,13 +181,13 @@ def login(self,
if not subscriptions:
return []
else:
bare_tenant = tenant or auth_profile.tenant_id
bare_tenant = tenant or auth_record.tenant_id
subscriptions = self._build_tenant_level_accounts([bare_tenant])

home_account_id = None
if auth_profile:
username = auth_profile.username
home_account_id = auth_profile.home_account_id
if auth_record:
username = auth_record.username
home_account_id = auth_record.home_account_id

consolidated = self._normalize_properties(username, subscriptions,
is_service_principal, bool(use_cert_sn_issuer),
Expand Down Expand Up @@ -781,7 +781,7 @@ def find_from_raw_token(self, tenant, token):
self.tenants = [tenant]
return result

def find_using_common_tenant(self, auth_profile, credential=None):
def find_using_common_tenant(self, auth_record, credential=None):
import adal
all_subscriptions = []
empty_tenants = []
Expand All @@ -803,7 +803,7 @@ def find_using_common_tenant(self, auth_profile, credential=None):

identity = Identity(self.authority, tenant_id)
try:
specific_tenant_credential = identity.get_user_credential(auth_profile.home_account_id, auth_profile.username)
specific_tenant_credential = identity.get_user_credential(auth_record.home_account_id, auth_record.username)
# todo: remove after ADAL deprecation
if self.adal_cache:
self.adal_cache.add_credential(specific_tenant_credential)
Expand Down
6 changes: 3 additions & 3 deletions src/azure-cli-core/azure/cli/core/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@ def _get_token(self, *scopes):
if in_cloud_console():
AuthenticationWrapper._log_hostname()
raise err
except ClientAuthenticationError:
except ClientAuthenticationError as err:
# pylint: disable=no-member
if in_cloud_console():
AuthenticationWrapper._log_hostname()

raise CLIError("Credentials have expired due to inactivity or "
"configuration of your account was changed.{}".format(
"Please run 'az login'" if not in_cloud_console() else ''))
"configuration of your account was changed. {}Error details: {}".format(
"Please run 'az login'. " if not in_cloud_console() else '', err))
# todo: error type
# err = (getattr(err, 'error_response', None) or {}).get('error_description') or ''
# if 'AADSTS70008' in err: # all errors starting with 70008 should be creds expiration related
Expand Down