|
6 | 6 | using GitCredentialManager.Interop.Windows.Native;
|
7 | 7 | using Microsoft.Identity.Client;
|
8 | 8 | using Microsoft.Identity.Client.Extensions.Msal;
|
| 9 | +using System.Text; |
9 | 10 | using System.Threading;
|
10 |
| -using System.Runtime.InteropServices; |
| 11 | +using GitCredentialManager.UI; |
| 12 | +using GitCredentialManager.UI.ViewModels; |
| 13 | +using GitCredentialManager.UI.Views; |
11 | 14 |
|
12 | 15 | #if NETFRAMEWORK
|
13 | 16 | using System.Drawing;
|
@@ -72,7 +75,8 @@ public async Task<IMicrosoftAuthenticationResult> GetTokenAsync(
|
72 | 75 | AuthenticationResult result = null;
|
73 | 76 |
|
74 | 77 | // 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) |
76 | 80 | {
|
77 | 81 | result = await GetAccessTokenSilentlyAsync(app, scopes, userName);
|
78 | 82 | }
|
@@ -106,12 +110,29 @@ public async Task<IMicrosoftAuthenticationResult> GetTokenAsync(
|
106 | 110 | // If we're using the OS broker then delegate everything to that
|
107 | 111 | if (useBroker)
|
108 | 112 | {
|
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 | + } |
115 | 136 | }
|
116 | 137 | else
|
117 | 138 | {
|
@@ -173,6 +194,61 @@ public async Task<IMicrosoftAuthenticationResult> GetTokenAsync(
|
173 | 194 | }
|
174 | 195 | }
|
175 | 196 |
|
| 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 | + |
176 | 252 | internal MicrosoftAuthenticationFlowType GetFlowType()
|
177 | 253 | {
|
178 | 254 | if (Context.Settings.TryGetSetting(
|
@@ -209,11 +285,20 @@ private async Task<AuthenticationResult> GetAccessTokenSilentlyAsync(IPublicClie
|
209 | 285 | {
|
210 | 286 | try
|
211 | 287 | {
|
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}'..."); |
213 | 297 |
|
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 | + } |
217 | 302 | }
|
218 | 303 | catch (MsalUiRequiredException)
|
219 | 304 | {
|
@@ -429,6 +514,16 @@ private void OnMsalLogMessage(LogLevel level, string message, bool containspii)
|
429 | 514 | Context.Trace.WriteLine($"[{level.ToString()}] {message}", memberName: "MSAL");
|
430 | 515 | }
|
431 | 516 |
|
| 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 | + |
432 | 527 | private class MsalHttpClientFactoryAdaptor : IMsalHttpClientFactory
|
433 | 528 | {
|
434 | 529 | private readonly IHttpClientFactory _factory;
|
@@ -462,8 +557,8 @@ public bool CanUseBroker()
|
462 | 557 | return false;
|
463 | 558 | }
|
464 | 559 |
|
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(); |
467 | 562 |
|
468 | 563 | if (Context.Settings.TryGetSetting(Constants.EnvironmentVariables.MsAuthUseBroker,
|
469 | 564 | Constants.GitConfiguration.Credential.SectionName,
|
|
0 commit comments