Using The Windows Authentication Broker For Simpler User Logins With MSAL
Table of Contents
If you ever wrote any client applications that have to deal with user authentication, you probably know that it’s usually a hassle, especially if you have to deal with OAuth. You have to worry about properly configuring the application, setting up the redirect URLs, and then performing the token acquisition dance. It’s just a pain.
To remove this toil, our team at Microsoft has been working on adding a new tool to the developer toolbox - an authentication broker. An authentication broker acts as the intermediary between you, the engineer writing your application, and the identity provider (IDP). The broker doesn’t quite eliminate the need to properly configure your application, but if you’re already using a library like the Microsoft Authentication Library (MSAL) then the adoption process is fairly simple. But before we talk about it, I haven’t yet answered the question - why in the world you want to use the authentication broker in the first place?
Benefits of using an authentication broker #
There are a few core benefits, that we’ve also documented:
- Better security. One of the biggest headaches that developers have is around library updates. Every time you update a library in your dependency chain, things can break, and break in unexpected ways. Using brokers shifts some of the logic to a shared OS/application component that lives independent from your own application. In the case of Windows, the authentication broker, also known as the Web Account Manager (WAM), can be updated through Windows Update, and there is zero need to update client applications when a new broker release drops.
- Advanced feature support. If you want to have your users authenticate with Windows Hello or FIDO keys, you will need to use an authentication broker.
- Integrated with Windows. Now, not every authentication broker is on Windows - you can have brokers on macOS and Linux, and on iOS and Android devices, the Microsoft Authenticator app acts as the broker. But, on Windows, WAM is deeply integrated into the OS, which means that all Entra ID (formerly known as Azure Active Directory) and personal Microsoft accounts can be easily shared between applications. If your customer signed into Office on their desktop machine, when your application wants to sign them in they can choose an already connected account rather than try to enter everything from scratch.
- Token protection. If you are using a broker such as WAM then the refresh tokens (some of the scariest tokens to leak) are device-bound. Even if exfiltrated, they would be useless.
- Simpler caching. The runtime layer that handles broker communications also handles token caching for certain scenarios (e.g., using Work and School accounts), so you don’t need to worry about implementing it from scratch.
So let’s take a look at how the code would work for the top three programming languages we support - C#, Python, and Java.
Using WAM across languages and platforms #
For the purposes of this blog post, I will focus exclusively on Windows and WAM, however similar patterns will be applicable to other brokers on other platforms. Those brokers will also be accessible through the same API abstractions.
Before we start #
Context is important, and in this context we’re going to be working on public client applications (PCA). PCAs are running on client devices that cannot securely store a secret. These are applications that end-users interact with on their phones, personal computers, or through their web browser. Confidential client applications (CCA) are applications that run on the server and can securely store a secret cannot use authentication brokers, but that’s OK because a lot of the time they don’t need to deal with identity in the same pattern a client application would (i.e., have user interaction).
.NET #
For .NET applications, you will need three packages in the mix:
Microsoft.Identity.Client
- the core MSAL library.Microsoft.Identity.Client.Broker
- functionality related to interacting with the authentication broker (WAM, in this case).Microsoft.Identity.Client.Extensions.Msal
- a set of helpful extensions, that will primarily be used for caching logic.
Hold on, but didn't you say that if we use brokers we don't need to worry about caching? If that's the case, why do we need a whole new package being added into the mix?
Great question! As a matter of fact, you don’t need the extensions package if you want to only operate on work and school (i.e., Microsoft Entra ID) accounts. However, the moment you want to use personal (MSA) Microsoft accounts, you will need to add some manual caching hooks.
Now, the actual code looks like this:
var scopes = new[] { "User.Read" };
var options = new BrokerOptions(BrokerOptions.OperatingSystems.Windows) { Title = "My Awesome Application" };
var storageProperties = new StorageCreationPropertiesBuilder("cache.bin", AppDomain.CurrentDomain.BaseDirectory).Build();
var app = PublicClientApplicationBuilder
.Create("YOUR_CLIENT_ID")
.WithBroker(options)
.WithParentActivityOrWindow(GetConsoleOrTerminalWindow)
.WithAuthority("https://login.microsoftonline.com/common")
.WithDefaultRedirectUri()
.Build();
var cacheHelper = await MsalCacheHelper.CreateAsync(storageProperties);
cacheHelper.RegisterCache(app.UserTokenCache);
var accounts = await app.GetAccountsAsync();
var existingAccount = accounts.FirstOrDefault();
AuthenticationResult result;
try
{
result = existingAccount != null
? await app.AcquireTokenSilent(scopes, existingAccount).ExecuteAsync()
: await app.AcquireTokenInteractive(scopes).ExecuteAsync();
}
catch (MsalUiRequiredException)
{
result = await app.AcquireTokenInteractive(scopes).ExecuteAsync();
}
We set up the PublicClientApplication
instance with all the relevant broker details and parent it to a terminal window with the help of WithParentActivityOrWindow
. In this case, GetConsoleOrTerminalWindow
is a helper function:
[DllImport("user32.dll", ExactSpelling = true)]
static extern IntPtr GetAncestor(IntPtr hwnd, GetAncestorFlags flags);
[DllImport("kernel32.dll")]
static extern IntPtr GetConsoleWindow();
enum GetAncestorFlags
{
GetParent = 1,
GetRoot = 2,
GetRootOwner = 3
}
public IntPtr GetConsoleOrTerminalWindow()
{
IntPtr consoleHandle = GetConsoleWindow();
IntPtr handle = GetAncestor(consoleHandle, GetAncestorFlags.GetRootOwner);
return handle;
}
Of course, if you are running this in the context of modern frameworks like WinUI 3 or WPF, you might want to use a different approach to window handle acquisition.
When I run the application from Visual Studio, I should see the WAM prompt pop up:
To get the latest details on integrating WAM with MSAL.NET, you can refer to the official documentation.
Python #
To get started with using WAM in the context of a Python application, you will need to install the msal
package:
pip install msal[broker]>=1.20,<2
By adding the [broker]
you will install a variant of the msal
package that contains support for authentication brokers. From there, you will also need to implement a barebones token cache with built-in capabilities, such as SerializableTokenCache
. The complete code that both takes the cache into consideration and acquires tokens looks like this:
from msal import PublicClientApplication, SerializableTokenCache
import pickle
import os
import atexit;
cache = SerializableTokenCache()
if os.path.exists('cache.bin'):
print ("Loading token cache from disk")
cache.deserialize(open("cache.bin", "r").read())
print("Loaded token cache from disk")
atexit.register(lambda:
open("cache.bin", "w").write(cache.serialize())
if cache.has_state_changed else None
)
app = PublicClientApplication(
"YOUR_CLIENT_ID",
authority="https://login.microsoftonline.com/common",
allow_broker=True,
token_cache=cache)
accounts = app.get_accounts();
result = None
if accounts:
print("Account exists in the cache.")
result = app.acquire_token_silent(["User.Read"], account=accounts[0])
else:
print("No accounts in the cache")
result = app.acquire_token_interactive(["User.Read"],
parent_window_handle=app.CONSOLE_WINDOW_HANDLE)
print(result)
First, we attempt to verify if a local cache.bin
file exists. Then, we register a lambda function with atexit
to ensure that once the script finishes, we store the updated cache locally. And we wrap it up by trying to acquire a token silently from the cache if accounts are available. Otherwise, an interactive prompt is shown.
Once the prompt exits, if the acquisition was successful you can get the token from result.access_token
. It’s worth noting that you will want to add some exception handling here to make sure that you’re catching any other issues that might arise that are not related to the fact that the token was missing in the cache.
Do we need to implement any custom window handle logic in Python? I don't see anything in the snippet above that deals with it, and we've done it for .NET.
For Python, because we’re operating without a Graphical User Interface (GUI), we have a helper function that is implemented directly into MSAL for Python. It’s exposed to the developer via the CONSOLE_WINDOW_HANDLE
property on the PublicClientApplication
instance. Given that most Python applications run as scripts, this should be sufficient. You can, however, override the parent_window_handle
argument value with a window handle of your choice if the scenario calls for it.
And just like for .NET, we have a dedicated documentation page where you can learn more about all the changes to Windows-related broker APIs in MSAL for Python.
Java #
Last but not least, for Java applications, you will need two packages:
msal4j
- the core MSAL Java library.msal4j-brokers
- package responsible for interacting with authentication brokers, such as WAM.
This is very similar to what we’ve done in .NET, and it’s actually pretty close code-wise as well. To use the code below, I use Eclipse IDE, where I created a new Maven project.
Inside that project, prior to building it, I added the following entries to the pom.xml
file dependencies
node:
<dependencies>
<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>msal4j</artifactId>
<version>1.15.0</version>
</dependency>
<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>msal4j-brokers</artifactId>
<version>1.0.3-beta</version>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>5.9.0</version>
</dependency>
</dependencies>
The code itself deals with setting up a PublicClientApplication
instance that is configured to use the broker via the [Broker
] class (which, in turn, is implemented via IBroker
).
Just like with the .NET and Python code, we’re adding some simple caching scaffolding as well as logic to detect the window handle, because we need to parent the broker dialog to it. MSAL for Java does not, at this time, have an abstraction that provides the window handle either for console or UI applications.
import com.microsoft.aad.msal4j.*;
import com.microsoft.aad.msal4jbrokers.Broker;
import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.User32;
import com.sun.jna.platform.win32.WinDef.HWND;
import java.io.*;
import java.net.URI;
import java.util.Collections;
import java.util.Set;
public class WAMTest {
private static final String CLIENT_ID = "YOUR_CLIENT_ID";
private static final Set<String> SCOPES = Collections.singleton("user.read");
private static final String AUTHORITY = "https://login.microsoftonline.com/common";
public static void main(String[] args) throws Exception {
HWND hwnd = User32.INSTANCE.GetForegroundWindow();
long windowHandle = Pointer.nativeValue(hwnd.getPointer());
Broker broker = new Broker.Builder().supportWindows(true).build();
PublicClientApplication pca = PublicClientApplication.builder(CLIENT_ID).authority(AUTHORITY).broker(broker)
.build();
InteractiveRequestParameters parameters = InteractiveRequestParameters.builder(new URI("http://localhost"))
.scopes(SCOPES).windowHandle(windowHandle).build();
IAuthenticationResult result;
IAccount account = null;
File accountsFile = new File("accounts.bin");
if (accountsFile.exists()) {
try (FileInputStream fileIn = new FileInputStream(accountsFile);
ObjectInputStream in = new ObjectInputStream(fileIn)) {
account = (IAccount) in.readObject();
System.out.println("Account object deserialized");
}
}
try {
SilentParameters silentParameters = SilentParameters.builder(SCOPES, account).build();
result = pca.acquireTokenSilently(silentParameters).join();
} catch (Exception ex) {
result = pca.acquireToken(parameters).join();
}
account = result.account();
try (FileOutputStream fileOut = new FileOutputStream(accountsFile);
ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
out.writeObject(account);
System.out.println("Account object serialized");
}
System.out.println(result.accessToken());
}
}
When you execute the code from Eclipse IDE, you should be able to see the WAM prompt with existing accounts (if any are already linked to the operating system).
Selecting an account should grant you a token (assuming user consent is granted) and you can use it for other APIs that need it.
Head over to the Using Web Account Manager with MSAL Java article to learn more about WAM integration with your Java code.
How WAM caches tokens #
Tokens are stored in special files on disk, however due to their sensitive nature they should only be retrieved through Windows APIs. Part of the reason why I mentioned MSAL above is because MSAL abstracts out the complexity of working with WAM-related Windows APIs directly, along with adding a whole bunch of nice things that are not available within the OS natively.
If you ever need to clear accounts in the cache that were touched by your own application but don’t want to write any code, you can use a custom PowerShell script that is provided by Microsoft. Inside it, replace the GUID with your application client ID and run it.
There is also the more radical option of using DSREGCMD
with /cleanupaccounts
option if you ever need it; however that will delete all accounts associated with WAM, not just your own application-specific cached entities.
Brokers on other platforms #
I alluded to it earlier that WAM is not the only authentication broker. If you are on company-managed devices, you might have Company Portal installed. On your mobile devices, you might use Microsoft Authenticator to get temporary authentication codes or login notifications. Both of these act as authentication brokers, although the interaction with them is outside the scope of this blog post.
Conclusion #
Brokers really make it easier for your customers to authenticate in your application, especially if they already have a Microsoft account (work, school, or personal) connected to their Windows machine. Quite a few applications, both inside and outside Microsoft, are already using WAM as the default (you might’ve noticed that experience with Microsoft Office), and my team highly recommends that third-party developers make that transition as well.
Now that you’re familiar with using authentication brokers on Windows, I recommend you learn more about the MSAL family of libraries by visiting the MSAL documentation.