Skip to content

Commit 22bd06d

Browse files
committed
Merged PR 585338: Cut new release including security fix
**Changes:** 1. Fix NTLM proxy authentication 2. Fix reading empty Git configuration entry values 3. Allow users to select the type of interactive authentication flow for the Microsoft auth stack
2 parents 1f4c6db + 61c0388 commit 22bd06d

File tree

17 files changed

+500
-88
lines changed

17 files changed

+500
-88
lines changed

docs/configuration.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,46 @@ git config --global credential.plaintextStorePath /mnt/external-drive/credential
226226
```
227227

228228
**Also see: [GCM_PLAINTEXT_STORE_PATH](environment.md#GCM_PLAINTEXT_STORE_PATH)**
229+
230+
---
231+
232+
### credential.msauthFlow
233+
234+
Specify which authentication flow should be used when performing Microsoft authentication and an interactive flow is required.
235+
236+
Defaults to the value `auto`.
237+
238+
**Note:** This setting will be ignored if a native authentication helper is configured and available. See [`credential.msauthHelper`](#credentialmsauthhelper) for more information.
239+
240+
Value|Credential Store
241+
-|-
242+
`auto` _(default)_|Select the best option depending on the current environment and platform.
243+
`embedded`|Show a window with embedded web view control.
244+
`system`|Open the user's default web browser.
245+
`devicecode`|Show a device code.
246+
247+
#### Example
248+
249+
```shell
250+
git config --global credential.msauthFlow devicecode
251+
```
252+
253+
**Also see: [GCM_MSAUTH_FLOW](environment.md#GCM_MSAUTH_FLOW)**
254+
255+
---
256+
257+
### credential.msauthHelper
258+
259+
Full path to an external 'helper' tool to which Microsoft authentication should be delegated.
260+
261+
On macOS this defaults to the included native `Microsoft.Authentication.Helper` tool. On all other platforms this is not set.
262+
263+
**Note:** If a helper is set and available then all Microsoft authentication will be delegated to this helper and the [`credential.msauthFlow`](#credentialmsauthflow) setting will be ignored. Setting the value to the empty string (`""`) will unset any default helper.
264+
265+
#### Example
266+
267+
```shell
268+
git config --global credential.msauthHelper "C:\path\to\helper.exe"
269+
```
270+
271+
**Also see: [GCM_MSAUTH_HELPER](environment.md#GCM_MSAUTH_HELPER)**

docs/environment.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,3 +373,58 @@ export GCM_PLAINTEXT_STORE_PATH=/mnt/external-drive/credentials
373373
```
374374

375375
**Also see: [credential.plaintextStorePath](configuration.md#credentialplaintextstorepath)**
376+
377+
---
378+
379+
### GCM_MSAUTH_FLOW
380+
381+
Specify which authentication flow should be used when performing Microsoft authentication and an interactive flow is required.
382+
383+
Defaults to the value `auto`.
384+
385+
**Note:** This setting will be ignored if a native authentication helper is configured and available. See [`GCM_MSAUTH_HELPER`](#gcm_msauth_helper) for more information.
386+
387+
Value|Credential Store
388+
-|-
389+
`auto` _(default)_|Select the best option depending on the current environment and platform.
390+
`embedded`|Show a window with embedded web view control.
391+
`system`|Open the user's default web browser.
392+
`devicecode`|Show a device code.
393+
394+
##### Windows
395+
396+
```batch
397+
SET GCM_MSAUTH_FLOW="devicecode"
398+
```
399+
400+
##### macOS/Linux
401+
402+
```bash
403+
export GCM_MSAUTH_FLOW="devicecode"
404+
```
405+
406+
**Also see: [credential.msauthFlow](configuration.md#credentialmsauthflow)**
407+
408+
---
409+
410+
### GCM_MSAUTH_HELPER
411+
412+
Full path to an external 'helper' tool to which Microsoft authentication should be delegated.
413+
414+
On macOS this defaults to the included native `Microsoft.Authentication.Helper` tool. On all other platforms this is not set.
415+
416+
**Note:** If a helper is set and available then all Microsoft authentication will be delegated to this helper and the [`GCM_MSAUTH_FLOW`](#gcm_msauth_flow) setting will be ignored. Setting the value to the empty string (`""`) will unset any default helper.
417+
418+
##### Windows
419+
420+
```batch
421+
SET GCM_MSAUTH_HELPER="C:\path\to\helper.exe"
422+
```
423+
424+
##### macOS/Linux
425+
426+
```bash
427+
export GCM_MSAUTH_HELPER="/usr/local/bin/msauth-helper"
428+
```
429+
430+
**Also see: [credential.msauthHelper](configuration.md#credentialmsauthhelper)**

src/shared/Microsoft.AzureRepos/AzureDevOpsConstants.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@ internal static class AzureDevOpsConstants
1313
// We share this to be able to consume existing access tokens from the VS caches
1414
public const string AadClientId = "872cd9fa-d31f-45e0-9eab-6e460a02d1f1";
1515

16-
// Standard redirect URI for native client 'v1 protocol' applications
17-
// https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-protocols-oauth-code#request-an-authorization-code
18-
public static readonly Uri AadRedirectUri = new Uri("urn:ietf:wg:oauth:2.0:oob");
16+
// Redirect URI specified by the Visual Studio application configuration
17+
public static readonly Uri AadRedirectUri = new Uri("http://localhost");
1918

2019
public const string VstsHostSuffix = ".visualstudio.com";
2120
public const string AzureDevOpsHost = "dev.azure.com";
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System.Collections.Generic;
2+
using Microsoft.Git.CredentialManager.Interop.Windows;
3+
using Microsoft.Git.CredentialManager.Tests.Objects;
4+
using Xunit;
5+
6+
namespace Microsoft.Git.CredentialManager.Tests
7+
{
8+
public class EnvironmentTests
9+
{
10+
[PlatformFact(Platforms.Windows)]
11+
public void WindowsEnvironment_TryLocateExecutable_NotExists_ReturnFalse()
12+
{
13+
string pathVar = @"C:\Users\john.doe\bin;C:\Windows\system32;C:\Windows";
14+
string execName = "foo.exe";
15+
var fs = new TestFileSystem();
16+
var envars = new Dictionary<string, string> {["PATH"] = pathVar};
17+
var env = new WindowsEnvironment(fs, envars);
18+
19+
bool actualResult = env.TryLocateExecutable(execName, out string actualPath);
20+
21+
Assert.False(actualResult);
22+
Assert.Null(actualPath);
23+
}
24+
25+
[PlatformFact(Platforms.Windows)]
26+
public void WindowsEnvironment_TryLocateExecutable_Windows_Exists_ReturnTrueAndPath()
27+
{
28+
string pathVar = @"C:\Users\john.doe\bin;C:\Windows\system32;C:\Windows";
29+
string execName = "foo.exe";
30+
string expectedPath = @"C:\Windows\system32\foo.exe";
31+
var fs = new TestFileSystem
32+
{
33+
Files = new Dictionary<string, byte[]>
34+
{
35+
[@"C:\Windows\system32\foo.exe"] = new byte[0],
36+
}
37+
};
38+
var envars = new Dictionary<string, string> {["PATH"] = pathVar};
39+
var env = new WindowsEnvironment(fs, envars);
40+
41+
bool actualResult = env.TryLocateExecutable(execName, out string actualPath);
42+
43+
Assert.True(actualResult);
44+
Assert.Equal(expectedPath, actualPath);
45+
}
46+
47+
[PlatformFact(Platforms.Windows)]
48+
public void WindowsEnvironment_TryLocateExecutable_Windows_ExistsMultiple_ReturnTrueAndFirstPath()
49+
{
50+
string pathVar = @"C:\Users\john.doe\bin;C:\Windows\system32;C:\Windows";
51+
string execName = "foo.exe";
52+
string expectedPath = @"C:\Users\john.doe\bin\foo.exe";
53+
var fs = new TestFileSystem
54+
{
55+
Files = new Dictionary<string, byte[]>
56+
{
57+
[@"C:\Users\john.doe\bin\foo.exe"] = new byte[0],
58+
[@"C:\Windows\system32\foo.exe"] = new byte[0],
59+
[@"C:\Windows\foo.exe"] = new byte[0],
60+
}
61+
};
62+
var envars = new Dictionary<string, string> {["PATH"] = pathVar};
63+
var env = new WindowsEnvironment(fs, envars);
64+
65+
bool actualResult = env.TryLocateExecutable(execName, out string actualPath);
66+
67+
Assert.True(actualResult);
68+
Assert.Equal(expectedPath, actualPath);
69+
}
70+
}
71+
}

src/shared/Microsoft.Git.CredentialManager.Tests/HttpClientFactoryTests.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,104 @@ public void HttpClientFactory_TryCreateProxy_ProxyWithCredentials_ReturnsTrueOut
118118
Assert.Equal(proxyPass, configuredCredentials.Password);
119119
}
120120

121+
[Fact]
122+
public void HttpClientFactory_TryCreateProxy_ProxyWithNonEmptyUserAndEmptyPass_ReturnsTrueOutProxyWithUrlConfiguredCredentials()
123+
{
124+
const string proxyScheme = "https";
125+
const string proxyUser = "john.doe";
126+
const string proxyHost = "proxy.example.com/git";
127+
const string repoPath = "/tmp/repos/foo";
128+
const string repoRemote = "https://remote.example.com/foo.git";
129+
130+
string proxyConfigString = $"{proxyScheme}://{proxyUser}:@{proxyHost}";
131+
string expectedProxyUrl = $"{proxyScheme}://{proxyHost}";
132+
var repoRemoteUri = new Uri(repoRemote);
133+
134+
var settings = new TestSettings
135+
{
136+
RemoteUri = repoRemoteUri,
137+
RepositoryPath = repoPath,
138+
ProxyConfiguration = new Uri(proxyConfigString)
139+
};
140+
var httpFactory = new HttpClientFactory(Mock.Of<ITrace>(), settings, Mock.Of<IStandardStreams>());
141+
142+
bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);
143+
144+
Assert.True(result);
145+
Assert.NotNull(proxy);
146+
var configuredProxyUrl = proxy.GetProxy(repoRemoteUri);
147+
Assert.Equal(expectedProxyUrl, configuredProxyUrl.ToString());
148+
149+
Assert.NotNull(proxy.Credentials);
150+
Assert.IsType<NetworkCredential>(proxy.Credentials);
151+
var configuredCredentials = (NetworkCredential) proxy.Credentials;
152+
Assert.Equal(proxyUser, configuredCredentials.UserName);
153+
Assert.True(string.IsNullOrWhiteSpace(configuredCredentials.Password));
154+
}
155+
156+
[Fact]
157+
public void HttpClientFactory_TryCreateProxy_ProxyWithEmptyUserAndNonEmptyPass_ReturnsTrueOutProxyWithUrlConfiguredCredentials()
158+
{
159+
const string proxyScheme = "https";
160+
const string proxyPass = "letmein";
161+
const string proxyHost = "proxy.example.com/git";
162+
const string repoPath = "/tmp/repos/foo";
163+
const string repoRemote = "https://remote.example.com/foo.git";
164+
165+
string proxyConfigString = $"{proxyScheme}://:{proxyPass}@{proxyHost}";
166+
string expectedProxyUrl = $"{proxyScheme}://{proxyHost}";
167+
var repoRemoteUri = new Uri(repoRemote);
168+
169+
var settings = new TestSettings
170+
{
171+
RemoteUri = repoRemoteUri,
172+
RepositoryPath = repoPath,
173+
ProxyConfiguration = new Uri(proxyConfigString)
174+
};
175+
var httpFactory = new HttpClientFactory(Mock.Of<ITrace>(), settings, Mock.Of<IStandardStreams>());
176+
177+
bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);
178+
179+
Assert.True(result);
180+
Assert.NotNull(proxy);
181+
var configuredProxyUrl = proxy.GetProxy(repoRemoteUri);
182+
Assert.Equal(expectedProxyUrl, configuredProxyUrl.ToString());
183+
184+
Assert.NotNull(proxy.Credentials);
185+
Assert.IsType<NetworkCredential>(proxy.Credentials);
186+
var configuredCredentials = (NetworkCredential) proxy.Credentials;
187+
Assert.True(string.IsNullOrWhiteSpace(configuredCredentials.UserName));
188+
Assert.Equal(proxyPass, configuredCredentials.Password);
189+
}
190+
191+
[Fact]
192+
public void HttpClientFactory_TryCreateProxy_ProxyEmptyUserAndEmptyPass_ReturnsTrueOutProxyWithUrlDefaultCredentials()
193+
{
194+
const string repoPath = "/tmp/repos/foo";
195+
const string repoRemote = "https://remote.example.com/foo.git";
196+
var repoRemoteUri = new Uri(repoRemote);
197+
198+
string proxyConfigString = "https://:@proxy.example.com/git";
199+
string expectedProxyUrl = "https://proxy.example.com/git";
200+
201+
var settings = new TestSettings
202+
{
203+
RemoteUri = repoRemoteUri,
204+
RepositoryPath = repoPath,
205+
ProxyConfiguration = new Uri(proxyConfigString)
206+
};
207+
var httpFactory = new HttpClientFactory(Mock.Of<ITrace>(), settings, Mock.Of<IStandardStreams>());
208+
209+
bool result = httpFactory.TryCreateProxy(out IWebProxy proxy);
210+
211+
Assert.True(result);
212+
Assert.NotNull(proxy);
213+
var configuredProxyUrl = proxy.GetProxy(repoRemoteUri);
214+
Assert.Equal(expectedProxyUrl, configuredProxyUrl.ToString());
215+
216+
AssertDefaultCredentials(proxy.Credentials);
217+
}
218+
121219
private static void AssertDefaultCredentials(ICredentials credentials)
122220
{
123221
var netCred = (NetworkCredential) credentials;

src/shared/Microsoft.Git.CredentialManager/Authentication/AuthenticationBase.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ protected bool TryFindHelperExecutablePath(string envar, string configName, stri
106106
helperName = PlatformUtils.IsWindows() ? $"{defaultValue}.exe" : defaultValue;
107107
}
108108

109+
// If the user set the helper override to the empty string then they are signalling not to use a helper
110+
if (string.IsNullOrEmpty(helperName))
111+
{
112+
path = null;
113+
return false;
114+
}
115+
109116
if (Path.IsPathRooted(helperName))
110117
{
111118
path = helperName;

0 commit comments

Comments
 (0)