skip to Main Content

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


  1. Chosen as BEST ANSWER

    I've answered my own question after days of research with this GitHub comment.

    You're configuring two different instances of CustomAuthStateProvider. You're toggling the state on the second instance, but your components are receiving the first instance.

    The solution was to change

    builder.Services.AddScoped<CustomAuthStateProvider>();
    builder.Services.AddScoped<ICustomAuthStateProvider, CustomAuthStateProvider>();
    

    to

    builder.Services.AddScoped<CustomAuthStateProvider>();
    builder.Services.AddScoped<AuthenticationStateProvider>(provider => provider.GetRequiredService<CustomAuthStateProvider>());
    

    in Program.cs, as to not supply two different CustomAuthStateProvider 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.

    @inject CustomAuthStateProvider AuthStateProvider
    

  2. 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:

    public class CustomAuthenticationStateProvider : AuthenticationStateProvider
    {
        // default is an empty ClaimsPrincipal which won't authenticate
        private ClaimsPrincipal _user = new ClaimsPrincipal();
        private readonly ProtectedSessionStorage _protectedSessionStore;
    
        public CustomAuthenticationStateProvider(ProtectedSessionStorage protectedSessionStore)
        {
            _protectedSessionStore = protectedSessionStore;
        }
    
        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            string? token = null;
            var result = await _protectedSessionStore.GetAsync<string>("authToken");
            token = result.Success ? result.Value : null;
    
            if (token is not null)
            {
                //creates a claims array with the provided information
                Claim[] Claims = new[]{
                new Claim(ClaimTypes.Name, token ?? "Anonymous"),
                new Claim(ClaimTypes.Role, "User")
                };
    
                _user = new ClaimsPrincipal(new ClaimsIdentity(Claims, "BasicAuthType"));
                await _protectedSessionStore.SetAsync("authToken", token!);
            }
    
            return new AuthenticationState(_user);
        }
    
        public async Task<AuthenticationState> ChangeIdentityAsync(string? username)
        {
    
            if (username == null)
            {
                await _protectedSessionStore.DeleteAsync("authToken");
                _user = new ClaimsPrincipal();
            }
            else
            {
                await _protectedSessionStore.SetAsync("authToken", username);
    
                // creates a claims array with the provided information
                Claim[] Claims = new[]{
                new Claim(ClaimTypes.Name, username ?? "Anonymous"),
                new Claim(ClaimTypes.Role, "User")
                };
    
                _user = new ClaimsPrincipal(new ClaimsIdentity(Claims, "BasicAuthType"));
            }
    
            // Get a new AuthenticationState and Notify any listeners that the state has changed
            var task = this.GetAuthenticationStateAsync();
            this.NotifyAuthenticationStateChanged(task);
            return await task;
        }
    }
    

    Registered like this:

    builder.Services.AddAuthentication();
    builder.Services.AddAuthorization();
    
    builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
    

    And used in Login like this:

    @page "/Login"
    @using System.Security.Claims
    
    @inject AuthenticationStateProvider AuthenticationStateProvider
    @inject NavigationManager NavManager
    <h3>Log @_logText</h3>
    
    @if (!_loggedIn)
    {
        <div class="m-2">
            <label class="form-label">User Name</label>
            <InputText class="form-control" @bind-Value="_model.Username" />
        </div>
    }
    
    <div>
        <button disabled="@(_loggedIn)" class="btn btn-success" @onclick="OnLogOnAsync">Log In</button>
        <button disabled="@(!_loggedIn)" class="btn btn-danger" @onclick="OnLogOffAsync">Log Off</button>
    </div>
    
    <UserViewer />
    
    
    @code {
        [Parameter, SupplyParameterFromQuery] public string? ReturnUrl { get; set; }
        private LoginModel _model = new();
        private ClaimsPrincipal? _user;
        private bool _loggedIn => _user?.Identity?.IsAuthenticated ?? false;
        private string _logText => _loggedIn ? "Out" : "In";
        private CustomAuthenticationStateProvider _customAuthenticationStateProvider = default!;
    
        protected override async Task OnInitializedAsync()
        {
            if (AuthenticationStateProvider is CustomAuthenticationStateProvider customAuthenticationStateProvider)
                _customAuthenticationStateProvider = customAuthenticationStateProvider;
            else
                ArgumentNullException.ThrowIfNull(_customAuthenticationStateProvider);
    
            var state = await this.AuthenticationStateProvider.GetAuthenticationStateAsync();
            _user = state.User;
        }
    
        private async Task OnLogOnAsync()
        {
            if (!string.IsNullOrWhiteSpace(_model.Username))
            {
                // Fake an async call to the aunthentication provider to get the token
                await Task.Delay(250);
    
                // Need to handle bad responses
    
                //Pass the token to the custom Authentication Provider
                await _customAuthenticationStateProvider.ChangeIdentityAsync(_model.Username);
    
                if (!string.IsNullOrWhiteSpace(this.ReturnUrl))
                    this.NavManager.NavigateTo(this.ReturnUrl);
            }
        }
    
        private async Task OnLogOffAsync()
        {
            await _customAuthenticationStateProvider.ChangeIdentityAsync(null);
        }
    
        public class LoginModel
        {
            public string? Username { get; set; }
        }
    }
    

    UserViewer looks like this:

    @inject AuthenticationStateProvider AuthenticationStateProvider
    
    <div class="alert alert-primary m-2">
        User: @(_user?.Identity?.Name ?? "Not Logged In")
    </div>
    
    @code {
        private ClaimsPrincipal? _user;
    
        protected override async Task OnInitializedAsync()
        {
            var state = await this.AuthenticationStateProvider.GetAuthenticationStateAsync();
            _user = state.User;
        }
    
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search