Skip to content

Commit d2e0b39

Browse files
committed
msauth: use default OS account when enabled
Add the ability to configure MSAL to use the default OS account when the broker is enabled. Default to disabled.
1 parent d6a4cf3 commit d2e0b39

File tree

16 files changed

+576
-12
lines changed

16 files changed

+576
-12
lines changed

docs/configuration.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,27 @@ git config --global credential.msauthUseBroker true
564564

565565
---
566566

567+
### credential.msauthUseDefaultAccount _(experimental)_
568+
569+
Use the current operating system account by default when the broker is enabled.
570+
571+
Defaults to `false`. This default is subject to change in the future.
572+
573+
Value|Description
574+
-|-
575+
`true`|Use the current operating system account by default.
576+
`false` _(default)_|Do not assume any account to use by default.
577+
578+
#### Example
579+
580+
```shell
581+
git config --global credential.msauthUseDefaultAccount true
582+
```
583+
584+
**Also see: [GCM_MSAUTH_USEDEFAULTACCOUNT][gcm-msauth-usedefaultaccount]**
585+
586+
---
587+
567588
### credential.useHttpPath
568589

569590
Tells Git to pass the entire repository URL, rather than just the hostname, when
@@ -690,6 +711,7 @@ git config --global credential.azreposCredentialType oauth
690711
[gcm-interactive]: environment.md#GCM_INTERACTIVE
691712
[gcm-msauth-flow]: environment.md#GCM_MSAUTH_FLOW
692713
[gcm-msauth-usebroker]: environment.md#GCM_MSAUTH_USEBROKER-experimental
714+
[gcm-msauth-usedefaultaccount]: environment.md#GCM_MSAUTH_USEDEFAULTACCOUNT-experimental
693715
[gcm-namespace]: environment.md#GCM_NAMESPACE
694716
[gcm-plaintext-store-path]: environment.md#GCM_PLAINTEXT_STORE_PATH
695717
[gcm-provider]: environment.md#GCM_PROVIDER

docs/environment.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,33 @@ export GCM_MSAUTH_USEBROKER="false"
803803

804804
---
805805

806+
### GCM_MSAUTH_USEDEFAULTACCOUNT _(experimental)_
807+
808+
Use the current operating system account by default when the broker is enabled.
809+
810+
Defaults to `false`. This default is subject to change in the future.
811+
812+
Value|Description
813+
-|-
814+
`true`|Use the current operating system account by default.
815+
`false` _(default)_|Do not assume any account to use by default.
816+
817+
#### Windows
818+
819+
```batch
820+
SET GCM_MSAUTH_USEDEFAULTACCOUNT="true"
821+
```
822+
823+
#### macOS/Linux
824+
825+
```bash
826+
export GCM_MSAUTH_USEDEFAULTACCOUNT="false"
827+
```
828+
829+
**Also see: [credential.msauthUseDefaultAccount][credential-msauth-usedefaultaccount]**
830+
831+
---
832+
806833
### GCM_AZREPOS_CREDENTIALTYPE
807834

808835
Specify the type of credential the Azure Repos host provider should return.
@@ -849,6 +876,7 @@ export GCM_AZREPOS_CREDENTIALTYPE="oauth"
849876
[credential-namespace]: configuration.md#credentialnamespace
850877
[credential-msauth-flow]: configuration.md#credentialmsauthflow
851878
[credential-msauth-usebroker]: configuration.md#credentialmsauthusebroker-experimental
879+
[credential-msauth-usedefaultaccount]: configuration.md#credentialmsauthusedefaultaccount-experimental
852880
[credential-plain-text-store]: configuration.md#credentialplaintextstorepath
853881
[credential-provider]: configuration.md#credentialprovider
854882
[credential-stores]: credstores.md

docs/windows-broker.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ fewer multi-factor authentication prompts, and the ability to use additional
3434
authentication technologies like smart cards and Windows Hello. These
3535
convenience and security features make a good case for enabling WAM.
3636

37+
## Using the current OS account by default
38+
39+
Enabling WAM does not currently automatically use the current Windows account
40+
for authentication. In order to opt-in to this behavior you can set the
41+
[`GCM_MSAUTH_USEDEFAULTACCOUNT`][GCM_MSAUTH_USEDEFAULTACCOUNT] environment
42+
variable or set the
43+
[`credential.msauthUseDefaultAccount`][credential.msauthUseDefaultAccount] Git
44+
configuration value to `true`.
45+
3746
## Surprising behaviors
3847

3948
The WAM and Windows identity systems are complex, addressing a very broad range
@@ -175,7 +184,9 @@ In order to fix the problem, there are a few options:
175184
[azure-conditional-access]: https://docs.microsoft.com/azure/active-directory/conditional-access/overview
176185
[azure-devops]: https://dev.azure.com
177186
[GCM_MSAUTH_USEBROKER]: environment.md#GCM_MSAUTH_USEBROKER
187+
[GCM_MSAUTH_USEDEFAULTACCOUNTR]: environment.md#GCM_MSAUTH_USEDEFAULTACCOUNTR
178188
[credential.msauthUseBroker]: configuration.md#credentialmsauthusebroker
189+
[credential.msauthUseDefaultAccount]: configuration.md#credentialmsauthusedefaultaccount
179190
[aad-questions]: img/aad-questions.png
180191
[aad-questions-21h1]: img/aad-questions-21H1.png
181192
[aad-bitlocker]: img/aad-bitlocker.png

src/shared/Core/Authentication/MicrosoftAuthentication.cs

Lines changed: 107 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
using GitCredentialManager.Interop.Windows.Native;
77
using Microsoft.Identity.Client;
88
using Microsoft.Identity.Client.Extensions.Msal;
9+
using System.Text;
910
using System.Threading;
10-
using System.Runtime.InteropServices;
11+
using GitCredentialManager.UI;
12+
using GitCredentialManager.UI.ViewModels;
13+
using GitCredentialManager.UI.Views;
1114

1215
#if NETFRAMEWORK
1316
using System.Drawing;
@@ -72,7 +75,8 @@ public async Task<IMicrosoftAuthenticationResult> GetTokenAsync(
7275
AuthenticationResult result = null;
7376

7477
// Try silent authentication first if we know about an existing user
75-
if (!string.IsNullOrWhiteSpace(userName))
78+
bool hasExistingUser = !string.IsNullOrWhiteSpace(userName);
79+
if (hasExistingUser)
7680
{
7781
result = await GetAccessTokenSilentlyAsync(app, scopes, userName);
7882
}
@@ -106,12 +110,29 @@ public async Task<IMicrosoftAuthenticationResult> GetTokenAsync(
106110
// If we're using the OS broker then delegate everything to that
107111
if (useBroker)
108112
{
109-
Context.Trace.WriteLine("Performing interactive auth with broker...");
110-
result = await app.AcquireTokenInteractive(scopes)
111-
.WithPrompt(Prompt.SelectAccount)
112-
// We must configure the system webview as a fallback
113-
.WithSystemWebViewOptions(GetSystemWebViewOptions())
114-
.ExecuteAsync();
113+
// If the user has enabled the default account feature then we can try to acquire an access
114+
// token 'silently' without knowing the user's UPN. Whilst this could be done truly silently,
115+
// we still prompt the user to confirm this action because if the OS account is the incorrect
116+
// account then the user may become stuck in a loop of authentication failures.
117+
if (!hasExistingUser && Context.Settings.UseMsAuthDefaultAccount)
118+
{
119+
result = await GetAccessTokenSilentlyAsync(app, scopes, null);
120+
121+
if (result is null || !await UseDefaultAccountAsync(result.Account.Username))
122+
{
123+
result = null;
124+
}
125+
}
126+
127+
if (result is null)
128+
{
129+
Context.Trace.WriteLine("Performing interactive auth with broker...");
130+
result = await app.AcquireTokenInteractive(scopes)
131+
.WithPrompt(Prompt.SelectAccount)
132+
// We must configure the system webview as a fallback
133+
.WithSystemWebViewOptions(GetSystemWebViewOptions())
134+
.ExecuteAsync();
135+
}
115136
}
116137
else
117138
{
@@ -173,6 +194,61 @@ public async Task<IMicrosoftAuthenticationResult> GetTokenAsync(
173194
}
174195
}
175196

197+
private async Task<bool> UseDefaultAccountAsync(string userName)
198+
{
199+
ThrowIfUserInteractionDisabled();
200+
201+
if (Context.SessionManager.IsDesktopSession && Context.Settings.IsGuiPromptsEnabled)
202+
{
203+
if (TryFindHelperCommand(out string command, out string args))
204+
{
205+
var sb = new StringBuilder(args);
206+
sb.Append("default-account");
207+
sb.AppendFormat(" --username {0}", QuoteCmdArg(userName));
208+
209+
IDictionary<string, string> result = await InvokeHelperAsync(command, sb.ToString());
210+
211+
if (result.TryGetValue("use_default_account", out string str) && !string.IsNullOrWhiteSpace(str))
212+
{
213+
return str.ToBooleanyOrDefault(false);
214+
}
215+
else
216+
{
217+
throw new Trace2Exception(Context.Trace2, "Missing use_default_account in response");
218+
}
219+
}
220+
221+
var viewModel = new DefaultAccountViewModel(Context.Environment)
222+
{
223+
UserName = userName
224+
};
225+
226+
await AvaloniaUi.ShowViewAsync<DefaultAccountView>(
227+
viewModel, GetParentWindowHandle(), CancellationToken.None);
228+
229+
ThrowIfWindowCancelled(viewModel);
230+
231+
return viewModel.UseDefaultAccount;
232+
}
233+
else
234+
{
235+
string question = $"Continue with current account ({userName})?";
236+
237+
var menu = new TerminalMenu(Context.Terminal, question);
238+
TerminalMenuItem yesItem = menu.Add("Yes");
239+
TerminalMenuItem noItem = menu.Add("No, use another account");
240+
TerminalMenuItem choice = menu.Show();
241+
242+
if (choice == yesItem)
243+
return true;
244+
245+
if (choice == noItem)
246+
return false;
247+
248+
throw new Exception();
249+
}
250+
}
251+
176252
internal MicrosoftAuthenticationFlowType GetFlowType()
177253
{
178254
if (Context.Settings.TryGetSetting(
@@ -209,11 +285,20 @@ private async Task<AuthenticationResult> GetAccessTokenSilentlyAsync(IPublicClie
209285
{
210286
try
211287
{
212-
Context.Trace.WriteLine($"Attempting to acquire token silently for user '{userName}'...");
288+
if (userName is null)
289+
{
290+
Context.Trace.WriteLine("Attempting to acquire token silently for current operating system account...");
291+
292+
return await app.AcquireTokenSilent(scopes, PublicClientApplication.OperatingSystemAccount).ExecuteAsync();
293+
}
294+
else
295+
{
296+
Context.Trace.WriteLine($"Attempting to acquire token silently for user '{userName}'...");
213297

214-
// We can either call `app.GetAccountsAsync` and filter through the IAccount objects for the instance with the correct user name,
215-
// or we can just pass the user name string we have as the `loginHint` and let MSAL do exactly that for us instead!
216-
return await app.AcquireTokenSilent(scopes, loginHint: userName).ExecuteAsync();
298+
// We can either call `app.GetAccountsAsync` and filter through the IAccount objects for the instance with the correct user name,
299+
// or we can just pass the user name string we have as the `loginHint` and let MSAL do exactly that for us instead!
300+
return await app.AcquireTokenSilent(scopes, loginHint: userName).ExecuteAsync();
301+
}
217302
}
218303
catch (MsalUiRequiredException)
219304
{
@@ -429,6 +514,16 @@ private void OnMsalLogMessage(LogLevel level, string message, bool containspii)
429514
Context.Trace.WriteLine($"[{level.ToString()}] {message}", memberName: "MSAL");
430515
}
431516

517+
private bool TryFindHelperCommand(out string command, out string args)
518+
{
519+
return TryFindHelperCommand(
520+
Constants.EnvironmentVariables.GcmUiHelper,
521+
Constants.GitConfiguration.Credential.UiHelper,
522+
Constants.DefaultUiHelper,
523+
out command,
524+
out args);
525+
}
526+
432527
private class MsalHttpClientFactoryAdaptor : IMsalHttpClientFactory
433528
{
434529
private readonly IHttpClientFactory _factory;

src/shared/Core/Constants.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ public static class EnvironmentVariables
8383
public const string GcmParentWindow = "GCM_MODAL_PARENTHWND";
8484
public const string MsAuthFlow = "GCM_MSAUTH_FLOW";
8585
public const string MsAuthUseBroker = "GCM_MSAUTH_USEBROKER";
86+
public const string MsAuthUseDefaultAccount = "GCM_MSAUTH_USEDEFAULTACCOUNT";
8687
public const string GcmCredNamespace = "GCM_NAMESPACE";
8788
public const string GcmCredentialStore = "GCM_CREDENTIAL_STORE";
8889
public const string GcmCredCacheOptions = "GCM_CREDENTIAL_CACHE_OPTIONS";
@@ -141,6 +142,7 @@ public static class Credential
141142
public const string GuiPromptsEnabled = "guiPrompt";
142143
public const string UiHelper = "uiHelper";
143144
public const string DevUseLegacyUiHelpers = "devUseLegacyUiHelpers";
145+
public const string MsAuthUseDefaultAccount = "msauthUseDefaultAccount";
144146

145147
public const string OAuthAuthenticationModes = "oauthAuthModes";
146148
public const string OAuthClientId = "oauthClientId";
@@ -198,6 +200,7 @@ public static class HelpUrls
198200
public const string GcmWamComSecurity = "https://aka.ms/gcm/wamadmin";
199201
public const string GcmAutoDetect = "https://aka.ms/gcm/autodetect";
200202
public const string GcmExecRename = "https://aka.ms/gcm/rename";
203+
public const string GcmDefaultAccount = "https://aka.ms/gcm/defaultaccount";
201204
}
202205

203206
private static Version _gcmVersion;

src/shared/Core/Settings.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ public interface ISettings : IDisposable
172172
/// </summary>
173173
int AutoDetectProviderTimeout { get; }
174174

175+
/// <summary>
176+
/// Automatically use the default/current operating system account if no other account information is given
177+
/// for Microsoft Authentication.
178+
/// </summary>
179+
bool UseMsAuthDefaultAccount { get; }
180+
175181
/// <summary>
176182
/// Get TRACE2 settings.
177183
/// </summary>
@@ -768,6 +774,15 @@ ProxyConfiguration CreateConfiguration(Uri uri, bool isLegacy = false)
768774
? credStore
769775
: null;
770776

777+
public bool UseMsAuthDefaultAccount =>
778+
TryGetSetting(
779+
KnownEnvars.MsAuthUseDefaultAccount,
780+
KnownGitCfg.Credential.SectionName,
781+
KnownGitCfg.Credential.MsAuthUseDefaultAccount,
782+
out string str)
783+
? str.IsTruthy()
784+
: false;
785+
771786
#region IDisposable
772787

773788
public void Dispose()

src/shared/Core/UI/Assets/Images.axaml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
<ResourceDictionary xmlns="https://github.com/avaloniaui"
22
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
3+
<ResourceDictionary.ThemeDictionaries>
4+
<ResourceDictionary x:Key="Default">
5+
<Color x:Key="IconColor">#FF25292F</Color>
6+
<SolidColorBrush x:Key="IconBrush" Color="{StaticResource IconColor}"/>
7+
</ResourceDictionary>
8+
<ResourceDictionary x:Key="Dark">
9+
<Color x:Key="IconColor">White</Color>
10+
<SolidColorBrush x:Key="IconBrush" Color="{StaticResource IconColor}"/>
11+
</ResourceDictionary>
12+
</ResourceDictionary.ThemeDictionaries>
13+
314
<DrawingImage x:Key="GcmLogo">
415
<DrawingImage.Drawing>
516
<DrawingGroup>
@@ -10,4 +21,33 @@
1021
</DrawingGroup>
1122
</DrawingImage.Drawing>
1223
</DrawingImage>
24+
<DrawingImage x:Key="PersonIcon">
25+
<DrawingImage.Drawing>
26+
<DrawingGroup>
27+
<DrawingGroup.Children>
28+
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="M12 2.5a5.5 5.5 0 0 1 3.096 10.047 9.005 9.005 0 0 1 5.9 8.181.75.75 0 1 1-1.499.044 7.5 7.5 0 0 0-14.993 0 .75.75 0 0 1-1.5-.045 9.005 9.005 0 0 1 5.9-8.18A5.5 5.5 0 0 1 12 2.5ZM8 8a4 4 0 1 0 8 0 4 4 0 0 0-8 0Z"/>
29+
</DrawingGroup.Children>
30+
</DrawingGroup>
31+
</DrawingImage.Drawing>
32+
</DrawingImage>
33+
<DrawingImage x:Key="InfoIcon">
34+
<DrawingImage.Drawing>
35+
<DrawingGroup>
36+
<DrawingGroup.Children>
37+
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="M13 7.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Zm-3 3.75a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 .75.75v4.25h.75a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1 0-1.5h.75V12h-.75a.75.75 0 0 1-.75-.75Z"/>
38+
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="M12 1c6.075 0 11 4.925 11 11s-4.925 11-11 11S1 18.075 1 12 5.925 1 12 1ZM2.5 12a9.5 9.5 0 0 0 9.5 9.5 9.5 9.5 0 0 0 9.5-9.5A9.5 9.5 0 0 0 12 2.5 9.5 9.5 0 0 0 2.5 12Z"/>
39+
</DrawingGroup.Children>
40+
</DrawingGroup>
41+
</DrawingImage.Drawing>
42+
</DrawingImage>
43+
<DrawingImage x:Key="HelpIcon">
44+
<DrawingImage.Drawing>
45+
<DrawingGroup>
46+
<DrawingGroup.Children>
47+
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="M10.97 8.265a1.45 1.45 0 0 0-.487.57.75.75 0 0 1-1.341-.67c.2-.402.513-.826.997-1.148C10.627 6.69 11.244 6.5 12 6.5c.658 0 1.369.195 1.934.619a2.45 2.45 0 0 1 1.004 2.006c0 1.033-.513 1.72-1.027 2.215-.19.183-.399.358-.579.508l-.147.123a4.329 4.329 0 0 0-.435.409v1.37a.75.75 0 1 1-1.5 0v-1.473c0-.237.067-.504.247-.736.22-.28.486-.517.718-.714l.183-.153.001-.001c.172-.143.324-.27.47-.412.368-.355.569-.676.569-1.136a.953.953 0 0 0-.404-.806C12.766 8.118 12.384 8 12 8c-.494 0-.814.121-1.03.265ZM13 17a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/>
48+
<GeometryDrawing Brush="{DynamicResource IconBrush}" Geometry="M12 1c6.075 0 11 4.925 11 11s-4.925 11-11 11S1 18.075 1 12 5.925 1 12 1ZM2.5 12a9.5 9.5 0 0 0 9.5 9.5 9.5 9.5 0 0 0 9.5-9.5A9.5 9.5 0 0 0 12 2.5 9.5 9.5 0 0 0 2.5 12Z"/>
49+
</DrawingGroup.Children>
50+
</DrawingGroup>
51+
</DrawingImage.Drawing>
52+
</DrawingImage>
1353
</ResourceDictionary>

0 commit comments

Comments
 (0)