diff --git a/src/azure-cli-core/azure/cli/core/_identity.py b/src/azure-cli-core/azure/cli/core/_identity.py index 582f19a2fad..292ac7c65e0 100644 --- a/src/azure-cli-core/azure/cli/core/_identity.py +++ b/src/azure-cli-core/azure/cli/core/_identity.py @@ -13,7 +13,7 @@ from knack.log import get_logger from azure.identity import ( - AuthProfile, + AuthenticationRecord, InteractiveBrowserCredential, DeviceCodeCredential, UsernamePasswordCredential, @@ -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 @@ -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 @@ -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, + 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) @@ -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')) @@ -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,\ diff --git a/src/azure-cli-core/azure/cli/core/_profile.py b/src/azure-cli-core/azure/cli/core/_profile.py index 1b49570f0fe..624982dc420 100644 --- a/src/azure-cli-core/azure/cli/core/_profile.py +++ b/src/azure-cli-core/azure/cli/core/_profile.py @@ -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: @@ -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: @@ -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) @@ -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), @@ -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 = [] @@ -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) diff --git a/src/azure-cli-core/azure/cli/core/authentication.py b/src/azure-cli-core/azure/cli/core/authentication.py index 18ea00edb7e..bc45b667945 100644 --- a/src/azure-cli-core/azure/cli/core/authentication.py +++ b/src/azure-cli-core/azure/cli/core/authentication.py @@ -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