I have a server app that connects to a custom login server, receives a JWT, and stores it in session storage for use throughout the app. I use <AuthorizeView>
to contain restricted content, but it always resolves to <NotAuthorized>
until I refresh the page. After the page is refreshed, the login state is correctly propagated and <Authorized>
is visible. This also happens during logout — any authorization change.
I have a custom AuthenticationStateProvider
that overrides GetAuthenticationStateAsync()
. During this method, I retrieve the auth token from session storage. I am pretty sure that while waiting for this task, the app renders, and doesn’t wait for the authentication state to resolve. Thus, causing the issue. (maybe?)
CustomAuthStateProvider.cs
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var result = await protectedSessionStore.GetAsync<string>("authToken");
var token = result.Success ? result.Value : string.Empty;
var identity = string.IsNullOrWhiteSpace(token)
? new ClaimsIdentity()
: new ClaimsIdentity(ParseClaimsFromJwt(token), "jwt");
var task = Task.FromResult(new AuthenticationState(new ClaimsPrincipal(identity)));
NotifyAuthenticationStateChanged(task);
return await task;
}
public async Task<AuthenticationState> ChangeUser(string token)
{
ClaimsPrincipal claimsPrincipal;
if (string.IsNullOrWhiteSpace(token))
{
await protectedSessionStore.DeleteAsync("authToken");
claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity());
}
else
{
await protectedSessionStore.SetAsync("authToken", token);
claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(ParseClaimsFromJwt(token), "jwt"));
}
Console.WriteLine($"UpdateAuthenticationState - IsAuthenticated: {claimsPrincipal.Identity?.IsAuthenticated}");
foreach (var claim in claimsPrincipal.Claims)
{
Console.WriteLine($"Claim: {claim.Type} = {claim.Value}");
}
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(claimsPrincipal)));
}
I then inject it like this
Program.cs
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
In Login
, I have a form that takes a username/password, sends an HTTP request to the login endpoint, gets a JWT, and authenticates the user with it.
This is where the behavior becomes unexpected. Instead of the components on the screen re-rendering to reflect the new authorization status, they stay the same. Even when I navigate away from the page (via <a>
) it doens’t update. It ONLY updates when I refresh.
Login.razor
<AuthorizeView>
<Authorized>
@{ Navigation.NavigateTo("/"); }
</Authorized>
</AuthorizeView>
<EditForm Model="@Model" FormName="Login" OnValidSubmit="HandleLogin">
<InputText @bind-Value="Model.Username" placeholder="Username" />
<InputText @bind-Value="Model.Password" type="password" />
<button type="submit">Login</button>
</EditForm>
@code {
[SupplyParameterFromForm]
public LoginModel Model { get; set; } = new();
private async Task HandleLogin(EditContext obj) => await HandleLoginInternal();
private async Task HandleLoginInternal()
{
var response = await Http.PostAsJsonAsync("User/login", Model);
if (response.IsSuccessStatusCode)
{
var token = await response.Content.ReadAsStringAsync();
await ProtectedSessionStore.SetAsync("authToken", token);
await AuthStateProvider.ChangeUser(token);
StateHasChanged();
}
}
public class LoginModel
{
public string Username { get; set; }
public string Password { get; set; }
}
}
I’ve noticed that <AuthorizeView>
never shows <Authorizing>
, leading me to believe that the app thinks it has the authentication state before it actually does. I’d like to fix this issue, as it allows logged in users to access pages they shouldn’t while logged in (for example, the login page)
I’ve tried a lot of disjointed things to get this to work, specifically adding StateHasChanged()
to the end of HandleLoginInternal()
but it doesn’t do anything. I expect the page to redirect to "/"
, and the navbar to say "Logout" instead of "Login" upon authorization.
Any ideas? Thanks! I have no idea what I’m doing wrong as I’m new to Blazor and ASP.NET.
2
Answers
I've answered my own question after days of research with this GitHub comment.
The solution was to change
to
in
Program.cs
, as to not supply two differentCustomAuthStateProvider
objects.What I think is happening is that a scoped service is added to the app that runs a lambda to return the already existing
CustomAuthStateProvider
service. I still don't fully understand how this works, so correct me if I'm wrong.This also allows use of the following in Razor components, which is much more elegant than casting it every time I need to access a member of
CustomAuthStateProvider
.I’ve just see you have posted an answer. I was looking at your code and couldn’t see where the problem was so was doing a refactoring exercise to get it to work. I’ll show you want I came up with.
Customer AuthenticationStateProvider:
Registered like this:
And used in Login like this:
UserViewer looks like this: