6
6
using GitCredentialManager . Interop . Windows . Native ;
7
7
using Microsoft . Identity . Client ;
8
8
using Microsoft . Identity . Client . Extensions . Msal ;
9
+ using System . Threading ;
10
+ using System . Runtime . InteropServices ;
9
11
10
12
#if NETFRAMEWORK
13
+ using System . Drawing ;
14
+ using System . Windows . Forms ;
11
15
using Microsoft . Identity . Client . Broker ;
12
16
#endif
13
17
@@ -42,6 +46,10 @@ public class MicrosoftAuthentication : AuthenticationBase, IMicrosoftAuthenticat
42
46
"live" , "liveconnect" , "liveid" ,
43
47
} ;
44
48
49
+ #if NETFRAMEWORK
50
+ private DummyWindow _dummyWindow ;
51
+ #endif
52
+
45
53
public MicrosoftAuthentication ( ICommandContext context )
46
54
: base ( context ) { }
47
55
@@ -54,106 +62,116 @@ public async Task<IMicrosoftAuthenticationResult> GetTokenAsync(
54
62
bool useBroker = CanUseBroker ( ) ;
55
63
Context . Trace . WriteLine ( useBroker
56
64
? "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." ) ;
61
66
62
- AuthenticationResult result = null ;
63
-
64
- // Try silent authentication first if we know about an existing user
65
- if ( ! string . IsNullOrWhiteSpace ( userName ) )
67
+ try
66
68
{
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 ) ;
69
71
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 ;
96
73
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 ) )
99
76
{
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 ) ;
106
78
}
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 )
108
103
{
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
112
118
{
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
+ }
152
163
}
153
164
}
154
- }
155
165
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
+ }
157
175
}
158
176
159
177
internal MicrosoftAuthenticationFlowType GetFlowType ( )
@@ -229,20 +247,33 @@ private async Task<IPublicClientApplication> CreatePublicClientApplicationAsync(
229
247
if ( PlatformUtils . IsWindows ( ) )
230
248
{
231
249
// If we have a parent window ID then use that, otherwise use the hosting terminal window.
232
- IntPtr parentHandle ;
233
250
if ( ! string . IsNullOrWhiteSpace ( Context . Settings . ParentWindowId ) &&
234
251
int . TryParse ( Context . Settings . ParentWindowId , out int hWndInt ) && hWndInt > 0 )
235
252
{
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 ) ) ;
237
255
}
238
256
else
239
257
{
240
258
IntPtr consoleHandle = Kernel32 . GetConsoleWindow ( ) ;
241
- parentHandle = User32 . GetAncestor ( consoleHandle , GetAncestorFlags . GetRootOwner ) ;
242
- }
259
+ IntPtr parentHandle = User32 . GetAncestor ( consoleHandle , GetAncestorFlags . GetRootOwner ) ;
243
260
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
+ }
246
277
}
247
278
248
279
// Configure the broker if enabled
@@ -515,5 +546,73 @@ public MsalResult(AuthenticationResult msalResult)
515
546
public string AccessToken => _msalResult . AccessToken ;
516
547
public string AccountUpn => _msalResult . Account . Username ;
517
548
}
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
518
617
}
519
618
}
0 commit comments