- Table of Contents
Watch the How-To video at https://thedotnetshow.com Look for episode 24.
In this episode, we are going to build a secure ASP.NET Core Web API
application, and deploy it to Azure
. Then, we are going to build a .NET Multi-platform App UI (.NET MAUI)
application, and I am going to show you how you can leverage theMicrosoft Authentication Library (MSAL)
for .NET
to get an access token, which we are going to use to call the Web API application.
The Microsoft Authentication Library (MSAL)
allows you to acquire tokens from the Microsoft identity platform
, authenticate users, and call secure web APIs not only from .NET, but from multiple platforms such as JavaScript, Java, Python, Android, and iOS.
You can find more information about MSAL
here Overview of the Microsoft Authentication Library (MSAL)
End results will look like this:
Let's get started.
The following prerequisites are needed for this demo.
Download the latest version of the .NET 6.0 SDK here.
For this demo, we are going to use the latest version of Visual Studio 2022.
In order to build ASP.NET Core Web API applications, the ASP.NET and web development
workload needs to be installed. In order to build .NET MAUI
applications, you also need the .NET Multi-platform App UI development
workload, so if you do not have them installed let's do that now.
Here's a screen shot of the Visual Studio Installer.
In the demo we will perform the following actions:
- Create a
ASP.NET Core Web API
application - Secure the
ASP.NET Core Web API
application - Create and configure an
Azure AD B2C
app registration to provide authentication workflows - Deploy the
ASP.NET Core Web API
application to Azure - Configure an
Azure AD B2C
Scope - Set API Permissions
- Create a
.NET MAUI
application - Configure our
.NET MAUI
application to authenticate users and get an access token - Call our secure
ASP.NET Core Web API
application from our.NET MAUI
application
As you can see there are many steps in this demo, so let's get to it.
In this demo, we are going to start by creating an ASP.NET Core Web API
application using the default template, which will not be secure. We are going to make it secure by using the Microsoft identity
platform.
We will create an Azure AD B2C
app registration to provide an authentication flow, and configure our ASP.NET Core Web API
application to use it.
And finally, we will deploy the ASP.NET Core Web API
application to Azure.
Name it SecureWebApi
☝️ Notice I unchecked
Use controllers (uncheck to use minimal APIs)
to create a minimal API, and checkedEnable OpenAPI support
to includeSwagger
.
You can learn more about minimal APIs here: Minimal APIs overview
Run the application to make sure the default templates is working.
Expand GET /weatherforecast
, click on Try it out
, then on Execute
.
We get data, so it is working, but it is not secure.
Let's make our ASP.NET Core Web API
app secure.
Open the Package Manager Console
:
And add the following NuGet
packages:
- Microsoft.AspNetCore.Authentication.JwtBearer
- Microsoft.Identity.Web
- Microsoft.Identity.Web.MicrosoftGraph
- Microsoft.Identity.Web.UI
By running the following commands:
install-package Microsoft.AspNetCore.Authentication.JwtBearer
install-package Microsoft.Identity.Web
install-package Microsoft.Identity.Web.MicrosoftGraph
install-package Microsoft.Identity.Web.UI
Your project file should look like this:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.8" />
<PackageReference Include="Microsoft.Identity.Web" Version="1.25.1" />
<PackageReference Include="Microsoft.Identity.Web.MicrosoftGraph" Version="1.25.1" />
<PackageReference Include="Microsoft.Identity.Web.UI" Version="1.25.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
</ItemGroup>
</Project>
Open the Program.cs file and add the following using statements:
using Microsoft.Identity.Web;
using Microsoft.AspNetCore.Authentication.JwtBearer;
Below var builder = WebApplication.CreateBuilder(args);
, add the following code:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi()
.AddMicrosoftGraph(builder.Configuration.GetSection("MicrosoftGraph"))
.AddInMemoryTokenCaches()
.AddDownstreamWebApi("DownstreamApi", builder.Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
builder.Services.AddAuthorization();
At the bottom, before app.Run();
add the following two lines:
app.UseAuthentication();
app.UseAuthorization();
And finally, in the app.MapGet("/weatherforecast"
code, add the following line after .WithName("GetWeatherForecast")
:
.RequireAuthorization()
The complete Program.cs file should look like this now:
using Microsoft.Identity.Web;
using Microsoft.AspNetCore.Authentication.JwtBearer;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi()
.AddMicrosoftGraph(builder.Configuration.GetSection("MicrosoftGraph"))
.AddInMemoryTokenCaches()
.AddDownstreamWebApi("DownstreamApi", builder.Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
builder.Services.AddAuthorization();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateTime.Now.AddDays(index),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.RequireAuthorization();
app.UseAuthentication();
app.UseAuthorization();
app.Run();
internal record WeatherForecast(DateTime Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
The ASP.NET Core Web API
app is secure now, but we need to add some IDs, and settings in the appsettings.json file.
Open the appsettings.json file, and add the following section above the "Logging"
section:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "REPLACE-WITH-YOUR-DOMAIN",
"TenantId": "REPLACE-WITH-YOUR-TENANT-ID",
"ClientId": "REPLACE-WITH-YOUR-CLIENT-ID",
"CallbackPath": "/signin-oidc",
"Scopes": "access_as_user",
"ClientSecret": "REPLACE-WITH-YOUR-CLIENT-SECRET",
"ClientCertificates": []
},
In order to get the settings required, we need to create an Azure AD B2C
app registration.
Go to https://portal.azure.com and sign-in.
📘 If you do not have an Azure account, you can sign-up for free at https://azure.microsoft.com/en-us/free/.
Search for Azure AD B2C
, and select it from the list:
Click on App registrations
.
Then click on Add new registration
.
Fill-out the following values and click Register
.
You will be presented with the Overview page, which has useful information such as Application ID, and Tenant ID. There are also some valuable links to quick start guides. Feel free to look around.
Copy the Application (client) ID
value, and use that to fill the "ClientId"
setting, and then copy the Directory (tenant) ID
value to fill the "TenantId"
setting in the appsettings.json file.
Set Instance to https://login.microsoftonline.com/
, and CallbackPath to "/signin-oidc"
.
For the "Domain"
, go to Branding & properties
, and copy the value under Publisher domain
.
Now, we need to create a client secret. Go to Certificates & secrets
, then click on + New client secret
, give it a description, set an expiration option, and click on the Add
button.
This will generate a client secret. Copy the value, paste it under the "ClientSecret"
setting in the appsettings.json file.
⚠️ The client secret will only display at this moment; if you move to another screen, you will not be able to retrieve the value anymore. You may choose to store this value safely at this point inAzure Key Vault
, or some other safe location. If you lose it, you will have to create a new client secret.
Set the "Scopes"
value to "access_as_user"
, which we are going to configure in Azure AD B2C
in the Configure Azure AD B2C Scope section, after we deploy our application to Azure.
Go to Authentication
, and change the following settings:
Under, Mobile and desktop applications
, check the Redirect URIs
https://login.microsoftonline.com/common/oauth2/nativeclient
, and msal13e64f59-38fb-4497-80d2-0a0f939564b3://auth
.
Then, under Advanced settings
click Yes
to allow Client public flows
, next to Enable the following mobile and desktop flows
.
Click on Save
.
The complete appsettings.json should look like this:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "*********.onmicrosoft.com",
"TenantId": "********-****-****-*****************",
"ClientId": "13e64f59-38fb-4497-80d2-0a0f939564b3",
"CallbackPath": "/signin-oidc",
"Scopes": "access_as_user",
"ClientSecret": "eoc8Q~9HZMliF5NY1***********************",
"ClientCertificates": []
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
☝️ Some values were replaced with asterisks for security reasons.
Build and run the application, expand GET /weatherforecast
again, click on Try it out
, then on Execute
.
This time, you should get an Unauthorized 401 HTTP code back.
Our Web API application is secure!
Right-click on the SecureWebApi.csproj file, and select Publish...
, then follow the following steps:
Make sure to select Skip this step
for the API Management option.
After deployment, the application will launch but you will get a HTTP Error 404.
Worry not, this is because for security reason, Swagger is only enabled running in Development mode.
If you want to enable it for testing purposes, you can comment-out the if (app.Environment.IsDevelopment())
condition in the Program.cs file.
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
If you append /weatherforecast
to the URL, you will see that indeed, the application is running, as the proper Unauthorized 401
shows up, as we are not passing an access token.
Now, we need to create our access_as_user
scope we specified in the appsettings.json file.
Go back to the Azure portal, select Expose an API
, then click on + Add a scope
, leave the default value for Application ID URI
, and click Save and continue
.
Fill-in the required values as shown below, and click on Add scope
:
- access_as_user
- Call the SecureWebApi on behalf of the user
- Allows the MsalAuthInMaui app to call the SecureWebApi on behalf of the user
- Call the SecureWebApi on your behalf
- Allows the MsalAuthInMaui app to call the SecureWebApi on your behalf
The access_as_user
scope has been added.
Finally, we need to set the API Permissions
, so our MAUI
application can call the Web API with an access token, after authentication.
In order to do that, click on API permissions
, then + Add a permission
. Select My APIs
, and click on MsalAuthInMaui
.
Then keep the Delegated permissions
selected, check the access_as_user
permission, and click on Add permissions
.
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
<!-- Required for API Level 30 to make sure we can detect browsers that support custom tabs -->
<!-- https://developers.google.com/web/updates/2020/07/custom-tabs-android-11#detecting_browsers_that_support_custom_tabs -->
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
<uses-sdk android:minSdkVersion="21" />
Finally, open MainActivity.cs, also under Platforms/Android, and replace the code with this:
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using Android.Runtime;
using Microsoft.Identity.Client;
using MsalAuthInMaui.MsalClient;
namespace MsalAuthInMaui
{
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
private const string AndroidRedirectURI = $"msauth://com.companyname.msalauthinmaui/snaHlgr4autPsfVDSBVaLpQXnqU=";
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
// Configure platform specific parameters
PlatformConfig.Instance.RedirectUri = AndroidRedirectURI;
PlatformConfig.Instance.ParentWindow = this;
}
/// <summary>
/// This is a callback to continue with the authentication
/// Info about redirect URI: https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-client-application-configuration#redirect-uri
/// </summary>
/// <param name="requestCode">request code </param>
/// <param name="resultCode">result code</param>
/// <param name="data">intent of the actvity</param>
protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
AuthenticationContinuationHelper.SetAuthenticationContinuationEventArgs(requestCode, resultCode, data);
}
}
}
Now, go back to Azure, and under Authentication click + Add a platform
.
Click on Web.
Type https://msalsecurewebapi.azurewebsites.net/signin-oidc
for the Redirect URI, check Access tokens, and ID tokens, and click on Configure.
The new Web platform will show up with your selections.
And that is all! Run the app, and you should be able to log in, see the access token retrieved, as well as log out.
📘 Notice, that you will get some prompts to accept Chrome conditions, turn on sync, multi-factor authentication if you have it setup, accept the app conditions (the ones we setup when we created the
access_as_user
scope,) etc.
Finally, the access token:
⚠️ If you get the following error, follow the following steps.
Long-click on the com.companyname... text, to highlight it.
Click on Share so the complete text shows up.
Type the text in notepad, in my case msauth://com.companyname.msalauthinmaui/snaHlgr4autPsfVDSBVaLpQXnqU=
, and go back to your Azure AD B2C app registration, under Authentication
, and add a new Redirect URI with that value.
Save, and give it another try. The app should display the access token.
For the end of this demo, and now that we have an access token, let's call our secure Web API.
Let's add a Get Weather Forecast
button.
Open MainPage.xaml, and add the button below the HorizontalStackLayout
containing the Login
and Logout
buttons:
<Button x:Name="GetWeatherForecastButton"
Text="Get Weather Forecast"
SemanticProperties.Hint="Get weather forecast data"
Clicked="OnGetWeatherForecastButtonClicked"
HorizontalOptions="Center"
IsEnabled="{Binding IsLoggedIn}"/>
Update the MainPage.xaml.cs file with the following code:
using Microsoft.Identity.Client;
using MsalAuthInMaui.MsalClient;
namespace MsalAuthInMaui
{
public partial class MainPage : ContentPage
{
private string _accessToken = string.Empty;
bool _isLoggedIn = false;
public bool IsLoggedIn
{
get => _isLoggedIn;
set
{
if (value == _isLoggedIn) return;
_isLoggedIn = value;
OnPropertyChanged(nameof(IsLoggedIn));
}
}
public MainPage()
{
BindingContext = this;
InitializeComponent();
_ = Login();
}
private async void OnLoginButtonClicked(object sender, EventArgs e)
{
await Login().ConfigureAwait(false);
}
private async Task Login()
{
try
{
// Attempt silent login, and obtain access token.
var result = await PCAWrapper.Instance.AcquireTokenSilentAsync(PCAWrapper.Scopes).ConfigureAwait(false);
IsLoggedIn = true;
// Set access token.
_accessToken = result.AccessToken;
// Display Access Token from AcquireTokenSilentAsync call.
await ShowOkMessage("Access Token from AcquireTokenSilentAsync call", _accessToken).ConfigureAwait(false);
}
// A MsalUiRequiredException will be thrown, if this is the first attempt to login, or after logging out.
catch (MsalUiRequiredException)
{
// Perform interactive login, and obtain access token.
var result = await PCAWrapper.Instance.AcquireTokenInteractiveAsync(PCAWrapper.Scopes).ConfigureAwait(false);
IsLoggedIn = true;
// Set access token.
_accessToken = result.AccessToken;
// Display Access Token from AcquireTokenInteractiveAsync call.
await ShowOkMessage("Access Token from AcquireTokenInteractiveAsync call", _accessToken).ConfigureAwait(false);
}
catch (Exception ex)
{
IsLoggedIn = false;
await ShowOkMessage("Exception in AcquireTokenSilentAsync", ex.Message).ConfigureAwait(false);
}
}
private async void OnLogoutButtonClicked(object sender, EventArgs e)
{
// Log out.
_ = await PCAWrapper.Instance.SignOutAsync().ContinueWith(async (t) =>
{
await ShowOkMessage("Signed Out", "Sign out complete.").ConfigureAwait(false);
IsLoggedIn = false;
_accessToken = string.Empty;
}).ConfigureAwait(false);
}
private async void OnGetWeatherForecastButtonClicked(object sender, EventArgs e)
{
// Call the Secure Web API to get the weatherforecast data.
var weatherForecastData = await CallSecureWebApi(_accessToken).ConfigureAwait(false);
// Show the data.
if (weatherForecastData != string.Empty)
await ShowOkMessage("WeatherForecast data", weatherForecastData).ConfigureAwait(false);
}
// Call the Secure Web API.
private static async Task<string> CallSecureWebApi(string accessToken)
{
if (accessToken == string.Empty)
return string.Empty;
try
{
// Get the weather forecast data from the Secure Web API.
var client = new HttpClient();
// Create the request.
var message = new HttpRequestMessage(HttpMethod.Get, "https://msalsecurewebapi.azurewebsites.net/weatherforecast");
// Add the Authorization Bearer header.
message.Headers.Add("Authorization", $"Bearer {accessToken}");
// Send the request.
var response = await client.SendAsync(message).ConfigureAwait(false);
// Get the response.
var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
// Return the response.
return responseString;
}
catch (Exception ex)
{
return ex.ToString();
}
}
private Task ShowOkMessage(string title, string message)
{
_ = Dispatcher.Dispatch(async () =>
{
await DisplayAlert(title, message, "OK").ConfigureAwait(false);
});
return Task.CompletedTask;
}
}
}
Let's run the app one more time, and if you are already logged in, it should log you in silently automatically as soon as you open the app, and the access token should be display.
Then click the Get Weather Forecast
button, and you should be able to call our Secure Web API, and the data should display:
In this episode, we built a secure ASP.NET Core Web API
application, and we deployed it to Azure
. Then, we built a .NET Multi-platform App UI (.NET MAUI)
application, and leveraged the Microsoft Authentication Library (MSAL)
for .NET
to get an access token, and used the token call the Web API application securely.
For more information about the Microsoft Authentication Library (MSAL)
, check out the links in the resources section below.
The complete code for this demo can be found in the link below.
Resource Title | Url |
---|---|
The .NET Show with Carl Franklin | https://thedotnetshow.com |
Download .NET | https://dotnet.microsoft.com/en-us/download |
Overview of the Microsoft Authentication Library (MSAL) | https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-overview |
Minimal APIs overview | https://docs.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-6.0 |
Microsoft Authentication Library (MSAL) for .NET, UWP, .NET Core, Xamarin Android and iOS | https://github.com/AzureAD/microsoft-authentication-library-for-dotnet |
Microsoft identity platform code samples | https://docs.microsoft.com/en-us/azure/active-directory/develop/sample-v2-code |