It's easy to build an authentication enabled Blazor Wasm App from the default project template.
One day, I started to build an authentication enabled Blazor WebAssembly App that is hosted on ASP.NET Core server.
I'm usually using Visual Studio on Windows OS for development Blazor App, so it is very easy to create an authentication enabled Blazor WebAssembly App from the project template.
It works fine very well.
The Web API endpoint which is annotated with [Authorizaition]
attribute was protected from anonymous access, as I expected.
Next, I added a public API endpoint.
Next step, I started to add a new feature to the app.
I tried to implement listing "Recently Updated" news feed about this web app.
I implemented the "Recently Updated" list to be generated on the server-side and provide it to the client-side via a Web API endpoint.
I thought this news feed should be public for every user even who is not signed-in.
Therefore, I didn't annotate the getting "Recently Updated" list API endpoint with [Authorization]
, for allowing anonymous access.
[ApiController]
[Route("[controller]")]
public class RecentlyUpdatesController : ControllerBase
{
[HttpGet]
public IEnumerable<string> Get()
{
...
I tested that API by cURL
command without any credentials, and it worked fine of course.
But... unhandled exception occurred!
The server-side implements looked like completed, so I started to implement the client-side.
I coded fetching the "Recently Updated" list from that public Web API using the "HttpClient" object that was injected via DI.
After did it, I tried to run the app.
Unfortunately and unexpectedly, I could not see the "Recently Updated" list on the screen, instead, I saw something error reports.
Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
Unhandled exception rendering component: ''
Microsoft.AspNetCore.Components.WebAssembly.Authentication.AccessTokenNotAvailableException: ''
at Microsoft.AspNetCore.Components.WebAssembly.Authentication.AuthorizationMessageHandler.SendAsync(
System.Net.Http.HttpRequestMessage request,
System.Threading.CancellationToken cancellationToken)
What happened!?
The reason for this exception is...
the Blazor app which was created from the default project template with the "Authentication" option enabled always tries to attach an access token to any HTTP requests whether the user is signed in or not.
This is the reason that AccessTokenNotAvailableException
was thrown.
Solutions
I was thinking for a while, I came up with 3 solutions.
Solution 1
Solution 1 is, configuring AuthorizationMessageHanlder
to allow attaching an access token to only selected endpoints which are under the specified subpath.
If you want to choose this solution, at first, you have to rearrange the URLs of endpoints on the server-side (those you want to protect) to be under the same subpath (ex: "/authorized/...").
[Authorize]
[ApiController]
//[Route("[controller]")]
[Route("authorized/[controller]")] // <- Change the URL of this API to be under the "authorized" subpath.
public class WeatherForecastController : ControllerBase
{
...
Second, you have to configure AuthorizationMessageHandler
with custom options on the client-side.
You can do this in the Main
method of the Program
class of the client-side project.
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
// 👇 Add this conifiguration code.
builder.Services.AddTransient<AuthorizationMessageHandler>(sp =>
{
// 👇 Get required services from DI.
var provider = sp.GetRequiredService<IAccessTokenProvider>();
var naviManager = sp.GetRequiredService<NavigationManager>();
// 👇 Create a new "AuthorizationMessageHandler" instance,
// and return it after configuring it.
var handler = new AuthorizationMessageHandler(provider, naviManager);
handler.ConfigureHandler(authorizedUrls: new[] {
// List up URLs which to be attached access token.
naviManager.ToAbsoluteUri("authorized/").AbsoluteUri
});
return handler;
});
...
Finally, you have to configure HttpClientFactory
to using the AuthorizationMessageHandler
that is configured by you.
...
// 👇 Use "AuthorizationMessageHandler" that is configured above
// instead of "BaseAddressAuthorizationMessageHandler".
builder.Services.AddHttpClient("BlazorWasmApp.ServerAPI", client => ...)
// .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
.AddHttpMessageHandler<AuthorizationMessageHandler>();
// Supply HttpClient instances that include access tokens when making requests to the server project
builder.Services.AddTransient(sp => sp.GetRequiredService<IHttpClientFactory>()
.CreateClient("BlazorWasmApp.ServerAPI"));
...
After doing this, the access token will be attached for only the URLs which are under the "/authorized/..."
subpath! 👍
Solution 2
Solution 2 is, explicitly getting a named HTTP client which is configured to attach an access token from "IHttpClientFactory", when sending HTTP requests to authorization required endpoint.
At first, register a plain HttpCient
to the DI with just configured only "base address", instead of retrieving from an IHttpClient
service, in the Main
method of the Program
class of client-side project.
public static async Task Main(string[] args)
{
...
// 👇 Register plain "HttpClient" servcie to DI.
// builder.Services.AddTransient(sp => ...CreateClient("BlazorWasmApp.ServerAPI"));
builder.Services.AddTransient(sp => new HttpClient {
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});
...
And, get a HttpClient
object from IHttpClientFactory
with name, instead of getting it from DI directly, when accessing to a protected API endpoint.
@* @inject HttpClient Http *@
@inject IHttpClientFactory HttpClientFactory
...
@code {
protected override async Task OnInitializedAsync()
{
...
// 👇 Don't get a HttpClient from DI directly for accessing a proected API.
// forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
// 👇 Instead, get a HttpCient from IHttpClientFactory service with name explicitly.
var http = HttpClientFactory.CreateClient("BlazorWasmApp.ServerAPI");
forecasts = await http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
}
Solution 3
Solution 3 is another solution of opposite to "solution 2".
This solution explicitly gets a named HTTP client which is plain one from "IHttpClientFactory", when sending HTTP requests to anonymous access allowed endpoint.
To do this, register plain HttpClient
to the IHttpClientFactory
service with a name.
public static async Task Main(string[] args)
{
...
// 👇 Add a plain "HttpClient" with a name.
builder.Services.AddHttpClient("BlazorWasmApp.AnonymousAPI", client => {
client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
});
builder.Services.AddHttpClient("BlazorWasmApp.ServerAPI", ...)
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
...
And, get a HttpClient
object from IHttpClientFactory
with name, instead of getting it from DI directly, when accessing a public API endpoint.
@* @inject HttpClient Http *@
@inject IHttpClientFactory HttpClientFactory
...
@code {
protected override async Task OnInitializedAsync()
{
...
// 👇 Don't get a HttpClient from DI directly for accessing a public API.
// RecentlyUpdates = await Http.GetFromJsonAsync<string[]>("RecentlyUpdates");
// 👇 Instead, get a HttpCient from IHttpClientFactory service with name explicitly.
var http = HttpClientFactory.CreateClient("BlazorWasmApp.AnonymousAPI");
RecentlyUpdates = await http.GetFromJsonAsync<string[]>("RecentlyUpdates");
}
Conclusion
The Blazor app which was created from the default project template with the "Authentication" option enabled always tries to attach an access token to any HTTP requests whether the user is signed in or not.
This implementation will cause AccessTokenNotAvailableException
exception when the user is not signed-in even if the accessing is for an anonymously accessible endpoint.
You can avoid this exception by one of these solutions:
- Solution 1. - limit attaching the access token to only URLs under the specified subpath.
-
Solution 2. - Get a configured
HttpClient
explicitly by name to access a protected API. -
Solution 3. - Get a plain
HttpClient
explicitly by name to access an anonymously accessible API.
The entire of my sample code is public on the GitHub repository of the following URL:
https://github.com/sample-by-jsakamoto/Blazor-AllowNoAuthHttpRequest
Happy coding! :)
Top comments (6)
3 is the recommended way by MSFT. Reference here: docs.microsoft.com/en-us/aspnet/co...
I worked with MSFT on this ;)
I agree #3 is nice
You are a life saver! I was stuck on this for a while, I used option 1 because Syncfusion does some magic behind that I have no control over, so that's what it nailed it.
Thanks, I went with number 3
I signed up to this site just so I could say thank you. I went with solution 3 and it works great.
Thanks!
jsakamoto this was a fine solution. A big thanks to you! I was stuck on this issue for 2 days.