This article shows how to setup an ASP.NET Core application to authorize multiple access tokens from different Azure AD App registrations. Each endpoint can only accept a single AAD access token and it is important that the other access tokens do not work on the incorrect API. ASP.NET Core Schemes and Policies are used to force the delegated authorization.
Code: https://github.com/damienbod/AspNetCoreApiAuthMultiIdentityProvider/tree/main/AadMultiApis
Setup
Azure Active Directory is used to implement the identity provider and is responsible for creating the access tokens. Two Azure App registrations are used to implement the APIs. One Azure App registration is created for application clients and accepts tokens from multiple tenants which have the correct roles claims. A secret is required to get and access token for the App registration. Any tenant could used this endpoint so maybe extra authorization is required in case the clients shared the secrets, certificates or something like this. It would probably make sense to validate the tenant used to acquire the access token. The delegated Azure App registration is implemented as a single tenant and can only be used in the second API.

A test application implemented as a server rendered UI confidential client is used to send the API calls. The application can acquire both types of access tokens and send the tokens to the correct endpoints. (Or incorrect endpoints for testing)
Implement the API
The AddMicrosoftIdentityWebApi method from the Micorsoft.Identity.Web Nuget package is used to implement the AAD MSAL clients. Separate ASP.NET Core schemes are used for the different access tokens. The different tokens use different configurations and also use separate ASP.NET Core policies for forcing the authorization and the specific claims.
services.AddAuthentication(Consts.AAD_MULTI_SCHEME)
.AddMicrosoftIdentityWebApi(Configuration,
"AzureADMultiApi",
Consts.AAD_MULTI_SCHEME);
services.AddAuthentication(Consts.AAD_SINGLE_SCHEME)
.AddMicrosoftIdentityWebApi(Configuration,
"AzureADSingleApi",
Consts.AAD_SINGLE_SCHEME);
The Azure AD configurations are added to the app settings and are specific for each client.
"AzureADMultiApi": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "damienbodhotmail.onmicrosoft.com",
"TenantId": "7ff95b15-dc21-4ba6-bc92-824856578fc1",
"ClientId": "967925d5-87ea-46e6-b0eb-1223c001fd77"
},
"AzureADSingleApi": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "damienbodhotmail.onmicrosoft.com",
"TenantId": "7ff95b15-dc21-4ba6-bc92-824856578fc1",
"ClientId": "b2a09168-54e2-4bc4-af92-a710a64ef1fa"
},
The AddAuthorization is used to add the ASP.NET Core policies which are specific for the schemes and not global. The policies validate the required claims. If using a multi tenant App registration, you might need to validate the tenant used to acquire the access token as well. Access tokens for both clients must be acquired using a secret (or certificate would also be ok, type 2). This is important for multi-tenant App registrations if allowing any enterprise application to use this.
services.AddAuthorization(policies =>
{
policies.AddPolicy(Consts.MUTLI_AAD_POLICY, p =>
{
// application access token
// "roles": [
// "application-api-role"
// ],
// "azp": "967925d5-87ea-46e6-b0eb-1223c001fd77",
p.RequireClaim("azp", "967925d5-87ea-46e6-b0eb-1223c001fd77");
// client secret = 1, 2 if certificate is used
p.RequireClaim("azpacr", "1");
});
policies.AddPolicy(Consts.SINGLE_AAD_POLICY, p =>
{
// delegated access token => "scp": "access_as_user",
// "azp": "46d2f651-813a-4b5c-8a43-63abcb4f692c",
p.RequireClaim("azp", "46d2f651-813a-4b5c-8a43-63abcb4f692c");
// client secret = 1, 2 if certificate is used
p.RequireClaim("azpacr", "1");
});
});
An authorization filter is added to the AddControllers method which requires one of our defined schemes.
services.AddControllers(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(
Consts.AAD_MULTI_SCHEME,
Consts.AAD_SINGLE_SCHEME)
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
The middelware is setup like any ASP.NET Core application using authentication. You could add a RequireAuthorization method to the MapControllers method as well.
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
The Controllers used to expose the endpoints use both the scheme and the policy to validate the access token. It is important that correct access token only works for the correct endpoint. Controllers support authorization using attributes in a developer friendly way. You can develop secure endpoints really efficiently using this.
[Authorize(AuthenticationSchemes = Consts.AAD_MULTI_SCHEME,
Policy = Consts.MUTLI_AAD_POLICY)]
[Route("api/[controller]")]
public class MultiController : Controller
{
[HttpGet]
public IEnumerable<string> Get()
{
return new string[] {
"data 1 from the multi api",
"data 2 from multi api" };
}
}
Test Confidential Client
To test the API, I created an ASP.NET Core Razor page application which authenticates using a confidential OpenID Connect code flow client. The application acquires the different access tokens using services. The single tenant service gets a delegated access token to access the single tenant API.
using Microsoft.Identity.Web;
using System.Net.Http.Headers;
namespace RazorAzureAD;
public class SingleTenantApiService
{
private readonly IHttpClientFactory _clientFactory;
private readonly ITokenAcquisition _tokenAcquisition;
private readonly IConfiguration _configuration;
public SingleTenantApiService(IHttpClientFactory clientFactory,
ITokenAcquisition tokenAcquisition,
IConfiguration configuration)
{
_clientFactory = clientFactory;
_tokenAcquisition = tokenAcquisition;
_configuration = configuration;
}
public async Task<List<string>> GetApiDataAsync(bool testIncorrectMultiEndpoint = false)
{
var client = _clientFactory.CreateClient();
var scope = _configuration["AzureADSingleApi:ScopeForAccessToken"];
var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { scope });
client.BaseAddress = new Uri(_configuration["AzureADSingleApi:ApiBaseAddress"]);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
HttpResponseMessage response;
if (testIncorrectMultiEndpoint)
{
response = await client.GetAsync("api/Multi"); // must fail
}
else
{
response = await client.GetAsync("api/Single");
}
if (response.IsSuccessStatusCode)
{
var responseContent = await response.Content.ReadAsStringAsync();
var data = System.Text.Json.JsonSerializer.Deserialize<List<string>>(responseContent);
if(data != null)
return data;
}
throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
}
}
The MultiTenantApplicationApiService class is used to get an application access token using the OAuth client credentials flow. This requires a secret (or certificate) and no user is involved in this flow.
using Microsoft.Identity.Client;
using System.Net.Http.Headers;
namespace RazorAzureAD;
public class MultiTenantApplicationApiService
{
private readonly IHttpClientFactory _clientFactory;
private readonly IConfiguration _configuration;
public MultiTenantApplicationApiService(IHttpClientFactory clientFactory,
IConfiguration configuration)
{
_clientFactory = clientFactory;
_configuration = configuration;
}
public async Task<List<string>> GetApiDataAsync(bool testIncorrectMultiEndpoint = false)
{
// 1. Client client credentials client
var app = ConfidentialClientApplicationBuilder
.Create(_configuration["AzureADMultiApi:ClientId"])
.WithClientSecret(_configuration["AzureADMultiApi:ClientSecret"])
.WithAuthority(_configuration["AzureADMultiApi:Authority"])
.Build();
var scopes = new[] { _configuration["AzureADMultiApi:Scope"] }; // default scope
// 2. Get access token
var authResult = await app.AcquireTokenForClient(scopes)
.ExecuteAsync();
// 3. Use access token to access token
var client = _clientFactory.CreateClient();
client.BaseAddress = new Uri(_configuration["AzureADMultiApi:ApiBaseAddress"]);
client.DefaultRequestHeaders.Authorization
= new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
client.DefaultRequestHeaders.Accept
.Add(new MediaTypeWithQualityHeaderValue("application/json"));
HttpResponseMessage response;
if (testIncorrectMultiEndpoint)
{
response = await client.GetAsync("api/Single"); // must fail
}
else
{
response = await client.GetAsync("api/Multi");
}
if (response.IsSuccessStatusCode)
{
Console.WriteLine(await response.Content.ReadAsStringAsync());
}
if (response.IsSuccessStatusCode)
{
var responseContent = await response.Content.ReadAsStringAsync();
var data = System.Text.Json.JsonSerializer.Deserialize<List<string>>(responseContent);
if (data != null)
return data;
}
throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
}
}
When the application is run, the APIs can be tested and validated. If running this locally, you need to setup your own Azure App registrations and change the configuration.

Links
https://github.com/AzureAD/microsoft-identity-web
https://learn.microsoft.com/en-us/aspnet/core/introduction-to-aspnet-core?view=aspnetcore-6.0
[…] Use multiple Azure AD access tokens in an ASP.NET Core API (Damien Bowden) […]
[…] Use multiple Azure AD access tokens in an ASP.NET Core API – Damien Bowden […]