diff --git a/src/azure-cli-core/azure/cli/core/_identity.py b/src/azure-cli-core/azure/cli/core/_identity.py index 80b085579fe..31810f7ef4d 100644 --- a/src/azure-cli-core/azure/cli/core/_identity.py +++ b/src/azure-cli-core/azure/cli/core/_identity.py @@ -85,7 +85,7 @@ def login_with_interactive_browser(self): 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: \ + prompt_callback = lambda verification_uri, user_code, expires_on: \ logger.warning(message.format(verification_uri, user_code)) if self.tenant_id: cred, auth_profile = DeviceCodeCredential.authenticate(client_id=_CLIENT_ID, @@ -115,7 +115,7 @@ def login_with_service_principal_secret(self, client_id, client_secret): # https://github.com/AzureAD/microsoft-authentication-extensions-for-python/pull/44 sp_auth = ServicePrincipalAuth(client_id, self.tenant_id, secret=client_secret) entry = sp_auth.get_entry_to_persist() - cred_cache = ServicePrincipalCredentialCache() + cred_cache = ADALCredentialCache() cred_cache.save_service_principal_cred(entry) credential = ClientSecretCredential(self.tenant_id, client_id, client_secret, authority=self.authority) @@ -127,7 +127,7 @@ def login_with_service_principal_certificate(self, client_id, certificate_path): # https://github.com/AzureAD/microsoft-authentication-extensions-for-python/pull/44 sp_auth = ServicePrincipalAuth(client_id, self.tenant_id, certificate_file=certificate_path) entry = sp_auth.get_entry_to_persist() - cred_cache = ServicePrincipalCredentialCache() + cred_cache = ADALCredentialCache() cred_cache.save_service_principal_cred(entry) # TODO: support use_cert_sn_issuer in CertificateCredential @@ -143,7 +143,7 @@ def get_user_credential(self, home_account_id, username): return InteractiveBrowserCredential(profile=auth_profile, silent_auth_only=True) def get_service_principal_credential(self, client_id, use_cert_sn_issuer): - cred_cache = ServicePrincipalCredentialCache() + cred_cache = ADALCredentialCache() client_secret, certificate_path = cred_cache.retrieve_secret_of_service_principal(client_id, self.tenant_id) # TODO: support use_cert_sn_issuer in CertificateCredential if client_secret: @@ -158,16 +158,26 @@ def get_msi_credential(client_id=None): return ManagedIdentityCredential(client_id=client_id) -class ServicePrincipalCredentialCache: - """Caches service principal secrets, and persistence will also be handled +TOKEN_FIELDS_EXCLUDED_FROM_PERSISTENCE = ['familyName', + 'givenName', + 'isUserIdDisplayable', + 'tenantId'] +_TOKEN_ENTRY_USER_ID = 'userId' + + +class ADALCredentialCache: + """Caches secrets in ADAL format, will be deprecated """ - # TODO: Persist to encrypted cache - def __init__(self, async_persist=True): + + # TODO: Persist SP to encrypted cache + def __init__(self, async_persist=True, cli_ctx=None): # AZURE_ACCESS_TOKEN_FILE is used by Cloud Console and not meant to be user configured self._token_file = (os.environ.get('AZURE_ACCESS_TOKEN_FILE', None) or os.path.join(get_config_dir(), 'accessTokens.json')) self._service_principal_creds = [] + self._adal_token_cache_attr = None self._should_flush_to_disk = False + self._cli_ctx = cli_ctx self._async_persist = async_persist if async_persist: import atexit @@ -182,7 +192,16 @@ def flush_to_disk(self): if self._should_flush_to_disk: with os.fdopen(os.open(self._token_file, os.O_RDWR | os.O_CREAT | os.O_TRUNC, 0o600), 'w+') as cred_file: - cred_file.write(json.dumps(self._service_principal_creds)) + items = self.adal_token_cache.read_items() + all_creds = [entry for _, entry in items] + + # trim away useless fields (needed for cred sharing with xplat) + for i in all_creds: + for key in TOKEN_FIELDS_EXCLUDED_FROM_PERSISTENCE: + i.pop(key, None) + + all_creds.extend(self._service_principal_creds) + cred_file.write(json.dumps(all_creds)) def retrieve_secret_of_service_principal(self, sp_id, tenant): self.load_service_principal_creds() @@ -199,7 +218,7 @@ def retrieve_secret_of_service_principal(self, sp_id, tenant): "Trying credential under tenant %s, assuming that is an app credential.", sp_id, tenant, matched[0][_SERVICE_PRINCIPAL_TENANT]) cred = matched[0] - return cred.get(_ACCESS_TOKEN, None), cred.get(_SERVICE_PRINCIPAL_CERT_FILE, None) + return cred.get(_ACCESS_TOKEN, None), cred.get(_SERVICE_PRINCIPAL_CERT_FILE, None) def save_service_principal_cred(self, sp_entry): self.load_service_principal_creds() @@ -210,7 +229,8 @@ def save_service_principal_cred(self, sp_entry): if matched: # pylint: disable=line-too-long if (sp_entry.get(_ACCESS_TOKEN, None) != matched[0].get(_ACCESS_TOKEN, None) or - sp_entry.get(_SERVICE_PRINCIPAL_CERT_FILE, None) != matched[0].get(_SERVICE_PRINCIPAL_CERT_FILE, None)): + sp_entry.get(_SERVICE_PRINCIPAL_CERT_FILE, None) != matched[0].get(_SERVICE_PRINCIPAL_CERT_FILE, + None)): self._service_principal_creds.remove(matched[0]) self._service_principal_creds.append(sp_entry) state_changed = True @@ -221,18 +241,70 @@ def save_service_principal_cred(self, sp_entry): if state_changed: self.persist_cached_creds() - def load_service_principal_creds(self): - creds = _load_tokens_from_file(self._token_file) + # noinspection PyBroadException + def add_credential(self, credential): + try: + query = { + "client_id": _CLIENT_ID, + "environment": credential._profile.environment, + "home_account_id": credential._profile.home_account_id + } + refresh_token = credential._cache.find( + credential._cache.CredentialType.REFRESH_TOKEN, + # target=scopes, # AAD RTs are scope-independent + query=query) + access_token = credential.get_token(self._cli_ctx.cloud.endpoints.active_directory_resource_id.rstrip('/') + + '/.default') + import datetime + entry = { + "tokenType": "Bearer", + "expiresOn": datetime.datetime.fromtimestamp(access_token.expires_on).strftime("%Y-%m-%d %H:%M:%S.%f"), + "resource": self._cli_ctx.cloud.endpoints.active_directory_resource_id, + "userId": credential._profile.username, + "accessToken": access_token.token, + "refreshToken": refresh_token[0]['secret'], + "_clientId": _CLIENT_ID, + "_authority": self._cli_ctx.cloud.endpoints.active_directory.rstrip('/') + + "/" + credential._profile.tenant_id + } + self.adal_token_cache.add([entry]) + except Exception as e: + logger.debug("Failed to store ADAL token: {}".format(e)) + # swallow all errors since it does not impact az + + @property + def adal_token_cache(self): + return self.load_adal_token_cache() + + def load_adal_token_cache(self): + if self._adal_token_cache_attr is None: + import adal + all_entries = _load_tokens_from_file(self._token_file) + self.load_service_principal_creds(all_entries=all_entries) + real_token = [x for x in all_entries if x not in self._service_principal_creds] + self._adal_token_cache_attr = adal.TokenCache(json.dumps(real_token)) + return self._adal_token_cache_attr + + def load_service_principal_creds(self, **kwargs): + creds = kwargs.pop("all_entries", None) + if not creds: + creds = _load_tokens_from_file(self._token_file) for c in creds: if c.get(_SERVICE_PRINCIPAL_ID): self._service_principal_creds.append(c) return self._service_principal_creds - def remove_cached_creds(self, sp): + def remove_cached_creds(self, user_or_sp): state_changed = False + # clear AAD tokens + tokens = self.adal_token_cache.find({_TOKEN_ENTRY_USER_ID: user_or_sp}) + if tokens: + state_changed = True + self.adal_token_cache.remove(tokens) + # clear service principal creds matched = [x for x in self._service_principal_creds - if x[_SERVICE_PRINCIPAL_ID] == sp] + if x[_SERVICE_PRINCIPAL_ID] == user_or_sp] if matched: state_changed = True self._service_principal_creds = [x for x in self._service_principal_creds diff --git a/src/azure-cli-core/azure/cli/core/_profile.py b/src/azure-cli-core/azure/cli/core/_profile.py index c68eba2f599..3dcedd96432 100644 --- a/src/azure-cli-core/azure/cli/core/_profile.py +++ b/src/azure-cli-core/azure/cli/core/_profile.py @@ -100,8 +100,6 @@ def _get_cloud_console_token_endpoint(): # pylint: disable=too-many-lines,too-many-instance-attributes class Profile(object): - _global_creds_cache = None - def __init__(self, storage=None, auth_ctx_factory=None, use_global_creds_cache=True, async_persist=True, cli_ctx=None): from azure.cli.core import get_default_cli @@ -111,9 +109,7 @@ def __init__(self, storage=None, auth_ctx_factory=None, use_global_creds_cache=T self._management_resource_uri = self.cli_ctx.cloud.endpoints.management self._ad_resource_uri = self.cli_ctx.cloud.endpoints.active_directory_resource_id - self._msal_scope = self.cli_ctx.cloud.endpoints.active_directory_resource_id + '/.default' self._ad = self.cli_ctx.cloud.endpoints.active_directory - self._msi_creds = None def login(self, interactive, @@ -131,9 +127,10 @@ def login(self, auth_profile=None authority = self.cli_ctx.cloud.endpoints.active_directory.replace('https://', '') identity = Identity(authority, tenant) + adal_cache = ADALCredentialCache(cli_ctx=self.cli_ctx) if not subscription_finder: - subscription_finder = SubscriptionFinder(self.cli_ctx) + subscription_finder = SubscriptionFinder(self.cli_ctx, adal_cache=adal_cache) if interactive: if not use_device_code and (in_cloud_console() or not can_launch_browser()): logger.info('Detect no GUI is available, so fall back to device code') @@ -149,6 +146,8 @@ def login(self, if use_device_code: credential, auth_profile = identity.login_with_device_code() + # todo: remove after ADAL token deprecation + adal_cache.add_credential(credential) else: if is_service_principal: if not tenant: @@ -197,6 +196,8 @@ def login(self, home_account_id=home_account_id) self._set_subscriptions(consolidated) + # todo: remove after ADAL token deprecation + adal_cache.persist_cached_creds() # use deepcopy as we don't want to persist these changes to file. return deepcopy(consolidated) @@ -710,13 +711,14 @@ def msi_auth_factory(cli_account_name, identity, resource): class SubscriptionFinder(object): # An ARM client. It finds subscriptions for a user or service principal. It shouldn't do any # authentication work, but only find subscriptions - def __init__(self, cli_ctx, arm_client_factory=None): + def __init__(self, cli_ctx, arm_client_factory=None, **kwargs): self.user_id = None # will figure out after log user in self.cli_ctx = cli_ctx self.secret = None self._graph_resource_id = cli_ctx.cloud.endpoints.active_directory_resource_id self.authority = self.cli_ctx.cloud.endpoints.active_directory.replace('https://', '') + self.adal_cache = kwargs.pop("adal_cache", None) def create_arm_client_factory(credentials): if arm_client_factory: @@ -765,7 +767,9 @@ 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) - + # todo: remove after ADAL deprecation + if self.adal_cache: + self.adal_cache.add_credential(specific_tenant_credential) # TODO: handle MSAL exceptions except adal.AdalError as ex: # because user creds went through the 'common' tenant, the error here must be