Skip to content

Commit 72a1c6d

Browse files
authored
Merge pull request #9 from mjcheetham/generic-provider
Add a generic provider supporting basic and WIA
2 parents 8d0ebef + 6a0872e commit 72a1c6d

17 files changed

+817
-51
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
using System;
4+
using System.Text;
5+
6+
namespace Microsoft.Git.CredentialManager.Authentication
7+
{
8+
public interface IBasicAuthentication
9+
{
10+
GitCredential GetCredentials(string resource, string userName);
11+
}
12+
13+
public static class BasicAuthenticationExtensions
14+
{
15+
public static GitCredential GetCredentials(this IBasicAuthentication basicAuth, string resource)
16+
{
17+
return basicAuth.GetCredentials(resource, null);
18+
}
19+
}
20+
21+
public class TtyPromptBasicAuthentication : IBasicAuthentication
22+
{
23+
private readonly ICommandContext _context;
24+
25+
public TtyPromptBasicAuthentication(ICommandContext context)
26+
{
27+
EnsureArgument.NotNull(context, nameof(context));
28+
29+
_context = context;
30+
}
31+
32+
public GitCredential GetCredentials(string resource, string userName)
33+
{
34+
EnsureArgument.NotNullOrWhiteSpace(resource, nameof(resource));
35+
36+
// Are terminal prompt disabled?
37+
if (_context.TryGetEnvironmentVariable(
38+
Constants.EnvironmentVariables.GitTerminalPrompts, out string envarPrompts)
39+
&& envarPrompts == "0")
40+
{
41+
_context.Trace.WriteLine($"{Constants.EnvironmentVariables.GitTerminalPrompts} is 0; terminal prompts have been disabled.");
42+
43+
throw new InvalidOperationException("Cannot show basic credential prompt because terminal prompts have been disabled.");
44+
}
45+
46+
_context.StdError.WriteLine("Enter credentials for '{0}':", resource);
47+
48+
if (!string.IsNullOrWhiteSpace(userName))
49+
{
50+
// Don't need to prompt for the username if it has been specified already
51+
_context.StdError.WriteLine("Username: {0}", userName);
52+
}
53+
else
54+
{
55+
// Prompt for username
56+
userName = _context.Prompt("Username");
57+
}
58+
59+
// Prompt for password
60+
string password = _context.PromptSecret("Password");
61+
62+
return new GitCredential(userName, password);
63+
}
64+
}
65+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
using System;
4+
using System.Net.Http;
5+
using System.Net.Http.Headers;
6+
using System.Threading.Tasks;
7+
8+
namespace Microsoft.Git.CredentialManager.Authentication
9+
{
10+
public interface IWindowsIntegratedAuthentication
11+
{
12+
Task<bool> GetIsSupportedAsync(Uri uri);
13+
}
14+
15+
public class WindowsIntegratedAuthentication : IWindowsIntegratedAuthentication
16+
{
17+
private readonly ICommandContext _context;
18+
private readonly IHttpClientFactory _httpFactory;
19+
20+
public WindowsIntegratedAuthentication(ICommandContext context)
21+
: this(context, new HttpClientFactory()) { }
22+
23+
public WindowsIntegratedAuthentication(ICommandContext context, IHttpClientFactory httpFactory)
24+
{
25+
_context = context;
26+
_httpFactory = httpFactory;
27+
}
28+
29+
public async Task<bool> GetIsSupportedAsync(Uri uri)
30+
{
31+
EnsureArgument.AbsoluteUri(uri, nameof(uri));
32+
33+
bool supported = false;
34+
35+
_context.Trace.WriteLine($"HTTP HEAD {uri}");
36+
using (HttpClient client = _httpFactory.GetClient())
37+
using (HttpResponseMessage response = await client.HeadAsync(uri))
38+
{
39+
_context.Trace.WriteLine("HTTP Response - Ignoring response code");
40+
41+
_context.Trace.WriteLine("Inspecting WWW-Authenticate headers...");
42+
foreach (AuthenticationHeaderValue wwwHeader in response.Headers.WwwAuthenticate)
43+
{
44+
if (StringComparer.OrdinalIgnoreCase.Equals(wwwHeader.Scheme, Constants.WwwAuthenticateNegotiateScheme))
45+
{
46+
_context.Trace.WriteLine("Found WWW-Authenticate header for Negotiate");
47+
supported = true;
48+
}
49+
else if (StringComparer.OrdinalIgnoreCase.Equals(wwwHeader.Scheme, Constants.WwwAuthenticateNtlmScheme))
50+
{
51+
_context.Trace.WriteLine("Found WWW-Authenticate header for NTLM");
52+
supported = true;
53+
}
54+
}
55+
}
56+
57+
return supported;
58+
}
59+
}
60+
}

src/Microsoft.Git.CredentialManager/CommandContext.cs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System.Collections;
55
using System.Collections.Generic;
66
using System.IO;
7-
using System.Linq;
87
using System.Text;
98
using Microsoft.Git.CredentialManager.SecureStorage;
109

@@ -30,6 +29,25 @@ public interface ICommandContext
3029
/// </summary>
3130
TextWriter StdError { get; }
3231

32+
/// <summary>
33+
/// Shows a prompt and reads input.
34+
/// </summary>
35+
/// <param name="prompt">The prompt text to show.</param>
36+
/// <returns>The result from the prompt.</returns>
37+
string Prompt(string prompt);
38+
39+
/// <summary>
40+
/// Shows a prompt for capturing secret/sensitive information such as passwords, suppresses key echo,
41+
/// and reads the input.
42+
/// </summary>
43+
/// <param name="prompt">The prompt text to show.</param>
44+
/// <returns>The result from the prompt.</returns>
45+
/// <exception cref="T:System.InvalidOperationException">
46+
/// If <see cref="echo"/> is false, and the <see cref="System.Console.In"/> property is redirected from some
47+
/// stream other than the console.
48+
/// </exception>
49+
string PromptSecret(string prompt);
50+
3351
/// <summary>
3452
/// Application tracing system.
3553
/// </summary>
@@ -114,6 +132,46 @@ public TextWriter StdError
114132
}
115133
}
116134

135+
public string Prompt(string prompt)
136+
{
137+
StdError.Write($"{prompt}: ");
138+
139+
return StdIn.ReadLine();
140+
}
141+
142+
public string PromptSecret(string prompt)
143+
{
144+
StdError.Write($"{prompt}: ");
145+
146+
var value = new StringBuilder();
147+
bool done = false;
148+
149+
do
150+
{
151+
// TODO: Can & should we directly disable 'stdin echo' and then just use a
152+
// inStream/StdIn.ReadLine() call rather than needing to use Console.ReadKey?
153+
ConsoleKeyInfo keyInfo = Console.ReadKey(intercept: true);
154+
switch (keyInfo.Key)
155+
{
156+
case ConsoleKey.Enter:
157+
done = true;
158+
StdError.WriteLine();
159+
break;
160+
case ConsoleKey.Backspace:
161+
if (value.Length > 0)
162+
{
163+
value.Remove(value.Length - 1, 1);
164+
}
165+
break;
166+
default:
167+
value.Append(keyInfo.KeyChar);
168+
break;
169+
}
170+
} while (!done);
171+
172+
return value.ToString();
173+
}
174+
117175
public ITrace Trace { get; } = new Trace();
118176

119177
public IFileSystem FileSystem { get; } = new FileSystem();

src/Microsoft.Git.CredentialManager/Commands/StoreCommand.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,23 @@ protected override Task ExecuteInternalAsync(ICommandContext context, InputArgum
1919
// Create the credential based on Git's input
2020
string userName = input.UserName;
2121
string password = input.Password;
22-
var credential = new GitCredential(userName, password);
2322

24-
// Add or update the credential in the store.
25-
context.Trace.WriteLine("Storing credential...");
26-
context.CredentialStore.AddOrUpdate(credentialKey, credential);
27-
context.Trace.WriteLine("Credential was successfully stored.");
23+
// NTLM-authentication is signaled to Git as an empty username/password pair
24+
// and we will get called to 'store' these NTLM credentials.
25+
// We avoid storing empty credentials.
26+
if (string.IsNullOrWhiteSpace(userName) && string.IsNullOrWhiteSpace(password))
27+
{
28+
context.Trace.WriteLine("Not storing empty credential.");
29+
}
30+
else
31+
{
32+
var credential = new GitCredential(userName, password);
33+
34+
// Add or update the credential in the store.
35+
context.Trace.WriteLine("Storing credential...");
36+
context.CredentialStore.AddOrUpdate(credentialKey, credential);
37+
context.Trace.WriteLine("Credential was successfully stored.");
38+
}
2839

2940
return Task.CompletedTask;
3041
}

src/Microsoft.Git.CredentialManager/Constants.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ public static class Constants
99
public const string GcmVersion = "1.0";
1010
public const string PersonalAccessTokenUserName = "PersonalAccessToken";
1111

12+
public const string WwwAuthenticateNegotiateScheme = "Negotiate";
13+
public const string WwwAuthenticateNtlmScheme = "NTLM";
14+
1215
public static class EnvironmentVariables
1316
{
14-
public const string GcmTrace = "GCM_TRACE";
15-
public const string GcmTraceSecrets = "GCM_TRACE_SECRETS";
16-
public const string GcmDebug = "GCM_DEBUG";
17+
public const string GcmTrace = "GCM_TRACE";
18+
public const string GcmTraceSecrets = "GCM_TRACE_SECRETS";
19+
public const string GcmDebug = "GCM_DEBUG";
20+
public const string GitTerminalPrompts = "GIT_TERMINAL_PROMPT";
1721
}
1822

1923
/// <summary>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Microsoft.Git.CredentialManager.Authentication;
4+
5+
namespace Microsoft.Git.CredentialManager
6+
{
7+
public class GenericHostProvider : HostProvider
8+
{
9+
private readonly IBasicAuthentication _basicAuth;
10+
private readonly IWindowsIntegratedAuthentication _winAuth;
11+
12+
public GenericHostProvider(ICommandContext context)
13+
: this(context, new TtyPromptBasicAuthentication(context), new WindowsIntegratedAuthentication(context)) { }
14+
15+
public GenericHostProvider(ICommandContext context,
16+
IBasicAuthentication basicAuth,
17+
IWindowsIntegratedAuthentication winAuth)
18+
: base(context)
19+
{
20+
EnsureArgument.NotNull(basicAuth, nameof(basicAuth));
21+
EnsureArgument.NotNull(winAuth, nameof(winAuth));
22+
23+
_basicAuth = basicAuth;
24+
_winAuth = winAuth;
25+
}
26+
27+
#region HostProvider
28+
29+
public override string Name => "Generic";
30+
31+
public override bool IsSupported(InputArguments input)
32+
{
33+
return input != null && (StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "http") ||
34+
StringComparer.OrdinalIgnoreCase.Equals(input.Protocol, "https"));
35+
}
36+
37+
public override string GetCredentialKey(InputArguments input)
38+
{
39+
return GetUriFromInput(input).AbsoluteUri;
40+
}
41+
42+
public override async Task<GitCredential> CreateCredentialAsync(InputArguments input)
43+
{
44+
Uri uri = GetUriFromInput(input);
45+
46+
// Determine the if the host supports Windows Integration Authentication (WIA)
47+
if (PlatformUtils.IsWindows())
48+
{
49+
Context.Trace.WriteLine($"Checking host '{uri.AbsoluteUri}' for Windows Integrated Authentication...");
50+
bool isWiaSupported = await _winAuth.GetIsSupportedAsync(uri);
51+
52+
if (!isWiaSupported)
53+
{
54+
Context.Trace.WriteLine("Host does not support WIA.");
55+
}
56+
else
57+
{
58+
Context.Trace.WriteLine($"Host supports WIA - generating empty credential...");
59+
60+
// WIA is signaled to Git using an empty username/password
61+
return new GitCredential(string.Empty, string.Empty);
62+
}
63+
}
64+
else
65+
{
66+
string osType = PlatformUtils.GetPlatformInformation().OperatingSystemType;
67+
Context.Trace.WriteLine($"Skipping check for Windows Integrated Authentication on {osType}.");
68+
}
69+
70+
Context.Trace.WriteLine("Prompting for basic credentials...");
71+
return _basicAuth.GetCredentials(uri.AbsoluteUri, uri.UserInfo);
72+
}
73+
74+
#endregion
75+
76+
#region Helpers
77+
78+
private static Uri GetUriFromInput(InputArguments input)
79+
{
80+
return new UriBuilder
81+
{
82+
Scheme = input.Protocol,
83+
UserName = input.UserName,
84+
Host = input.Host,
85+
Path = input.Path
86+
}.Uri;
87+
}
88+
89+
#endregion
90+
}
91+
}

src/git-credential-manager/Application.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public static async Task<int> RunAsync(string[] args)
6565

6666
// Register all supported host providers
6767
HostProviderRegistry.Register(
68-
// TODO
68+
new GenericHostProvider(context)
6969
);
7070

7171
// Trace the current version and program arguments

0 commit comments

Comments
 (0)