My other posts
Azure B2C Client Credentials with ASP.NET Core
Table of contents
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:
- Azure/Azure Active Directory/Azure B2C
- OAuth2/OpenID Connect
- 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:
- 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
.
- 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
.
- 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.