Blazor WebAssembly blocks WebApi AllowAnonymous

Viewed 1442

I have created an Blazor WebAssembly project and want to provide a WebAPI with one public available function.

[Route("api/[controller]")]
[ApiController]
[Authorize]
public class SystemEvalApiController : ControllerBase
{
    public SystemEvalApiController(AppDbContext context, IMapper mapper)
    {...}

    [Route("LatestEvals")]
    [AllowAnonymous]
    public ActionResult LatestEvals()

that is my Api controller and I should be able to call it with:

SystemEvalPublicViewModel = await Http
                .GetFromJsonAsync<SystemEvalPublicViewModel>(
                    HttpService.BuildUrl("api/SystemEvalApi/LatestEvals"));

When i am not logged into any account. But instead I get this error:

info: System.Net.Http.HttpClient.JPB.BorannRemapping.ServerAPI.LogicalHandler[100]
      Start processing HTTP request GET https://localhost:44330/api/SystemEvalApi/LatestEvals
blazor.webassembly.js:1 info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
      Authorization failed.

It looks like the "DefaultAuthorizationService" does not recognize the Anonymous attribute but I cannot find the point where it fails directly.

How do I declare an WebAPI function to be accessable from the HttpClient without Login. Microsoft.AspNetCore.Components.WebAssembly.Server 3.2.0.-rc1.20223.4

Edit: Here is the declaration for ClientServices:

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");

builder.Services.AddHttpClient("JPB.BorannRemapping.ServerAPI", client =>
    {
        client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
    })
    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();

// Supply HttpClient instances that include access tokens when making requests to the server project
builder.Services.AddTransient(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("JPB.BorannRemapping.ServerAPI"));
builder.Services.AddTransient(e => new HttpService(e.GetService<HttpClient>()));
builder.Services.AddApiAuthorization();
builder.Services.AddBlazoredLocalStorage();

await builder.Build().RunAsync();
2 Answers

So each time you acquire an HttpClient it use the BaseAddressAuthorizationMessageHandler which try to authentify the request. But it this case your request should not be authentified, so you can make something like :

Registration

builder.Services.AddHttpClient("JPB.BorannRemapping.ServerAPI.Anonymous", client =>
    {
        client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
    });

Usage

@inject IHttpClientFactory _factory

@code {
...
     var httpClient = _factory.CreateClient("JPB.BorannRemapping.ServerAPI.Anonymous");
     var httpService = new HttpService(httpClient);
     SystemEvalPublicViewModel = await httpClient
                .GetFromJsonAsync<SystemEvalPublicViewModel>(
                    httpService.BuildUrl("api/SystemEvalApi/LatestEvals"));
}

Building on the answer from @agua from mars.

Registration in Program.cs

You could add 2 named HttpClient to the services collection (the first for authenticated calls the second for anonymous):

builder.Services.AddHttpClient("YourProject.ServerAPI", 
        client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
                .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
builder.Services.AddHttpClient("YourProject.ServerAPI.Anonymous", 
        client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));

// Supply HttpClient instances that include access tokens when making requests to the server project
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("YourProject.ServerAPI"));
//Register a new service for getting an Anonymous HttpClient
builder.Services.AddScoped<IHttpAnonymousClientFactory, HttpAnonymousClientFactory>();

Add new Interface and Implementation for Dependency Injection:

    public interface IHttpAnonymousClientFactory
    {
        HttpClient HttpClient { get; }
    }

    public class HttpAnonymousClientFactory : IHttpAnonymousClientFactory
    {
        private readonly IHttpClientFactory httpClientFactory;

        public HttpAnonymousClientFactory(IHttpClientFactory httpClientFactory)
        {
            this.httpClientFactory = httpClientFactory;
        }

        public HttpClient HttpClient => httpClientFactory.CreateClient("YourProject.ServerAPI.Anonymous");
    }

Usage in Razor Component (for Anonymous HttpClient)

        [Inject]
        private IHttpAnonymousClientFactory httpAnonymousClientFactory { get; set; }

        private MyViewModel myModel;

        protected override async Task OnInitializedAsync()
        {
            myModel = await httpAnonymousClientFactory.HttpClient.GetFromJsonAsync<MyViewModel>($"api/mycontroller/myendpoint");
        }

Usage in Razor Component (for Authenticated HttpClient)

        [Inject]
        private HttpClient httpClient { get; set; }

        private MyOtherViewModel myModel;

        protected override async Task OnInitializedAsync()
        {
            myModel = await httpClient.GetFromJsonAsync<MyOtherViewModel>($"api/mycontroller/mysecureendpoint");
        }
Related