Skip to content

Commit 603541d

Browse files
committed
Add Microsoft Authentication helper for Windows
Add an authentication helper for AAD/MSA auth that runs on Windows. ADAL.NET is used to obtain an access token based on input fed over stdin by Git Credential Manager. The access token is returned back to GCM over stdout on success. Common application start-up code has been refactored into the `ApplicationBase` abstract class. This ensures consistent tracing and debug support using the GCM_TRACE & GCM_TRACE_SECRETS and GCM_DEBUG environment variables.
1 parent 29037f8 commit 603541d

File tree

18 files changed

+818
-118
lines changed

18 files changed

+818
-118
lines changed

Git-Credential-Manager.sln

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub", "common\src\GitHub
3131
EndProject
3232
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHub.Tests", "common\tests\GitHub.Tests\GitHub.Tests.csproj", "{03B9B82B-7DCA-4554-97AB-C98FF18FB385}"
3333
EndProject
34+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Authentication.Helper", "windows\src\Microsoft.Authentication.Helper\Microsoft.Authentication.Helper.csproj", "{8B984F78-4EAF-4BC0-A34E-BA3949700ED4}"
35+
EndProject
36+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Authentication.Helper.Tests", "windows\tests\Microsoft.Authentication.Helper.Tests\Microsoft.Authentication.Helper.Tests.csproj", "{E0391B02-16D5-4B49-9C33-349B25717011}"
37+
EndProject
3438
Global
3539
GlobalSection(SolutionConfigurationPlatforms) = preSolution
3640
Debug|Any CPU = Debug|Any CPU
@@ -103,6 +107,18 @@ Global
103107
{03B9B82B-7DCA-4554-97AB-C98FF18FB385}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
104108
{03B9B82B-7DCA-4554-97AB-C98FF18FB385}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
105109
{03B9B82B-7DCA-4554-97AB-C98FF18FB385}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
110+
{8B984F78-4EAF-4BC0-A34E-BA3949700ED4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
111+
{8B984F78-4EAF-4BC0-A34E-BA3949700ED4}.Release|Any CPU.ActiveCfg = Release|Any CPU
112+
{8B984F78-4EAF-4BC0-A34E-BA3949700ED4}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
113+
{8B984F78-4EAF-4BC0-A34E-BA3949700ED4}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
114+
{8B984F78-4EAF-4BC0-A34E-BA3949700ED4}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
115+
{8B984F78-4EAF-4BC0-A34E-BA3949700ED4}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
116+
{E0391B02-16D5-4B49-9C33-349B25717011}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
117+
{E0391B02-16D5-4B49-9C33-349B25717011}.Release|Any CPU.ActiveCfg = Release|Any CPU
118+
{E0391B02-16D5-4B49-9C33-349B25717011}.WindowsDebug|Any CPU.ActiveCfg = Debug|Any CPU
119+
{E0391B02-16D5-4B49-9C33-349B25717011}.WindowsDebug|Any CPU.Build.0 = Debug|Any CPU
120+
{E0391B02-16D5-4B49-9C33-349B25717011}.WindowsRelease|Any CPU.ActiveCfg = Release|Any CPU
121+
{E0391B02-16D5-4B49-9C33-349B25717011}.WindowsRelease|Any CPU.Build.0 = Release|Any CPU
106122
EndGlobalSection
107123
GlobalSection(SolutionProperties) = preSolution
108124
HideSolutionNode = FALSE
@@ -120,6 +136,8 @@ Global
120136
{5A7D9E8B-C1D2-4C5C-BE98-648C41D1F8BD} = {4B305AC9-153F-4EA3-822F-3E5023BABAF1}
121137
{3C840B06-A595-4FD9-9A76-56CD45B14780} = {A7FC1234-95E3-4496-B5F7-4306F41E6A0E}
122138
{03B9B82B-7DCA-4554-97AB-C98FF18FB385} = {4B305AC9-153F-4EA3-822F-3E5023BABAF1}
139+
{8B984F78-4EAF-4BC0-A34E-BA3949700ED4} = {1229E443-E66C-402F-8AA4-5AE6A207D3C7}
140+
{E0391B02-16D5-4B49-9C33-349B25717011} = {DD8B847B-286B-4928-867B-E09C6C90DA1F}
123141
EndGlobalSection
124142
GlobalSection(ExtensibilityGlobals) = postSolution
125143
SolutionGuid = {0EF9FC65-E6BA-45D4-A455-262A9EA4366B}
Lines changed: 20 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,20 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33
using System;
4-
using System.Diagnostics;
5-
using System.IO;
6-
using System.Text;
74
using System.Threading.Tasks;
85
using Microsoft.Git.CredentialManager.Commands;
96

107
namespace Microsoft.Git.CredentialManager
118
{
12-
public class Application : IDisposable
9+
public class Application : ApplicationBase
1310
{
14-
private readonly ICommandContext _context;
15-
16-
private TextWriter _traceFileWriter;
17-
1811
public IHostProviderRegistry ProviderRegistry { get; } = new HostProviderRegistry();
1912

2013
public Application(ICommandContext context)
21-
{
22-
EnsureArgument.NotNull(context, nameof(context));
23-
24-
_context = context;
25-
}
14+
: base(context) { }
2615

27-
public async Task<int> RunAsync(string[] args)
16+
protected override async Task<int> RunInternalAsync(string[] args)
2817
{
29-
// Launch debugger
30-
if (_context.IsEnvironmentVariableTruthy(Constants.EnvironmentVariables.GcmDebug, false))
31-
{
32-
_context.StdError.WriteLine("Waiting for debugger to be attached...");
33-
PlatformUtils.WaitForDebuggerAttached();
34-
35-
// Now the debugger is attached, break!
36-
Debugger.Break();
37-
}
38-
39-
// Enable tracing
40-
if (_context.TryGetEnvironmentVariable(Constants.EnvironmentVariables.GcmTrace, out string traceEnvar))
41-
{
42-
if (traceEnvar.IsTruthy()) // Trace to stderr
43-
{
44-
_context.Trace.AddListener(_context.StdError);
45-
}
46-
else if (Path.IsPathRooted(traceEnvar) && // Trace to a file
47-
TryCreateTextWriter(_context, traceEnvar, out var fileWriter))
48-
{
49-
_traceFileWriter = fileWriter;
50-
_context.Trace.AddListener(fileWriter);
51-
}
52-
else
53-
{
54-
_context.StdError.WriteLine($"warning: cannot write trace output to {traceEnvar}");
55-
}
56-
}
57-
58-
// Enable sensitive tracing and show warning
59-
if (_context.IsEnvironmentVariableTruthy(Constants.EnvironmentVariables.GcmTraceSecrets, false))
60-
{
61-
_context.Trace.EnableSecretTracing = true;
62-
_context.StdError.WriteLine("Secret tracing is enabled. Trace output may contain sensitive information.");
63-
}
64-
6518
// Construct all supported commands
6619
var commands = new CommandBase[]
6720
{
@@ -73,12 +26,12 @@ public async Task<int> RunAsync(string[] args)
7326
};
7427

7528
// Trace the current version and program arguments
76-
_context.Trace.WriteLine($"{Constants.GetProgramHeader()} '{string.Join(" ", args)}'");
29+
Context.Trace.WriteLine($"{Constants.GetProgramHeader()} '{string.Join(" ", args)}'");
7730

7831
if (args.Length == 0)
7932
{
80-
_context.StdError.WriteLine("Missing command.");
81-
HelpCommand.PrintUsage(_context.StdError);
33+
Context.StdError.WriteLine("Missing command.");
34+
HelpCommand.PrintUsage(Context.StdError);
8235
return -1;
8336
}
8437

@@ -88,72 +41,49 @@ public async Task<int> RunAsync(string[] args)
8841
{
8942
try
9043
{
91-
await cmd.ExecuteAsync(_context, args);
44+
await cmd.ExecuteAsync(Context, args);
9245
return 0;
9346
}
9447
catch (Exception e)
9548
{
9649
if (e is AggregateException ae)
9750
{
98-
ae.Handle(x => WriteException(_context, x));
51+
ae.Handle(WriteException);
9952
}
10053
else
10154
{
102-
WriteException(_context, e);
55+
WriteException(e);
10356
}
10457

10558
return -1;
10659
}
10760
}
10861
}
10962

110-
_context.StdError.WriteLine("Unrecognized command '{0}'.", args[0]);
111-
HelpCommand.PrintUsage(_context.StdError);
63+
Context.StdError.WriteLine("Unrecognized command '{0}'.", args[0]);
64+
HelpCommand.PrintUsage(Context.StdError);
11265
return -1;
11366
}
11467

115-
#region Helpers
116-
117-
private static bool WriteException(ICommandContext context, Exception e)
68+
protected override void Dispose(bool disposing)
11869
{
119-
context.StdError.WriteLine("fatal: {0}", e.Message);
120-
if (e.InnerException != null)
70+
if (disposing)
12171
{
122-
context.StdError.WriteLine("fatal: {0}", e.InnerException.Message);
72+
ProviderRegistry.Dispose();
12373
}
12474

125-
return true;
75+
base.Dispose(disposing);
12676
}
12777

128-
private static bool TryCreateTextWriter(ICommandContext context, string path, out TextWriter writer)
78+
protected bool WriteException(Exception e)
12979
{
130-
writer = null;
131-
132-
try
133-
{
134-
var utf8NoBomEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
135-
136-
var stream = context.FileSystem.OpenFileStream(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
137-
writer = new StreamWriter(stream, utf8NoBomEncoding, 4096, leaveOpen: false);
138-
}
139-
catch
80+
Context.StdError.WriteLine("fatal: {0}", e.Message);
81+
if (e.InnerException != null)
14082
{
141-
// Swallow all exceptions
83+
Context.StdError.WriteLine("fatal: {0}", e.InnerException.Message);
14284
}
14385

144-
return writer != null;
145-
}
146-
147-
#endregion
148-
149-
#region IDisposable
150-
151-
public void Dispose()
152-
{
153-
ProviderRegistry.Dispose();
154-
_traceFileWriter?.Dispose();
86+
return true;
15587
}
156-
157-
#endregion
15888
}
15989
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.IO;
4+
using System.Text;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
8+
namespace Microsoft.Git.CredentialManager
9+
{
10+
public abstract class ApplicationBase : IDisposable
11+
{
12+
private static readonly Encoding Utf8NoBomEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
13+
14+
private TextWriter _traceFileWriter;
15+
16+
protected ICommandContext Context { get; }
17+
18+
protected ApplicationBase(ICommandContext context)
19+
{
20+
EnsureArgument.NotNull(context, nameof(context));
21+
22+
Context = context;
23+
}
24+
25+
public Task<int> RunAsync(string[] args)
26+
{
27+
// Launch debugger
28+
if (Context.IsEnvironmentVariableTruthy(Constants.EnvironmentVariables.GcmDebug, false))
29+
{
30+
Context.StdError.WriteLine("Waiting for debugger to be attached...");
31+
WaitForDebuggerAttached();
32+
33+
// Now the debugger is attached, break!
34+
Debugger.Break();
35+
}
36+
37+
// Enable tracing
38+
if (Context.TryGetEnvironmentVariable(Constants.EnvironmentVariables.GcmTrace, out string traceEnvar))
39+
{
40+
if (traceEnvar.IsTruthy()) // Trace to stderr
41+
{
42+
Context.Trace.AddListener(Context.StdError);
43+
}
44+
else if (Path.IsPathRooted(traceEnvar)) // Trace to a file
45+
{
46+
try
47+
{
48+
Stream stream = Context.FileSystem.OpenFileStream(traceEnvar, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
49+
_traceFileWriter = new StreamWriter(stream, Utf8NoBomEncoding, 4096, leaveOpen: false);
50+
51+
Context.Trace.AddListener(_traceFileWriter);
52+
}
53+
catch (Exception ex)
54+
{
55+
Context.StdError.WriteLine($"warning: unable to trace to file '{traceEnvar}': {ex.Message}");
56+
}
57+
}
58+
else
59+
{
60+
Context.StdError.WriteLine($"warning: unknown value for {Constants.EnvironmentVariables.GcmTrace} '{traceEnvar}'");
61+
}
62+
}
63+
64+
// Enable sensitive tracing and show warning
65+
if (Context.IsEnvironmentVariableTruthy(Constants.EnvironmentVariables.GcmTraceSecrets, false))
66+
{
67+
Context.Trace.IsSecretTracingEnabled = true;
68+
Context.Trace.WriteLine("Tracing of secrets is enabled. Trace output may contain sensitive information.");
69+
}
70+
71+
return RunInternalAsync(args);
72+
}
73+
74+
protected abstract Task<int> RunInternalAsync(string[] args);
75+
76+
#region Helpers
77+
78+
/// <summary>
79+
/// Wait until a debugger has attached to the currently executing process.
80+
/// </summary>
81+
private static void WaitForDebuggerAttached()
82+
{
83+
// Attempt to launch the debugger if the OS supports the explicit launching
84+
if (!Debugger.Launch())
85+
{
86+
// The prompt to debug was declined
87+
return;
88+
}
89+
90+
// Some platforms do not support explicit debugger launching..
91+
// Wait for the debugger to attach - poll & sleep until then
92+
while (!Debugger.IsAttached)
93+
{
94+
Thread.Sleep(TimeSpan.FromSeconds(1));
95+
}
96+
}
97+
98+
#endregion
99+
100+
#region IDisposable
101+
102+
protected virtual void Dispose(bool disposing)
103+
{
104+
if (disposing)
105+
{
106+
_traceFileWriter?.Dispose();
107+
}
108+
}
109+
110+
public void Dispose()
111+
{
112+
Dispose(true);
113+
GC.SuppressFinalize(this);
114+
}
115+
116+
#endregion
117+
}
118+
}

common/src/Microsoft.Git.CredentialManager/PlatformUtils.cs

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license.
33
using System;
4-
using System.Diagnostics;
54
using System.Runtime.InteropServices;
6-
using System.Threading;
75

86
namespace Microsoft.Git.CredentialManager
97
{
@@ -85,25 +83,6 @@ public static void EnsureLinux()
8583
}
8684
}
8785

88-
/// <summary>
89-
/// Wait until a debugger has attached to the currently executing process.
90-
/// </summary>
91-
public static void WaitForDebuggerAttached()
92-
{
93-
// Attempt to launch the debugger if the OS supports the explicit launching
94-
if (!Debugger.Launch())
95-
{
96-
// The prompt to debug was declined
97-
return;
98-
}
99-
100-
// Wait for the debugger to attach and poll & sleep until then
101-
while (!Debugger.IsAttached)
102-
{
103-
Thread.Sleep(TimeSpan.FromSeconds(1));
104-
}
105-
}
106-
10786
#region Platform information helper methods
10887

10988
private static string GetOSType()

0 commit comments

Comments
 (0)