diff --git a/src/azure-cli-core/azure/cli/core/auth/identity.py b/src/azure-cli-core/azure/cli/core/auth/identity.py index 91629e89441..cc4ebd0e542 100644 --- a/src/azure-cli-core/azure/cli/core/auth/identity.py +++ b/src/azure-cli-core/azure/cli/core/auth/identity.py @@ -211,7 +211,7 @@ def logout_all_users(self): self._msal_app.remove_account(account) # Also remove token cache file - for e in file_extensions.values(): + for e in file_extensions: _try_remove(self._token_cache_file + e) def logout_service_principal(self, client_id): @@ -229,7 +229,7 @@ def logout_all_service_principal(self): # remove service principal secrets # TODO: As MSAL provides no interface to get all service principals in its token cache, this method can't # clear all service principals' access tokens from MSAL token cache. - for e in file_extensions.values(): + for e in file_extensions: _try_remove(self._secret_file + e) def get_user(self, user=None): diff --git a/src/azure-cli-core/azure/cli/core/auth/persistence.py b/src/azure-cli-core/azure/cli/core/auth/persistence.py index eb51a82660c..5f9a7388708 100644 --- a/src/azure-cli-core/azure/cli/core/auth/persistence.py +++ b/src/azure-cli-core/azure/cli/core/auth/persistence.py @@ -20,36 +20,64 @@ logger = get_logger(__name__) # Files extensions for encrypted and plaintext persistence -file_extensions = {True: '.bin', False: '.json'} +file_extension_encrypted = '.bin' +file_extension_plaintext = '.json' +file_extension_signal = '.sig' +file_extensions = [file_extension_encrypted, file_extension_plaintext, file_extension_signal] +KEYCHAIN_SERVICE_NAME = 'Microsoft Azure CLI' +LIBSECRET_SCHEMA_NAME = 'Microsoft Azure CLI' def load_persisted_token_cache(location, encrypt): - persistence = build_persistence(location, encrypt) + persistence = build_persistence(location, encrypt, type="Token cache") return PersistedTokenCache(persistence) def load_secret_store(location, encrypt): - persistence = build_persistence(location, encrypt) + persistence = build_persistence(location, encrypt, type="Secret store") return SecretStore(persistence) -def build_persistence(location, encrypt): +def build_persistence(location, encrypt, type=None): """Build a suitable persistence instance based your current OS""" - location += file_extensions[encrypt] - logger.debug("build_persistence: location=%r, encrypt=%r", location, encrypt) + logger.debug("build_persistence: location=%r, encrypt=%r, type=%r", location, encrypt, type) if encrypt: if sys.platform.startswith('win'): - return FilePersistenceWithDataProtection(location) + # For FilePersistenceWithDataProtection, location is where the credential is stored. + path = location + file_extension_encrypted + logger.debug("Initializing FilePersistenceWithDataProtection: location=%r", path) + return FilePersistenceWithDataProtection(path) if sys.platform.startswith('darwin'): - return KeychainPersistence(location, "my_service_name", "my_account_name") + # For KeychainPersistence, location is only used as a signal for the credential's last modified time. + # The credential is stored in Keychain identified by (service_name, account_name) combination. + # msal-extensions automatically computes account_name from signal_location. + # https://github.com/AzureAD/microsoft-authentication-extensions-for-python/pull/103 + path = location + file_extension_signal + logger.debug("Initializing KeychainPersistence: location=%r", path) + return KeychainPersistence(path, service_name=KEYCHAIN_SERVICE_NAME, account_name=type) if sys.platform.startswith('linux'): - return LibsecretPersistence( - location, - schema_name="my_schema_name", - attributes={"my_attr1": "foo", "my_attr2": "bar"} - ) - else: - return FilePersistence(location) + # For LibsecretPersistence, location is only used as a signal for the credential's last modified time. + # The credential is stored in libsecret identified by (schema_name, attributes) combination. + # Doesn't seem to be a reason to use attributes to further filter the credential. + path = location + file_extension_signal + logger.debug("Initializing LibsecretPersistence: location=%r", path) + try: + attributes = {"type": type} if type else {} + return LibsecretPersistence( + path, + schema_name=LIBSECRET_SCHEMA_NAME, + attributes=attributes + ) + except Exception as e: + # Warn the user and continue with FilePersistence. + # LibsecretPersistence are known to be unavailable in some Linux environments. + logger.debug("Failed to initialize LibsecretPersistence: %s", e) + logger.warning("TBD: Encryption is unavailable. Falling back to plaintext persistence." + "Please follow https://aka.ms/azure-cli-credential-encryption to enable encryption.") + # Either encryption is opted out or the OS is not supported for encryption. Use FilePersistence. + path = location + file_extension_plaintext + logger.debug("Initializing FilePersistence: location=%r", path) + return FilePersistence(path) class SecretStore: diff --git a/src/azure-cli-core/azure/cli/core/util.py b/src/azure-cli-core/azure/cli/core/util.py index 67c7650f5fa..bd5749ba72b 100644 --- a/src/azure-cli-core/azure/cli/core/util.py +++ b/src/azure-cli-core/azure/cli/core/util.py @@ -1514,9 +1514,10 @@ def get_secret_store(cli_ctx, name): def should_encrypt_token_cache(cli_ctx): - # Only enable encryption for Windows (for now). - fallback = sys.platform.startswith('win32') + # Encryption enabled by default + fallback = True + # TODO: Remove the config and always enable encryption # EXPERIMENTAL: Use core.encrypt_token_cache=False to turn off token cache encryption. # encrypt_token_cache affects both MSAL token cache and service principal entries. encrypt = cli_ctx.config.getboolean('core', 'encrypt_token_cache', fallback=fallback)