Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/azrepos-users-and-tokens.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ 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!

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

In versions of Git Credential Manager that support Microsoft identity OAuth
Expand Down Expand Up @@ -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
22 changes: 22 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 CLI, and
allows those tools to reuse authentication performed by GCM.
Comment on lines +830 to +835

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These additions say the selected cache allows authentication reuse with Azure CLI. Azure CLI maintains its own cache under $AZURE_CONFIG_DIR/msal_token_cache.{bin,json}, rather than the .IdentityService/msal.cache cache configured here. Any broker-based SSO on supported systems is separate from sharing this cache and may still occur when this option is disabled.

Remove Azure CLI from these examples or describe the narrower set of tools that actually consume the Microsoft developer shared cache.


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.
Expand Down Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions docs/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -920,6 +920,33 @@ export GCM_MSAUTH_USEDEFAULTACCOUNT="false"

---

### GCM_AZREPOS_USE_MSFT_CACHE

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


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 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.
Expand Down Expand Up @@ -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
Expand Down
33 changes: 24 additions & 9 deletions src/Core/Commands/DiagnoseCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,7 @@ private async Task<int> ExecuteAsync(string output)
DiagnosticResult result = await diagnostic.RunAsync();
fullLog.WriteLine("Success: {0}", result.IsSuccess);

if (result.Exception is null)
{
fullLog.WriteLine("Exception: None");
}
else
{
fullLog.WriteLine("Exception:");
fullLog.WriteLine(result.Exception.ToString());
}
WriteException(fullLog, result.Exception);

fullLog.WriteLine("Log:");
fullLog.WriteLine(result.DiagnosticLog);
Expand Down Expand Up @@ -195,6 +187,29 @@ private async Task<int> ExecuteAsync(string output)
return numFailed;
}

private void WriteException(StreamWriter log, Exception exception)
{
if (exception is null)
{
log.WriteLine("Exception: None");
return;
}

if (exception is AggregateException aex)
{
log.WriteLine("Exception: AggregateException");
log.WriteLine("InnerExceptions (flattened):");
foreach (var inner in aex.Flatten().InnerExceptions)
{
log.WriteLine(inner.ToString());
}
}
else
{
log.WriteLine("Exception: {0}", exception);
}
}

private static class ConsoleEx
{
public static void WriteLineIndent(string str)
Expand Down
77 changes: 51 additions & 26 deletions src/Core/Diagnostics/EntraAuthenticationDiagnostic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,48 +15,73 @@ public EntraAuthenticationDiagnostic(ICommandContext context)

protected override async Task<bool> RunInternalAsync(StringBuilder log, IList<string> additionalFiles)
{
var entraAuth = new EntraAuthentication(CommandContext, new PublicClientConfig
{
UseSharedCache = true,
});
var failures = new List<Exception>();

log.Append("Gathering MSAL token cache data...");
StorageCreationProperties cacheProps = entraAuth.CreateUserTokenCacheProps(true);
log.AppendLine(" OK");
log.AppendLine($"CacheDirectory: {cacheProps.CacheDirectory}");
log.AppendLine($"CacheFileName: {cacheProps.CacheFileName}");
log.AppendLine($"CacheFilePath: {cacheProps.CacheFilePath}");
await RunCacheDiagnosticAsync("Shared Microsoft developer tools", true, log, failures);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diagnostic now tests both caches and throws if either fails. Consequently, a user who explicitly selects the GCM-specific cache can still get a failed git-credential-manager diagnose result solely because the unused shared cache is inaccessible, and vice versa. This is especially misleading because DiagnoseCommand reports that failures indicate an installation problem.

Consider logging results for both caches but making only the configured cache determine diagnostic success, or registering each cache check as a separate diagnostic so the relevant and optional failures are distinguishable.

log.AppendLine();
await RunCacheDiagnosticAsync("Git Credential Manager", false, log, failures);

if (PlatformUtils.IsMacOS())
if (failures.Count == 1)
{
log.AppendLine($"MacKeyChainAccountName: {cacheProps.MacKeyChainAccountName}");
log.AppendLine($"MacKeyChainServiceName: {cacheProps.MacKeyChainServiceName}");
throw failures[0];
}
else if (PlatformUtils.IsLinux())

if (failures.Count > 1)
{
log.AppendLine($"KeyringCollection: {cacheProps.KeyringCollection}");
log.AppendLine($"KeyringSchemaName: {cacheProps.KeyringSchemaName}");
log.AppendLine($"KeyringSecretLabel: {cacheProps.KeyringSecretLabel}");
log.AppendLine($"KeyringAttribute1: ({cacheProps.KeyringAttribute1.Key},{cacheProps.KeyringAttribute1.Value})");
log.AppendLine($"KeyringAttribute2: ({cacheProps.KeyringAttribute2.Key},{cacheProps.KeyringAttribute2.Value})");
throw new AggregateException("Multiple MSAL token cache diagnostics failed.", failures);
}

log.Append("Creating cache helper...");
var cacheHelper = await MsalCacheHelper.CreateAsync(cacheProps);
log.AppendLine(" OK");
return true;
}

private async Task RunCacheDiagnosticAsync(
string cacheName,
bool useSharedCache,
StringBuilder log,
ICollection<Exception> failures)
{
log.AppendLine($"{cacheName} cache");

var entraAuth = new EntraAuthentication(CommandContext, new PublicClientConfig
{
UseSharedCache = useSharedCache,
});

try
{
log.Append("Gathering MSAL token cache data...");
StorageCreationProperties cacheProps = entraAuth.CreateUserTokenCacheProps(true);
log.AppendLine(" OK");
log.AppendLine($"CacheDirectory: {cacheProps.CacheDirectory}");
log.AppendLine($"CacheFileName: {cacheProps.CacheFileName}");
log.AppendLine($"CacheFilePath: {cacheProps.CacheFilePath}");

if (PlatformUtils.IsMacOS())
{
log.AppendLine($"MacKeyChainAccountName: {cacheProps.MacKeyChainAccountName}");
log.AppendLine($"MacKeyChainServiceName: {cacheProps.MacKeyChainServiceName}");
}
else if (PlatformUtils.IsLinux())
{
log.AppendLine($"KeyringCollection: {cacheProps.KeyringCollection}");
log.AppendLine($"KeyringSchemaName: {cacheProps.KeyringSchemaName}");
log.AppendLine($"KeyringSecretLabel: {cacheProps.KeyringSecretLabel}");
log.AppendLine($"KeyringAttribute1: ({cacheProps.KeyringAttribute1.Key},{cacheProps.KeyringAttribute1.Value})");
log.AppendLine($"KeyringAttribute2: ({cacheProps.KeyringAttribute2.Key},{cacheProps.KeyringAttribute2.Value})");
}

log.Append("Creating cache helper...");
var cacheHelper = await MsalCacheHelper.CreateAsync(cacheProps);
log.AppendLine(" OK");
log.Append("Verifying MSAL token cache persistence...");
cacheHelper.VerifyPersistence();
log.AppendLine(" OK");
}
catch (Exception)
catch (Exception ex)
{
log.AppendLine(" Failed");
throw;
failures.Add(new Exception($"{cacheName} cache diagnostic failed.", ex));
}

return true;
}
}
}
31 changes: 31 additions & 0 deletions src/Microsoft.AzureRepos.Tests/AzureReposHostProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {"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<string> {"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()
{
Expand Down
2 changes: 2 additions & 0 deletions src/Microsoft.AzureRepos/AzureDevOpsConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down
18 changes: 17 additions & 1 deletion src/Microsoft.AzureRepos/AzureReposHostProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -406,14 +406,30 @@ private PublicClientConfig GetEntraConfig()
{
ClientId = GetClientId(),
IsMsaPassthroughEnabled = true,
UseSharedCache = true,
UseSharedCache = GetUseSharedCache(),
Comment thread
mjcheetham marked this conversation as resolved.
SupportsWindowsBroker = true,
// TODO: enable once our app registration has the appropriate redirect URLs
//SupportsMacBroker = true,
//SupportsLinuxBroker = true,
};
}

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
Expand Down
Loading