Blazor WebAssembly has shipped with a host of new options for authentication. We now have the ability to create Blazor Wasm apps which can authenticate against Active Directory, Azure AD, Azure AD B2C, Identity Server, in fact any OIDC provider should work with Blazor. But, there is a little gotcha which has tripped a few people up when building applications which mix protected and unprotected endpoints using the Blazor WebAssembly ASP.NET Core Hosted template with Individual user accounts enabled.
The default configuration in the template uses an HTTP client with a custom message handler called BaseAddressAuthorizationMessageHandler
. This handler attempts to attach an access token to any outgoing requests and if it can’t find one, it throws an exception.
This makes sense if you’re only calling protected endpoints, but if your application allows the user to call unprotected endpoints without logging in then you might see a AccessTokenNotAvailableException
in the browser console.
In this post, I’m going to show you how to configure an additional HttpClient
instance which can be used by unauthenticated users to call unprotected endpoints, avoiding an AccessTokenNotAvailableException
.
Default Configuration
When you create a new Blazor WebAssembly application using ASP.NET Core Identity, you’ll find the following configuration in the Program.Main
method.
builder.Services.AddHttpClient("BlazorApp.ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
This sets up the HttpClient
for the application using a custom handler, BaseAddressAuthorizationMessageHandler
. This handler is really useful as it saves us having to worry about adding access tokens to our API requests manually. However, as I explained in the introduction, this is also a potential issue.
If we delve into the source code, we can see that the BaseAddressAuthorizationMessageHandler
inherits from another class called AuthorizationMessageHandler
, this is the class where we find the potential gotcha.
var tokenResult = _tokenOptions != null ?
await _provider.RequestAccessToken(_tokenOptions) :
await _provider.RequestAccessToken();
if (tokenResult.TryGetToken(out var token))
{
_lastToken = token;
_cachedHeader = new AuthenticationHeaderValue("Bearer", _lastToken.Value);
}
else
{
throw new AccessTokenNotAvailableException(_navigation, tokenResult, _tokenOptions?.Scopes);
}
The piece of code above is looking for a token so it can add it to the authentication header of the outgoing request. And as you can see, if one isn’t found then an exception is thrown, AccessTokenNotAvailableExcpetion
.
The default template configuration actually gives us a hint of this potential issue on the FetchData
component.
protected override async Task OnInitializedAsync()
{
try
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
}
There is a try..catch
setup to specifically check for this exception and redirect the user to login.
Now we can see where the problem comes from, what’s the solution?
Configuring a second HttpClient for unauthenticated requests
The answer is to add a second HTTP Client instance which doesn’t use this message handler. We can then use this instance when we want to make unprotected requests, and use the original one for everything else.
The easiest way we can achieve this is by adding the following line to our Program.Main
.
builder.Services.AddHttpClient("BlazorApp.PublicServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));
This will add another named HTTP Client instance to our application but, as you can see, there is no special message handler added this time.
Once this is done, we can get an instance of this client by injecting IHttpClientFactory
into our component or service and using the Create
method with the name of the client.
@inject IHttpClientFactory HttpClientFactory
...
@code {
private async Task GetSomethingFromAPI()
{
var client = HttpClientFactory.Create("BlazorApp.PublicServerAPI");
var result = client.GetFromJsonAsync<Something>("/api/something");
}
}
Another option, which in my opinion is a bit nicer as I’m not a fan of magic strings, is to create a typed client. This means we can request an instance by type rather than use the magic string solution.
To do this we first need to add a new class, we’ll call it PublicClient
and add the following code.
public class PublicClient
{
public HttpClient Client { get; }
public PublicClient(HttpClient httpClient)
{
Client = httpClient;
}
}
This code is quite straightforward, we’re just accepting a instance of HttpClient
through DI and saving it off to the Client
property.
Then in Program.Main
we add the following code.
builder.Services.AddHttpClient<PublicClient>(client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));
This is going to inject an HttpClient
instance, configured with the base address we’ve specified, into our PublicClient
class. It will also add our PublicClient
class into the DI container so we can request an instance of it with the preconfigured HttpClient
.
We can then use it like this.
@inject PublicClient PublicClient
...
@code {
private async Task GetSomethingFromAPI()
{
var result = PublicClient.Client.GetFromJsonAsync<Something>("/api/something");
}
}
I think this is a much cleaner way to do things, it’s saved us a line of code in our component and it’s more obvious at a glance the type of client we are using. Using this approach also allows us to abstract the underlying HttpClient
entirely, if we wish. We can write methods on the PublicClient
so we never expose the HttpClient
to calling code.
public class PublicClient
{
private HttpClient _client
public PublicClient(HttpClient httpClient)
{
_client = httpClient;
}
public async Task<SomeThing> GetSomething()
{
var result = await _client.GetFromJsonAsync<SomeThing>("/api/something");
return result;
}
}
@inject PublicClient PublicClient
...
@code {
private async Task GetSomethingFromAPI()
{
var result = PublicClient.GetSomething();
}
}
This code is now really succinct, we’ve also centralised all our API calling code which should make any future maintenance simpler.
Summary
In this post we’ve explored a potential issue with unauthenticated users calling unprotected endpoints using the default HttpClient
that comes with the Blazor WebAssembly auth templates. We’ve investigated Blazors source to understand where this comes from and then we’ve applied a solution which was to add a second HttpClient
instance.
Once we had this working we also looked at a different implementation of the solution which made our code a bit simpler and easier to understand, as well as potentially making API calling code easier to maintain in the long run.