From d6c3d50baa371b075e9a9d06dc70e748ab734282 Mon Sep 17 00:00:00 2001 From: isra-fel <11371776+isra-fel@users.noreply.github.com> Date: Tue, 4 Aug 2026 09:43:53 +1000 Subject: [PATCH 1/4] enabled encryption of token cache by default for macos and linux --- .../azure/cli/core/auth/persistence.py | 19 ++++++++++++++++--- src/azure-cli-core/azure/cli/core/util.py | 5 +++-- 2 files changed, 19 insertions(+), 5 deletions(-) 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..bda4c3e3d58 100644 --- a/src/azure-cli-core/azure/cli/core/auth/persistence.py +++ b/src/azure-cli-core/azure/cli/core/auth/persistence.py @@ -22,6 +22,8 @@ # Files extensions for encrypted and plaintext persistence file_extensions = {True: '.bin', False: '.json'} +KEYCHAIN_SERVICE_NAME = 'azure-cli' +LIBSECRET_SCHEMA_NAME = 'azure-cli' def load_persisted_token_cache(location, encrypt): persistence = build_persistence(location, encrypt) @@ -39,14 +41,25 @@ def build_persistence(location, encrypt): logger.debug("build_persistence: location=%r, encrypt=%r", location, encrypt) if encrypt: if sys.platform.startswith('win'): + # For FilePersistenceWithDataProtection, location is where the credential is stored. + logger.debug("Initializing FilePersistenceWithDataProtection.") return FilePersistenceWithDataProtection(location) 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 + logger.debug("Initializing KeychainPersistence") + return KeychainPersistence(location, service_name=KEYCHAIN_SERVICE_NAME) if sys.platform.startswith('linux'): + # 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. + logger.debug("Initializing LibsecretPersistence.") return LibsecretPersistence( location, - schema_name="my_schema_name", - attributes={"my_attr1": "foo", "my_attr2": "bar"} + schema_name=LIBSECRET_SCHEMA_NAME, + attributes={} ) else: return FilePersistence(location) 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) From 2dbe201ca9863b886ace9ab738dfd6bcafe263a4 Mon Sep 17 00:00:00 2001 From: isra-fel <11371776+isra-fel@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:46:47 +1000 Subject: [PATCH 2/4] refactor: token cache file extension --- .../azure/cli/core/auth/persistence.py | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) 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 bda4c3e3d58..9bbd8ee6a8e 100644 --- a/src/azure-cli-core/azure/cli/core/auth/persistence.py +++ b/src/azure-cli-core/azure/cli/core/auth/persistence.py @@ -20,10 +20,13 @@ 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 = 'azure-cli' -LIBSECRET_SCHEMA_NAME = 'azure-cli' +KEYCHAIN_SERVICE_NAME = 'Microsoft Azure CLI MSAL Token Cache' +LIBSECRET_SCHEMA_NAME = 'Microsoft Azure CLI MSAL Token Cache' def load_persisted_token_cache(location, encrypt): persistence = build_persistence(location, encrypt) @@ -37,32 +40,43 @@ def load_secret_store(location, encrypt): def build_persistence(location, encrypt): """Build a suitable persistence instance based your current OS""" - location += file_extensions[encrypt] logger.debug("build_persistence: location=%r, encrypt=%r", location, encrypt) if encrypt: if sys.platform.startswith('win'): # For FilePersistenceWithDataProtection, location is where the credential is stored. - logger.debug("Initializing FilePersistenceWithDataProtection.") - return FilePersistenceWithDataProtection(location) + path = location + file_extension_encrypted + logger.debug("Initializing FilePersistenceWithDataProtection: location=%r", path) + return FilePersistenceWithDataProtection(path) if sys.platform.startswith('darwin'): # 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 - logger.debug("Initializing KeychainPersistence") - return KeychainPersistence(location, service_name=KEYCHAIN_SERVICE_NAME) + path = location + file_extension_signal + logger.debug("Initializing KeychainPersistence: location=%r", path) + return KeychainPersistence(path, service_name=KEYCHAIN_SERVICE_NAME) if sys.platform.startswith('linux'): # 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. - logger.debug("Initializing LibsecretPersistence.") - return LibsecretPersistence( - location, - schema_name=LIBSECRET_SCHEMA_NAME, - attributes={} - ) - else: - return FilePersistence(location) + path = location + file_extension_signal + logger.debug("Initializing LibsecretPersistence: location=%r", path) + try: + return LibsecretPersistence( + path, + schema_name=LIBSECRET_SCHEMA_NAME, + 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: From 1c6f437bcb07d32bda4aae004af9b729fdd1e975 Mon Sep 17 00:00:00 2001 From: isra-fel <11371776+isra-fel@users.noreply.github.com> Date: Wed, 5 Aug 2026 15:01:25 +1000 Subject: [PATCH 3/4] use different types for token cache and secret store --- .../azure/cli/core/auth/persistence.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) 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 9bbd8ee6a8e..5f9a7388708 100644 --- a/src/azure-cli-core/azure/cli/core/auth/persistence.py +++ b/src/azure-cli-core/azure/cli/core/auth/persistence.py @@ -25,22 +25,22 @@ file_extension_signal = '.sig' file_extensions = [file_extension_encrypted, file_extension_plaintext, file_extension_signal] -KEYCHAIN_SERVICE_NAME = 'Microsoft Azure CLI MSAL Token Cache' -LIBSECRET_SCHEMA_NAME = 'Microsoft Azure CLI MSAL Token Cache' +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""" - 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'): # For FilePersistenceWithDataProtection, location is where the credential is stored. @@ -54,7 +54,7 @@ def build_persistence(location, encrypt): # 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) + return KeychainPersistence(path, service_name=KEYCHAIN_SERVICE_NAME, account_name=type) if sys.platform.startswith('linux'): # 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. @@ -62,10 +62,11 @@ def build_persistence(location, encrypt): 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=attributes ) except Exception as e: # Warn the user and continue with FilePersistence. From 233ceb959d4d67279bbcdb4234fd721a1fd4cc9d Mon Sep 17 00:00:00 2001 From: Ming Xu Date: Thu, 6 Aug 2026 09:21:56 +1000 Subject: [PATCH 4/4] fix az logout failure by using list values instead of dictionary values --- src/azure-cli-core/azure/cli/core/auth/identity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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):