.NET 6, JWTs, Authentication and Authorization

Introduction

This post is about how to deal with OAuth2 and the usage of JWT (JSON Web Tokens) in a non-trivial system consisting of rich clients and web APIs built using Microsoft .NET 6.

Suppose we have the following architecture:

                            --> Web API 2
                           /
Client app --> Web API 1 --
                           \
                            --> Web API 3

So we have a “front-end” or “composition” API that is dependent on other APIs. The challenge here is that all (or some) of these APIs require a user token, so we must enable API 1 to call API 2 and API 3 on behalf of a user.

My first take on this was to create a token in the client app and let API 1 validate the token and then forward it to other APIs. But that doesn’t work as a general solution if different APIs require different scopes since you cannot mix scopes for different resources in the same token. (Well, you can, but the validation will fail.)

It actually turned out that, using the Microsoft Identity libraries, it is not that hard to have the front API create new tokens on behalf of users and cache them. I will describe how to develop and configure such a solution in this post. An important prerequisite is that Azure Active Directory is used for identity.

Back-end API

Application Registration

To protect the back-end API (API 2 and 3 above) we must first add an app registration in Azure Active Directory and configure it accordingly:

Authentication Section

Redirect URIs: (none)

Front-channel logout URL: (none)

Implicit grant and hybrid flows:

  • Access tokens: false
  • ID tokens: false

Allow public client flows: No

Expose an API Section

Scopes defined by this API

ScopesWho can consentAdmin consent display nameUser consent display nameState
api://{application/client ID}/access_as_userAdmins and usersAccess the API on behalf of a userAccess the API on your behalfEnabled

Authorized client applications: Add the applications that are authorized to access this API, in our case API 1.

Code

Codewise, the template code is what we need. In order to use that, we need the following NuGet packages:

Microsoft.AspNetCore.Authentication.JwtBearer
Microsoft.AspNetCore.Authentication.OpenIdConnect
Microsoft.Identity.Web

During start-up, we need to add some service registrations:

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

We also need to configure the request pipeline:

// Must be before app.MapControllers():
app.UseAuthentication();
app.UseAuthorization();

In appsettings.json, we have:

"AzureAd": {
  "Instance": "https://login.microsoftonline.com/",
  "TenantId": "(tenant GUID or domain, e.g. yourcompany.onmicrosoft.com)",
  "ClientId": "(client ID GUID of this API in Azure Active Directory app registration)"
},
"RequiredScope": "access_as_user"

With this, no authentication or authorization is actually required. We can put the [Authorize] attribute on the appropriate endpoints or controllers, but to reduce the risk of forgetting that somewhere, I prefer to have it by default. We can do that during startup:

services.AddAuthorization(options =>
    options.FallbackPolicy = new AuthorizationPolicyBuilder()
        .RequireScope(Configuration.GetValue<string>("RequiredScope"))
        .Build());
    // The above means that endpoints without [Authorize] attribute etc get this policy.

Then, use the [AllowAnonymous] attribute where authentication and authorization is not required.

Front-End API

This the tricky one. What is needed is fairly well documented once you find it: Scenario: A web API that calls web APIs.

Application Registration

We need to create one app registration for the front-end API:

Authentication Section

Redirect URIs: (none)

Front-channel logout URL: (none)

Implicit grant and hybrid flows:

  • Access tokens: false
  • ID tokens: false

Allow public client flows: No

Expose an API Section

Scopes defined by this API

ScopesWho can consentAdmin consent display nameUser consent display nameState
api://{application/client ID}/access_as_userAdmins and usersAccess the API on behalf of a userAccess the API on your behalfEnabled

Authorized client applications: Add the applications that are authorized to access this API, in our case the client app.

Code

We need to enable token acquisition to call downstream APIs (API 2 and 3) and we do that during service registration:

builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"))
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches();

For this to work, we must configure a client secret or certificate:

"AzureAd": {
  "Instance": "https://login.microsoftonline.com/",
  "TenantId": "(tenant GUID or domain, e.g. yourcompany.onmicrosoft.com)",
  "ClientId": "(client ID GUID of this API)"

  // To call an API use either ClientSecret or ClientCertificates:
  "ClientSecret": "[Copy the client secret added to the app from the Azure portal]",
  "ClientCertificates": [

    {
      "SourceType": "KeyVault",
      "KeyVaultUrl": "https://msidentitywebsamples.vault.azure.net",
      "KeyVaultCertificateName": "MicrosoftIdentitySamplesCert"
    }
  ]
}

In each endpoint that calls the downstream API, we can inject a Microsoft.Identity.Web.ITokenAcquisition instance and use that for getting a token:

var token = await tokenAcquisition.GetAccessTokenForUserAsync(new[] { $"api://{downstream API client ID}/{downstream API scope}" });
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
// or
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

There are even more clever techniques that might work for documented in A web API that calls web APIs: Call an API.

Client App

The client app in my example is a rich client (WPF).

Application Registration

Authentication Section

Mobile and desktop applications

Redirect URIs

(Notice that the Application (client) ID is part of the last two.)

Allow public client flows: No

Code

Here, we reference the Microsoft.Identity.Client NuGet library and set target framework to net6.0-windows10.0.17763.0 or higher.

In App.xml.cs, add a method that creates and returns an Microsoft.Identity.Client.IPublicClientApplication that can be registered in the dependency injection container:

private static Microsoft.Identity.Client.IPublicClientApplication CreateIdentityClient()
{

    var builder = Microsoft.Identity.Client.PublicClientApplicationBuilder.Create("{client ID}")
        .WithAuthority(Microsoft.Identity.Client.AzureCloudInstance.AzurePublic, "{tenant, e.g. mycompany.onmicrosoft.com)
        .WithDefaultRedirectUri()
        .WithBroker();
    return builder.Build();
}

To create a token, use something along the lines of:

AuthenticationResult authenticationResult;
var scopes = new[] { $"api://{API client ID}/{API scope}" };
var account = PublicClientApplication.OperatingSystemAccount;
try
{
    authenticationResult = await publicClientApplication
        .AcquireTokenSilent(scopes, account)
        .ExecuteAsync();
}
catch (MsalUiRequiredException ex)
{
    logger.LogError(ex, "Could not acquire token silently.");
    // A MsalUiRequiredException happened on AcquireTokenSilent. 
    // This indicates you need to call AcquireTokenInteractive to acquire a token
    authenticationResult = await publicClientApplication.AcquireTokenInteractive(api.Scopes)
        .WithAccount(account)
        .WithPrompt(Prompt.SelectAccount)
        .ExecuteAsync();
}