diff --git a/docs/azrepos-users-and-tokens.md b/docs/azrepos-users-and-tokens.md
index d5b58186bc..58654f79a3 100644
--- a/docs/azrepos-users-and-tokens.md
+++ b/docs/azrepos-users-and-tokens.md
@@ -38,9 +38,14 @@ Unlike PATs, Microsoft identity OAuth tokens get automatically refreshed and
renewed as long as you are actively using them to perform Git operations.
These tokens are also securely shared with other Microsoft developer tools
-including the Visual Studio IDE and Azure CLI. This means that as long as you're
-using Git or one of these tools with the same account, you'll never need to
-re-authenticate due to expired tokens!
+including the Visual Studio IDE. This means that as long as you're using Git or
+one of these tools with the same account, you'll never need to re-authenticate
+due to expired tokens!
+
+GCM uses the shared Microsoft developer tooling token cache by default. To keep
+Azure Repos OAuth tokens in a GCM-specific cache instead, set
+[`credential.azreposUseMicrosoftSharedCache`][credential-azrepos-shared-cache]
+or [`GCM_AZREPOS_USE_MSFT_CACHE`][gcm-azrepos-shared-cache] to `false`.
#### User accounts
@@ -220,6 +225,8 @@ fabrikam:
[azure-devops-pats]: https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page
[credential-azreposCredentialType]: configuration.md#credentialazreposcredentialtype
+[credential-azrepos-shared-cache]: configuration.md#credentialazreposusemicrosoftsharedcache
[gcm-azrepos-credential-type]: environment.md#GCM_AZREPOS_CREDENTIALTYPE
+[gcm-azrepos-shared-cache]: environment.md#GCM_AZREPOS_USE_MSFT_CACHE
[azure-devops-api]: https://docs.microsoft.com/en-gb/rest/api/azure/devops/tokens/pats
[rfc3986-s321]: https://www.rfc-editor.org/rfc/rfc3986#section-3.2.1
diff --git a/docs/configuration.md b/docs/configuration.md
index af5d410f41..5fc8959fde 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -827,6 +827,27 @@ Credential: "git:https://bob@github.com/example/myrepo" (user = bob)
---
+### credential.azreposUseMicrosoftSharedCache
+
+Use the token cache shared by Microsoft developer tools when authenticating to
+Azure Repos with Microsoft identity OAuth tokens. This allows GCM to reuse
+authentication performed by tools such as Visual Studio and Azure PowerShell
+CLI, and allows those tools to reuse authentication performed by GCM.
+
+Set this value to `false` to use a GCM-specific token cache instead.
+
+Defaults to `true`.
+
+#### Example
+
+```shell
+git config --global credential.azreposUseMicrosoftSharedCache false
+```
+
+**Also see: [GCM_AZREPOS_USE_MSFT_CACHE][gcm-azrepos-shared-cache]**
+
+---
+
### credential.azreposCredentialType
Specify the type of credential the Azure Repos host provider should return.
@@ -1179,6 +1200,7 @@ Defaults to disabled.
[gcm-authority]: environment.md#GCM_AUTHORITY-deprecated
[gcm-autodetect-timeout]: environment.md#GCM_AUTODETECT_TIMEOUT
[gcm-azrepos-credentialtype]: environment.md#GCM_AZREPOS_CREDENTIALTYPE
+[gcm-azrepos-shared-cache]: environment.md#GCM_AZREPOS_USE_MSFT_CACHE
[gcm-azrepos-credentialmanagedidentity]: environment.md#GCM_AZREPOS_MANAGEDIDENTITY
[gcm-azrepos-wif]: environment.md#GCM_AZREPOS_WIF
[gcm-azrepos-wif-clientid]: environment.md#GCM_AZREPOS_WIF_CLIENTID
diff --git a/docs/environment.md b/docs/environment.md
index 01f570c9d5..ed82b28381 100644
--- a/docs/environment.md
+++ b/docs/environment.md
@@ -920,6 +920,33 @@ export GCM_MSAUTH_USEDEFAULTACCOUNT="false"
---
+### GCM_AZREPOS_USE_MSFT_CACHE
+
+Use the token cache shared by Microsoft developer tools when authenticating to
+Azure Repos with Microsoft identity OAuth tokens. This allows GCM to reuse
+authentication performed by tools such as Visual Studio and Azure PowerShell
+CLI, and allows those tools to reuse authentication performed by GCM.
+
+Set this value to `false` to use a GCM-specific token cache instead.
+
+Defaults to `true`.
+
+#### Windows
+
+```batch
+SET GCM_AZREPOS_USE_MSFT_CACHE=false
+```
+
+#### macOS/Linux
+
+```bash
+export GCM_AZREPOS_USE_MSFT_CACHE="false"
+```
+
+**Also see: [credential.azreposUseMicrosoftSharedCache][shared-cache]**
+
+---
+
### GCM_AZREPOS_CREDENTIALTYPE
Specify the type of credential the Azure Repos host provider should return.
@@ -1351,6 +1378,7 @@ Defaults to disabled.
[credential-authority]: configuration.md#credentialauthority-deprecated
[credential-autodetecttimeout]: configuration.md#credentialautodetecttimeout
[credential-azrepos-credential-type]: configuration.md#credentialazreposcredentialtype
+[shared-cache]: configuration.md#credentialazreposusemicrosoftsharedcache
[credential-azrepos-managedidentity]: configuration.md#credentialazreposmanagedidentity
[credential-azrepos-wif]: configuration.md#credentialazreposworkloadfederation
[credential-azrepos-wif-clientid]: configuration.md#credentialazreposworkloadfederationclientid
diff --git a/docs/windows-broker.md b/docs/windows-broker.md
index bfe5afcb04..679e0e5a0f 100644
--- a/docs/windows-broker.md
+++ b/docs/windows-broker.md
@@ -5,8 +5,8 @@ Git Credential Manager (GCM) knows how to integrate with the
uses WAM to store credentials for Azure DevOps. Authentication requests are said
to be "brokered" to the operating system. Currently, GCM will share
authentication state with a few other Microsoft developer tools like Visual
-Studio and the Azure CLI, meaning fewer authentication prompts. Enabling WAM
-integration may also be required with certain
+Studio, meaning fewer authentication prompts.
+Enabling WAM integration may also be required with certain
[Conditional Access policies][azure-conditional-access], which enterprises use
to help protect their assets, including source code.
diff --git a/src/Core/Authentication/Entra/EntraAuthentication.Caching.cs b/src/Core/Authentication/Entra/EntraAuthentication.Caching.cs
index 3771ed81cd..1787510146 100644
--- a/src/Core/Authentication/Entra/EntraAuthentication.Caching.cs
+++ b/src/Core/Authentication/Entra/EntraAuthentication.Caching.cs
@@ -35,7 +35,7 @@ private async Task RegisterCacheAsync(ITokenCache cache, StoragePropertiesBuilde
}
// 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.
+ // as other GCM processes, and other Microsoft developer tools such as Visual Studio.
MsalCacheHelper helper = null;
try
{
@@ -86,8 +86,8 @@ private async Task RegisterCacheAsync(ITokenCache cache, StoragePropertiesBuilde
///
/// 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 if the shared cache is enabled.
+ /// This cache is shared between GCM processes, and also other Microsoft developer tools such as
+ /// Visual Studio if the shared cache is enabled.
///
internal StorageCreationProperties CreateUserTokenCacheProps(bool useLinuxFallback)
{
@@ -102,7 +102,7 @@ internal StorageCreationProperties CreateUserTokenCacheProps(bool useLinuxFallba
// If we are using the shared Microsoft Developer cache there are a different set of
// file paths, names, and keychain/keyring attributes to use.
- // The shared cache is used by other Microsoft developer tools such as the Azure PowerShell CLI.
+ // The shared cache is used by other Microsoft developer tools such as Visual Studio.
if (_publicClientConfig.UseSharedCache)
{
Context.Trace.WriteLine("Using shared Microsoft Developer MSAL cache");
diff --git a/src/Core/Authentication/Entra/EntraAuthentication.PublicClient.cs b/src/Core/Authentication/Entra/EntraAuthentication.PublicClient.cs
index f0d79e40b6..760d4f1b3d 100644
--- a/src/Core/Authentication/Entra/EntraAuthentication.PublicClient.cs
+++ b/src/Core/Authentication/Entra/EntraAuthentication.PublicClient.cs
@@ -32,7 +32,7 @@ public record PublicClientConfig
///
///
/// If then use the token cache shared by Microsoft
- /// developer tools such as the Azure PowerShell CLI. Otherwise, use the
+ /// developer tools such as Visual Studio. Otherwise, use the
/// Git Credential Manager cache, used only by GCM.
///
public bool UseSharedCache { get; init; }
diff --git a/src/Core/Diagnostics/EntraAuthenticationDiagnostic.cs b/src/Core/Diagnostics/EntraAuthenticationDiagnostic.cs
index ef0e69948e..e4946830e0 100644
--- a/src/Core/Diagnostics/EntraAuthenticationDiagnostic.cs
+++ b/src/Core/Diagnostics/EntraAuthenticationDiagnostic.cs
@@ -12,13 +12,22 @@ public EntraAuthenticationDiagnostic(ICommandContext context)
{ }
protected override async Task RunInternalAsync(IDiagnosticReporter reporter)
+ {
+ await RunDiagnosticAsync(reporter, useSharedCache: false);
+ await RunDiagnosticAsync(reporter, useSharedCache: true);
+ }
+
+ private async Task RunDiagnosticAsync(IDiagnosticReporter reporter, bool useSharedCache)
{
var entraAuth = new EntraAuthentication(Context, new PublicClientConfig
{
- UseSharedCache = true,
+ UseSharedCache = useSharedCache,
});
- reporter.ReportProgress("Gathering MSAL token cache data");
+ reporter.ReportProgress(useSharedCache
+ ? "Checking shared Microsoft developer tool MSAL token cache"
+ : "Checking Git Credential Manager MSAL token cache");
+
StorageCreationProperties cacheProps = entraAuth.CreateUserTokenCacheProps(true);
reporter.ReportInfo($"CacheDirectory: {cacheProps.CacheDirectory}");
reporter.ReportInfo($"CacheFileName: {cacheProps.CacheFileName}");
@@ -47,7 +56,7 @@ protected override async Task RunInternalAsync(IDiagnosticReporter reporter)
}
catch (Exception ex)
{
- reporter.ReportError("Failed cache persistence test", ex);
+ reporter.ReportWarning($"Failed cache persistence test: {ex.Message}");
}
}
}
diff --git a/src/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs b/src/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs
index 786e24a655..79a34ccd75 100644
--- a/src/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs
+++ b/src/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs
@@ -17,8 +17,39 @@ public class AzureReposHostProviderTests
$"{Constants.GitConfiguration.Credential.SectionName}.{Constants.GitConfiguration.Credential.Helper}";
private static readonly string AzDevUseHttpPathKey =
$"{Constants.GitConfiguration.Credential.SectionName}.https://dev.azure.com.{Constants.GitConfiguration.Credential.UseHttpPath}";
+ private static readonly string AzDevUseSharedCacheKey =
+ $"{Constants.GitConfiguration.Credential.SectionName}.{AzureDevOpsConstants.GitConfiguration.Credential.UseSharedCache}";
private static readonly string OrgName = "org";
+ [Fact]
+ public void AzureReposProvider_GetUseSharedCache_NoConfiguration_ReturnsTrue()
+ {
+ var provider = new AzureReposHostProvider(new TestCommandContext());
+
+ Assert.True(provider.GetUseSharedCache());
+ }
+
+ [Fact]
+ public void AzureReposProvider_GetUseSharedCache_GitConfigFalse_ReturnsFalse()
+ {
+ var context = new TestCommandContext();
+ context.Git.Configuration.Global[AzDevUseSharedCacheKey] = new List {"false"};
+ var provider = new AzureReposHostProvider(context);
+
+ Assert.False(provider.GetUseSharedCache());
+ }
+
+ [Fact]
+ public void AzureReposProvider_GetUseSharedCache_EnvironmentOverridesGitConfig()
+ {
+ var context = new TestCommandContext();
+ context.Git.Configuration.Global[AzDevUseSharedCacheKey] = new List {"false"};
+ context.Environment.Variables[AzureDevOpsConstants.EnvironmentVariables.UseSharedCache] = "true";
+ var provider = new AzureReposHostProvider(context);
+
+ Assert.True(provider.GetUseSharedCache());
+ }
+
[Fact]
public void AzureReposProvider_IsSupported_AzureHost_UnencryptedHttp_ReturnsTrue()
{
diff --git a/src/Microsoft.AzureRepos/AzureDevOpsConstants.cs b/src/Microsoft.AzureRepos/AzureDevOpsConstants.cs
index ac09fed130..cd4d0241c2 100644
--- a/src/Microsoft.AzureRepos/AzureDevOpsConstants.cs
+++ b/src/Microsoft.AzureRepos/AzureDevOpsConstants.cs
@@ -36,6 +36,7 @@ public static class EnvironmentVariables
{
public const string DevAadClientId = "GCM_DEV_AZREPOS_CLIENTID";
public const string DevAadAuthorityBaseUri = "GCM_DEV_AZREPOS_AUTHORITYBASEURI";
+ public const string UseSharedCache = "GCM_AZREPOS_USE_MSFT_CACHE";
public const string CredentialType = "GCM_AZREPOS_CREDENTIALTYPE";
public const string ServicePrincipalId = "GCM_AZREPOS_SERVICE_PRINCIPAL";
public const string ServicePrincipalSecret = "GCM_AZREPOS_SP_SECRET";
@@ -56,6 +57,7 @@ public static class Credential
{
public const string DevAadClientId = "azreposDevClientId";
public const string DevAadAuthorityBaseUri = "azreposDevAuthorityBaseUri";
+ public const string UseSharedCache = "azreposUseMicrosoftSharedCache";
public const string CredentialType = "azreposCredentialType";
public const string AzureAuthority = "azureAuthority";
public const string ServicePrincipal = "azreposServicePrincipal";
diff --git a/src/Microsoft.AzureRepos/AzureReposHostProvider.cs b/src/Microsoft.AzureRepos/AzureReposHostProvider.cs
index ff26f772fe..335baa5669 100644
--- a/src/Microsoft.AzureRepos/AzureReposHostProvider.cs
+++ b/src/Microsoft.AzureRepos/AzureReposHostProvider.cs
@@ -406,7 +406,7 @@ private PublicClientConfig GetEntraConfig()
{
ClientId = GetClientId(),
IsMsaPassthroughEnabled = true,
- UseSharedCache = true,
+ UseSharedCache = GetUseSharedCache(),
SupportsWindowsBroker = true,
// TODO: enable once our app registration has the appropriate redirect URLs
//SupportsMacBroker = true,
@@ -414,6 +414,22 @@ private PublicClientConfig GetEntraConfig()
};
}
+ internal bool GetUseSharedCache()
+ {
+ const bool defaultValue = true; // prefer using the shared Microsoft dev cache
+
+ if (_context.Settings.TryGetSetting(
+ AzureDevOpsConstants.EnvironmentVariables.UseSharedCache,
+ Constants.GitConfiguration.Credential.SectionName,
+ AzureDevOpsConstants.GitConfiguration.Credential.UseSharedCache,
+ out string str))
+ {
+ return str.ToBooleanyOrDefault(defaultValue);
+ }
+
+ return defaultValue;
+ }
+
private string GetClientId()
{
// Check for developer override value