In the previous post, I showed how you could set up Azure B2C in a Blazor application. I will continue this series by showing three ways you can communicate with your backend API.
JSON Clients
I have a JSON base client that both my un/authenticated clients inherit from. This base class provides a common wrapper for an HTTP client calls and has methods for de/serializing a generic.
//snip
public enum ClientConfiguration
{
WebApi,
Unauthenticated
}
protected readonly HttpClient HttpClient;
// ReSharper disable once MemberCanBeProtected.Global
public JsonClient(IHttpClientFactory factory,
ClientConfiguration configuration = ClientConfiguration.WebApi)
{
HttpClient = factory.CreateClient(configuration.ToString());
HttpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
}
public async Task<string> GetAsync(string url)
{
var responseMessage = await HttpClient.GetAsync(url);
return await VerifySuccessAsync(responseMessage);
}
protected async Task<T?> GetAsync<T>(string url)
{
return JsonSerializer.Deserialize<T>(await GetAsync(url));
}
In the startup class for each project, I call into a Shared Configuration Builder class . Here, I can register services that are, as you might have guessed, shared, between each of the front end projects. Notice that the HTTP Client named WebApi
also uses the CustomAuthorizationMessageHandler
. This message handler will include the necessary scopes for authorizing our client calls to the API.
public static class SharedConfigurationBuilder
{
public static void Service(IServiceCollection builderServices, IConfiguration configuration)
{
var downstreamApiConfiguration = configuration.GetSection("DownstreamApi")
.Get<DownstreamApiConfiguration>()!;
builderServices.AddScoped<CustomAuthorizationMessageHandler>();
builderServices.AddHttpClient(JsonClient.ClientConfiguration.WebApi.ToString(),
client => client.BaseAddress = new Uri(downstreamApiConfiguration.BaseUrl!))
.AddHttpMessageHandler<CustomAuthorizationMessageHandler>();
builderServices.AddHttpClient(JsonClient.ClientConfiguration.Unauthenticated.ToString(),
client => client.BaseAddress = new Uri(downstreamApiConfiguration.BaseUrl!));
builderServices.AddScoped<IAuthenticatedClient, AuthenticatedClient>();
builderServices.AddScoped<IUnauthenticatedClient, UnauthenticatedClient>();
}
}
public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
{
public CustomAuthorizationMessageHandler(
IConfiguration configuration,
IAccessTokenProvider provider,
NavigationManager navigationManager)
: base(provider, navigationManager)
{
var downstreamApi = configuration.GetRequiredSection("DownstreamApi")
.Get<DownstreamApiConfiguration>()!;
ConfigureHandler(
authorizedUrls: new[] { downstreamApi.BaseUrl },
scopes: downstreamApi.Scopes
);
}
}
Unauthenticated Client
public class UnauthenticatedClient : JsonClient, IUnauthenticatedClient
{
public UnauthenticatedClient(IHttpClientFactory clientFactory) :
base(clientFactory, ClientConfiguration.Unauthenticated)
{
HttpClient.BaseAddress = new Uri(HttpClient.BaseAddress!, "/api/WeatherForecast/");
}
public async Task<List<WeatherForecast>> GetAsync()
{
var result = await GetAsync<List<WeatherForecast>>("");
return result ?? new List<WeatherForecast>();
}
}
Sometimes you might need to make an unauthenticated API call. This might be for getting some configuration values or other data where the user is irrelevant.
The API project contains a WeatherForecastController
. This is a classic MVC/Web API controller. It’s not very interesting except that it allows anonymous calls, and has a single endpoint that randomly gets some fake weather.
I can call this API with the UnauthenticatedClient
which is found under the data folder in the shared project. This client uses a named HTTP client and sets the base address for all the calls we might have against this particular endpoint. For the name, I decided to use an enum even though the HTTP Client Factory takes a string. I found it easier to reference and restrict my base class this way, but is certainly optional. Because my base class allows me to simply provide a generic and deserialize that for me, I don’t have to complicate the child class with that code.
The index page of the shared project injects this client, calls the get method from OnInitializedAsync
and renders some Font Awesome icons based on the summaries.
private List<string> _weatherSummary = new();
[Inject] private IUnauthenticatedClient UnauthenticatedClient { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
await GetUnauthenticatedWeatherAsync();
}
private async Task GetUnauthenticatedWeatherAsync()
{
var weather = await UnauthenticatedClient.GetAsync();
_weatherSummary = weather.Select(x => x.Summary).ToList();
}
//Blazor
/*
@foreach (var weather in _weatherSummary)
{
<span class="me-3">
<i class="fa-solid fa-@weather"></i>
</span>
}
*/
Authenticated Client
The authenticated client code is much different at this point. I have it pointed to a relative URL of a Color Controller. This controller uses the new minimal APIs. This project has all the controllers protected by both a policy that checks a registered user claim and requires an authenticated user by default.
When a user logs in, and needs to access one of the authenticated endpoints, the customer message handler I mentioned earlier kicks in and requests a token with the downstream API scopes.
Back on the index page, I first get the authentication state, so I can check whether the user is signed in or not. If I have a user, then I can make a call using the authenticated client. I’m simply setting the colored block to a random value from the API. It’s always a good idea to do your auth checks on both the client and the server side.
private bool _isSignedIn;
private List<string> _weatherSummary = new();
[Inject] private AuthenticationStateProvider AuthenticationStateProvider { get; set; } = default!;
[Inject] private IAuthenticatedClient AuthenticatedClient { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
var authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
_isSignedIn = authenticationState.User.Identity is { IsAuthenticated: true };
if (_isSignedIn) await GetAuthenticatedColorAsync();
}
private async Task GetAuthenticatedColorAsync()
{
_color = await AuthenticatedClient.GetAsync();
}
SignalR
From the API project, the hubs can simply have the Authorize attribute added to it. Obviously, you need to register the bits in the main program file.
On the client side is where this is a bit messy. This is another place where the downstream token provider is helpful.
In the shared project under the Chat folder, I have a MainChat
file .
private HubConnection? _hubConnection;
private readonly List<MessageBody> _messages = new();
private DownstreamApiConfiguration _downstreamApiConfiguration = default!;
private bool _isSignedIn;
[Inject] private AuthenticationStateProvider AuthenticationStateProvider { get; set; } = default!;
[Inject] private IConfiguration Configuration { get; set; } = default!;
[Inject] private IDownstreamTokenProvider DownstreamTokenProvider { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
var authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
_isSignedIn = authenticationState.User.Identity is { IsAuthenticated: true };
_downstreamApiConfiguration = Configuration.GetRequiredSection("DownstreamApi")
.Get<DownstreamApiConfiguration>()!;
await ConnectToChatHubAsync(authenticationState);
}
private async Task ConnectToChatHubAsync(AuthenticationState authenticationState)
{
if (!_isSignedIn) return;
_userName = authenticationState.User.Identity?.Name;
_hubConnection = new HubConnectionBuilder()
.WithUrl($"{_downstreamApiConfiguration.BaseUrl}ChatHub",
options => { options.AccessTokenProvider = DownstreamTokenProvider.TryGetToken; })
.WithAutomaticReconnect()
.Build();
_hubConnection.On<string, string, string>("ReceiveMessage", (user, message, timestamp) =>
{
_messages.Add(new MessageBody
{
...
});
InvokeAsync(StateHasChanged);
});
await _hubConnection.StartAsync();
}
private async Task Send()
{
if (IsConnected && !string.IsNullOrEmpty(_messageInput))
{
try
{
await _hubConnection!.SendAsync("SendMessage", _userName, _messageInput);
_messageInput = "";
}
catch (Exception ex)
{
await Js.TableAsync(ex);
}
}
}
public bool IsConnected =>
_hubConnection?.State == HubConnectionState.Connected;
In the OnInitializedAsync
method, I’m once again getting the authenticated state and setting _isSignedIn
. I’m also getting the downstream API configuration, as I will need this to create the SignalR connection. In this simple example, where I’m always requiring the user to be auth’d and I only have one hub to connect to, I’m ok with just building the hub connection here; otherwise I would probably build out some extension methods. Note that the WithUrl
uses the downstream token provider to attempt to get a token. This is the same token that the authenticated client would use. This was the main trick to getting SignalR to authenticate with my API using Azure B2C.
The rest is pretty standard SignalR wire up. If the hub is connected, then allow the user to send messages. Once the message is sent, I clear the message textbox and update the UI when a message is received on this hub. Something else of note here is the use of InvokeAsync(StateHasChanged);
. After adding the new message to the list, we need to inform Blazor’s state that this list has been changed; otherwise it will not be reflected in the UI.
Downstream Token Provider
public class DownstreamTokenProvider : IDownstreamTokenProvider
{
private readonly IAccessTokenProvider _tokenProvider;
private readonly DownstreamApiConfiguration _downstreamApiConfiguration;
public DownstreamTokenProvider(IConfiguration configuration,
IAccessTokenProvider tokenProvider)
{
_tokenProvider = tokenProvider;
_downstreamApiConfiguration = configuration.GetRequiredSection("DownstreamApi")
.Get<DownstreamApiConfiguration>()!;
}
public async Task<string?> TryGetToken()
{
var accessTokenResult = await _tokenProvider.RequestAccessToken(new AccessTokenRequestOptions
{
Scopes = _downstreamApiConfiguration.Scopes
});
if (!accessTokenResult.TryGetToken(out var token))
{
return null;
}
var accessToken = token.Value;
return accessToken;
}
}
I created this class as a way to get a token from Azure B2C that is scoped to the API scope I set up previously (see my previous article). The Blazor web project has a built-in token provider that can handle this request for me. However, for the native projects, I had to create a IAccessTokenProvider
that uses the MSAL code to get a token. Thankfully, everything is an interface, so we just have to meet the interface contracts.
Wrap up
There you have it! Three different ways to communicate your Blazor projects to an API backend with Azure B2C in the middle. If you have questions or think I've missed something, please reach out to me.
Comments
You can also comment directly on GitHub.