Upcoming book:

Modern .NET Development with Azure and DevOps

Azure B2C Client Credentials with ASP.NET Core

Introduction

Azure B2C is a pretty awesome Customer Identity and Access Management (CIAM) solution. However, as of May 2023, it still lacks support for flows that allow us to contact multiple applications from one, such as the On Behalf Of (OBO) flow, and even requesting multiple scopes in one token request.

In this post, I will provide a sample solution for contacting multiple APIs from one application, using the Client Credentials flow and ASP.NET Core.

Prerequisites

I will not go into the details, and expect you to at least be familiar with:

  1. Azure/Azure Active Directory/Azure B2C
  2. OAuth2/OpenID Connect
  3. ASP.NET Core

I also expect you to have already set up an Azure B2C tenant and have either a User Flow or a Custom Policy set up. I have a blog post on setting up Azure B2C with Azure Active Directory here if you want to take a look.

For the purposes of this post, I will assume that you have set up the following 3 App Registrations in your B2C tenant:

  1. Test.Command.Api

An App Registration with ID of 11111111-1111-1111-1111-111111111111. It exposes the scope https://contoso.onmicrosoft.com/11111111-1111-1111-1111-111111111111/access_as_user.

  1. Test.Query.Api

An App Registration with ID of 22222222-2222-2222-2222-222222222222. It exposes the scope https://contoso.onmicrosoft.com/22222222-2222-2222-2222-222222222222/access_as_user.

  1. Test.Client

An App Registration with ID of 33333333-3333-3333-3333-333333333333. It has a Client Secret generated, and has administrator access provided to both APIs listed above.

The rest of this post will go through the steps of setting up the ASP.NET Core application to use the Client Credentials flow to contact both APIs. If you prefer to skip to the code, you can find the GitHub repository here.

Configuring the client

The first step to add the support is to install the Microsoft.Identity.Client NuGet package. This package contains the classes we'll need to generate the tokens:

dotnet add package Microsoft.Identity.Client

After that package is installed, we have to add the configuration so that we can easily change client IDs and secrets without having to recompile the application. For this sample, let's add that to the appsettings.json file, as follows:

"AzureB2C": {
  "TenantId": "contoso.onmicrosoft.com",
  "ClientId": "33333333-3333-3333-3333-333333333333",
  "ClientSecret": "33333333-3333-3333-3333-333333333333",
  "Authority": "https://contoso.b2clogin.com/tfp/contoso.onmicrosoft.com/B2C_1A_SignInSignUp",
  "Apis": {
    "TestCommandApi": {
      "Url": "https://localhost:8001/",
      "Scope": "https://contoso.onmicrosoft.com/11111111-1111-1111-1111-111111111111/.default"
    },
    "TestQueryApi": {
      "Url": "https://localhost:8002/",
      "Scope": "https://contoso.onmicrosoft.com/22222222-2222-2222-2222-222222222222/.default"
    }
  }
}

You'll see that we need a number of properties that we can easily obtain from the Azure Portal. The Url properties have a random value for the sake of the demo. Also note that for the Authority property I used the standard name B2C_1A_SignInSignUp, make sure that you use the name of the User Flow or Custom Policy you have set up.

With that in place, we can now create the classes to hold that data:

public class ClientCredentialsConfiguration
{
    public string TenantId { get; set; }
    public string ClientId { get; set; }
    public string ClientSecret { get; set; }
    public string Authority { get; set; }

    public Dictionary<string, ClientCredentialsApiConfiguration> Apis { get; set; } = new(StringComparer.InvariantCultureIgnoreCase);
}

public class ClientCredentialsApiConfiguration
{
    public string Url { get; set; }
    public string Scope { get; set; }
}

Notice that I am constructing the Dictionary passing a StringComparer that ignores casing. This is to simplify the code if you want to use environment variables, for example, which tend to be in upper case.

And finally, we can create the client instance in the Startup class:

var builder = WebApplication.CreateBuilder(args);

var clientCredentialsConfiguration = builder.Configuration.GetSection("AzureB2C").Get<ClientCredentialsConfiguration>();
var confidentialClient = ConfidentialClientApplicationBuilder.Create(clientCredentialsConfiguration.ClientId)
    .WithClientSecret(clientCredentialsConfiguration.ClientSecret)
    .WithTenantId(clientCredentialsConfiguration.TenantId)
    .WithB2CAuthority(clientCredentialsConfiguration.Authority)
    .Build();

Note that Microsoft recommends using a single instance of the client for the entire application, so we'll be injecting it as a singleton later on.

Creating a service class to obtain the tokens

As I mentioned before, Azure B2C does not allow you to request multiple scopes in a single token. If you only need to contact a single API, you don't need the following.

Let's start by creating an Enum to hold our APIs:

public enum ClientApiType
{
    TestQueryApi,
    TestCommandApi
}

And now we can create the service class:

public class HttpRequestFactory : IHttpRequestFactory
{
    private readonly IConfidentialClientApplication _confidentialClientApplication;
    private readonly Dictionary<ClientApiType, string> _scopes;

    public HttpRequestFactory(IConfidentialClientApplication confidentialClientApplication, Dictionary<ClientApiType, string> scopes)
    {
        _confidentialClientApplication = confidentialClientApplication;
        _scopes = scopes;
    }

    public async Task<HttpRequestMessage> GetRequestMessageAsync(HttpMethod method, string url, ClientApiType clientApiType)
    {
        var requestMessage = new HttpRequestMessage(method, url);

        await AddAuthorizationHeaderAsync(requestMessage, clientApiType);

        return requestMessage;
    }

    private async Task AddAuthorizationHeaderAsync(HttpRequestMessage requestMessage, ClientApiType clientApiType)
    {
        var authenticationResult = await _confidentialClientApplication.AcquireTokenForClient(new[] { _scopes[clientApiType] }).ExecuteAsync();

        requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", authenticationResult.AccessToken);
    }
}

This allow us to hide the verbosity of obtaining the tokens and adding them to the HTTP request message from the rest of the application. I would typically add a few overloads of GetRequestMessageAsync as well, one with an object or generic parameter to add a JSON body, and others to get the response as a typed object.

We now need to inject the service in the Startup class:

var scopes = clientCredentialsConfiguration.Apis.ToDictionary(x => Enum.Parse<ClientApiType>(x.Key, ignoreCase: true), x => x.Value.Scope);

builder.Services.AddSingleton<IHttpRequestFactory>(sp => new HttpRequestFactory(confidentialClient, scopes));

Using the service

With the service class done and injected, we only need to create the typed HTTP clients to contact the APIs.

The following is a sample typed HTTP client for the TestQueryApi:

public class TestQueryApiClient : ITestQueryApiClient
{
    private readonly HttpClient _httpClient;
    private readonly IHttpRequestFactory _httpRequestFactory;

    public TestQueryApiClient(HttpClient httpClient, IHttpRequestFactory httpRequestFactory)
    {
        _httpClient = httpClient;
        _httpRequestFactory = httpRequestFactory;
    }

    public async Task<string> GetDataAsync()
    {
        var requestMessage = await _httpRequestFactory.GetRequestMessageAsync(HttpMethod.Get, "testquery", ClientApiType.TestQueryApi);

        var response = await _httpClient.SendAsync(requestMessage);

        return await response.Content.ReadAsStringAsync();
    }
}

For the sake of keeping the sample short, I am not adding any error handling here.

To finalize, we just need to inject the client in the Startup class:

builder.Services.AddHttpClient<ITestQueryApiClient, TestQueryApiClient>(client =>
{
    client.BaseAddress = new Uri(clientCredentialsConfiguration.Apis[ClientApiType.TestQueryApi.ToString()].Url);
});

Closing

With the B2C client configuration added, the service class to obtain the tokens and the typed HTTP clients, we can now call the APis from our application as needed.

I hope you've found this post useful. You can find a full sample as usual on my GitHub samples repository.

If you have any questions or comments, please leave them below.