From 26e420c764aa81b80004f008a4c34fed1d3a2908 Mon Sep 17 00:00:00 2001 From: Thomas Aarholt Date: Mon, 17 Aug 2026 19:45:40 +0200 Subject: [PATCH] msauth: isolate macOS user token cache Use a GCM-owned Keychain item on macOS so other Microsoft developer tools cannot replace the shared cache item and discard GCM's access control entry. Preserve shared cache behavior on Windows and Linux. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../MicrosoftAuthenticationTests.cs | 47 ++++++++++++++++ .../Authentication/MicrosoftAuthentication.cs | 53 ++++++++++++------- 2 files changed, 82 insertions(+), 18 deletions(-) diff --git a/src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs b/src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs index 0e1a70659e..f3caf40741 100644 --- a/src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs +++ b/src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs @@ -1,8 +1,10 @@ using System; +using System.IO; using System.Threading.Tasks; using GitCredentialManager.Authentication; using GitCredentialManager.Tests.Objects; using Microsoft.Identity.Client.AppConfig; +using Microsoft.Identity.Client.Extensions.Msal; using Xunit; namespace GitCredentialManager.Tests.Authentication @@ -29,6 +31,51 @@ await Assert.ThrowsAsync( () => msAuth.GetTokenForUserAsync(authority, clientId, redirectUri, scopes, userName, false)); } + [MacOSFact] + public void MicrosoftAuthentication_CreateUserTokenCacheProps_OnMacOS_UsesGcmKeychain() + { + var context = new TestCommandContext(); + var msAuth = new MicrosoftAuthentication(context); + + StorageCreationProperties actual = msAuth.CreateUserTokenCacheProps(useLinuxFallback: false); + + Assert.Equal("user.cache", actual.CacheFileName); + Assert.Equal(Path.Combine(context.FileSystem.UserDataDirectoryPath, "msal"), actual.CacheDirectory); + Assert.Equal("GitCredentialManager.MSAL", actual.MacKeyChainServiceName); + Assert.Equal("UserCache", actual.MacKeyChainAccountName); + } + + [WindowsFact] + public void MicrosoftAuthentication_CreateUserTokenCacheProps_OnWindows_UsesSharedCache() + { + var context = new TestCommandContext(); + var msAuth = new MicrosoftAuthentication(context); + + StorageCreationProperties actual = msAuth.CreateUserTokenCacheProps(useLinuxFallback: false); + + Assert.Equal("msal.cache", actual.CacheFileName); + Assert.Equal( + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ".IdentityService"), + actual.CacheDirectory); + } + + [LinuxFact] + public void MicrosoftAuthentication_CreateUserTokenCacheProps_OnLinux_UsesSharedCache() + { + var context = new TestCommandContext(); + var msAuth = new MicrosoftAuthentication(context); + + StorageCreationProperties actual = msAuth.CreateUserTokenCacheProps(useLinuxFallback: false); + + Assert.Equal("msal.cache", actual.CacheFileName); + Assert.Equal( + Path.Combine(context.FileSystem.UserHomePath, ".local", ".IdentityService"), + actual.CacheDirectory); + Assert.Equal("msal.cache", actual.KeyringSchemaName); + Assert.Equal("default", actual.KeyringCollection); + Assert.Equal("MSALCache", actual.KeyringSecretLabel); + } + [Theory] [InlineData(null)] [InlineData("")] diff --git a/src/shared/Core/Authentication/MicrosoftAuthentication.cs b/src/shared/Core/Authentication/MicrosoftAuthentication.cs index 86b0feff08..eae5437334 100644 --- a/src/shared/Core/Authentication/MicrosoftAuthentication.cs +++ b/src/shared/Core/Authentication/MicrosoftAuthentication.cs @@ -123,6 +123,10 @@ public enum MicrosoftAuthenticationFlowType public class MicrosoftAuthentication : AuthenticationBase, IMicrosoftAuthentication { + private const string GcmMacKeychainServiceName = "GitCredentialManager.MSAL"; + private const string GcmMacKeychainUserAccountName = "UserCache"; + private const string GcmMacKeychainAppAccountName = "AppCache"; + public static readonly string[] AuthorityIds = { "msa", "microsoft", "microsoftaccount", @@ -719,8 +723,8 @@ private async Task RegisterTokenCacheAsync(ITokenCache cache, StoragePropertiesB return; } - // We use the MSAL extension library to provide us consistent cache file access semantics (synchronisation, etc) - // as other GCM processes, and other Microsoft developer tools such as the Azure PowerShell CLI. + // We use the MSAL extension library to provide consistent cache file access semantics (synchronisation, etc) + // between GCM processes. On Windows and Linux this cache is also shared with other Microsoft developer tools. MsalCacheHelper helper = null; try { @@ -771,32 +775,45 @@ private async Task RegisterTokenCacheAsync(ITokenCache cache, StoragePropertiesB /// /// Create the properties for the user token cache. This is used by public client applications only. - /// This cache is shared between GCM processes, and also other Microsoft developer tools such as the Azure - /// PowerShell CLI. + /// This cache is shared between GCM processes. On Windows and Linux it is also shared with other Microsoft + /// developer tools such as the Azure PowerShell CLI. /// /// /// internal StorageCreationProperties CreateUserTokenCacheProps(bool useLinuxFallback) { - const string cacheFileName = "msal.cache"; + string cacheFileName; string cacheDirectory; - if (PlatformUtils.IsWindows()) + StorageCreationPropertiesBuilder builder; + + if (PlatformUtils.IsMacOS()) { - // The shared MSAL cache is located at "%LocalAppData%\.IdentityService\msal.cache" on Windows. - cacheDirectory = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - ".IdentityService" - ); + // Keep the macOS cache isolated from other Microsoft developer tools. Those tools can recreate the + // shared Keychain item and discard GCM's access control entry, causing repeated password prompts. + cacheFileName = "user.cache"; + cacheDirectory = Path.Combine(Context.FileSystem.UserDataDirectoryPath, "msal"); + builder = new StorageCreationPropertiesBuilder(cacheFileName, cacheDirectory) + .WithMacKeyChain(GcmMacKeychainServiceName, GcmMacKeychainUserAccountName); } else { - // The shared MSAL cache metadata is located at "~/.local/.IdentityService/msal.cache" on UNIX. - cacheDirectory = Path.Combine(Context.FileSystem.UserHomePath, ".local", ".IdentityService"); - } + cacheFileName = "msal.cache"; + if (PlatformUtils.IsWindows()) + { + // The shared MSAL cache is located at "%LocalAppData%\.IdentityService\msal.cache" on Windows. + cacheDirectory = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + ".IdentityService" + ); + } + else + { + // The shared MSAL cache metadata is located at "~/.local/.IdentityService/msal.cache" on Linux. + cacheDirectory = Path.Combine(Context.FileSystem.UserHomePath, ".local", ".IdentityService"); + } - // The keychain is used on macOS with the following service & account names - var builder = new StorageCreationPropertiesBuilder(cacheFileName, cacheDirectory) - .WithMacKeyChain("Microsoft.Developer.IdentityService", "MSALCache"); + builder = new StorageCreationPropertiesBuilder(cacheFileName, cacheDirectory); + } if (useLinuxFallback) { @@ -872,7 +889,7 @@ internal StorageCreationProperties CreateAppTokenCacheProps(bool useLinuxFallbac // The keychain is used on macOS with the following service & account names var builder = new StorageCreationPropertiesBuilder(cacheFileName, cacheDirectory) - .WithMacKeyChain("GitCredentialManager.MSAL", "AppCache"); + .WithMacKeyChain(GcmMacKeychainServiceName, GcmMacKeychainAppAccountName); if (useLinuxFallback) {