Implement a Web APP and an ASP.NET Core Secure API using Azure AD which delegates to a second API

This article shows how an ASP.NET Core Web application can authenticate and access a downstream API using user access tokens and delegate to another API in Azure AD also using user access tokens. Microsoft.Identity.Web is used in all three applications to acquire the tokens for the Web API and the access tokens for the two APIs.

Code: https://github.com/damienbod/AzureADAuthRazorUiServiceApiCertificate

Setup and App registrations

The applications are setup as follows.

The applications implement the OAuth 2.0 On-Behalf-Of flow (OBO) and is made easy be using the Microsoft.Identity.Web Nuget packages.

The three applications require App registrations. The first Azure App registration exposes an API using the access_as_user scope. Nothing more is required here. This is the API at the end of the chain.

The API in the middle requires the API permission from the previously created App registration and exposes its own API, again the access_as_user scope. The Web API requires a secret to get the delegated access token and so a client secret is configured in this App registration. (Or a client certificate).

The API permissions is setup to use the scope from the other API.

And it exposes it’s own access_as_user scope.

The Web App requires a Web setup with a client secret (or client certificate) and the API permission from the middle API is added here.

Web Application which calls the first API

The Web APP with the UI interaction uses two Nuget packages, Microsoft.Identity.Web and Microsoft.Identity.Web.UI to implement the authentication and the authorization client for the API. The application is setup to acquire an access token using the EnableTokenAcquisitionToCallDownstreamApi method with the scope from the User API One.

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

	services.AddOptions();

	string[] initialScopes = Configuration.GetValue<string>(
		"UserApiOne:ScopeForAccessToken")?.Split(' ');

	services.AddMicrosoftIdentityWebAppAuthentication(Configuration)
		.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
		.AddInMemoryTokenCaches();

	services.AddRazorPages().AddMvcOptions(options =>
	{
		var policy = new AuthorizationPolicyBuilder()
			.RequireAuthenticatedUser()
			.Build();
		options.Filters.Add(new AuthorizeFilter(policy));
	}).AddMicrosoftIdentityUI();
}

The two nuget packages are added to the csproj file.

<PackageReference Include="Microsoft.Identity.Web" Version="1.2.0" />
<PackageReference Include="Microsoft.Identity.Web.UI" Version="1.2.0" />

The configuration is setup to use the data for the applicaitons defined in the APP registrations. The scope matches the scope from the User API One.

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "damienbodhotmail.onmicrosoft.com",
    "TenantId": "7ff95b15-dc21-4ba6-bc92-824856578fc1",
    "ClientId": "46d2f651-813a-4b5c-8a43-63abcb4f692c",
    "CallbackPath": "/signin-oidc",
    "SignedOutCallbackPath ": "/signout-callback-oidc"
  },
  "UserApiOne": {
    // UserApiOne
    "ScopeForAccessToken": "api://b2a09168-54e2-4bc4-af92-a710a64ef1fa/access_as_user",
    "ApiBaseAddress": "https://localhost:44395"
  },

}

The API client implementation uses the ITokenAcquisition to get the access token for the identity and access the API.

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

namespace WebAppUserApis
{
    public class UserApiOneService
    {
        private readonly IHttpClientFactory _clientFactory;
        private readonly ITokenAcquisition _tokenAcquisition;
        private readonly IConfiguration _configuration;

        public UserApiOneService(IHttpClientFactory clientFactory, 
            ITokenAcquisition tokenAcquisition, 
            IConfiguration configuration)
        {
            _clientFactory = clientFactory;
            _tokenAcquisition = tokenAcquisition;
            _configuration = configuration;
        }

        public async Task<JArray> GetApiDataAsync()
        {
            try
            {
                var client = _clientFactory.CreateClient();

                var scope = _configuration["UserApiOne:ScopeForAccessToken"];
                var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { scope });

                client.BaseAddress = new Uri(_configuration["UserApiOne:ApiBaseAddress"]);
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
       
                var response = await client.GetAsync("weatherforecast");
                if (response.IsSuccessStatusCode)
                {
                    var responseContent = await response.Content.ReadAsStringAsync();
                    var data = JArray.Parse(responseContent);

                    return data;
                }

                throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
            }
            catch (Exception e)
            {
                throw new ApplicationException($"Exception {e}");
            }
        }
    }
}

The Web App requires a user secret to access and authenticate. This could also be done using a client certificate. A client secret is used in this example and this must match the secret setup in the Web App registration.

{
  "AzureAd": {
    "ClientSecret": "--your secret for WebApp App Registration--" 
  }
}

API which calls the second API

The UI facing API uses a second API for separate data. The second API is also a user access token API and uses delegated tokens to access the data it protects. The API is not used from the UI application. When the access token from the the UI application is used to access the first API, it uses this to get another token to access the access token. This is all setup in the Startup class of the UI facing API. The AddMicrosoftIdentityWebApiAuthentication method is used to setup the API and it enables token acquisition for the second API. This is very simple when using Microsoft.Identity.Web.

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

	services.AddOptions();

	JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
	// IdentityModelEventSource.ShowPII = true;
	// JwtSecurityTokenHandler.DefaultMapInboundClaims = false;

	services.AddMicrosoftIdentityWebApiAuthentication(Configuration)
		.EnableTokenAcquisitionToCallDownstreamApi()
		.AddInMemoryTokenCaches();

	services.AddControllers(options =>
	{
		var policy = new AuthorizationPolicyBuilder()
			.RequireAuthenticatedUser()
		   // .RequireClaim("email")
			.Build();
		options.Filters.Add(new AuthorizeFilter(policy));
	});
}

The app.settings are configured to use the Azure AD API registration and the scope for the second application.

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "damienbodhotmail.onmicrosoft.com",
    "TenantId": "7ff95b15-dc21-4ba6-bc92-824856578fc1",
    "ClientId": "b2a09168-54e2-4bc4-af92-a710a64ef1fa"
  },
  "UserApiTwo": {
    "ScopeForAccessToken": "api://72286b8d-5010-4632-9cea-e69e565a5517/access_as_user",
    "ApiBaseAddress": "https://localhost:44396"
  },

}

The UserApiTwoService gets an access token for the API two scope and this is used to access the Web API controllers to return the data.

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

namespace UserApiOne
{
    public class UserApiTwoService
    {
        private readonly IHttpClientFactory _clientFactory;
        private readonly ITokenAcquisition _tokenAcquisition;
        private readonly IConfiguration _configuration;

        public UserApiTwoService(IHttpClientFactory clientFactory, 
            ITokenAcquisition tokenAcquisition, 
            IConfiguration configuration)
        {
            _clientFactory = clientFactory;
            _tokenAcquisition = tokenAcquisition;
            _configuration = configuration;
        }

        public async Task<JArray> GetApiDataAsync()
        {
            try
            {
                var client = _clientFactory.CreateClient();

                var scope = _configuration["UserApiTwo:ScopeForAccessToken"];
                var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { scope });

                client.BaseAddress = new Uri(_configuration["UserApiTwo:ApiBaseAddress"]);
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
       
                var response = await client.GetAsync("weatherforecast");
                if (response.IsSuccessStatusCode)
                {
                    var responseContent = await response.Content.ReadAsStringAsync();
                    var data = JArray.Parse(responseContent);

                    return data;
                }

                throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
            }
            catch (Exception e)
            {
                throw new ApplicationException($"Exception {e}");
            }
        }
    }
}

The get an access token for the second API, a client secret or a client certificate is required. The client second is used and this is defined in the first Web API. This can be added to your user secrets or an Azure Key Vault.

{
  "AzureAd": {
    "ClientSecret": "--your secret for UserApiOne  App Registration--" 
  }
}

Second API

The API two is configured in the Startup class to require Azure AD delegrated access tokens. The AddMicrosoftIdentityWebApiAuthentication method is used with no extra configuration. Scopes and roles should be validated as well. This can be done her, or in policies or using the helper methods from the Azure AD Microsoft.Identity.Web packages.

public void ConfigureServices(IServiceCollection services)
{
	JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
	// IdentityModelEventSource.ShowPII = true;
	// JwtSecurityTokenHandler.DefaultMapInboundClaims = false;

	services.AddMicrosoftIdentityWebApiAuthentication(Configuration);

	services.AddControllers(options =>
	{
		var policy = new AuthorizationPolicyBuilder()
			.RequireAuthenticatedUser()
		   // .RequireClaim("email") 
			.Build();
		options.Filters.Add(new AuthorizeFilter(policy));
	});
}

The Azure AD configuration in the app.settings are standard like in the documentation.

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "damienbodhotmail.onmicrosoft.com",
    "TenantId": "7ff95b15-dc21-4ba6-bc92-824856578fc1",
    "ClientId": "72286b8d-5010-4632-9cea-e69e565a5517"
  },

}

The VerifyUserHasAnyAcceptedScope can be used to validate a required scope for the delegated access token.

[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
	string[] scopeRequiredByApi = new string[] { "access_as_user" };
	HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);

	// ...
}

When the applications are run, the UI web application authenticates and gets an access token for Web API one. Web API one authorizes the access token and gets an access token for Web API two. Web API two authorizes the access token and returns the data. Web API one gets the data from Web API two and then returns data to the Web App. The full request chain works and uses user access tokens without making the second aPI availoble to the UI application.

Links

https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/howto-saml-token-encryption

Authentication and the Azure SDK

https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate

https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Client-credential-flows

https://tools.ietf.org/html/rfc7523

https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication

https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens

https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Client-Assertions

https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow

https://github.com/AzureAD/microsoft-identity-web/wiki/Using-certificates#describing-client-certificates-to-use-by-configuration

API Security with OAuth2 and OpenID Connect in Depth with Kevin Dockx, August 2020

https://www.scottbrady91.com/OAuth/Removing-Shared-Secrets-for-OAuth-Client-Authentication

https://github.com/KevinDockx/ApiSecurityInDepth

https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki

https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-protected-web-api-verification-scope-app-roles

2 comments

  1. […] Implement a Web APP and an ASP.NET Core Secure API using Azure AD which delegates to second API (Damien Bowden) […]

  2. […] Implement a Web APP and an ASP.NET Core Secure API using Azure AD which delegates to a second API – Damien Bowden […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: