ASP.NET Core: Securing the ASP.NET Core API using Azure AD and User Access Tokens

In the previous tutorial, we have seen the ASP.NET Core application authentication using Azure AD. In this tutorial we will implement the ASP.NET Core API Authentication using Azure AD and Access tokens. Most of you those who have worked on ASP.NET Core API security, must have used JSON Web Token (JWT) for authentication. If you are not aware about JWT then you can read this tutorial.  Generally, we use API to expose our application over publicly accessible endpoints to client application like browser based JavaScript e.g. Angular, React, then to mobile clients or even ASP.NET Core Web App, etc. In this case providing secure access to the API is recommended. If you choose to deploy the API on Microsoft Azure then, integrating Azure AD for API authentication will be the possible alternative. The figure 1 explains the approach of accessing and authenticating API using Azure AD




Figure 1: The access of Secure API

As explained in figure 1, the API and Client applications are registered as applications in Azure AD.  The behaviour is explained using numbering matched with the figure 

  1. The browser access the client App.
  2. The client app responds the Login page to the browser.
  3. Browser login ins using credentials
  4. These credentials are verified by the client app using Azure AD
  5. If the credentials are successful, the access token scope is issued to the client. The client uses this token to make call to the API
  6. API verifies the token using Azure AD 
  7. If the token is verified then the API generates response
  8. The response is sent back to the browser.   

Step 1: To implement the tutorial, I have created ASP.NET Core API project with Microsoft Identity Platform as shown in figure 2



Figure 2: The API Project creation        

The API project is created with default WeatherForecastController. I have used it as it it. In the project, expand dependencies, here you will see that the project is already added with Identity packages as shown in figure 3



Figure 3: The Identity Packages  

  • Microsoft.AspNetCore.Authentication.JwtBearer: This package is an ASP.NET Core Middleware which is used to enable the application to receive an OpenID connect bearer token. 
  • Microsoft.AspNetCore.Authentication.OpenIdConnect: This package is an ASP.NET Core middleware which enables an application to support the OpenID Connect authentication workflow to authenticate the client application.
  • Microsoft.Identity.Web: This package is used to enable the ASP.NET Core web apps and web APIs to use the Microsoft identity platform for security. This package is specifically used for web applications, which sign-in users, and protect web APIs, which optionally call downstream web APIs.
The API project has appsettings.json file. This file contains sample configuration for Azure AD. We will update these settings once we register the API as App in Azure AD.

To consume the API in client application, I have also created a ASP.NET Core Web App by selecting Authentication Type as Microsoft Identity Platform. In this project we will again have appsettings.json file with sample configuration for Azure AD.  In this client application, we will have same packages added as we have seen in API project.  

Since we need to register API project and Web App project in Azure AD as App, we need HTTP Urls for both projects. Right-Click on each project and select properties. In the properties page, from Debug tab copy the SSL HTTP address. 

In my case I have API HTTP address as https://localhost:44304/ and the HTTP SSL address for Web App is https://localhost:44370/

We will visit back to out Web App client application afterwards. First of all, we have to perform very important operations for the registration of API and Web App in Azure AD. 

Step 2: To implement these steps, the Azure Subscription is required. Please visit this link to register for free to access Microsoft Azure Services. Once the Azure subscription is completed, login to the portal.  

Step 3: In the Azure Active Directory, click on App Registrations and create a new App registration by clicking on the New Registration button as shown in figure 4





Figure 4: Thew new App Registration

This will show the Register an application page. In this page provide the name for the application as per your choice.  The Supported account types will be set to the default selection as Accounts in this organizational directory only (Default Directory only -  Single tenant).  This means that all users of the default directory will be able to access the application based on credentials. Click on the Register button. So the app is now registered.  Since, we will be using this App registration for API application, we want to use the API for user access tokens, to do that, click on  Expose an API link and Add a Scope as shown in figure 5



Figure 5: Expose as API and add scope
We need add a custom scope to protect data and functionality that is protected by API. The client application requesting to API  can request user consent to access data from API.  When we add a scope, it will create only delegated permission.  Once the Add a scope button is clicked, the new blade for Add a scope will be displayed as shown in figure 6



Figure 6: Add a scope     

We need to create Application ID URL before the required scope is added, to do that, click on Save and continue button. The new blade will be opened where we can define Scope name, Consent, etc. as shown in figure , and then click on Add scope button.



Figure 7: Add a Scope with details

Once the scope is added, we need to add permission so that the we can add claim in access token. In our case we will be using  email claim. After the scope is added, copy this scope value. We will need this value in application settings of the client application. The example value for the scope will be api://1111111-8a56-4b8b-b7c5-c8b16a960661/access_api_user. This scope is added for the API, so we must make sure that the API will use this scope to accept the request from the client. Modify the code in WeatherForecaseController class to use the scope and verify user as shown in the listing 1 (Yellow Marked code)
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Identity.Web.Resource;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Core_API.Controllers
{
	[Authorize]
	[ApiController]
	[Route("[controller]")]
	public class WeatherForecastController : ControllerBase
	{
		private static readonly string[] Summaries = new[]
		{
			"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", 
"Balmy", "Hot", "Sweltering", "Scorching"
		};

		private readonly ILogger<WeatherForecastController> _logger;

		// The Web API will only accept tokens 1) for users, and 2) 
// having the "access_api_user" scope for this API
		static readonly string[] scopeRequiredByApi = new string[] 
{ "access_api_user" };

		public WeatherForecastController
         (ILogger<WeatherForecastController> logger)
		{
			_logger = logger;
		}

		[HttpGet]
public IEnumerable<WeatherForecastController>Get()
{
  HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);

  var rng = new Random();
  return Enumerable.Range(1, 5).Select(index => new WeatherForecast
  {
    Date = DateTime.Now.AddDays(index),
    TemperatureC = rng.Next(-20, 55),  
    Summary = Summaries[rng.Next(Summaries.Length)]
  }).ToArray();
  }
 }
}





Listing 1: The WeatherforecaseController modification to access the scope

To add claim, click on API Permissions link and then click on Add Permission button, as show in figure 8




Figure 8: Adding Permissions
This step is important because client applications are authorized to call APIs when they are granted permissions by users or admins as a part of consent process. Once the Add a permission button is clicked, a new blade will be displayed as Request API permissions as shown in figure 9



Figure 9: Request API permissions  
On the blade shown in figure 9, click on Microsoft Graph, this will show new blade where select Delegated permissions.  The Delegated permissions will show list of permissions, where we will select email as shown in figure 10, and then click on Add permissions button

 




Figure 10: The List of permissions   

Once permissions are added, it is time for to configure token. As shown in the figure 11, click on Token Configuration and then click on Add optional claim. 




Figure 11: The Token Configuration  

Once the Add optional claim button is clicked, the claim blade will be shown where select Token type as Access and claim as email as show in figure 12



Figure 12: The Claim configuration

The email claim is added for the API Application. This completes the API Configuration. 

Step 2: Now, it is time for use to update the appsettings.json file of the API project by adding the ClientId and  TenantId. Click on the Overview of the API app and copy Application (client) ID and Directory (tenant) ID and update it in appsettings.json as shown in listing 2

"AzureAd": {
   "Instance": "https://login.microsoftonline.com/",
   "Domain": "mydomain.onmicrosoft.com",
   "TenantId": "11111111-8c95-4c81-97e5-d9400ddfcbbd",
   "ClientId": "22222222-8a56-4b8b-b7c5-c8b16a960661",
   "CallbackPath": "/signin-oidc"
 }

Listing 2 : The API app configuration settings

Step 3: Let's modify the ConfigureServices() method of the Startup class and add the code as shown in listing 3 for CORS policy in it so that browser clients will be able to access it
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddCors(options =>
{
  options.AddPolicy("cors",
	policy =>{ policy
				.AllowCredentials()
				.SetIsOriginAllowedToAllowWildcardSubdomains()
				.AllowAnyHeader()
				.AllowAnyMethod();
			});
});

services.AddMicrosoftIdentityWebApiAuthentication(Configuration);
services.AddControllers(options =>
{
  var policy = new AuthorizationPolicyBuilder()
	.RequireAuthenticatedUser()
	.Build();
   options.Filters.Add(new AuthorizeFilter(policy));
});



Listing 3: The CORS code 

The code in the listing 1 configures the CORS policy and add Authentication to API. The AddController() method is configured with the authorization policy for authenticated users. Modify the Configure() method of the Startup class by adding the CORS middleware. 

Step 4: Now let's add a new new App Registration in the Active Directory for the UI application. Set the application Name as core-web-client-app, select Supported Account App as Accounts in this organizational directory only (Default directory only- Single tenant) set the Redirect URI  as https://localhost:44370/signin-oidc as shown in figure 13



Figure 13: The Client app registration  

We will set the Logout Url of Authentication for the Web App and set  Front-channel logout URL as https://localhost:44370/signout-callback-oidc and check the Implicit grant and hybrid flows as ID tokens checkbox as shown in figure 14 and the click on Save button.



Figure 14: The Logout URL   


We need to set permission to access API from this client application. In the API Permission, click on Add a permission button, the Request API permission blade will be displayed, on the blade, select My APIs and select core-auth-api as shown in the figure 15



Figure 15: The Permission to Access API  

Once the core-auth-api is selected, access_api_user permission will be displayed, select this permission as shown in Figure 16, once this permission is selected, click on Add permission button



Figure 16: Selecting API Permission

The client application also needs the secret to access API.  We need to configure this secret in the application settings of the client application. To set the secret click on Certificates & secrets and then click on the New client secret button to create new secret. This will open the Add client secret blade when we ned to add Description and Expires as shown in figure 17, after adding these values click on Add button to generate the secret value


  
Figure 17: The Client Secret

Please copy the value of the secret, we will be requiring this vale in application settings of the client application.  

Step 5: Modify the appsettings.json  file of the ASP.NET Core Web App  as shown in Listing 4
 "AzureAd": {
        "Instance": "https://login.microsoftonline.com/",
        "ClientSecret": "a2Z_v__6erSKeD8z-6jYENYd5K5cXE-X89",
        "Domain": "sabnisthotmail.onmicrosoft.com",
        "TenantId": "e4c947ee-8c95-4c81-97e5-d9400ddfsevr",
        "ClientId": "eb28e5fb-cde4-45a3-9c7d-1111111",
        "CallbackPath": "/signin-oidc",
        "SignedOutCallbackPath ": "/signout-callback-oidc"
    },
    "ApiConfig": {
        "AccessToken": "api://929434f4-8a56-4b8b-b7c5-1111111111/access_api_user",
        "ApiBaseAddress": "https://localhost:44304/"

    },




Listing 4: The appsettings.json from the ASP.NET Core Web App


Step 6:  In the client application add a new folder and name this folder as Services. In this folder add a new class file and name this class file as ServiceClient.cs. In this file add the code as shown in listing 5 to define HTTP request to API service  by reading configurations from the appsettings.json


using Microsoft.Extensions.Configuration;
using Microsoft.Identity.Web;
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace Core_ClientApp.Services
{
	public class ServiceClient
    {
        private readonly IHttpClientFactory httpClient;
        private readonly ITokenAcquisition token;
        private readonly IConfiguration cfg;

        public ServiceClient(IHttpClientFactory client,
            ITokenAcquisition token,
            IConfiguration cfg)
        {
            this.httpClient = client;
            this.token = token;
            this.cfg = cfg;
        }

        public async Task<string> GetDataAsync()
        {
            try
            {
                // create HTTP Client
                var client = httpClient.CreateClient();
                // read api token
                var apiToken = cfg["ApiConfig:AccessToken"];
                // get access token for authenticated user
                var accessToken = await token.GetAccessTokenForUserAsync(new[] 
                 { apiToken });
                // read the service base adddress and define HTTP Request details
                client.BaseAddress = new Uri(cfg["ApiConfig:ApiBaseAddress"]);
                client.DefaultRequestHeaders.Authorization
        = new AuthenticationHeaderValue("Bearer", accessToken);
                client.DefaultRequestHeaders.Accept
         .Add(new MediaTypeWithQualityHeaderValue("application/json"));
                // make call to service
                var response = await client.GetAsync("weatherforecast");
                if (response.IsSuccessStatusCode)
                {
                    // generate response
                    var receivedData = await response.Content.ReadAsStringAsync();
                    return receivedData;
                }
                // if error occured then return the error
                throw new ApplicationException($"Status code: {response.StatusCode}, 
      Error Message: {response.ReasonPhrase}");
            }
            catch (Exception e)
            {
                throw new ApplicationException($"Error {e}");
            }
        }
    }
}

Listing 5: The call to API

Step 7: Modify the Startup class by adding the code as shown in listing 6 in CongureServices() method to register the ServiceClient class in dependency container and adding the Identity to the Web application



public void ConfigureServices(IServiceCollection services)
{
  services.AddTransient<ServiceClient>();
  services.AddHttpClient();

  services.AddOptions();
  // get the access token
  var token = Configuration.GetValue<string>("ApiConfig:AccessToken")?.Split(' ');
//configure the Microsoft Identity and enable token to access the API by passing 
// token
 services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
		.EnableTokenAcquisitionToCallDownstreamApi(token)
		.AddInMemoryTokenCaches();
// Add Authorization Policy to Razor views
 services.AddRazorPages().AddMvcOptions(options =>
 {
	var policy = new AuthorizationPolicyBuilder()
			.RequireAuthenticatedUser()
			.Build();
		options.Filters.Add(new AuthorizeFilter(policy));
			}).AddMicrosoftIdentityUI();
		}
Listing 6: The Identity configuration to Web Application

Step 8: In the client app, add a new Razor Page in the Pages folder. Name this as Client.cshtml.  Modify the code in Client.cshtml.cs as shown in listing 7. This code contains the ClientModel class which is constructor injected using ServiceClient class. The ClientModel class using its OnGetAsync() method invokes GetDataAsync() method from the ServiceClient class to make call to the API.  The ClientModel class is applied with AuthorizeForScope attribute. This is an action filter which is used to trigger an incremental consent using the Scope defined for the API as shown in figure 7. The API will accept the request and based on the user consent the request will be processed.   


[AuthorizeForScopes(Scopes = new string[] { "api://929434f4-8a56-4b8b-b7c5-c8b16a960661/access_api_user" })]
public class ClientModel : PageModel
{
  private readonly ServiceClient service;

  public string ResponseData { get; set; }
  public ClientModel(ServiceClient serv)
  {
    service = serv;
  }

  public async Task OnGetAsync()
  {
    ResponseData = await service.GetDataAsync();
  }
}
Listing 7: The ClientModel code

Add the code as shown in listing 8 markup in Client.cshtml to show data received from API


  
@page
@model Core_ClientApp.Pages.ClientModel
@{
}


<h1>Making Secure call to API Protected using  Azure AD</h1>

<h2>The response is</h2>
<hr/>

<div>
	<strong>
		@Model.ResponseData
	</strong>
</div>
  
Listing 8: The Client UI

Please make sure that you have users added in the Azure Active Directory. We will use these users to login using the Client Web Application.

To run the application, in visual studio configure both projects as multiple startup projects and run the application.  The client application will ask for the login using Microsoft Online Page as shown in figure 18



Figure 18: Login 
 
Enter the user credentials in Login page. If the credentials are valid then the user will login. Now in the address bar of the client application navigate to the Client page, the call to API will takes place and response from the API will be displayed as shown in the figure 19




Figure 19: The response

Thats it. The client app is successfully able to access API which is protected by Azure AD.

The code for this article is available on this link.

Conclusion: Azure AD authentication with Microsoft Identity Platform makes its easy to protect APIs easily with simple and easy to use configuration. So if APIs are hosted on Azure then integrating it with Azure AD for authentication will be a good practice.  

Comments

Popular posts from this blog

Understanding Token Based Authentication in ASP.NET Core 3.1 using JSON WEB TOKENS

Using Model Binders in ASP.NET Core

Using Session State in ASP.NET Core