1 //----------------------------------------------------------------------
3 // Copyright (c) Microsoft Corporation.
4 // All rights reserved.
6 // This code is licensed under the MIT License.
8 // Permission is hereby granted, free of charge, to any person obtaining a copy
9 // of this software and associated documentation files(the "Software"), to deal
10 // in the Software without restriction, including without limitation the rights
11 // to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
12 // copies of the Software, and to permit persons to whom the Software is
13 // furnished to do so, subject to the following conditions :
15 // The above copyright notice and this permission notice shall be included in
16 // all copies or substantial portions of the Software.
18 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
21 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26 //------------------------------------------------------------------------------
29 using System.Collections.Generic;
30 using System.ComponentModel;
31 using System.Diagnostics;
33 using System.Globalization;
34 using System.Runtime.InteropServices;
35 using System.Windows.Forms;
37 namespace Microsoft.IdentityModel.Clients.ActiveDirectory.Internal
40 /// Base class for web form
43 [EditorBrowsable(EditorBrowsableState.Never)]
44 public abstract class WindowsFormsWebAuthenticationDialogBase : Form
46 private static readonly NavigateErrorStatus NavigateErrorStatus = new NavigateErrorStatus();
48 private const int UIWidth = 566;
50 private Panel webBrowserPanel;
51 private readonly CustomWebBrowser webBrowser;
53 private Uri desiredCallbackUri;
58 protected IWin32Window ownerWindow;
60 private Keys key = Keys.None;
62 private readonly HashSet<string> whiteListedSchemes = new HashSet<string>();
63 internal AuthorizationResult Result { get; set; }
68 /// <param name="ownerWindow"></param>
69 protected WindowsFormsWebAuthenticationDialogBase(object ownerWindow)
71 // From MSDN (http://msdn.microsoft.com/en-us/library/ie/dn720860(v=vs.85).aspx):
72 // The net session count tracks the number of instances of the web browser control.
73 // When a web browser control is created, the net session count is incremented. When the control
74 // is destroyed, the net session count is decremented. When the net session count reaches zero,
75 // the session cookies for the process are cleared. SetQueryNetSessionCount can be used to prevent
76 // the session cookies from being cleared for applications where web browser controls are being created
77 // and destroyed throughout the lifetime of the application. (Because the application lives longer than
78 // a given instance, session cookies must be retained for a longer periods of time.
79 int sessionCount = NativeMethods.SetQueryNetSessionCount(NativeMethods.SessionOp.SESSION_QUERY);
80 if (sessionCount == 0)
82 NativeMethods.SetQueryNetSessionCount(NativeMethods.SessionOp.SESSION_INCREMENT);
85 if (ownerWindow == null)
87 this.ownerWindow = null;
89 else if (ownerWindow is IWin32Window)
91 this.ownerWindow = (IWin32Window)ownerWindow;
93 else if (ownerWindow is IntPtr)
95 this.ownerWindow = new WindowsFormsWin32Window { Handle = (IntPtr)ownerWindow };
99 throw new AdalException(AdalError.InvalidOwnerWindowType,
100 "Invalid owner window type. Expected types are IWin32Window or IntPtr (for window handle).");
103 this.webBrowser = new CustomWebBrowser();
104 this.webBrowser.PreviewKeyDown += webBrowser_PreviewKeyDown;
105 this.InitializeComponent();
107 whiteListedSchemes.Add("browser");
110 private void webBrowser_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
112 if (e.KeyCode == Keys.Back)
119 /// Gets Web Browser control used by the dialog.
121 public WebBrowser WebBrowser
123 get { return this.webBrowser; }
129 /// <param name="sender"></param>
130 /// <param name="e"></param>
131 protected virtual void WebBrowserNavigatingHandler(object sender, WebBrowserNavigatingEventArgs e)
133 if (this.webBrowser.IsDisposed)
135 // we cancel all flows in disposed object and just do nothing, let object to close.
136 // it just for safety.
141 if (key == Keys.Back)
143 //navigation is being done via back key. This needs to be disabled.
148 // we cancel further processing, if we reached final URL.
149 // Security issue: we prohibit navigation with auth code
150 // if redirect URI is URN, then we prohibit navigation, to prevent random browser popup.
151 e.Cancel = this.CheckForClosingUrl(e.Url);
153 // check if the url scheme is of type browser://
154 // this means we need to launch external browser
155 if (!e.Cancel && e.Url.Scheme.Equals("browser", StringComparison.CurrentCultureIgnoreCase))
157 Process.Start(e.Url.AbsoluteUri.Replace("browser://", "https://"));
163 PlatformPlugin.Logger.Verbose(null,
164 string.Format(CultureInfo.CurrentCulture, " Navigating to '{0}'.",
165 EncodingHelper.UrlDecode(e.Url.ToString())));
169 private void WebBrowserNavigatedHandler(object sender, WebBrowserNavigatedEventArgs e)
171 if (!this.CheckForClosingUrl(e.Url))
173 PlatformPlugin.Logger.Verbose(null,
174 string.Format(CultureInfo.CurrentCulture, " Navigated to '{0}'.",
175 EncodingHelper.UrlDecode(e.Url.ToString())));
182 protected virtual void WebBrowserNavigateErrorHandler(object sender, WebBrowserNavigateErrorEventArgs e)
184 // e.StatusCode - Contains error code which we are able to translate this error to text
185 // ADAL.Native contains a code for translation.
186 if (this.DialogResult == DialogResult.OK)
191 if (this.webBrowser.IsDisposed)
193 // we cancel all flow in disposed object.
198 if (this.webBrowser.ActiveXInstance != e.WebBrowserActiveXInstance)
200 // this event came from internal frame, ignore this.
204 if (e.StatusCode >= 300 && e.StatusCode < 400)
206 // we could get redirect flows here as well.
211 this.StopWebBrowser();
212 // in this handler object could be already disposed, so it should be the last method
213 this.OnNavigationCanceled(e.StatusCode);
216 private bool CheckForClosingUrl(Uri url)
219 bool canClose = false;
220 if (url.Authority.Equals(this.desiredCallbackUri.Authority, StringComparison.CurrentCultureIgnoreCase) &&
221 url.AbsolutePath.Equals(this.desiredCallbackUri.AbsolutePath, StringComparison.CurrentCulture))
223 this.Result = new AuthorizationResult(AuthorizationStatus.Success, url.OriginalString);
227 // if redirect_uri is not hit and scheme of the url is not whitelisted and url is not about:blank
228 // and url scheme is not https then fail to load.
229 if (!canClose && !whiteListedSchemes.Contains(url.Scheme.ToLower(CultureInfo.CurrentCulture)) &&
230 !url.AbsoluteUri.Equals("about:blank", StringComparison.CurrentCultureIgnoreCase)
231 && !url.Scheme.Equals("https", StringComparison.CurrentCultureIgnoreCase)
232 && !url.Scheme.Equals("javascript", StringComparison.CurrentCultureIgnoreCase))
234 this.Result = new AuthorizationResult(AuthorizationStatus.ErrorHttp);
235 this.Result.Error = AdalError.NonHttpsRedirectNotSupported;
236 this.Result.ErrorDescription = AdalErrorMessage.NonHttpsRedirectNotSupported;
242 this.StopWebBrowser();
243 // in this handler object could be already disposed, so it should be the last method
250 private void StopWebBrowser()
252 if (!this.webBrowser.IsDisposed)
254 if (this.webBrowser.IsBusy)
256 PlatformPlugin.Logger.Verbose(null,
257 string.Format(CultureInfo.CurrentCulture,
258 " WebBrowser state: IsBusy: {0}, ReadyState: {1}, Created: {2}, Disposing: {3}, IsDisposed: {4}, IsOffline: {5}",
259 this.webBrowser.IsBusy, this.webBrowser.ReadyState, this.webBrowser.Created,
260 this.webBrowser.Disposing, this.webBrowser.IsDisposed, this.webBrowser.IsOffline));
261 this.webBrowser.Stop();
262 PlatformPlugin.Logger.Verbose(null,
263 string.Format(CultureInfo.CurrentCulture,
264 " WebBrowser state (after Stop): IsBusy: {0}, ReadyState: {1}, Created: {2}, Disposing: {3}, IsDisposed: {4}, IsOffline: {5}",
265 this.webBrowser.IsBusy, this.webBrowser.ReadyState, this.webBrowser.Created,
266 this.webBrowser.Disposing, this.webBrowser.IsDisposed, this.webBrowser.IsOffline));
274 protected abstract void OnClosingUrl();
279 /// <param name="statusCode"></param>
280 protected abstract void OnNavigationCanceled(int statusCode);
282 internal AuthorizationResult AuthenticateAAD(Uri requestUri, Uri callbackUri)
284 this.desiredCallbackUri = callbackUri;
287 // The WebBrowser event handlers must not throw exceptions.
288 // If they do then they may be swallowed by the native
289 // browser com control.
290 this.webBrowser.Navigating += this.WebBrowserNavigatingHandler;
291 this.webBrowser.Navigated += this.WebBrowserNavigatedHandler;
292 this.webBrowser.NavigateError += this.WebBrowserNavigateErrorHandler;
294 this.webBrowser.Navigate(requestUri);
295 this.OnAuthenticate();
303 protected virtual void OnAuthenticate()
307 private void InitializeComponent()
309 Screen screen = (this.ownerWindow != null)
310 ? Screen.FromHandle(this.ownerWindow.Handle)
311 : Screen.PrimaryScreen;
313 // Window height is set to 70% of the screen height.
314 int uiHeight = (int)(Math.Max(screen.WorkingArea.Height, 160) * 70.0 / DpiHelper.ZoomPercent);
315 this.webBrowserPanel = new Panel();
316 this.webBrowserPanel.SuspendLayout();
317 this.SuspendLayout();
322 this.webBrowser.Dock = DockStyle.Fill;
323 this.webBrowser.Location = new Point(0, 25);
324 this.webBrowser.MinimumSize = new Size(20, 20);
325 this.webBrowser.Name = "webBrowser";
326 this.webBrowser.Size = new Size(UIWidth, 565);
327 this.webBrowser.TabIndex = 1;
328 this.webBrowser.IsWebBrowserContextMenuEnabled = false;
333 this.webBrowserPanel.Controls.Add(this.webBrowser);
334 this.webBrowserPanel.Dock = DockStyle.Fill;
335 this.webBrowserPanel.BorderStyle = BorderStyle.None;
336 this.webBrowserPanel.Location = new Point(0, 0);
337 this.webBrowserPanel.Name = "webBrowserPanel";
338 this.webBrowserPanel.Size = new Size(UIWidth, uiHeight);
339 this.webBrowserPanel.TabIndex = 2;
342 // BrowserAuthenticationWindow
344 this.AutoScaleDimensions = new SizeF(6, 13);
345 this.AutoScaleMode = AutoScaleMode.Font;
346 this.ClientSize = new Size(UIWidth, uiHeight);
347 this.Controls.Add(this.webBrowserPanel);
348 this.FormBorderStyle = FormBorderStyle.FixedSingle;
349 this.Name = "BrowserAuthenticationWindow";
351 // Move the window to the center of the parent window only if owner window is set.
352 this.StartPosition = (this.ownerWindow != null)
353 ? FormStartPosition.CenterParent
354 : FormStartPosition.CenterScreen;
355 this.Text = string.Empty;
356 this.ShowIcon = false;
357 this.MaximizeBox = false;
358 this.MinimizeBox = false;
360 // If we don't have an owner we need to make sure that the pop up browser
361 // window is in the task bar so that it can be selected with the mouse.
362 this.ShowInTaskbar = (null == this.ownerWindow);
364 this.webBrowserPanel.ResumeLayout(false);
365 this.ResumeLayout(false);
368 private sealed class WindowsFormsWin32Window : IWin32Window
370 public IntPtr Handle { get; set; }
376 /// <param name="disposing"></param>
377 protected override void Dispose(bool disposing)
384 base.Dispose(disposing);
390 /// <param name="statusCode"></param>
391 /// <returns></returns>
392 protected AdalException CreateExceptionForAuthenticationUiFailed(int statusCode)
394 if (NavigateErrorStatus.Messages.ContainsKey(statusCode))
396 return new AdalServiceException(
397 AdalError.AuthenticationUiFailed,
398 string.Format(CultureInfo.CurrentCulture,
399 " The browser based authentication dialog failed to complete. Reason: {0}",
400 NavigateErrorStatus.Messages[statusCode]))
401 { StatusCode = statusCode };
404 return new AdalServiceException(
405 AdalError.AuthenticationUiFailed,
406 string.Format(CultureInfo.CurrentCulture,
407 " The browser based authentication dialog failed to complete for an unknown reason. StatusCode: {0}",
409 { StatusCode = statusCode };
415 protected static class DpiHelper
419 const double DefaultDpi = 96.0;
421 const int LOGPIXELSX = 88;
422 const int LOGPIXELSY = 90;
427 IntPtr dC = NativeWrapper.NativeMethods.GetDC(IntPtr.Zero);
428 if (dC != IntPtr.Zero)
430 deviceDpiX = NativeWrapper.NativeMethods.GetDeviceCaps(dC, LOGPIXELSX);
431 deviceDpiY = NativeWrapper.NativeMethods.GetDeviceCaps(dC, LOGPIXELSY);
432 NativeWrapper.NativeMethods.ReleaseDC(IntPtr.Zero, dC);
436 deviceDpiX = DefaultDpi;
437 deviceDpiY = DefaultDpi;
440 int zoomPercentX = (int)(100 * (deviceDpiX / DefaultDpi));
441 int zoomPercentY = (int)(100 * (deviceDpiY / DefaultDpi));
443 ZoomPercent = Math.Min(zoomPercentX, zoomPercentY);
449 public static int ZoomPercent { get; private set; }
453 internal static class NativeMethods
455 internal enum SessionOp
462 [DllImport("IEFRAME.dll", CallingConvention = CallingConvention.StdCall, ExactSpelling = true)]
463 internal static extern int SetQueryNetSessionCount(SessionOp sessionOp);