Skip to content

Latest commit

 

History

History
1037 lines (757 loc) · 41.8 KB

README.md

File metadata and controls

1037 lines (757 loc) · 41.8 KB

Table of Contents

Introduction

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:

MsalAuthInMaui app

Let's get started.

Prerequisites

The following prerequisites are needed for this demo.

.NET 6.0

Download the latest version of the .NET 6.0 SDK here.

Visual Studio 2022

For this demo, we are going to use the latest version of Visual Studio 2022.

Required Workloads

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.

ASP.NET and web development

Demo

In the demo we will perform the following actions:

  1. Create a ASP.NET Core Web API application
  2. Secure the ASP.NET Core Web API application
  3. Create and configure an Azure AD B2C app registration to provide authentication workflows
  4. Deploy the ASP.NET Core Web API application to Azure
  5. Configure an Azure AD B2C Scope
  6. Set API Permissions
  7. Create a .NET MAUI application
  8. Configure our .NET MAUI application to authenticate users and get an access token
  9. 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.

Secure an ASP.NET Core Web API Application

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.

Create an ASP.NET Core Web API Application

Create a new ASP.NET Core Web API project

Name it SecureWebApi

Configure your new project

Additional information

☝️ Notice I unchecked Use controllers (uncheck to use minimal APIs) to create a minimal API, and checked Enable OpenAPI support to include Swagger.

You can learn more about minimal APIs here: Minimal APIs overview

Run the application to make sure the default templates is working.

Swagger

Expand GET /weatherforecast, click on Try it out, then on Execute.

WeatherForecast

We get data, so it is working, but it is not secure.

Secure the ASP.NET Core Web API

Let's make our ASP.NET Core Web API app secure.

Open the Package Manager Console:

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": []
  },

Azure AD B2C App Registration

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:

Azure AD B2C

Click on App registrations.

App registrations

Then click on Add new registration.

Add new registration

Fill-out the following values and click Register.

pApp registration settings

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.

Overview

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.

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.

Certificates & secrets

This will generate a client secret. Copy the value, paste it under the "ClientSecret" setting in the appsettings.json file.

Client Secret

⚠️ 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 in Azure 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.

Enable the following mobile and desktop flows

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.

Secure Web API

Our Web API application is secure!

Deploy ASP.NET Core Web API to Azure

Right-click on the SecureWebApi.csproj file, and select Publish..., then follow the following steps:

Publish...

Azure

Azure App Service (Windows)

Create New

Create New App Service (Windows)

App Service

API Management

Make sure to select Skip this step for the API Management option.

Finish

Publish

Publish succeeded

After deployment, the application will launch but you will get a HTTP Error 404.

Web API in Azure

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.

Unauthorized 401

Configure Azure AD B2C Scope

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.

Add a scope

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

Add scope values

The access_as_user scope has been added.

Scope

Set API Permissions

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.

Add a permission

Then keep the Delegated permissions selected, check the access_as_user permission, and click on Add permissions.

Delegated permission

API permission

	<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.

Add a platform

Click on Web.

Web

Type https://msalsecurewebapi.azurewebsites.net/signin-oidc for the Redirect URI, check Access tokens, and ID tokens, and click on Configure.

image-20220816013738348

The new Web platform will show up with your selections.

Web platform

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.

Screenshot Screenshot Screenshot
Screenshot Screenshot Screenshot

Finally, the access token:

Screenshot

⚠️ If you get the following error, follow the following steps.

Screenshot

Long-click on the com.companyname... text, to highlight it.

Long-click

Click on Share so the complete text shows up.

Share

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.

Redirect URI

Save, and give it another try. The app should display the access token.

Access Token

Call our secure ASP.NET Core Web API application from our .NET MAUI application

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.

MsalAuthInMaui app

Then click the Get Weather Forecast button, and you should be able to call our Secure Web API, and the data should display:

WeatherForecast Data

Summary

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.

Complete Code

The complete code for this demo can be found in the link below.

Resources

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