Securing .NET Core Web App calling Web API using MSAL and Azure AD

Securing .NET Core Web App calling Web API using MSAL and Azure AD

Real world applications are composed of multiple components. Each component may itself be an application. For example, a web app may call another application which exposes only Web APIs.

In this article we will have look at this particular scenario and see how can we secure the calls using Azure AD.

Scenario

Let’s assume we have a web application (either MVC or Razors view). This application is calling web APIs. These web APIs are also ours and not third party.

Also, in this case, there are two deployed applications, one hosts only web app, the other hosts web APIs. Let’s see how can we secure Web API, Web App and how to get token in Web App to call Web API.

Securing Web API

We will have to register the API application in Azure AD. Then we will have to configure our web API application to use appropriate Client ID and Tenant ID. Also, we will have to configure middleware to allow only authenticated / authorized users to call the APIs.

Refer my previous blog article about securing web APIs for detailed steps.

Secure Web API using Azure AD

Securing Web App

We will have to create a web app (either MVC or Razors view) using Visual Studio. The high level steps are almost the same.

Refer my previous blog article about securing web app for detailed steps.

Secure Web App using Azure AD

If you have followed all steps correctly till now, then you have two applications which are independently secured. The web app is not yet able to call Web API app. If you try to call it through web app, the call will return HTTP 401 unauthorized.

Azure AD API Permissions

In Web App Registrations, we will need to add the API permissions which are required to call the API we have created.

Login to Azure Portal and navigate to Azure Active Directory from left navigation menu. Select App Registrations under Azure AD. We will be able to see SampleWebApp and SampleWebApi both in the list view.

Click on SampleWebApp entry and select API permissions from the left navigation. Then if we click on “Add a permission” button, a new panel on the left side will be shown.

On the left side panel, we can select any API that we want to call from web app. As we want to call our own API (i.e. SampleWebApi), go to “My APIs” tab. This tab will show all API app registrations from the same Azure AD. Select SampleWebApi from the list.

Next, you will be asked to select the permissions. There are two types of permissions as shown in below snapshot

  • Delegated permissions, select this when you want to call the API as the signed-in user. This is applicable for applications which allow end-users to sign in and perform some actions. This option means the API would be called on behalf of logged in user. This option is enabled and by default selected.
  • Application Permissions, this is disabled by default for us, because SampleWebApp is not a background service. This should be selected only if the application is running as background service / daemon.

Select Delegated Permissions option. Anyway Application Permissions option would be disabled as our application is web application.

Also select the API permission access_as_user and click on Add permissions button.

After adding it, the API permissions list would look similar to below snapshot.

Azure AD Client Secret

When any application calls secured Web API, that application becomes confidential client application.

Confidential Client App

A confidential client application can be

  • A Web App which intends to call secured Web API
  • A Web API which intends to call secured Web API
  • A desktop application (WinForms or WPF)
  • A background process / service / daemon

Azure AD also requires the application to prove it’s identity to ensure that only correct confidential client application is requesting the tokens.

The Confidential client application can prove it’s identity by two ways, either by presenting the certificate, or by presenting the client secret.

Certificates are issued by certificate authority. And buying certificates involve cost. So, for this demo, we will use Client Secret.

Client Secret is a string value which can either be generated using Azure Portal user interface, or we can also use Azure CLI or PowerShell to generate it.

Generate Client Secret

Let’s see how to generate client Secret using Azure Portal.

Navigate to SampleWebApp registration in Azure AD, select Certificates and Secrets and it will show a panel like below. Then click on New Client Secret.

A new popup will be shown. We need to enter Description and Expiry duration.

Expiry duration of 1 year is sufficiently long. Also, in my opinion, it is better to keep the secret changing, so that guessing it becomes more and more difficult.

After entering these two inputs, click on Add button.

Then it shows a client secret entry under Client Secrets section as shown below snapshot. Make sure to copy and keep it in a safe place. You won’t be able to access it again after you leave the portal.

Web App AppSettings.JSON

Let’s add the copied client secret value in AppSettings.JSON as shown in below snippet. This ClientSecret will be automatically picked up by middleware. No additional code is required to read client secret value.

{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "da41245a5-11b3-996c-00a8-4d99re19f292",
"ClientId": "5a886671-26ae-4844-84d8-19cb3e1cfbb5",
"CallbackPath": "/signin-oidc",
"SignedOutCallbackPath ": "/signout-callback-oidc",
"ClientSecret": "6.91Wh3e–aeStjS-834SyEL6.Yc4=b="
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
view raw appsettings.json5 hosted with ❤ by GitHub

Web App Startup.cs

There are two important modifications required in startup.cs

  • AddWebAppCallsProtectedWebApi, this will enable the web app to call the protected web API. This middleware requires configuration and scope collection. The scope collection should include the full scope including API permission. (e.g. api://{{guid}}/access_as_user)
  • AddInMemoryTokenCaches, this will enable InMemory token cache serializer.
  • AddDistributedMemoryCache, this will enable distributed memory cache implementation. This is helpful if you want to retain token caches even after your web app is restarted. On your development machine, even if you do not add this middleware, the solution will work.

In this article, we are going to use AddDistributedMemoryCache middleware for InMemory implementation. But we also have option to use Redis Cache or SQL table as the token cache.

Refer this sample for SQL cache configuration in startup.

The Startup.cs in the web application should be similar to below snapshot.

public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
// Handling SameSite cookie according to https://docs.microsoft.com/en-us/aspnet/core/security/samesite?view=aspnetcore-3.1
options.HandleSameSiteCookieCompatibility();
});
services.AddSignIn(Configuration, "AzureAd");
// Below code is required to call Web API from Web App
services.AddWebAppCallsProtectedWebApi(Configuration,
initialScopes: new string[] { "user.read", "api://5e971e5c-a661-4d82-ba97-935480492129/access_as_user" })
.AddInMemoryTokenCaches();
// Then, choose Token Cache implementation, InMemory or RedisCache or SQL Table etc.
// For instance, the distributed in-memory cache (not cleared when you stop the app):
services.AddDistributedMemoryCache();
services.AddRazorPages().AddMvcOptions(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
services.AddControllersWithViews();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
view raw Startup.cs hosted with ❤ by GitHub

Calling Web API

For keeping the things simple, let’s try to add the API call from the HomeController‘s Index option.

On high level, what we are going to do in this action is

  • Get the token for the required scopes
  • Set bearer token and in HTTP request
  • Call the API using HttpClient object
  • Get Response and Render on UI

Token Acquisition

Microsoft.Identity.Web maintains one token cache per user account for security and performance reasons.

When we registered middleware AddWebAppCallsProtectedWebApi, it also registered dependency implementation for ITokenAcquisition interface. This TokenAcquisition implementation has method to get the token on behalf of the user.

To make use of this class, the TokenAcquisition should be injected in HomeController and method GetAccessTokenForUserAsync should be called on its instance.

Below is completed code for HomeController.

public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly ITokenAcquisition _tokenAcquisition;
public HomeController(ILogger<HomeController> logger, ITokenAcquisition tokenAcquisition)
{
_logger = logger;
_tokenAcquisition = tokenAcquisition;
}
public async Task<IActionResult> Index()
{
//// Acquire the access token.
string[] scopes = new string[] { "api://5e971e5c-a661-4d82-ba97-935480492129/access_as_user" };
string accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes);
// Use the access token to call a protected web API.
HttpClient client = new HttpClient();
string url = "https://localhost:44389/weatherforecast";
// Set Bearer Token
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
// Call API
string json = await client.GetStringAsync(url);
var apiOutput = JsonConvert.DeserializeObject<List<WeatherForecast>>(json);
// Send Data To View
return View(apiOutput);
}
public IActionResult Privacy()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
view raw HomeController.cs hosted with ❤ by GitHub

Add below code to show result from weather forecast API in Index.cshtml:

@{
ViewData["Title"] = "Home Page";
}
@using WebAPI;
@model List<WeatherForecast>
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>
Learn about
<a href="https://docs.microsoft.com/aspnet/core">
building Web apps with ASP.NET Core
</a>.
</p>
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temperature</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@if (Model != null)
{
@foreach (var item in Model)
{
<tr>
<td>@item.Date</td>
<td>@item.TemperatureC</td>
<td>@item.Summary</td>
</tr>
}
}
</tbody>
</table>
</div>
view raw Index.cshtml hosted with ❤ by GitHub

Run and Verify

Now, all the configurations are done and we have added web API call.

Let’s run the Web API project and Web App project. The Web App will get redirected to Azure AD and will show login screen. After successful login, The MVC application will try to render Home controller’s Index action. This action will call Web API and will render the data on UI.

If you see page like below snapshot, that means our setup is running as expected.

GitHub Repository

If you want, you can download the sample solution from my GitHub repository. The solution has 3 projects, web api, razors view web app, mvc web app.

After downloading, you will just have to register the apps in Azure AD and update the appsettings.json files with appropriate identifiers and client secrets.

I hope you enjoyed this article. Let me know your thoughts.

Leave a Reply

This Post Has 7 Comments

  1. Ajmal Yazdani

    Hi Manoj,

    Thanks much for the article and great read. As I can see we need to put ClientSecret into appsettings file, what if I want to read it from azure KeyValut. Is there any API?

    {
    “AzureAd”: {
    “Instance”: “https://login.microsoftonline.com/”,
    “TenantId”: “da41245a5-11b3-996c-00a8-4d99re19f292”,
    “ClientId”: “ffff3e7b-20cb-42cb-8a23-2d6c8003ee3a”,
    “ClientSecret”: “P7.DwiexOwE4vw9~e_qBt33uqm7R~ia_V8”
    },

  2. Ajmal Yazdani

    Thanks Manoj. What does mean “load all configurations while setting up IHostBuilder and then pass them in startup”? how I can do this?

      1. Ajmal Yazdani

        Thanks a lot Manoj, appreciate.

  3. raj

    Hi Manoj, is it standard behaviour that when a user closes their browser, the in memory cache will be cleared and they would need to re-authenticate? If so, what is the best place to store the cache so we can avoid users having to re-sign in?

    1. Manoj Choudhari

      Hi Raj, I am not sure if you are referring to Middleware `AddInMemoryTokenCaches`. If you are, then it is applicable only for server side caching. It has nothing to do with browser closing. If you have followed both the parts of tutorial, then you might have observed that the browser receives a cookie after user is authenticated in web application. This cookie is used to acquire token to call the API and that token is kept in InMemory cache. So, if your session needs to call the same API again, the API token would be found in the memory cache on the web server side.

      If you want to keep the cookie in browser, then you might have to implement persistent cookie, a cookie which does not get cleared if user closes / reopens the browser.

      Hope this clarifies.