Skip to content

Commit c347cee

Browse files
committed
msauth: add WAM workaround for elevated procs
Add a workaround to a broker bug whereby the account control does not appear when running from an elevated process. AzureAD/microsoft-authentication-library-for-dotnet#2560 The underlying issue is to do with COM and the OS account control not being able to call-back in to the elevated process. The workaround is to set the process COM security to "none" iif we are on Windows 10, the process is elevated, and the user hasn't disabled the broker. It is possible the call to CoInitializeSecurity may fail, as this can only be called once in the lifetime of a process, and must be called before any COM interactions occur. The CLR may perform some COM interop before we even get to the Main method(!) We try our best here and call the CoInitializeSecurity function as soon as we reasonably can in the lifetime of our process.
1 parent 39efe03 commit c347cee

File tree

7 files changed

+194
-9
lines changed

7 files changed

+194
-9
lines changed

docs/wam.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Web Account Manager integration
2+
3+
## Running as administrator
4+
5+
The Windows broker ("WAM") makes heavy use of
6+
[COM](https://docs.microsoft.com/en-us/windows/win32/com/the-component-object-model),
7+
an IPC and RPC technology built-in to the Windows operating system. In order to
8+
integrate with WAM, Git Credential Manager and the underlying
9+
[Microsoft Authentication Library (MSAL)](https://aka.ms/msal-net)
10+
must use COM interfaces and remote procedure calls (RPC).
11+
12+
When you run Git Credential Manager as an elevated process, such as when you run
13+
a `git` command from an Administrator command-prompt or perform Git operations
14+
from Visual Studio running as Administrator, some of the calls made between GCM
15+
and WAM may fail due to differing process security levels.
16+
17+
If you have enabled using the broker and GCM detects it is running in an
18+
elevated process, it will automatically attempt to modify the COM security
19+
settings for the running process so that GCM and WAM can work together.
20+
21+
However, this automatic process security change is not guaranteed to succeed
22+
depending on various external factors like registry or system-wide COM settings.
23+
If GCM fails to modify the process COM security settings, a warning message is
24+
printed and use of the broker is disabled for this invocation of GCM:
25+
26+
```text
27+
warning: broker initialization failed
28+
Failed to set COM process security to allow Windows broker from an elevated process (0x80010119).
29+
See https://aka.ms/gcmcore-wamadmin for more information.
30+
```
31+
32+
### Possible solutions
33+
34+
In order to fix the problem there are a few options:
35+
36+
1. Do not run Git or Git Credential Manager as elevated processes.
37+
2. Disable the broker by setting the
38+
[`GCM_MSAUTH_USEBROKER`](environment.md#gcm_msauth_usebroker)
39+
environment variable or the
40+
[`credential.msauthUseBroker`](configuration.md#credentialmsauthusebroker)
41+
Git configuration setting to `false`.

src/shared/Git-Credential-Manager/Program.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
using System;
44
using System.IO;
55
using System.Reflection;
6-
using System.Runtime.InteropServices;
76
using Atlassian.Bitbucket;
87
using GitHub;
98
using Microsoft.AzureRepos;
9+
using Microsoft.Git.CredentialManager.Authentication;
1010

1111
namespace Microsoft.Git.CredentialManager
1212
{
@@ -18,6 +18,17 @@ public static void Main(string[] args)
1818
using (var context = new CommandContext(appPath))
1919
using (var app = new Application(context))
2020
{
21+
// Workaround for https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/2560
22+
if (MicrosoftAuthentication.CanUseBroker(context))
23+
try { MicrosoftAuthentication.InitializeBroker(); }
24+
catch (Exception ex)
25+
{
26+
context.Streams.Error.WriteLine(
27+
"warning: broker initialization failed{0}{1}",
28+
Environment.NewLine, ex.Message
29+
);
30+
}
31+
2132
// Register all supported host providers at the normal priority.
2233
// The generic provider should never win against a more specific one, so register it with low priority.
2334
app.RegisterProvider(new AzureReposHostProvider(context), HostProviderPriority.Normal);

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

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System.Collections.Generic;
55
using System.IO;
66
using System.Net.Http;
7-
using System.Runtime.InteropServices;
87
using System.Threading.Tasks;
98
using Microsoft.Identity.Client;
109
using Microsoft.Identity.Client.Extensions.Msal;
@@ -44,19 +43,65 @@ public class MicrosoftAuthentication : AuthenticationBase, IMicrosoftAuthenticat
4443
"live", "liveconnect", "liveid",
4544
};
4645

46+
#region Broker Initialization
47+
48+
public static bool IsBrokerInitialized { get; private set; }
49+
50+
public static void InitializeBroker()
51+
{
52+
if (IsBrokerInitialized) return;
53+
54+
IsBrokerInitialized = true;
55+
56+
// Broker is only supported on Windows 10
57+
if (!PlatformUtils.IsWindows10()) return;
58+
59+
// Nothing to do when not an elevated user
60+
if (!PlatformUtils.IsElevatedUser()) return;
61+
62+
// Lower COM security so that MSAL can make the calls to WAM
63+
int result = Interop.Windows.Native.Ole32.CoInitializeSecurity(
64+
IntPtr.Zero,
65+
-1,
66+
IntPtr.Zero,
67+
IntPtr.Zero,
68+
Interop.Windows.Native.Ole32.RpcAuthnLevel.None,
69+
Interop.Windows.Native.Ole32.RpcImpLevel.Impersonate,
70+
IntPtr.Zero,
71+
Interop.Windows.Native.Ole32.EoAuthnCap.None,
72+
IntPtr.Zero
73+
);
74+
75+
if (result != 0)
76+
{
77+
throw new Exception(
78+
$"Failed to set COM process security to allow Windows broker from an elevated process (0x{result:x})." +
79+
Environment.NewLine +
80+
$"See {Constants.HelpUrls.GcmWamComSecurity} for more information.");
81+
}
82+
}
83+
84+
#endregion
85+
4786
public MicrosoftAuthentication(ICommandContext context)
48-
: base(context) {}
87+
: base(context) { }
4988

5089
#region IMicrosoftAuthentication
5190

5291
public async Task<IMicrosoftAuthenticationResult> GetTokenAsync(
5392
string authority, string clientId, Uri redirectUri, string[] scopes, string userName)
5493
{
5594
// Check if we can and should use OS broker authentication
56-
bool useBroker = CanUseBroker();
57-
if (useBroker)
95+
bool useBroker = false;
96+
if (CanUseBroker(Context))
5897
{
59-
Context.Trace.WriteLine("OS broker is available and enabled.");
98+
// Can only use the broker if it has been initialized
99+
useBroker = IsBrokerInitialized;
100+
101+
if (IsBrokerInitialized)
102+
Context.Trace.WriteLine("OS broker is available and enabled.");
103+
else
104+
Context.Trace.WriteLine("OS broker has not been initialized and cannot not be used.");
60105
}
61106

62107
// Create the public client application for authentication
@@ -396,19 +441,19 @@ public HttpClient GetHttpClient()
396441

397442
#region Auth flow capability detection
398443

399-
private bool CanUseBroker()
444+
public static bool CanUseBroker(ICommandContext context)
400445
{
401446
#if NETFRAMEWORK
402447
// We only support the broker on Windows 10 and require an interactive session
403-
if (!Context.SessionManager.IsDesktopSession || !PlatformUtils.IsWindows10())
448+
if (!context.SessionManager.IsDesktopSession || !PlatformUtils.IsWindows10())
404449
{
405450
return false;
406451
}
407452

408453
// Default to not using the OS broker
409454
const bool defaultValue = false;
410455

411-
if (Context.Settings.TryGetSetting(Constants.EnvironmentVariables.MsAuthUseBroker,
456+
if (context.Settings.TryGetSetting(Constants.EnvironmentVariables.MsAuthUseBroker,
412457
Constants.GitConfiguration.Credential.SectionName,
413458
Constants.GitConfiguration.Credential.MsAuthUseBroker,
414459
out string valueStr))

src/shared/Microsoft.Git.CredentialManager/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ public static class HelpUrls
114114
public const string GcmHttpProxyGuide = "https://aka.ms/gcmcore-httpproxy";
115115
public const string GcmTlsVerification = "https://aka.ms/gcmcore-tlsverify";
116116
public const string GcmLinuxCredStores = "https://aka.ms/gcmcore-linuxcredstores";
117+
public const string GcmWamComSecurity = "https://aka.ms/gcmcore-wamadmin";
117118
}
118119

119120
private static Version _gcmVersion;

src/shared/Microsoft.Git.CredentialManager/Interop/Posix/Native/Unistd.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,8 @@ public static class Unistd
2121

2222
[DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
2323
public static extern int getppid();
24+
25+
[DllImport("libc", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
26+
public static extern int geteuid();
2427
}
2528
}
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.Runtime.InteropServices;
5+
6+
namespace Microsoft.Git.CredentialManager.Interop.Windows.Native
7+
{
8+
public static class Ole32
9+
{
10+
private const string LibraryName = "ole32.dll";
11+
12+
public const uint RPC_E_TOO_LATE = 0x80010119;
13+
14+
[DllImport(LibraryName)]
15+
public static extern int CoInitializeSecurity(
16+
IntPtr pVoid,
17+
int cAuthSvc,
18+
IntPtr asAuthSvc,
19+
IntPtr pReserved1,
20+
RpcAuthnLevel level,
21+
RpcImpLevel impers,
22+
IntPtr pAuthList,
23+
EoAuthnCap dwCapabilities,
24+
IntPtr pReserved3);
25+
26+
public enum RpcAuthnLevel
27+
{
28+
Default = 0,
29+
None = 1,
30+
Connect = 2,
31+
Call = 3,
32+
Pkt = 4,
33+
PktIntegrity = 5,
34+
PktPrivacy = 6
35+
}
36+
37+
public enum RpcImpLevel
38+
{
39+
Default = 0,
40+
Anonymous = 1,
41+
Identify = 2,
42+
Impersonate = 3,
43+
Delegate = 4
44+
}
45+
46+
public enum EoAuthnCap
47+
{
48+
None = 0x00,
49+
MutualAuth = 0x01,
50+
StaticCloaking = 0x20,
51+
DynamicCloaking = 0x40,
52+
AnyAuthority = 0x80,
53+
MakeFullSIC = 0x100,
54+
Default = 0x800,
55+
SecureRefs = 0x02,
56+
AccessControl = 0x04,
57+
AppID = 0x08,
58+
Dynamic = 0x10,
59+
RequireFullSIC = 0x200,
60+
AutoImpersonate = 0x400,
61+
NoCustomMarshal = 0x2000,
62+
DisableAAA = 0x1000
63+
}
64+
}
65+
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT license.
33
using System;
44
using System.Runtime.InteropServices;
5+
using Microsoft.Git.CredentialManager.Interop.Posix.Native;
56

67
namespace Microsoft.Git.CredentialManager
78
{
@@ -136,6 +137,24 @@ public static void EnsurePosix()
136137
}
137138
}
138139

140+
public static bool IsElevatedUser()
141+
{
142+
if (IsWindows())
143+
{
144+
#if NETFRAMEWORK
145+
var identity = System.Security.Principal.WindowsIdentity.GetCurrent();
146+
var principal = new System.Security.Principal.WindowsPrincipal(identity);
147+
return principal.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator);
148+
#endif
149+
}
150+
else if (IsPosix())
151+
{
152+
return Unistd.geteuid() == 0;
153+
}
154+
155+
return false;
156+
}
157+
139158
#region Platform information helper methods
140159

141160
private static string GetOSType()

0 commit comments

Comments
 (0)