Skip to content

Commit 7794a29

Browse files
committed
Add GitHub helper on Windows using WPF on netfx
Add a Windows authentication helper for GitHub using WPF on .NET Framework 4.6.1.
1 parent 3d880cb commit 7794a29

File tree

71 files changed

+6569
-86
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+6569
-86
lines changed

Git-Credential-Manager.sln

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Authentication.He
3535
EndProject
3636
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Installer.Mac", "src\osx\Installer.Mac\Installer.Mac.csproj", "{74FA0AA4-B5C1-4F3B-B182-277FC2D50715}"
3737
EndProject
38+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Authentication.Helper.Windows", "src\windows\GitHub.Authentication.Helper.Windows\GitHub.Authentication.Helper.Windows.csproj", "{47A55897-B044-4508-B0FD-85B3947AF842}"
39+
EndProject
40+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.UI", "src\windows\GitHub.UI\GitHub.UI.csproj", "{6AD4EA05-44A7-45DD-92C8-6A4C8870FB50}"
41+
EndProject
3842
Global
3943
GlobalSection(SolutionConfigurationPlatforms) = preSolution
4044
Debug|Any CPU = Debug|Any CPU
@@ -173,6 +177,22 @@ Global
173177
{74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.Release|Any CPU.ActiveCfg = Release|Any CPU
174178
{74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
175179
{74FA0AA4-B5C1-4F3B-B182-277FC2D50715}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
180+
{47A55897-B044-4508-B0FD-85B3947AF842}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
181+
{47A55897-B044-4508-B0FD-85B3947AF842}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
182+
{47A55897-B044-4508-B0FD-85B3947AF842}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
183+
{47A55897-B044-4508-B0FD-85B3947AF842}.Release|Any CPU.ActiveCfg = Release|Any CPU
184+
{47A55897-B044-4508-B0FD-85B3947AF842}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
185+
{47A55897-B044-4508-B0FD-85B3947AF842}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
186+
{47A55897-B044-4508-B0FD-85B3947AF842}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
187+
{47A55897-B044-4508-B0FD-85B3947AF842}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
188+
{6AD4EA05-44A7-45DD-92C8-6A4C8870FB50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
189+
{6AD4EA05-44A7-45DD-92C8-6A4C8870FB50}.MacDebug|Any CPU.ActiveCfg = Debug|Any CPU
190+
{6AD4EA05-44A7-45DD-92C8-6A4C8870FB50}.MacRelease|Any CPU.ActiveCfg = Release|Any CPU
191+
{6AD4EA05-44A7-45DD-92C8-6A4C8870FB50}.Release|Any CPU.ActiveCfg = Release|Any CPU
192+
{6AD4EA05-44A7-45DD-92C8-6A4C8870FB50}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
193+
{6AD4EA05-44A7-45DD-92C8-6A4C8870FB50}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
194+
{6AD4EA05-44A7-45DD-92C8-6A4C8870FB50}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
195+
{6AD4EA05-44A7-45DD-92C8-6A4C8870FB50}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
176196
EndGlobalSection
177197
GlobalSection(SolutionProperties) = preSolution
178198
HideSolutionNode = FALSE
@@ -193,6 +213,8 @@ Global
193213
{3D279E2D-E011-45CF-8EA8-3D71D1300443} = {A7FC1234-95E3-4496-B5F7-4306F41E6A0E}
194214
{206430B1-CEED-4C84-8D49-D0A399632202} = {3D279E2D-E011-45CF-8EA8-3D71D1300443}
195215
{74FA0AA4-B5C1-4F3B-B182-277FC2D50715} = {3D279E2D-E011-45CF-8EA8-3D71D1300443}
216+
{47A55897-B044-4508-B0FD-85B3947AF842} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9}
217+
{6AD4EA05-44A7-45DD-92C8-6A4C8870FB50} = {66722747-1B61-40E4-A89B-1AC8E6D62EA9}
196218
EndGlobalSection
197219
GlobalSection(ExtensibilityGlobals) = postSolution
198220
SolutionGuid = {0EF9FC65-E6BA-45D4-A455-262A9EA4366B}

src/osx/Installer.Mac/Installer.Mac.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project>
1+
<Project>
22
<!-- Implicit SDK props import -->
33
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
44

@@ -19,7 +19,7 @@
1919
<!-- Implicit SDK targets import (so we can override the default targets below) -->
2020
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
2121

22-
<Target Name="CoreCompile" DependsOnTargets="GetBuildVersion">
22+
<Target Name="CoreCompile" DependsOnTargets="GetBuildVersion" Condition="'$(OSPlatform)'=='osx'">
2323
<Message Text="$(MSBuildProjectDirectory)\build.sh --configuration='$(Configuration)' --version='$(BuildVersion)'" Importance="High" />
2424
<Exec Command="$(MSBuildProjectDirectory)\build.sh --configuration='$(Configuration)' --version='$(BuildVersion)'" />
2525
</Target>

src/osx/Microsoft.Authentication.Helper.Mac/Microsoft.Authentication.Helper.Mac.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<!-- Implicit SDK targets import (so we can override the default targets below) -->
1919
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
2020

21-
<Target Name="CoreCompile">
21+
<Target Name="CoreCompile" Condition="'$(OSPlatform)'=='osx'">
2222
<Message Text="$(MSBuildProjectDirectory)\build.sh --configuration='$(Configuration)'" Importance="High" />
2323
<Exec Command="$(MSBuildProjectDirectory)\build.sh --configuration='$(Configuration)'" />
2424
</Target>

src/shared/GitHub/GitHubAuthentication.cs

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
// Licensed under the MIT license.
33
using System;
44
using System.Threading.Tasks;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Reflection;
58
using Microsoft.Git.CredentialManager;
9+
using Microsoft.Git.CredentialManager.Authentication;
610

711
namespace GitHub
812
{
@@ -13,58 +17,98 @@ public interface IGitHubAuthentication
1317
Task<string> GetAuthenticationCodeAsync(Uri targetUri, bool isSms);
1418
}
1519

16-
public class TtyGitHubPromptAuthentication : IGitHubAuthentication
20+
public class GitHubAuthentication : AuthenticationBase, IGitHubAuthentication
1721
{
18-
private readonly ICommandContext _context;
22+
public GitHubAuthentication(ICommandContext context)
23+
: base(context) {}
1924

20-
public TtyGitHubPromptAuthentication(ICommandContext context)
25+
public async Task<ICredential> GetCredentialsAsync(Uri targetUri)
2126
{
22-
EnsureArgument.NotNull(context, nameof(context));
27+
string userName, password;
2328

24-
_context = context;
25-
}
29+
if (TryFindHelperExecutablePath(out string helperPath))
30+
{
31+
IDictionary<string, string> resultDict = await InvokeHelperAsync(helperPath, "--prompt userpass", null);
2632

27-
public Task<ICredential> GetCredentialsAsync(Uri targetUri)
28-
{
29-
EnsureTerminalPromptsEnabled();
33+
if (!resultDict.TryGetValue("username", out userName))
34+
{
35+
throw new Exception("Missing username in response");
36+
}
37+
38+
if (!resultDict.TryGetValue("password", out password))
39+
{
40+
throw new Exception("Missing password in response");
41+
}
42+
}
43+
else
44+
{
45+
EnsureTerminalPromptsEnabled();
3046

31-
_context.Terminal.WriteLine("Enter credentials for '{0}'...", targetUri);
47+
Context.Terminal.WriteLine("Enter credentials for '{0}'...", targetUri);
3248

33-
string userName = _context.Terminal.Prompt("Username");
34-
string password = _context.Terminal.PromptSecret("Password");
49+
userName = Context.Terminal.Prompt("Username");
50+
password = Context.Terminal.PromptSecret("Password");
51+
}
3552

36-
return Task.FromResult<ICredential>(new GitCredential(userName, password));
53+
return new GitCredential(userName, password);
3754
}
38-
39-
public Task<string> GetAuthenticationCodeAsync(Uri targetUri, bool isSms)
55+
public async Task<string> GetAuthenticationCodeAsync(Uri targetUri, bool isSms)
4056
{
41-
EnsureTerminalPromptsEnabled();
57+
if (TryFindHelperExecutablePath(out string helperPath))
58+
{
59+
IDictionary<string, string> resultDict = await InvokeHelperAsync(helperPath, "--prompt authcode", null);
4260

43-
_context.Terminal.WriteLine("Two-factor authentication is enabled and an authentication code is required.");
61+
if (!resultDict.TryGetValue("authcode", out string authCode))
62+
{
63+
throw new Exception("Missing authentication code in response");
64+
}
4465

45-
if (isSms)
46-
{
47-
_context.Terminal.WriteLine("An SMS containing the authentication code has been sent to your registered device.");
66+
return authCode;
4867
}
4968
else
5069
{
51-
_context.Terminal.WriteLine("Use your registered authentication app to generate an authentication code.");
52-
}
70+
EnsureTerminalPromptsEnabled();
71+
72+
Context.Terminal.WriteLine("Two-factor authentication is enabled and an authentication code is required.");
5373

54-
string authCode = _context.Terminal.Prompt("Authentication code");
74+
if (isSms)
75+
{
76+
Context.Terminal.WriteLine("An SMS containing the authentication code has been sent to your registered device.");
77+
}
78+
else
79+
{
80+
Context.Terminal.WriteLine("Use your registered authentication app to generate an authentication code.");
81+
}
5582

56-
return Task.FromResult(authCode);
83+
return Context.Terminal.Prompt("Authentication code");
84+
}
5785
}
5886

59-
private void EnsureTerminalPromptsEnabled()
87+
private bool TryFindHelperExecutablePath(out string path)
6088
{
61-
if (_context.TryGetEnvironmentVariable(Constants.EnvironmentVariables.GitTerminalPrompts, out string envarPrompts)
62-
&& envarPrompts == "0")
89+
string helperName = GitHubConstants.AuthHelperName;
90+
91+
if (PlatformUtils.IsWindows())
6392
{
64-
_context.Trace.WriteLine($"{Constants.EnvironmentVariables.GitTerminalPrompts} is 0; terminal prompts have been disabled.");
93+
helperName += ".exe";
94+
}
95+
96+
string executableDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
97+
path = Path.Combine(executableDirectory, helperName);
98+
if (!Context.FileSystem.FileExists(path))
99+
{
100+
Context.Trace.WriteLine($"Did not find helper '{helperName}' in '{executableDirectory}'");
65101

66-
throw new InvalidOperationException("Cannot show GitHub credential prompt because terminal prompts have been disabled.");
102+
// We currently only have a helper on Windows. If we failed to find the helper we should warn the user.
103+
if (PlatformUtils.IsWindows())
104+
{
105+
Context.StdError.WriteLine($"warning: missing '{helperName}' from installation.");
106+
}
107+
108+
return false;
67109
}
110+
111+
return true;
68112
}
69113
}
70114
}

src/shared/GitHub/GitHubConstants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ public static class GitHubConstants
77
public const string GitHubBaseUrlHost = "github.com";
88
public const string GistBaseUrlHost = "gist." + GitHubBaseUrlHost;
99

10+
public const string AuthHelperName = "GitHub.Authentication.Helper";
11+
1012
/// <summary>
1113
/// The GitHub required HTTP accepts header value
1214
/// </summary>

src/shared/GitHub/GitHubHostProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class GitHubHostProvider : HostProvider
1818
private readonly IGitHubAuthentication _gitHubAuth;
1919

2020
public GitHubHostProvider(ICommandContext context)
21-
: this(context, new GitHubRestApi(context), new TtyGitHubPromptAuthentication(context)) { }
21+
: this(context, new GitHubRestApi(context), new GitHubAuthentication(context)) { }
2222

2323
public GitHubHostProvider(ICommandContext context, IGitHubRestApi gitHubApi, IGitHubAuthentication gitHubAuth)
2424
: base(context)

src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public class AzureReposHostProvider : HostProvider
1414
private readonly IMicrosoftAuthentication _msAuth;
1515

1616
public AzureReposHostProvider(ICommandContext context)
17-
: this(context, new AzureDevOpsRestApi(context), new OutOfProcHelperMicrosoftAuthentication(context)) { }
17+
: this(context, new AzureDevOpsRestApi(context), new MicrosoftAuthentication(context)) { }
1818

1919
public AzureReposHostProvider(ICommandContext context, IAzureDevOpsRestApi azDevOps, IMicrosoftAuthentication msAuth)
2020
: base (context)
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Threading.Tasks;
5+
6+
namespace Microsoft.Git.CredentialManager.Authentication
7+
{
8+
public abstract class AuthenticationBase
9+
{
10+
protected readonly ICommandContext Context;
11+
12+
protected AuthenticationBase(ICommandContext context)
13+
{
14+
EnsureArgument.NotNull(context, nameof(context));
15+
16+
Context = context;
17+
}
18+
19+
protected async Task<IDictionary<string, string>> InvokeHelperAsync(string path, string args, IDictionary<string, string> standardInput)
20+
{
21+
var procStartInfo = new ProcessStartInfo(path)
22+
{
23+
Arguments = args,
24+
RedirectStandardInput = true,
25+
RedirectStandardOutput = true,
26+
RedirectStandardError = false, // Do not redirect stderr as tracing might be enabled
27+
UseShellExecute = false
28+
};
29+
30+
// We flush the trace writers here so that the we don't stomp over the
31+
// authentication helper's messages.
32+
Context.Trace.Flush();
33+
34+
var process = Process.Start(procStartInfo);
35+
if (process is null)
36+
{
37+
throw new Exception($"Failed to start helper process '{path}'");
38+
}
39+
40+
if (!(standardInput is null))
41+
{
42+
await process.StandardInput.WriteDictionaryAsync(standardInput);
43+
}
44+
45+
IDictionary<string, string> resultDict = await process.StandardOutput.ReadDictionaryAsync(StringComparer.OrdinalIgnoreCase);
46+
47+
await Task.Run(() => process.WaitForExit());
48+
int exitCode = process.ExitCode;
49+
50+
if (exitCode != 0)
51+
{
52+
if (!resultDict.TryGetValue("error", out string errorMessage))
53+
{
54+
errorMessage = "Unknown";
55+
}
56+
57+
throw new Exception($"helper error ({exitCode}): {errorMessage}");
58+
}
59+
60+
return resultDict;
61+
}
62+
63+
protected void EnsureTerminalPromptsEnabled()
64+
{
65+
if (Context.TryGetEnvironmentVariable(Constants.EnvironmentVariables.GitTerminalPrompts, out string envarPrompts)
66+
&& envarPrompts == "0")
67+
{
68+
Context.Trace.WriteLine($"{Constants.EnvironmentVariables.GitTerminalPrompts} is 0; terminal prompts have been disabled.");
69+
70+
throw new InvalidOperationException("Cannot show credential prompt because terminal prompts have been disabled.");
71+
}
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)