DEV Community

loading...
Cover image for How to send HTTP requests to public API without access token on an authentication enabled Blazor Wasm App?

How to send HTTP requests to public API without access token on an authentication enabled Blazor Wasm App?

jsakamoto
Microsoft MVP for Visual Studio and Development Tech. (prefer C#, .NET Core, ASP.NET Core, Azure Web Apps, TypeScript, and Blazor WebAssembly App!)
・5 min read

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.

fig1

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.

fig2

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)

fig3

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! :)

Discussion (5)

Collapse
alienroid profile image
Alienroid

3 is the recommended way by MSFT. Reference here: docs.microsoft.com/en-us/aspnet/co...

I worked with MSFT on this ;)

Collapse
pierrenygard profile image
Pierre

I agree #3 is nice

Collapse
superluis25 profile image
Luis Lepe

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.

Collapse
sethnejame profile image
Seth NeJame

I signed up to this site just so I could say thank you. I went with solution 3 and it works great.

Thanks!

Collapse
pierrenygard profile image
Pierre

jsakamoto this was a fine solution. A big thanks to you! I was stuck on this issue for 2 days.