Authenticating Users in a C# Command-Line App with MSAL and Web Account Manager
This post records how a C# console app combines MSAL and Web Account Manager to complete interactive sign-in and acquire the Access Token required to access pr…
Work scenarios often involve APIs protected by an authentication mechanism. Before clients call these APIs, they need to authenticate first, and then use the Access Token obtained through authentication to access those APIs. This post shows how to enable interactive authentication in a console app and then call a protected API.
Scenario Description #
See the documentation: Desktop app that calls a web API on behalf of a signed-in user
Create an App Registration in Microsoft Entra ID #
For concepts and explanations, see the related documentation:
The detailed steps are as follows:
- Go to the Azure Portal.
- Go to Microsoft Entra ID.
- In the left navigation bar, find App registrations. You may need to expand Manage before you can see it. If you still cannot see it, an administrator needs to configure it in the Entra ID Portal.
- Select New registration.
- Give it a name, leave everything else at the defaults, and then register it.
- Find the app name you just created and open it.
- Record the Application (client) ID and Directory (tenant) ID. They will be used later in the code.
- Expand Manage on the left, and select Authentication.
- Select Add a platform, and choose Mobile and desktop applications.
- Add a custom URL under Redirect URIs: ms-appx-web://microsoft.aad.brokerplugin/
. Here, the client id is the Application (client) ID recorded earlier. - Save.
Application #
Create a new C# Console App and add the packages:
<PackageReference Include="Microsoft.Graph" Version="5.55.0" />
<PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.61.2" />Edit the .csproj file:
- Change
net8.0inTargetFrameworktonet8.0-windows. - Add
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>toPropertyGroup. This is used when Interop obtains the native handle of the Console Window.
Helper Class Interop
#
This class is used to get the Windows native handle of the current window, even for a Console App. Web Account Manager needs this.
using System.Runtime.InteropServices;
namespace OboClient
{
internal static partial class Interop
{
internal enum GetAncestorFlags
{
GetParent = 1,
GetRoot = 2,
GetRootOwner = 3
}
[LibraryImport("user32.dll")]
internal static partial IntPtr GetAncestor(IntPtr hwnd, GetAncestorFlags flags);
[LibraryImport("kernel32.dll")]
internal static partial IntPtr GetConsoleWindow();
internal static IntPtr GetConsoleOrTerminalWindow()
{
IntPtr consoleHandle = GetConsoleWindow();
IntPtr handle = GetAncestor(consoleHandle, GetAncestorFlags.GetRootOwner);
return handle;
}
}
}Configure the Client #
ClientId and TenantId are the two pieces of data recorded earlier when registering the Application. In principle, they should be read from a configuration file; here, they are hard-coded to simplify the example. If you are not using the Azure public cloud, but instead a national cloud or 21Vianet, you need to adjust the AzureCloudInstance.AzurePublic parameter.
var scopes = new[] { "User.Read" };
var applicationClientId = "<client id>";
var tenantId = "<tenant id>";
BrokerOptions options = new(BrokerOptions.OperatingSystems.Windows)
{
Title = "<arbitrary application name display to end user>"
};
IPublicClientApplication app = PublicClientApplicationBuilder
.Create(applicationClientId)
.WithAuthority(AzureCloudInstance.AzurePublic, tenantId)
.WithDefaultRedirectUri()
.WithParentActivityOrWindow(Interop.GetConsoleOrTerminalWindow)
.WithBroker(options)
.Build();If you are interested in the concept of Public Client, see Public client and confidential client applications.
Authenticate the User with the Client #
IEnumerable<IAccount> accounts = await app.GetAccountsAsync();
IAccount? existingAccount = accounts.FirstOrDefault();
try
{
if (existingAccount != null)
{
result = await app
.AcquireTokenSilent(scopes, existingAccount)
.ExecuteAsync();
}
else
{
result = await app
.AcquireTokenSilent(scopes, PublicClientApplication.OperatingSystemAccount)
.ExecuteAsync();
}
}
// Can't get a token silently, try with interactive
catch (MsalUiRequiredException)
{
result = await app
.AcquireTokenInteractive(scopes)
.ExecuteAsync();
}result contains an AccessToken that can be used to access protected APIs. However, note that this Token has an expiration time limit. After it expires, it needs to be acquired again. So, simply put, every time you need to use the Token, you need to go through the flow above. This flow appears to cache the Token locally and check expiration, though I have not tested it. See the documentation: Get a token from the token cache using MSAL.NET — Desktop, command-line, and mobile applications.
Use the AccessToken to Access a Protected API #
This uses the Microsoft Graph RESTful API as an example. Other applications are similar.
HttpClient httpClient = new()
{
DefaultRequestHeaders =
{
Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken)
}
};
GraphServiceClient graphServiceClient = new(httpClient);
var user = await graphServiceClient.Me.GetAsync();
Console.WriteLine($"Hello {user?.DisplayName}");References #
Using MSAL.NET with Web Account Manager (WAM) Note that, as of the time this post was written, the example in this document did not work (June 4, 2024), and feedback has already been submitted.