Skip to content

Commit 2a18505

Browse files
committed
msauth: use dummy window for WAM if no parent
If we are unable to get a parent window handle; because for example, we don't have a console, then create a small 'dummy' window using WinForms that we can use to pass a handle to MSAL.
1 parent 134e622 commit 2a18505

File tree

1 file changed

+194
-95
lines changed

1 file changed

+194
-95
lines changed

src/shared/Core/Authentication/MicrosoftAuthentication.cs

Lines changed: 194 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@
66
using GitCredentialManager.Interop.Windows.Native;
77
using Microsoft.Identity.Client;
88
using Microsoft.Identity.Client.Extensions.Msal;
9+
using System.Threading;
10+
using System.Runtime.InteropServices;
911

1012
#if NETFRAMEWORK
13+
using System.Drawing;
14+
using System.Windows.Forms;
1115
using Microsoft.Identity.Client.Broker;
1216
#endif
1317

@@ -42,6 +46,10 @@ public class MicrosoftAuthentication : AuthenticationBase, IMicrosoftAuthenticat
4246
"live", "liveconnect", "liveid",
4347
};
4448

49+
#if NETFRAMEWORK
50+
private DummyWindow _dummyWindow;
51+
#endif
52+
4553
public MicrosoftAuthentication(ICommandContext context)
4654
: base(context) { }
4755

@@ -54,106 +62,116 @@ public async Task<IMicrosoftAuthenticationResult> GetTokenAsync(
5462
bool useBroker = CanUseBroker();
5563
Context.Trace.WriteLine(useBroker
5664
? "OS broker is available and enabled."
57-
: "OS broker has not been initialized and cannot not be used.");
58-
59-
// Create the public client application for authentication
60-
IPublicClientApplication app = await CreatePublicClientApplicationAsync(authority, clientId, redirectUri, useBroker);
65+
: "OS broker is not available or enabled.");
6166

62-
AuthenticationResult result = null;
63-
64-
// Try silent authentication first if we know about an existing user
65-
if (!string.IsNullOrWhiteSpace(userName))
67+
try
6668
{
67-
result = await GetAccessTokenSilentlyAsync(app, scopes, userName);
68-
}
69+
// Create the public client application for authentication
70+
IPublicClientApplication app = await CreatePublicClientApplicationAsync(authority, clientId, redirectUri, useBroker);
6971

70-
//
71-
// If we failed to acquire an AT silently (either because we don't have an existing user, or the user's RT has expired)
72-
// we need to prompt the user for credentials.
73-
//
74-
// If the user has expressed a preference in how the want to perform the interactive authentication flows then we respect that.
75-
// Otherwise, depending on the current platform and session type we try to show the most appropriate authentication interface:
76-
//
77-
// On Windows 10 & .NET Framework, MSAL supports the Web Account Manager (WAM) broker - we try to use that if possible
78-
// in the first instance.
79-
//
80-
// On .NET Framework MSAL supports the WinForms based 'embedded' webview UI. For Windows + .NET Framework this is the
81-
// best and natural experience.
82-
//
83-
// On other runtimes (e.g., .NET Core) MSAL only supports the system webview flow (launch the user's browser),
84-
// and the device-code flows.
85-
//
86-
// Note: .NET Core 3 allows using WinForms when run on Windows but MSAL does not yet support this.
87-
//
88-
// The system webview flow requires that the redirect URI is a loopback address, and that we are in an interactive session.
89-
//
90-
// The device code flow has no limitations other than a way to communicate to the user the code required to authenticate.
91-
//
92-
if (result is null)
93-
{
94-
// If the user has disabled interaction all we can do is fail at this point
95-
ThrowIfUserInteractionDisabled();
72+
AuthenticationResult result = null;
9673

97-
// If we're using the OS broker then delegate everything to that
98-
if (useBroker)
74+
// Try silent authentication first if we know about an existing user
75+
if (!string.IsNullOrWhiteSpace(userName))
9976
{
100-
Context.Trace.WriteLine("Performing interactive auth with broker...");
101-
result = await app.AcquireTokenInteractive(scopes)
102-
.WithPrompt(Prompt.SelectAccount)
103-
// We must configure the system webview as a fallback
104-
.WithSystemWebViewOptions(GetSystemWebViewOptions())
105-
.ExecuteAsync();
77+
result = await GetAccessTokenSilentlyAsync(app, scopes, userName);
10678
}
107-
else
79+
80+
//
81+
// If we failed to acquire an AT silently (either because we don't have an existing user, or the user's RT has expired)
82+
// we need to prompt the user for credentials.
83+
//
84+
// If the user has expressed a preference in how the want to perform the interactive authentication flows then we respect that.
85+
// Otherwise, depending on the current platform and session type we try to show the most appropriate authentication interface:
86+
//
87+
// On Windows 10 & .NET Framework, MSAL supports the Web Account Manager (WAM) broker - we try to use that if possible
88+
// in the first instance.
89+
//
90+
// On .NET Framework MSAL supports the WinForms based 'embedded' webview UI. For Windows + .NET Framework this is the
91+
// best and natural experience.
92+
//
93+
// On other runtimes (e.g., .NET Core) MSAL only supports the system webview flow (launch the user's browser),
94+
// and the device-code flows.
95+
//
96+
// Note: .NET Core 3 allows using WinForms when run on Windows but MSAL does not yet support this.
97+
//
98+
// The system webview flow requires that the redirect URI is a loopback address, and that we are in an interactive session.
99+
//
100+
// The device code flow has no limitations other than a way to communicate to the user the code required to authenticate.
101+
//
102+
if (result is null)
108103
{
109-
// Check for a user flow preference if they've specified one
110-
MicrosoftAuthenticationFlowType flowType = GetFlowType();
111-
switch (flowType)
104+
// If the user has disabled interaction all we can do is fail at this point
105+
ThrowIfUserInteractionDisabled();
106+
107+
// If we're using the OS broker then delegate everything to that
108+
if (useBroker)
109+
{
110+
Context.Trace.WriteLine("Performing interactive auth with broker...");
111+
result = await app.AcquireTokenInteractive(scopes)
112+
.WithPrompt(Prompt.SelectAccount)
113+
// We must configure the system webview as a fallback
114+
.WithSystemWebViewOptions(GetSystemWebViewOptions())
115+
.ExecuteAsync();
116+
}
117+
else
112118
{
113-
case MicrosoftAuthenticationFlowType.Auto:
114-
if (CanUseEmbeddedWebView())
115-
goto case MicrosoftAuthenticationFlowType.EmbeddedWebView;
116-
117-
if (CanUseSystemWebView(app, redirectUri))
118-
goto case MicrosoftAuthenticationFlowType.SystemWebView;
119-
120-
// Fall back to device code flow
121-
goto case MicrosoftAuthenticationFlowType.DeviceCode;
122-
123-
case MicrosoftAuthenticationFlowType.EmbeddedWebView:
124-
Context.Trace.WriteLine("Performing interactive auth with embedded web view...");
125-
EnsureCanUseEmbeddedWebView();
126-
result = await app.AcquireTokenInteractive(scopes)
127-
.WithPrompt(Prompt.SelectAccount)
128-
.WithUseEmbeddedWebView(true)
129-
.WithEmbeddedWebViewOptions(GetEmbeddedWebViewOptions())
130-
.ExecuteAsync();
131-
break;
132-
133-
case MicrosoftAuthenticationFlowType.SystemWebView:
134-
Context.Trace.WriteLine("Performing interactive auth with system web view...");
135-
EnsureCanUseSystemWebView(app, redirectUri);
136-
result = await app.AcquireTokenInteractive(scopes)
137-
.WithPrompt(Prompt.SelectAccount)
138-
.WithSystemWebViewOptions(GetSystemWebViewOptions())
139-
.ExecuteAsync();
140-
break;
141-
142-
case MicrosoftAuthenticationFlowType.DeviceCode:
143-
Context.Trace.WriteLine("Performing interactive auth with device code...");
144-
// We don't have a way to display a device code without a terminal at the moment
145-
// TODO: introduce a small GUI window to show a code if no TTY exists
146-
ThrowIfTerminalPromptsDisabled();
147-
result = await app.AcquireTokenWithDeviceCode(scopes, ShowDeviceCodeInTty).ExecuteAsync();
148-
break;
149-
150-
default:
151-
goto case MicrosoftAuthenticationFlowType.Auto;
119+
// Check for a user flow preference if they've specified one
120+
MicrosoftAuthenticationFlowType flowType = GetFlowType();
121+
switch (flowType)
122+
{
123+
case MicrosoftAuthenticationFlowType.Auto:
124+
if (CanUseEmbeddedWebView())
125+
goto case MicrosoftAuthenticationFlowType.EmbeddedWebView;
126+
127+
if (CanUseSystemWebView(app, redirectUri))
128+
goto case MicrosoftAuthenticationFlowType.SystemWebView;
129+
130+
// Fall back to device code flow
131+
goto case MicrosoftAuthenticationFlowType.DeviceCode;
132+
133+
case MicrosoftAuthenticationFlowType.EmbeddedWebView:
134+
Context.Trace.WriteLine("Performing interactive auth with embedded web view...");
135+
EnsureCanUseEmbeddedWebView();
136+
result = await app.AcquireTokenInteractive(scopes)
137+
.WithPrompt(Prompt.SelectAccount)
138+
.WithUseEmbeddedWebView(true)
139+
.WithEmbeddedWebViewOptions(GetEmbeddedWebViewOptions())
140+
.ExecuteAsync();
141+
break;
142+
143+
case MicrosoftAuthenticationFlowType.SystemWebView:
144+
Context.Trace.WriteLine("Performing interactive auth with system web view...");
145+
EnsureCanUseSystemWebView(app, redirectUri);
146+
result = await app.AcquireTokenInteractive(scopes)
147+
.WithPrompt(Prompt.SelectAccount)
148+
.WithSystemWebViewOptions(GetSystemWebViewOptions())
149+
.ExecuteAsync();
150+
break;
151+
152+
case MicrosoftAuthenticationFlowType.DeviceCode:
153+
Context.Trace.WriteLine("Performing interactive auth with device code...");
154+
// We don't have a way to display a device code without a terminal at the moment
155+
// TODO: introduce a small GUI window to show a code if no TTY exists
156+
ThrowIfTerminalPromptsDisabled();
157+
result = await app.AcquireTokenWithDeviceCode(scopes, ShowDeviceCodeInTty).ExecuteAsync();
158+
break;
159+
160+
default:
161+
goto case MicrosoftAuthenticationFlowType.Auto;
162+
}
152163
}
153164
}
154-
}
155165

156-
return new MsalResult(result);
166+
return new MsalResult(result);
167+
}
168+
finally
169+
{
170+
#if NETFRAMEWORK
171+
// If we created a dummy window during authentication we should dispose of it now that we're done
172+
_dummyWindow?.Dispose();
173+
#endif
174+
}
157175
}
158176

159177
internal MicrosoftAuthenticationFlowType GetFlowType()
@@ -229,20 +247,33 @@ private async Task<IPublicClientApplication> CreatePublicClientApplicationAsync(
229247
if (PlatformUtils.IsWindows())
230248
{
231249
// If we have a parent window ID then use that, otherwise use the hosting terminal window.
232-
IntPtr parentHandle;
233250
if (!string.IsNullOrWhiteSpace(Context.Settings.ParentWindowId) &&
234251
int.TryParse(Context.Settings.ParentWindowId, out int hWndInt) && hWndInt > 0)
235252
{
236-
parentHandle = new IntPtr(hWndInt);
253+
Context.Trace.WriteLine($"Using provided parent window ID '{hWndInt}' for MSAL authentication dialogs.");
254+
appBuilder.WithParentActivityOrWindow(() => new IntPtr(hWndInt));
237255
}
238256
else
239257
{
240258
IntPtr consoleHandle = Kernel32.GetConsoleWindow();
241-
parentHandle = User32.GetAncestor(consoleHandle, GetAncestorFlags.GetRootOwner);
242-
}
259+
IntPtr parentHandle = User32.GetAncestor(consoleHandle, GetAncestorFlags.GetRootOwner);
243260

244-
Context.Trace.WriteLine($"Using parent window ID '{parentHandle}' for MSAL authentication dialogs.");
245-
appBuilder.WithParentActivityOrWindow(() => parentHandle);
261+
// If we don't have a console window then create a dummy top-level window (for .NET Framework)
262+
// that we can use as a parent. When not on .NET Framework just use the Desktop window.
263+
if (parentHandle != IntPtr.Zero)
264+
{
265+
Context.Trace.WriteLine($"Using console parent window ID '{parentHandle}' for MSAL authentication dialogs.");
266+
appBuilder.WithParentActivityOrWindow(() => parentHandle);
267+
}
268+
else if (enableBroker) // Only actually need to set a parent window when using the Windows broker
269+
{
270+
#if NETFRAMEWORK
271+
Context.Trace.WriteLine($"Using dummy parent window for MSAL authentication dialogs.");
272+
_dummyWindow = new DummyWindow();
273+
appBuilder.WithParentActivityOrWindow(_dummyWindow.ShowAndGetHandle);
274+
#endif
275+
}
276+
}
246277
}
247278

248279
// Configure the broker if enabled
@@ -515,5 +546,73 @@ public MsalResult(AuthenticationResult msalResult)
515546
public string AccessToken => _msalResult.AccessToken;
516547
public string AccountUpn => _msalResult.Account.Username;
517548
}
549+
550+
#if NETFRAMEWORK
551+
private class DummyWindow : IDisposable
552+
{
553+
private readonly Thread _staThread;
554+
private readonly ManualResetEventSlim _readyEvent;
555+
private Form _window;
556+
private IntPtr _handle;
557+
558+
public DummyWindow()
559+
{
560+
_staThread = new Thread(ThreadProc);
561+
_staThread.SetApartmentState(ApartmentState.STA);
562+
_readyEvent = new ManualResetEventSlim();
563+
}
564+
565+
public IntPtr ShowAndGetHandle()
566+
{
567+
_staThread.Start();
568+
_readyEvent.Wait();
569+
return _handle;
570+
}
571+
572+
public void Dispose()
573+
{
574+
_window?.Invoke(() => _window.Close());
575+
576+
if (_staThread.IsAlive)
577+
{
578+
_staThread.Join();
579+
}
580+
}
581+
582+
private void ThreadProc()
583+
{
584+
System.Windows.Forms.Application.EnableVisualStyles();
585+
_window = new Form
586+
{
587+
TopMost = true,
588+
ControlBox = false,
589+
MaximizeBox = false,
590+
MinimizeBox = false,
591+
ClientSize = new Size(182, 46),
592+
FormBorderStyle = FormBorderStyle.None,
593+
StartPosition = FormStartPosition.CenterScreen,
594+
};
595+
596+
var progress = new ProgressBar
597+
{
598+
Style = ProgressBarStyle.Marquee,
599+
Location = new Point(12, 12),
600+
Size = new Size(158, 23),
601+
MarqueeAnimationSpeed = 30,
602+
};
603+
604+
_window.Controls.Add(progress);
605+
_window.Shown += (s, e) =>
606+
{
607+
_handle = _window.Handle;
608+
_readyEvent.Set();
609+
};
610+
611+
_window.ShowDialog();
612+
_window.Dispose();
613+
_window = null;
614+
}
615+
}
616+
#endif
518617
}
519618
}

0 commit comments

Comments
 (0)