Skip to content

Commit 92145d7

Browse files
authored
Add support for using the current Windows user for WAM on DevBox (#1197)
Add the ability to configure MSAL to use the default OS account when the broker is enabled. Also detect when we are in a Microsoft Dev Box environment, and if we are, then default to enabling the new setting (and enable WAM). Show a confirmation prompt before continuing to use the current OS account, which is similar to how Microsoft Teams operates. <img width="888" alt="windows-defaultaccount" src="https://user-images.githubusercontent.com/5658207/234346956-b08eb43f-c964-4978-84ec-a0f75f021f08.png"> Left: Avalonia UI, Right: fallback WPF window Fixes #917
2 parents 9077e4d + 65e6b71 commit 92145d7

File tree

17 files changed

+640
-18
lines changed

17 files changed

+640
-18
lines changed

docs/configuration.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,10 @@ git config --global credential.msauthFlow devicecode
623623

624624
Use the operating system account manager where available.
625625

626-
Defaults to `false`. This default is subject to change in the future.
626+
Defaults to `false`. In certain cloud hosted environments when using a work or
627+
school account, such as [Microsoft DevBox][devbox], the default is `true`.
628+
629+
These defaults are subject to change in the future.
627630

628631
_**Note:** before you enable this option on Windows, please review the
629632
[Windows Broker][wam] details for what this means to your local Windows user
@@ -644,6 +647,30 @@ git config --global credential.msauthUseBroker true
644647

645648
---
646649

650+
### credential.msauthUseDefaultAccount _(experimental)_
651+
652+
Use the current operating system account by default when the broker is enabled.
653+
654+
Defaults to `false`. In certain cloud hosted environments when using a work or
655+
school account, such as [Microsoft DevBox][devbox], the default is `true`.
656+
657+
These defaults are subject to change in the future.
658+
659+
Value|Description
660+
-|-
661+
`true`|Use the current operating system account by default.
662+
`false` _(default)_|Do not assume any account to use by default.
663+
664+
#### Example
665+
666+
```shell
667+
git config --global credential.msauthUseDefaultAccount true
668+
```
669+
670+
**Also see: [GCM_MSAUTH_USEDEFAULTACCOUNT][gcm-msauth-usedefaultaccount]**
671+
672+
---
673+
647674
### credential.useHttpPath
648675

649676
Tells Git to pass the entire repository URL, rather than just the hostname, when
@@ -820,6 +847,7 @@ Defaults to disabled.
820847
[credential-plaintextstorepath]: #credentialplaintextstorepath
821848
[credential-cache]: https://git-scm.com/docs/git-credential-cache
822849
[cred-stores]: credstores.md
850+
[devbox]: https://azure.microsoft.com/en-us/products/dev-box
823851
[enterprise-config]: enterprise-config.md
824852
[envars]: environment.md
825853
[freedesktop-ss]: https://specifications.freedesktop.org/secret-service/
@@ -840,6 +868,7 @@ Defaults to disabled.
840868
[gcm-interactive]: environment.md#GCM_INTERACTIVE
841869
[gcm-msauth-flow]: environment.md#GCM_MSAUTH_FLOW
842870
[gcm-msauth-usebroker]: environment.md#GCM_MSAUTH_USEBROKER-experimental
871+
[gcm-msauth-usedefaultaccount]: environment.md#GCM_MSAUTH_USEDEFAULTACCOUNT-experimental
843872
[gcm-namespace]: environment.md#GCM_NAMESPACE
844873
[gcm-plaintext-store-path]: environment.md#GCM_PLAINTEXT_STORE_PATH
845874
[gcm-provider]: environment.md#GCM_PROVIDER

docs/environment.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -776,7 +776,10 @@ export GCM_MSAUTH_FLOW="devicecode"
776776

777777
Use the operating system account manager where available.
778778

779-
Defaults to `false`. This default is subject to change in the future.
779+
Defaults to `false`. In certain cloud hosted environments when using a work or
780+
school account, such as [Microsoft DevBox][devbox], the default is `true`.
781+
782+
These defaults are subject to change in the future.
780783

781784
_**Note:** before you enable this option on Windows, please
782785
[review the details][windows-broker] about what this means to your local Windows
@@ -803,6 +806,36 @@ export GCM_MSAUTH_USEBROKER="false"
803806

804807
---
805808

809+
### GCM_MSAUTH_USEDEFAULTACCOUNT _(experimental)_
810+
811+
Use the current operating system account by default when the broker is enabled.
812+
813+
Defaults to `false`. In certain cloud hosted environments when using a work or
814+
school account, such as [Microsoft DevBox][devbox], the default is `true`.
815+
816+
These defaults are subject to change in the future.
817+
818+
Value|Description
819+
-|-
820+
`true`|Use the current operating system account by default.
821+
`false` _(default)_|Do not assume any account to use by default.
822+
823+
#### Windows
824+
825+
```batch
826+
SET GCM_MSAUTH_USEDEFAULTACCOUNT="true"
827+
```
828+
829+
#### macOS/Linux
830+
831+
```bash
832+
export GCM_MSAUTH_USEDEFAULTACCOUNT="false"
833+
```
834+
835+
**Also see: [credential.msauthUseDefaultAccount][credential-msauth-usedefaultaccount]**
836+
837+
---
838+
806839
### GCM_AZREPOS_CREDENTIALTYPE
807840

808841
Specify the type of credential the Azure Repos host provider should return.
@@ -937,13 +970,15 @@ Defaults to disabled.
937970
[credential-namespace]: configuration.md#credentialnamespace
938971
[credential-msauth-flow]: configuration.md#credentialmsauthflow
939972
[credential-msauth-usebroker]: configuration.md#credentialmsauthusebroker-experimental
973+
[credential-msauth-usedefaultaccount]: configuration.md#credentialmsauthusedefaultaccount-experimental
940974
[credential-plain-text-store]: configuration.md#credentialplaintextstorepath
941975
[credential-provider]: configuration.md#credentialprovider
942976
[credential-stores]: credstores.md
943977
[credential-trace]: configuration.md#credentialtrace
944978
[credential-trace-secrets]: configuration.md#credentialtracesecrets
945979
[credential-trace-msauth]: configuration.md#credentialtracemsauth
946980
[default-values]: enterprise-config.md
981+
[devbox]: https://azure.microsoft.com/en-us/products/dev-box
947982
[freedesktop-ss]: https://specifications.freedesktop.org/secret-service/
948983
[gcm]: usage.md
949984
[gcm-interactive]: #gcm_interactive

docs/windows-broker.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,23 @@ 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+
46+
In certain cloud hosted environments when using a work or school account, such
47+
as [Microsoft Dev Box][devbox], this setting is **_automatically enabled_**.
48+
49+
To disable this behavior, set the environment variable
50+
[`GCM_MSAUTH_USEDEFAULTACCOUNT`][GCM_MSAUTH_USEDEFAULTACCOUNT] or the
51+
[`credential.msauthUseDefaultAccount`][credential.msauthUseDefaultAccount] Git
52+
configuration value explicitly to `false`.
53+
3754
## Surprising behaviors
3855

3956
The WAM and Windows identity systems are complex, addressing a very broad range
@@ -174,8 +191,10 @@ In order to fix the problem, there are a few options:
174191
[azure-refresh-token-terms]: https://docs.microsoft.com/azure/active-directory/devices/concept-primary-refresh-token#key-terminology-and-components
175192
[azure-conditional-access]: https://docs.microsoft.com/azure/active-directory/conditional-access/overview
176193
[azure-devops]: https://dev.azure.com
177-
[GCM_MSAUTH_USEBROKER]: environment.md#GCM_MSAUTH_USEBROKER
178-
[credential.msauthUseBroker]: configuration.md#credentialmsauthusebroker
194+
[GCM_MSAUTH_USEBROKER]: environment.md#GCM_MSAUTH_USEBROKER-experimental
195+
[GCM_MSAUTH_USEDEFAULTACCOUNT]: environment.md#GCM_MSAUTH_USEDEFAULTACCOUNT-experimental
196+
[credential.msauthUseBroker]: configuration.md#credentialmsauthusebroker-experimental
197+
[credential.msauthUseDefaultAccount]: configuration.md#credentialmsauthusedefaultaccount-experimental
179198
[aad-questions]: img/aad-questions.png
180199
[aad-questions-21h1]: img/aad-questions-21H1.png
181200
[aad-bitlocker]: img/aad-bitlocker.png
@@ -186,3 +205,4 @@ In order to fix the problem, there are a few options:
186205
[apps-must-ask]: img/apps-must-ask.png
187206
[ms-com]: https://docs.microsoft.com/en-us/windows/win32/com/the-component-object-model
188207
[msal-dotnet]: https://aka.ms/msal-net
208+
[devbox]: https://azure.microsoft.com/en-us/products/dev-box

src/shared/Core/Authentication/MicrosoftAuthentication.cs

Lines changed: 109 additions & 14 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;
@@ -462,8 +557,8 @@ public bool CanUseBroker()
462557
return false;
463558
}
464559

465-
// Default to not using the OS broker
466-
const bool defaultValue = false;
560+
// Default to using the OS broker only on DevBox for the time being
561+
bool defaultValue = PlatformUtils.IsDevBox();
467562

468563
if (Context.Settings.TryGetSetting(Constants.EnvironmentVariables.MsAuthUseBroker,
469564
Constants.GitConfiguration.Credential.SectionName,

src/shared/Core/Constants.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ public static class Constants
1616

1717
public const string GcmDataDirectoryName = ".gcm";
1818

19+
public static readonly Guid DevBoxPartnerId = new("e3171dd9-9a5f-e5be-b36c-cc7c4f3f3bcf");
20+
1921
public static class CredentialStoreNames
2022
{
2123
public const string WindowsCredentialManager = "wincredman";
@@ -83,6 +85,7 @@ public static class EnvironmentVariables
8385
public const string GcmParentWindow = "GCM_MODAL_PARENTHWND";
8486
public const string MsAuthFlow = "GCM_MSAUTH_FLOW";
8587
public const string MsAuthUseBroker = "GCM_MSAUTH_USEBROKER";
88+
public const string MsAuthUseDefaultAccount = "GCM_MSAUTH_USEDEFAULTACCOUNT";
8689
public const string GcmCredNamespace = "GCM_NAMESPACE";
8790
public const string GcmCredentialStore = "GCM_CREDENTIAL_STORE";
8891
public const string GcmCredCacheOptions = "GCM_CREDENTIAL_CACHE_OPTIONS";
@@ -145,6 +148,7 @@ public static class Credential
145148
public const string GuiPromptsEnabled = "guiPrompt";
146149
public const string UiHelper = "uiHelper";
147150
public const string DevUseLegacyUiHelpers = "devUseLegacyUiHelpers";
151+
public const string MsAuthUseDefaultAccount = "msauthUseDefaultAccount";
148152

149153
public const string OAuthAuthenticationModes = "oauthAuthModes";
150154
public const string OAuthClientId = "oauthClientId";
@@ -189,6 +193,10 @@ public static class WindowsRegistry
189193
{
190194
public const string HKAppBasePath = @"SOFTWARE\GitCredentialManager";
191195
public const string HKConfigurationPath = HKAppBasePath + @"\Configuration";
196+
197+
public const string HKWindows365Path = @"SOFTWARE\Microsoft\Windows365";
198+
public const string IsW365EnvironmentKeyName = "IsW365Environment";
199+
public const string W365PartnerIdKeyName = "PartnerId";
192200
}
193201

194202
public static class HelpUrls
@@ -202,6 +210,7 @@ public static class HelpUrls
202210
public const string GcmWamComSecurity = "https://aka.ms/gcm/wamadmin";
203211
public const string GcmAutoDetect = "https://aka.ms/gcm/autodetect";
204212
public const string GcmExecRename = "https://aka.ms/gcm/rename";
213+
public const string GcmDefaultAccount = "https://aka.ms/gcm/defaultaccount";
205214
}
206215

207216
private static Version _gcmVersion;

0 commit comments

Comments
 (0)