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:
- You can't use embedded web browser UI.(Have to use System browsers as default)
- 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;
}
}
}
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.
And create an another app on B2C tenant, and export an API from the app, then add the permission from the client app.
And If you haven't created sign in user flow on the tenant, then create it.
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:
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.