I don't have a lot of experience in dotnet and authentication so I'm trying to wrap my head around this.
So I'm building a Blazor WASM app (.NET 8.0) for my company, we started from the blazor wasm template that includes authentication. So far this works wonderfully.
We also set up an API for this application that is deployed on an on-site Windows IIS server. We also have an API client for this API which is distributed on a private nuget feed via Azure DevOps.
My program.cs looks like this:
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
});
var authority = new AuthorityProvider();
var productsApiClient = new ProductsApiClient(
authority,
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }
);
builder.Services.AddScoped<IProductsApiClient>(sp => productsApiClient);
await builder.Build().RunAsync();
An example api call looks like this in a component/page (omitted some irrelevant stuff):
@page "/products/{Id:guid}"
@using Microsoft.AspNetCore.Authorization
@using Products.Contracts
@using Products.Contracts.API
@inject IProductsApiClient ApiClient
@attribute [Authorize]
<html/>
@code {
[Parameter]
public Guid Id { get; set; } = default!;
private Product product { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
product= await ApiClient.GetProduct(Id);
}
}
In the backend that call looks like this:
public async Task<Product> Getproduct(Guid id)
{
if (!Authority.GetAllowed)
throw new UnauthorizedAccessException(READ_ACTION_NOT_ALLOWED);
var url = $"{this.BaseRoute}/{id}";
var result = await _http.GetFromJsonAsync<Product>(url);
if (result == null)
return Product.Empty();
this.Authority.Update(result.Links);
return result;
}
So far so good, app runs, I can log in, I can go to the overview page of all products, and I can go to a detail page of a product.
As you can see from the component snippet, my intention is that if a user gets sent a url for a product by a colleague, they can just click the link and they will immediately be on the correct page and the info for that product gets fetched, which is a pretty standard setup imo (if they're not logged in they are redirected to login first).
This is where I hit the limits of my understanding of azure authentication and dotnet and the problems begin, everything works fine, but when I click the link and it opens in a new tab, or if I just refresh the detail page I get the following error:
crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
Unhandled exception rendering component: Read action not allowed.
System.UnauthorizedAccessException: Read action not allowed.
As that is the error that is defined in the API to be thrown if we don't have the authorisation for that endpoint, it kind of makes sense. But what happened to my authorisation? If i refresh the page I'm still in the same tab, my session is still active, I don't have to log back in and I can still see my tokens in sessionStorage for that tab. So what is going wrong here exactly? Where is my auth going and how can I persist it across tabs/refreshes? Also when the calls do work (if I strictly use in app navigation) I don't see any form of authorisation headers or bearer tokens on the request. So how does this work under the hood?
I removed the authority check from the products endpoint that returns all products to test it, and on refresh that call works perfectly fine, so it has to be authorisation/authentication related... I've done a bunch of research but haven't found anything that actually explains this in depth. I feel like I'm missing something (obvious). Am I supposed to store my tokens in localStorage instead?
Does anyone have any advice or experience they can share?
I've pretty much tried everything, dotnet docs are very convoluted and hard to follow if you're new to dotnet, pretty much every github thread gets closed by microsoft bots, and the one stackoverflow post I found that seems to describe the same problem is from 3 years ago and the OP never came back with the answer.