Skip to content

Commit cd5140b

Browse files
committed
Merge remote-tracking branch 'embargoed/ntlm' into release
2 parents 072a76d + 5fa7116 commit cd5140b

25 files changed

Lines changed: 1086 additions & 369 deletions

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.7.2.0
1+
2.7.3.0

docs/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ The following are links to GCM user support documentation:
1414
- [Azure Managed Identities and Service Principals][gcm-misp]
1515
- [GitLab support][gcm-gitlab]
1616
- [Generic OAuth support][gcm-oauth]
17+
- [NTLM and Kerberos authentication][gcm-ntlm-kerberos]
1718

1819
[gcm-azure-tokens]: azrepos-users-and-tokens.md
1920
[gcm-config]: configuration.md
@@ -27,3 +28,4 @@ The following are links to GCM user support documentation:
2728
[gcm-net-config]: netconfig.md
2829
[gcm-oauth]: generic-oauth.md
2930
[gcm-usage]: usage.md
31+
[gcm-ntlm-kerberos]: ntlm-kerberos.md

docs/img/ntlm-warning.png

54.4 KB
Loading

docs/ntlm-kerberos.md

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# NTLM and Kerberos Authentication
2+
3+
## Background
4+
5+
NTLM and Kerberos are two authentication protocols that are commonly used in
6+
Windows environments.
7+
8+
In Git Credential Manager (GCM), we refer to these protocols under the umbrella
9+
term "Windows Integrated Authentication".
10+
11+
### NTLM
12+
13+
[NTLM (NT LAN Manager)][ntlm-wiki] is a challenge-response authentication
14+
protocol used in various Microsoft network protocols, such as
15+
[SMB file sharing][smb-docs].
16+
17+
> [!CAUTION]
18+
> NTLM is now considered _**insecure**_ due to weak cryptographic algorithms and
19+
> vulnerabilities to various attacks, such as pass-the-hash and relay attacks.
20+
> As such, it is not recommended for use in modern applications.
21+
>
22+
> There are several versions of NTLM, with NTLMv2 being the latest, however
23+
> **all versions** are considered weak by modern security standards.
24+
>
25+
> Microsoft lists [NTLM as a deprecated protocol][ntlm-deprecated] and has
26+
> removed NTLMv1 from Windows as of Windows 11 build 24H2 / Server 2025.
27+
28+
NTLM is advertised by HTTP servers using the `WWW-Authenticate: NTLM` header.
29+
When a client receives this header, it can respond with an NTLM authentication
30+
message to prove its identity.
31+
32+
### Kerberos
33+
34+
[Kerberos][kerberos-wiki], on the other hand, is a more secure and robust
35+
authentication protocol that uses tickets to authenticate users and services.
36+
It is the recommended authentication protocol for Windows domains and is widely
37+
used in enterprise environments.
38+
39+
Unlike NTLM, Kerberos is typically not directly advertised by HTTP servers, but
40+
is instead advertised using "SPNEGO" and the `WWW-Authenticate: Negotiate`
41+
header.
42+
43+
#### GSS-API Negotiate and SPNEGO
44+
45+
Kerberos (or NTLM) authentication is typically initially established using the
46+
[GSS-API][gssapi-wiki] ([RFC 2743][gssapi-rfc]) negotiation mechanism
47+
["SPNEGO"][spnego-wiki] ([RFC 4178][spnego-rfc]). SPNEGO allows the client and
48+
server to agree on which authentication protocol to use (Kerberos or NTLM) based
49+
on their capabilities. Typically Kerberos is preferred if both the client and
50+
server support it, with NTLM acting as a fallback.
51+
52+
## Built-in Support in Git
53+
54+
Git provides built-in support for NTLM and Kerberos authentication through the
55+
use of [libcurl][libcurl], which is the underlying library used by Git for HTTP
56+
and HTTPS communications. When Git is compiled with libcurl support, it can
57+
leverage the authentication mechanisms provided by libcurl, including NTLM and
58+
Kerberos.
59+
60+
On Windows, Git can use the native Windows [SSPI][sspi-wiki] (Security Support
61+
Provider Interface) to perform NTLM and Kerberos authentication. This allows Git
62+
to integrate seamlessly with the Windows authentication infrastructure.
63+
64+
> [!NOTE]
65+
> As of Git for Windows version 2.XX.X, **NTLM support is disabled by default**.
66+
> Kerberos support _remains enabled_.
67+
68+
### Re-enabling NTLM Support
69+
70+
You can re-enable NTLM support in Git for Windows for a particular remote by
71+
setting Git config option [`http.<url>.allowNTLMAuth`][ntlm-config] to `true`.
72+
For example, to enable NTLM authentication for `https://example.com`, you would
73+
run the following command:
74+
75+
```shell
76+
git config --global http.https://example.com.allowNTLMAuth true
77+
```
78+
79+
> [!WARNING]
80+
> Enabling NTLM authentication may expose you to security risks, as NTLM is
81+
> considered insecure. It is recommended to use Kerberos authentication where
82+
> possible, and to only use NTLM with trusted servers in secure environments.
83+
84+
> [!WARNING]
85+
> Only ever use NTLM authentication over secure connections (i.e., HTTPS) to
86+
> protect against eavesdropping and man-in-the-middle attacks.
87+
88+
When using GCM with a remote that supports NTLM authentication, GCM will warn
89+
you if NTLM authentication is not enabled in Git but the remote server
90+
advertises NTLM support.
91+
92+
![GCM warning prompt that NTLM is disabled inside of Git][ntlm-warning-image]
93+
94+
* Selecting "Just this time" will continue with NTLM authentication, but only
95+
for the current operation. The next time you interact with that remote, you
96+
will be prompted again.
97+
98+
* Selecting "Always for this remote" will set the `http.<url>.allowNTLMAuth`
99+
configuration option to `true` for that remote, and continue with NTLM
100+
authentication.
101+
102+
* Selecting "No" will prompt for a basic username/password credential, and Git's
103+
NTLM authentication support will remain disabled. If the remote server only
104+
supports NTLM then authentication will fail.
105+
106+
### Seamless Authentication
107+
108+
When using NTLM or Kerberos authentication with Git on Windows, it is possible
109+
to achieve seamless authentication without prompting for credentials. This is
110+
because Git can leverage the existing Windows user credentials to authenticate
111+
with the server.
112+
113+
This means that if you are logged into your Windows account, Git can use those
114+
credentials to authenticate with the remote server automatically, without
115+
prompting you for a username or password.
116+
117+
This feature is enabled by default in Git. To disable this behavior, you can set
118+
the [`http.<url>.emptyAuth`][emptyauth] configuration option to `false`. For
119+
example, to disable seamless authentication for `https://example.com`, you would
120+
run the following command:
121+
122+
```shell
123+
git config --global http.https://example.com.emptyAuth false
124+
```
125+
126+
If you disable seamless authentication, Git will prompt you for credentials
127+
when accessing a remote that advertises NTLM or Kerberos support rather than
128+
using the current Windows user's credentials.
129+
130+
[ntlm-wiki]: https://en.wikipedia.org/wiki/NTLM
131+
[kerberos-wiki]: https://en.wikipedia.org/wiki/Kerberos_(protocol)
132+
[smb-docs]: https://learn.microsoft.com/en-gb/windows/win32/fileio/microsoft-smb-protocol-and-cifs-protocol-overview
133+
[ntlm-deprecated]: https://learn.microsoft.com/en-us/windows/whats-new/deprecated-features
134+
[ntlm-config]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpallowNTLMAuth
135+
[gssapi-rfc]: https://datatracker.ietf.org/doc/html/rfc2743
136+
[gssapi-wiki]: https://en.wikipedia.org/wiki/GSSAPI
137+
[spnego-rfc]: https://datatracker.ietf.org/doc/html/rfc4178
138+
[spnego-wiki]: https://en.wikipedia.org/wiki/SPNEGO
139+
[libcurl]: https://curl.se/libcurl/
140+
[sspi-wiki]: https://en.wikipedia.org/wiki/Security_Support_Provider_Interface
141+
[emptyauth]: https://git-scm.com/docs/git-config#Documentation/git-config.txt-httpemptyAuth
142+
[ntlm-warning-image]: img/ntlm-warning.png

src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,8 @@ public async Task BitbucketHostProvider_GetCredentialAsync_Valid_Stored_Basic(
121121

122122
var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object);
123123

124-
var credential = await provider.GetCredentialAsync(input);
124+
var result = await provider.GetCredentialAsync(input);
125+
ICredential credential = result.Credential;
125126

126127
Assert.Equal(username, credential.Account);
127128
Assert.Equal(password, credential.Password);
@@ -158,7 +159,8 @@ public async Task BitbucketHostProvider_GetCredentialAsync_Valid_Stored_OAuth(
158159

159160
var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object);
160161

161-
var credential = await provider.GetCredentialAsync(input);
162+
var result = await provider.GetCredentialAsync(input);
163+
ICredential credential = result.Credential;
162164

163165
Assert.Equal(username, credential.Account);
164166
Assert.Equal(token, credential.Password);
@@ -193,7 +195,8 @@ public async Task BitbucketHostProvider_GetCredentialAsync_Valid_New_Basic(
193195

194196
var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object);
195197

196-
var credential = await provider.GetCredentialAsync(input);
198+
var result = await provider.GetCredentialAsync(input);
199+
ICredential credential = result.Credential;
197200

198201
Assert.Equal(username, credential.Account);
199202
Assert.Equal(password, credential.Password);
@@ -217,7 +220,8 @@ public async Task BitbucketHostProvider_GetCredentialAsync_Valid_New_OAuth(
217220

218221
var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object);
219222

220-
var credential = await provider.GetCredentialAsync(input);
223+
var result = await provider.GetCredentialAsync(input);
224+
ICredential credential = result.Credential;
221225

222226
Assert.Equal(username, credential.Account);
223227
Assert.Equal(accessToken, credential.Password);
@@ -245,7 +249,8 @@ public async Task BitbucketHostProvider_GetCredentialAsync_MissingAT_OAuth_Refre
245249

246250
var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object);
247251

248-
var credential = await provider.GetCredentialAsync(input);
252+
var result = await provider.GetCredentialAsync(input);
253+
ICredential credential = result.Credential;
249254

250255
Assert.Equal(username, credential.Account);
251256
Assert.Equal(accessToken, credential.Password);
@@ -275,7 +280,8 @@ public async Task BitbucketHostProvider_GetCredentialAsync_ExpiredAT_OAuth_Refre
275280

276281
var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object);
277282

278-
var credential = await provider.GetCredentialAsync(input);
283+
var result = await provider.GetCredentialAsync(input);
284+
ICredential credential = result.Credential;
279285

280286
Assert.Equal(username, credential.Account);
281287
Assert.Equal(accessToken, credential.Password);
@@ -303,7 +309,8 @@ public async Task BitbucketHostProvider_GetCredentialAsync_PreconfiguredMode_OAu
303309

304310
var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object);
305311

306-
var credential = await provider.GetCredentialAsync(input);
312+
var result = await provider.GetCredentialAsync(input);
313+
ICredential credential = result.Credential;
307314

308315
Assert.NotNull(credential);
309316

@@ -330,7 +337,8 @@ public async Task BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredenti
330337

331338
var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object);
332339

333-
var credential = await provider.GetCredentialAsync(input);
340+
var result = await provider.GetCredentialAsync(input);
341+
ICredential credential = result.Credential;
334342

335343
Assert.Equal(username, credential.Account);
336344
Assert.Equal(newToken, credential.Password);
@@ -359,7 +367,8 @@ public async Task BitbucketHostProvider_GetCredentialAsync_AlwaysRefreshCredenti
359367

360368
var provider = new BitbucketHostProvider(context, bitbucketAuthentication.Object, MockRestApiRegistry(input, bitbucketApi).Object);
361369

362-
var credential = await provider.GetCredentialAsync(input);
370+
var result = await provider.GetCredentialAsync(input);
371+
ICredential credential = result.Credential;
363372

364373
Assert.Equal(username, credential.Account);
365374
Assert.Equal(freshPassword, credential.Password);

src/shared/Atlassian.Bitbucket/BitbucketHostProvider.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public bool IsSupported(HttpResponseMessage response)
7878
return supported;
7979
}
8080

81-
public async Task<ICredential> GetCredentialAsync(InputArguments input)
81+
public async Task<GetCredentialResult> GetCredentialAsync(InputArguments input)
8282
{
8383
// We should not allow unencrypted communication and should inform the user
8484
if (!_context.Settings.AllowUnsafeRemotes &&
@@ -93,8 +93,9 @@ public async Task<ICredential> GetCredentialAsync(InputArguments input)
9393

9494
var authModes = await GetSupportedAuthenticationModesAsync(input);
9595

96-
return await GetStoredCredentials(input, authModes) ??
97-
await GetRefreshedCredentials(input, authModes);
96+
ICredential credential = await GetStoredCredentials(input, authModes) ??
97+
await GetRefreshedCredentials(input, authModes);
98+
return new GetCredentialResult(credential);
9899
}
99100

100101
private async Task<ICredential> GetStoredCredentials(InputArguments input, AuthenticationModes authModes)

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,34 +19,34 @@ public async Task WindowsIntegratedAuthentication_GetIsSupportedAsync_NullUri_Th
1919
var context = new TestCommandContext();
2020
var wiaAuth = new WindowsIntegratedAuthentication(context);
2121

22-
await Assert.ThrowsAsync<ArgumentNullException>(() => wiaAuth.GetIsSupportedAsync(null));
22+
await Assert.ThrowsAsync<ArgumentNullException>(() => wiaAuth.GetAuthenticationTypesAsync(null));
2323
}
2424

2525
[Fact]
2626
public async Task WindowsIntegratedAuthentication_GetIsSupportedAsync_NegotiateAndNtlm_ReturnsTrue()
2727
{
28-
await TestGetIsSupportedAsync(new[] {NegotiateHeader, NtlmHeader}, expected: true);
28+
await TestGetIsSupportedAsync(new[] {NegotiateHeader, NtlmHeader}, expected: WindowsAuthenticationTypes.All);
2929

3030
// Also check with the headers in the other order
31-
await TestGetIsSupportedAsync(new[] {NtlmHeader, NegotiateHeader}, expected: true);
31+
await TestGetIsSupportedAsync(new[] {NtlmHeader, NegotiateHeader}, expected: WindowsAuthenticationTypes.All);
3232
}
3333

3434
[Fact]
3535
public async Task WindowsIntegratedAuthentication_Windows_GetIsSupportedAsync_Ntlm_ReturnsTrue()
3636
{
37-
await TestGetIsSupportedAsync(new[]{NtlmHeader}, expected: true);
37+
await TestGetIsSupportedAsync(new[]{NtlmHeader}, expected: WindowsAuthenticationTypes.Ntlm);
3838
}
3939

4040
[Fact]
4141
public async Task WindowsIntegratedAuthentication_Windows_GetIsSupportedAsync_Negotiate_ReturnsTrue()
4242
{
43-
await TestGetIsSupportedAsync(new[]{NegotiateHeader}, expected: true);
43+
await TestGetIsSupportedAsync(new[]{NegotiateHeader}, expected: WindowsAuthenticationTypes.Negotiate);
4444
}
4545

4646
[Fact]
4747
public async Task WindowsIntegratedAuthentication_Windows_GetIsSupportedAsync_NoHeaders_ReturnsFalse()
4848
{
49-
await TestGetIsSupportedAsync(new string[0], expected: false);
49+
await TestGetIsSupportedAsync(new string[0], expected: WindowsAuthenticationTypes.None);
5050
}
5151

5252
[Fact]
@@ -61,12 +61,12 @@ await TestGetIsSupportedAsync(
6161
"NotNegotiate test test Negotiate test",
6262
"NotKerberos test test Negotiate test"
6363
},
64-
expected: false);
64+
expected: WindowsAuthenticationTypes.None);
6565
}
6666

6767
#region Helpers
6868

69-
private static async Task TestGetIsSupportedAsync(string[] wwwAuthHeaders, bool expected)
69+
private static async Task TestGetIsSupportedAsync(string[] wwwAuthHeaders, WindowsAuthenticationTypes expected)
7070
{
7171
var context = new TestCommandContext();
7272
var uri = new Uri("https://example.com");
@@ -83,7 +83,7 @@ private static async Task TestGetIsSupportedAsync(string[] wwwAuthHeaders, bool
8383
context.HttpClientFactory.MessageHandler = httpHandler;
8484
var wiaAuth = new WindowsIntegratedAuthentication(context);
8585

86-
bool actual = await wiaAuth.GetIsSupportedAsync(uri);
86+
WindowsAuthenticationTypes actual = await wiaAuth.GetAuthenticationTypesAsync(uri);
8787

8888
Assert.Equal(expected, actual);
8989
httpHandler.AssertRequest(HttpMethod.Head, uri, expectedNumberOfCalls: 1);

src/shared/Core.Tests/Commands/GetCommandTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public async Task GetCommand_ExecuteAsync_CallsHostProviderAndWritesCredential()
2828

2929
var providerMock = new Mock<IHostProvider>();
3030
providerMock.Setup(x => x.GetCredentialAsync(It.IsAny<InputArguments>()))
31-
.ReturnsAsync(testCredential);
31+
.ReturnsAsync(new GetCredentialResult(testCredential));
3232
var providerRegistry = new TestHostProviderRegistry {Provider = providerMock.Object};
3333
var context = new TestCommandContext
3434
{

0 commit comments

Comments
 (0)