How to use embedded web UI of MSAL.NET on WPF on .NET Core
Published Apr 17 2020 12:40 AM 7,250 Views
Microsoft

Azure AD B2C is a powerful service for providing business-to-customer identity.

https://docs.microsoft.com/en-us/azure/active-directory-b2c/overview

 

Of cause, you can also use Azure AD B2C sign feature on WPF on .NET Core. However, at now(April 17, 2020), there are few limitations:

https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/System-Browser-on-.Net-C...

 

  1. You can't use embedded web browser UI.(Have to use System browsers as default)
  2. You can't use 'http://localhost'(No port) redirect URL.(The feature will be released soon: https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/issues/1213)

In this article, I will try implements custom web ui for embedded web browser UI for WPF on .NET Core.

How to impl?

That's really simple!
Just implements Microsoft.Identity.Client.Extensibility.ICustomWebUi interface. There is a single method:

 

Task<Uri> AcquireAuthorizationCodeAsync(Uri authorizationUri, Uri redirectUri, CancellationToken cancellationToken)

 

The return value is an URI that has code=CODE parameters.

If there are something wrong during authentication flow, then throws MsalExtensionException.

Impl a Browser Window and the interface

To show a custom web UI, create a window that has a WebBrowser control.

 

<Window x:Class="EmbeddedMsalCustomWebUi.Wpf.Internal.EmbeddedWebUiWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:EmbeddedMsalCustomWebUi.Wpf.Internal"
        mc:Ignorable="d"
        WindowStyle="ToolWindow"
        Loaded="Window_Loaded"
        Closed="Window_Closed"
        Title="EmbeddedWebUiWindow" Height="450" Width="800">
    <Grid>
        <WebBrowser x:Name="webBrowser"
                    Navigating="WebBrowser_Navigating" />
    </Grid>
</Window>

 

And then, implements the code behind.

 

using Microsoft.Identity.Client.Extensibility;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Windows;
using System.Windows.Navigation;

namespace EmbeddedMsalCustomWebUi.Wpf.Internal
{
    public partial class EmbeddedWebUiWindow : Window
    {
        private readonly Uri _authorizationUri;
        private readonly Uri _redirectUri;
        private readonly TaskCompletionSource<Uri> _taskCompletionSource;
        private readonly CancellationToken _cancellationToken;
        private CancellationTokenRegistration _token;

        public EmbeddedWebUiWindow(
            Uri authorizationUri,
            Uri redirectUri,
            TaskCompletionSource<Uri> taskCompletionSource,
            CancellationToken cancellationToken)
        {
            InitializeComponent();
            _authorizationUri = authorizationUri;
            _redirectUri = redirectUri;
            _taskCompletionSource = taskCompletionSource;
            _cancellationToken = cancellationToken;
        }

        private void WebBrowser_Navigating(object sender, NavigatingCancelEventArgs e)
        {
            if (!e.Uri.ToString().StartsWith(_redirectUri.ToString()))
            {
                // not redirect uri case
                return;
            }

            // parse query string
            var query = HttpUtility.ParseQueryString(e.Uri.Query);
            if (query.AllKeys.Any(x => x == "code"))
            {
                // It has a code parameter.
                _taskCompletionSource.SetResult(e.Uri);
            }
            else
            {
                // error.
                _taskCompletionSource.SetException(
                    new MsalExtensionException(
                        $"An error occurred, error: {query.Get("error")}, error_description: {query.Get("error_description")}"));
            }

            Close();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            _token = _cancellationToken.Register(() => _taskCompletionSource.SetCanceled());
            // navigating to an uri that is entry point to authorization flow.
            webBrowser.Navigate(_authorizationUri);
        }

        private void Window_Closed(object sender, EventArgs e)
        {
            _taskCompletionSource.TrySetCanceled();
            _token.Dispose();
        }
    }
}

 

Implements AcquireAuthorizationCodeAsync method using the above window.

 

using EmbeddedMsalCustomWebUi.Wpf.Internal;
using Microsoft.Identity.Client.Extensibility;
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace EmbeddedMsalCustomWebUi.Wpf
{
    /// <summary>
    /// Provides embedded web ui for WPF on .NET Core.
    /// The web ui is using WebBrowser control(Trident engine).
    /// </summary>
    public class EmbeddedBrowserWebUi : ICustomWebUi
    {
        public const int DefaultWindowWidth = 600;
        public const int DefaultWindowHeight = 800;

        private readonly Window _owner;
        private readonly string _title;
        private readonly int _windowWidth;
        private readonly int _windowHeight;
        private readonly WindowStartupLocation _windowStartupLocation;

        public EmbeddedBrowserWebUi(Window owner, 
            string title = "Sign in",
            int windowWidth = DefaultWindowWidth,
            int windowHeight = DefaultWindowHeight,
            WindowStartupLocation windowStartupLocation = WindowStartupLocation.CenterOwner)
        {
            _owner = owner ?? throw new ArgumentNullException(nameof(owner));
            _title = title;
            _windowWidth = windowWidth;
            _windowHeight = windowHeight;
            _windowStartupLocation = windowStartupLocation;
        }

        public Task<Uri> AcquireAuthorizationCodeAsync(Uri authorizationUri, Uri redirectUri, CancellationToken cancellationToken)
        {
            var tcs = new TaskCompletionSource<Uri>();
            _owner.Dispatcher.Invoke(() =>
            {
                new EmbeddedWebUiWindow(authorizationUri,
                    redirectUri,
                    tcs,
                    cancellationToken)
                {
                    Owner = _owner,
                    Title = _title,
                    Width = _windowWidth,
                    Height = _windowHeight,
                    WindowStartupLocation = _windowStartupLocation,
                }.ShowDialog();
            });

            return tcs.Task;
        }
    }
}
Spoiler
I decided using WPF WebBrowser control because it is easest way to use WebBrowser control on WPF. Other options are using old edge engine or new edge engine(the status is developer preview now).
If you would like to use them, then please replace WebBrowser control to one you want.

Test

 

Create an app for WPF client on Azure AD B2C tenant as public client app and check redirect URL.

KazukiOta_1-1587107372027.png

 

And create an another app on B2C tenant, and export an API from the app, then add the permission from the client app.

KazukiOta_2-1587107525419.png

And If you haven't created sign in user flow on the tenant, then create it.

KazukiOta_3-1587107952035.png

Collect following items to use WPF app:

  • Application ID(Client ID) (GUID)
  • Tenant ID (GUID)
  • Redirect URI: it is on the Authentication page on the B2C portal.
  • Azure AD B2C Authority: https://{tenant name}.b2clogin.com/tfp/{tenant name}.onmicrosoft.com/{user flow name}.
  • Scope: It is an URL (https://{tenant name}.onmicrosoft.com/{guid}/{name}) that is able to get at API permissions page of another app.

And then create a WPF App(.NET Core) project, and create an IPublicClientApplication instance on Startup event of App class.

 

PublicClientApplication = PublicClientApplicationBuilder.Create("{your client id}")
    .WithRedirectUri("{your redirect uri}")
    .WithTenantId("your tenant id")
    .WithB2CAuthority("{your azure ad b2c authority}")
    .Build();

 

The last step! Use EmbeddedBrowserWebUi class that implemented on this article with AcquireTokenInteractive method.

 

 var r = await PublicClientApplication
     .AcquireTokenInteractive(new[] { "{your scope}" })
     .WithCustomWebUi(new EmbeddedBrowserWebUi(this)) // here
     .ExecuteAsync();

 

It works as below:

signinflow.gif

 

Completed source code

The custom web ui and test app codes are on following github repo:

https://github.com/runceel/EmbeddedMsalCustomWebUi.Wpf

 

If you would like to try EmbeddedCustomWebUi class on your code, then you can get it from NuGet:

https://www.nuget.org/packages/EmbeddedMsalCustomWebUi.Wpf/

 

Important:
This is a just sample code. It is not tested for production.

 

3 Comments
Copper Contributor

Very nice article, this is so much better than opening the browser in terms of user experience. I use it with normal Azure AD and it works just as well.

Copper Contributor

For native apps this is a great solution - using the system browser is a strong disconnect in the user experience department. Thanks for sharing this!

Copper Contributor

Trying to implement this but the browser in the window stays blank after opening and I don't get any login page. Any ideas?

 

Edit: Of course figured it out right after I posted... missing the calls to Loaded and Closed events in XAML.

 

Thank you for the great example!

Version history
Last update:
‎Apr 17 2020 06:18 AM
Updated by: