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) {