Skip to content

Commit 58e34e3

Browse files
authored
GCM release 2.3.0 (#1361)
**Changes since 2.2.2:** - Fix a GCM/Git Trace2 file locking issue - Issue: #1323 - PR: #1340 - Remove symlinks to `git-credential-manager-core` exe - Issue: #1322 - PR: #1327 - Add fallback http uri to `diagnose` command - Issue: #1215 - PR: #1339 - Workaround MSAL tenant issue with silent auth - Issue: #1297 - PR: #1321
2 parents 5d7e823 + 8c430c9 commit 58e34e3

File tree

17 files changed

+204
-86
lines changed

17 files changed

+204
-86
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.2.2.0
1+
2.3.0.0

docs/install.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ the preferred install method for Linux because you can use it to install on any
205205
distribution][dotnet-supported-distributions]. You
206206
can also use this method on macOS if you so choose.
207207

208-
**Note:** Make sure you have installed [version 6.0 of the .NET
208+
**Note:** Make sure you have installed [version 7.0 of the .NET
209209
SDK][dotnet-install] before attempting to run the following `dotnet tool`
210210
commands. After installing, you will also need to follow the output instructions
211211
to add the tools directory to your `PATH`.

docs/multiple-users.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ which you should also be aware of if you're using it.
9393

9494
You can use the `github [list | login | logout]` commands to manage your GitHub
9595
accounts. These commands are documented in the [command-line usage][cli-usage]
96-
or by running `git-credential-manager github --help`.
96+
or by running `git credential-manager github --help`.
9797

9898
## TL;DR: Tell GCM to remember which account to use
9999

src/linux/Packaging.Linux/build.sh

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,6 @@ if [ $INSTALL_FROM_SOURCE = true ]; then
7070
"$LINK_TO/git-credential-manager" || exit 1
7171
fi
7272

73-
# Create legacy symlink with older name
74-
if [ ! -f "$LINK_TO/git-credential-manager-core" ]; then
75-
ln -s -r "$INSTALL_TO/git-credential-manager" \
76-
"$LINK_TO/git-credential-manager-core" || exit 1
77-
fi
78-
7973
echo "Install complete."
8074
else
8175
# Pack

src/linux/Packaging.Linux/pack.sh

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,6 @@ if [ ! -f "$LINK_TO/git-credential-manager" ]; then
126126
"$LINK_TO/git-credential-manager" || exit 1
127127
fi
128128

129-
# Create legacy symlink with older name
130-
if [ ! -f "$LINK_TO/git-credential-manager-core" ]; then
131-
ln -s -r "$INSTALL_TO/git-credential-manager" \
132-
"$LINK_TO/git-credential-manager-core" || exit 1
133-
fi
134-
135129
dpkg-deb -Zxz --build "$DEBROOT" "$DEBPKG" || exit 1
136130

137131
echo $MESSAGE

src/osx/Installer.Mac/scripts/postinstall

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,6 @@ fi
3030
mkdir -p /usr/local/bin
3131
/bin/ln -Fs "$INSTALL_DESTINATION/git-credential-manager" /usr/local/bin/git-credential-manager
3232

33-
# Create legacy symlink to GCMCore in /usr/local/bin
34-
/bin/ln -Fs "$INSTALL_DESTINATION/git-credential-manager" /usr/local/bin/git-credential-manager-core
35-
3633
# Configure GCM for the current user (running as the current user to avoid root
3734
# from taking ownership of ~/.gitconfig)
3835
sudo -u ${USER} "$INSTALL_DESTINATION/git-credential-manager" configure

src/shared/Core.Tests/Authentication/MicrosoftAuthenticationTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public async System.Threading.Tasks.Task MicrosoftAuthentication_GetAccessTokenA
2424
var msAuth = new MicrosoftAuthentication(context);
2525

2626
await Assert.ThrowsAsync<Trace2InvalidOperationException>(
27-
() => msAuth.GetTokenAsync(authority, clientId, redirectUri, scopes, userName));
27+
() => msAuth.GetTokenAsync(authority, clientId, redirectUri, scopes, userName, false));
2828
}
2929
}
3030
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System;
2+
using System.Net.Http;
3+
using System.Security.AccessControl;
4+
using System.Text;
5+
using GitCredentialManager.Diagnostics;
6+
using GitCredentialManager.Tests.Objects;
7+
using Xunit;
8+
9+
namespace Core.Tests.Commands;
10+
11+
public class DiagnoseCommandTests
12+
{
13+
[Fact]
14+
public void NetworkingDiagnostic_SendHttpRequest_Primary_OK()
15+
{
16+
var primaryUriString = "http://example.com";
17+
var sb = new StringBuilder();
18+
var context = new TestCommandContext();
19+
var networkingDiagnostic = new NetworkingDiagnostic(context);
20+
var primaryUri = new Uri(primaryUriString);
21+
var httpHandler = new TestHttpMessageHandler();
22+
var httpResponse = new HttpResponseMessage();
23+
var expected = $"Sending HEAD request to {primaryUriString}... OK{Environment.NewLine}";
24+
25+
httpHandler.Setup(HttpMethod.Head, primaryUri, httpResponse);
26+
27+
networkingDiagnostic.SendHttpRequest(sb, new HttpClient(httpHandler));
28+
29+
httpHandler.AssertRequest(HttpMethod.Head, primaryUri, expectedNumberOfCalls: 1);
30+
Assert.Contains(expected, sb.ToString());
31+
}
32+
33+
[Fact]
34+
public void NetworkingDiagnostic_SendHttpRequest_Backup_OK()
35+
{
36+
var primaryUriString = "http://example.com";
37+
var backupUriString = "http://httpforever.com";
38+
var sb = new StringBuilder();
39+
var context = new TestCommandContext();
40+
var networkingDiagnostic = new NetworkingDiagnostic(context);
41+
var primaryUri = new Uri(primaryUriString);
42+
var backupUri = new Uri(backupUriString);
43+
var httpHandler = new TestHttpMessageHandler { SimulatePrimaryUriFailure = true };
44+
var httpResponse = new HttpResponseMessage();
45+
var expected = $"Sending HEAD request to {primaryUriString}... warning: HEAD request failed{Environment.NewLine}" +
46+
$"Sending HEAD request to {backupUriString}... OK{Environment.NewLine}";
47+
48+
httpHandler.Setup(HttpMethod.Head, primaryUri, httpResponse);
49+
httpHandler.Setup(HttpMethod.Head, backupUri, httpResponse);
50+
51+
networkingDiagnostic.SendHttpRequest(sb, new HttpClient(httpHandler));
52+
53+
httpHandler.AssertRequest(HttpMethod.Head, primaryUri, expectedNumberOfCalls: 1);
54+
httpHandler.AssertRequest(HttpMethod.Head, backupUri, expectedNumberOfCalls: 1);
55+
Assert.Contains(expected, sb.ToString());
56+
}
57+
58+
[Fact]
59+
public void NetworkingDiagnostic_SendHttpRequest_No_Network()
60+
{
61+
var primaryUriString = "http://example.com";
62+
var backupUriString = "http://httpforever.com";
63+
var sb = new StringBuilder();
64+
var context = new TestCommandContext();
65+
var networkingDiagnostic = new NetworkingDiagnostic(context);
66+
var primaryUri = new Uri(primaryUriString);
67+
var backupUri = new Uri(backupUriString);
68+
var httpHandler = new TestHttpMessageHandler { SimulateNoNetwork = true };
69+
var httpResponse = new HttpResponseMessage();
70+
var expected = $"Sending HEAD request to {primaryUriString}... warning: HEAD request failed{Environment.NewLine}" +
71+
$"Sending HEAD request to {backupUriString}... warning: HEAD request failed{Environment.NewLine}";
72+
73+
httpHandler.Setup(HttpMethod.Head, primaryUri, httpResponse);
74+
httpHandler.Setup(HttpMethod.Head, backupUri, httpResponse);
75+
76+
networkingDiagnostic.SendHttpRequest(sb, new HttpClient(httpHandler));
77+
78+
httpHandler.AssertRequest(HttpMethod.Head, primaryUri, expectedNumberOfCalls: 1);
79+
httpHandler.AssertRequest(HttpMethod.Head, backupUri, expectedNumberOfCalls: 1);
80+
Assert.Contains(expected, sb.ToString());
81+
}
82+
}

src/shared/Core/Authentication/MicrosoftAuthentication.cs

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4+
using System.Linq;
45
using System.Net.Http;
56
using System.Threading.Tasks;
67
using GitCredentialManager.Interop.Windows.Native;
@@ -23,7 +24,7 @@ namespace GitCredentialManager.Authentication
2324
public interface IMicrosoftAuthentication
2425
{
2526
Task<IMicrosoftAuthenticationResult> GetTokenAsync(string authority, string clientId, Uri redirectUri,
26-
string[] scopes, string userName);
27+
string[] scopes, string userName, bool msaPt = false);
2728
}
2829

2930
public interface IMicrosoftAuthenticationResult
@@ -59,26 +60,31 @@ public MicrosoftAuthentication(ICommandContext context)
5960
#region IMicrosoftAuthentication
6061

6162
public async Task<IMicrosoftAuthenticationResult> GetTokenAsync(
62-
string authority, string clientId, Uri redirectUri, string[] scopes, string userName)
63+
string authority, string clientId, Uri redirectUri, string[] scopes, string userName, bool msaPt)
6364
{
6465
// Check if we can and should use OS broker authentication
6566
bool useBroker = CanUseBroker();
6667
Context.Trace.WriteLine(useBroker
6768
? "OS broker is available and enabled."
6869
: "OS broker is not available or enabled.");
6970

71+
if (msaPt)
72+
{
73+
Context.Trace.WriteLine("MSA passthrough is enabled.");
74+
}
75+
7076
try
7177
{
7278
// Create the public client application for authentication
73-
IPublicClientApplication app = await CreatePublicClientApplicationAsync(authority, clientId, redirectUri, useBroker);
79+
IPublicClientApplication app = await CreatePublicClientApplicationAsync(authority, clientId, redirectUri, useBroker, msaPt);
7480

7581
AuthenticationResult result = null;
7682

7783
// Try silent authentication first if we know about an existing user
7884
bool hasExistingUser = !string.IsNullOrWhiteSpace(userName);
7985
if (hasExistingUser)
8086
{
81-
result = await GetAccessTokenSilentlyAsync(app, scopes, userName);
87+
result = await GetAccessTokenSilentlyAsync(app, scopes, userName, msaPt);
8288
}
8389

8490
//
@@ -116,7 +122,7 @@ public async Task<IMicrosoftAuthenticationResult> GetTokenAsync(
116122
// account then the user may become stuck in a loop of authentication failures.
117123
if (!hasExistingUser && Context.Settings.UseMsAuthDefaultAccount)
118124
{
119-
result = await GetAccessTokenSilentlyAsync(app, scopes, null);
125+
result = await GetAccessTokenSilentlyAsync(app, scopes, null, msaPt);
120126

121127
if (result is null || !await UseDefaultAccountAsync(result.Account.Username))
122128
{
@@ -281,34 +287,62 @@ internal MicrosoftAuthenticationFlowType GetFlowType()
281287
/// <summary>
282288
/// Obtain an access token without showing UI or prompts.
283289
/// </summary>
284-
private async Task<AuthenticationResult> GetAccessTokenSilentlyAsync(IPublicClientApplication app, string[] scopes, string userName)
290+
private async Task<AuthenticationResult> GetAccessTokenSilentlyAsync(
291+
IPublicClientApplication app, string[] scopes, string userName, bool msaPt)
285292
{
286293
try
287294
{
288295
if (userName is null)
289296
{
290-
Context.Trace.WriteLine("Attempting to acquire token silently for current operating system account...");
297+
Context.Trace.WriteLine(
298+
"Attempting to acquire token silently for current operating system account...");
291299

292-
return await app.AcquireTokenSilent(scopes, PublicClientApplication.OperatingSystemAccount).ExecuteAsync();
300+
return await app.AcquireTokenSilent(scopes, PublicClientApplication.OperatingSystemAccount)
301+
.ExecuteAsync();
293302
}
294303
else
295304
{
296305
Context.Trace.WriteLine($"Attempting to acquire token silently for user '{userName}'...");
297306

298-
// We can either call `app.GetAccountsAsync` and filter through the IAccount objects for the instance with the correct user name,
299-
// or we can just pass the user name string we have as the `loginHint` and let MSAL do exactly that for us instead!
300-
return await app.AcquireTokenSilent(scopes, loginHint: userName).ExecuteAsync();
307+
// Enumerate all accounts and find the one matching the user name
308+
IEnumerable<IAccount> accounts = await app.GetAccountsAsync();
309+
IAccount account = accounts.FirstOrDefault(x =>
310+
StringComparer.OrdinalIgnoreCase.Equals(x.Username, userName));
311+
if (account is null)
312+
{
313+
Context.Trace.WriteLine($"No cached account found for user '{userName}'...");
314+
return null;
315+
}
316+
317+
var atsBuilder = app.AcquireTokenSilent(scopes, account);
318+
319+
// Is we are operating with an MSA passthrough app we need to ensure that we target the
320+
// special MSA 'transfer' tenant explicitly. This is a workaround for MSAL issue:
321+
// https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/3077
322+
if (msaPt && Guid.TryParse(account.HomeAccountId.TenantId, out Guid homeTenantId) &&
323+
homeTenantId == Constants.MsaHomeTenantId)
324+
{
325+
atsBuilder = atsBuilder.WithTenantId(Constants.MsaTransferTenantId.ToString("D"));
326+
}
327+
328+
return await atsBuilder.ExecuteAsync();
301329
}
302330
}
303331
catch (MsalUiRequiredException)
304332
{
305333
Context.Trace.WriteLine("Failed to acquire token silently; user interaction is required.");
306334
return null;
307335
}
336+
catch (Exception ex)
337+
{
338+
Context.Trace.WriteLine("Failed to acquire token silently.");
339+
Context.Trace.WriteException(ex);
340+
return null;
341+
}
308342
}
309343

310344
private async Task<IPublicClientApplication> CreatePublicClientApplicationAsync(
311-
string authority, string clientId, Uri redirectUri, bool enableBroker)
345+
string authority, string clientId, Uri redirectUri, bool enableBroker, bool msaPt)
312346
{
313347
var httpFactoryAdaptor = new MsalHttpClientFactoryAdaptor(Context.HttpClientFactory);
314348

@@ -370,7 +404,7 @@ private async Task<IPublicClientApplication> CreatePublicClientApplicationAsync(
370404
new BrokerOptions(BrokerOptions.OperatingSystems.Windows)
371405
{
372406
Title = "Git Credential Manager",
373-
MsaPassthrough = true,
407+
MsaPassthrough = msaPt,
374408
}
375409
);
376410
#endif

src/shared/Core/Constants.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@ public static class Constants
1818

1919
public static readonly Guid DevBoxPartnerId = new("e3171dd9-9a5f-e5be-b36c-cc7c4f3f3bcf");
2020

21+
/// <summary>
22+
/// Home tenant ID for Microsoft Accounts (MSA).
23+
/// </summary>
24+
public static readonly Guid MsaHomeTenantId = new("9188040d-6c67-4c5b-b112-36a304b66dad");
25+
26+
/// <summary>
27+
/// Special tenant ID for transferring between Microsoft Account (MSA) native tokens
28+
/// and AAD tokens. Only required for MSA-Passthrough applications.
29+
/// </summary>
30+
public static readonly Guid MsaTransferTenantId = new("f8cdef31-a31e-4b4a-93e4-5f571e91255a");
31+
2132
public static class CredentialStoreNames
2233
{
2334
public const string WindowsCredentialManager = "wincredman";
@@ -210,7 +221,6 @@ public static class HelpUrls
210221
public const string GcmCredentialStores = "https://aka.ms/gcm/credstores";
211222
public const string GcmWamComSecurity = "https://aka.ms/gcm/wamadmin";
212223
public const string GcmAutoDetect = "https://aka.ms/gcm/autodetect";
213-
public const string GcmExecRename = "https://aka.ms/gcm/rename";
214224
public const string GcmDefaultAccount = "https://aka.ms/gcm/defaultaccount";
215225
public const string GcmMultipleUsers = "https://aka.ms/gcm/multipleusers";
216226
}

src/shared/Core/Diagnostics/NetworkingDiagnostic.cs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ namespace GitCredentialManager.Diagnostics
1212
public class NetworkingDiagnostic : Diagnostic
1313
{
1414
private const string TestHttpUri = "http://example.com";
15+
private const string TestHttpUriFallback = "http://httpforever.com";
1516
private const string TestHttpsUri = "https://example.com";
1617

1718
public NetworkingDiagnostic(ICommandContext commandContext)
@@ -28,9 +29,7 @@ protected override async Task<bool> RunInternalAsync(StringBuilder log, IList<st
2829
bool hasNetwork = NetworkInterface.GetIsNetworkAvailable();
2930
log.AppendLine($"IsNetworkAvailable: {hasNetwork}");
3031

31-
log.Append($"Sending HEAD request to {TestHttpUri}...");
32-
using var httpResponse = await httpClient.HeadAsync(TestHttpUri);
33-
log.AppendLine(" OK");
32+
SendHttpRequest(log, httpClient);
3433

3534
log.Append($"Sending HEAD request to {TestHttpsUri}...");
3635
using var httpsResponse = await httpClient.HeadAsync(TestHttpsUri);
@@ -98,5 +97,23 @@ protected override async Task<bool> RunInternalAsync(StringBuilder log, IList<st
9897

9998
return true;
10099
}
100+
101+
internal /* For testing purposes */ async void SendHttpRequest(StringBuilder log, HttpClient httpClient)
102+
{
103+
foreach (var uri in new List<string> { TestHttpUri, TestHttpUriFallback })
104+
{
105+
try
106+
{
107+
log.Append($"Sending HEAD request to {uri}...");
108+
using var httpResponse = await httpClient.HeadAsync(uri);
109+
log.AppendLine(" OK");
110+
break;
111+
}
112+
catch (HttpRequestException)
113+
{
114+
log.AppendLine(" warning: HEAD request failed");
115+
}
116+
}
117+
}
101118
}
102119
}

src/shared/Core/Trace2FileWriter.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.IO;
2+
using System;
23

34
namespace GitCredentialManager;
45

@@ -13,6 +14,22 @@ public Trace2FileWriter(Trace2FormatTarget formatTarget, string path) : base(for
1314

1415
public override void Write(Trace2Message message)
1516
{
16-
File.AppendAllText(_path, Format(message));
17+
try
18+
{
19+
File.AppendAllText(_path, Format(message));
20+
}
21+
catch (DirectoryNotFoundException)
22+
{
23+
// Do nothing, as this either means we don't have the
24+
// parent directories above the file, or this trace2
25+
// target points to a directory.
26+
}
27+
catch (UnauthorizedAccessException)
28+
{
29+
// Do nothing, as this either means the file is not
30+
// accessible with current permissions, or we are on
31+
// Windows and the file is currently open for writing
32+
// by another process (likely Git itself.)
33+
}
1734
}
1835
}

0 commit comments

Comments
 (0)