Search code examples
blazorblazor-server-sideblazor-webassembly

Blazor AuthorizeView not updating If the page not reloaded


In my StickyMenu component IU have the following code

<AuthorizeView>
  <NotAuthorized>
    Not Authorized
  </NotAuthorized>
  <Authorized>
   Authorized
   @(context.User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value)
  </Authorized>
</AuthorizeView>

I set up a CustomAuthStateProvider like this

 public class CustomAuthStateProvider : AuthenticationStateProvider
{
    private readonly ProtectedSessionStorage _sessionStorage;
    private ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity());

    public CustomAuthStateProvider(ProtectedSessionStorage sessionStorage)
    {
        _sessionStorage = sessionStorage;
    }

    public async Task<Utenti?> GetUser()
    {
        var userSessionStorageResult = await _sessionStorage.GetAsync<Utenti>(Constants.sessionStorageUser);
        return userSessionStorageResult.Success ? userSessionStorageResult.Value : null;
    }

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        try
        {
            Utenti? userSession = await GetUser();
            if (userSession == null)
            {
                return await Task.FromResult(new AuthenticationState(_anonymous));
            }
            else
            {
                var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
                {                    
                new Claim(ClaimTypes.Name, userSession?.Nome ?? String.Empty),
                new Claim(ClaimTypes.Email, userSession?.email ?? String.Empty),
                new Claim(ClaimTypes.NameIdentifier, userSession.IdUtente.ToString())
            }, "CustomAuth"));

                return await Task.FromResult(new AuthenticationState(claimsPrincipal));
            }
        }
        catch
        {
            return await Task.FromResult(new AuthenticationState(_anonymous));
        }
    }

    public void UpdateAuthenticationState(Utenti parUser)
    {
        ClaimsPrincipal claimsPrincipal;
        if (parUser != null)
        {
            _sessionStorage.SetAsync(Constants.sessionStorageUser, parUser);
            claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
            {                   
                new Claim(ClaimTypes.Name, parUser.Nome),
                new Claim(ClaimTypes.Email, parUser.email),
                new Claim(ClaimTypes.NameIdentifier, parUser.IdUtente.ToString())
            }));
        }
        else
        {
            _sessionStorage.DeleteAsync(Constants.sessionStorageUser);
            claimsPrincipal = _anonymous;
        }

        NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(claimsPrincipal)));
        
    }
}

When I login whit LoginPage I call this method

public void Login()
    {
        Utenti u = Utenti.Login(Username, StringUtil.encryptpass(Password));
        if (u != null)
        {
            var customAuthStateProvider = (CustomAuthStateProvider)AuthenticationStateProvider;
            customAuthStateProvider.UpdateAuthenticationState(u);
            NavigationManager.NavigateTo("/",true);
        }
    }

And in the menu I see Authorize and the user name

THE PROBLEM is that, when I put something in the cart, I have to add a temporary user (now in the example I force the Login with an existing username and password) and I update the StickyMenu component.

public async void AddToCart()
    {
        Utenti ut = Utenti.Login("email", StringUtil.encryptpass("password"));

        if (ut.IdUtente > 0)
        {
            var customAuthStateProvider = (CustomAuthStateProvider)AuthenticationStateProvider;
            customAuthStateProvider.UpdateAuthenticationState(ut);

        }
    }

The problem is that, If I don't reload the page calling

NavigationManager.NavigateTo(someurl,true);

the AutorizeView doesn't change.

How I can fix this problem?


SIMPLE PROJECT ON GITHUB


Solution

  • Based on your other question this is your Layout:

    <header>
      <StickyMenu></StickyMenu>
    </header>
    
    <main>
      <Loader Loading=@IsLoading></Loader>
      @Body
    </main>
    

    As configured StickyMenu will only be rendered once when the Layout first renders. It will not re-render when Layout re-renders because it's parameter state hasn't changed: there are no parameters.

    If you want it to re-render it, you need a mechanism to trigger a render. The most appropriate in the context is a handler registered on the AuthenticationState change event.

    Here's a similar control from a demo Authorization project of mine. OnUserChanged is registered on the AuthState.AuthenticationStateChanged event. It gets the new user and calls StateHasChanged to render the component.

    Note implementing IDisposable and de-registering the handler in Dispose.

    @implements IDisposable
    @namespace Blazr.Demo.Authorization.UI
    
    <span class="text-nowrap ms-3">
        <AuthorizeView>
            <Authorized>
                Hello, @(this.user.Identity?.Name ?? string.Empty)
            </Authorized>
            <NotAuthorized>
                Not Logged In
            </NotAuthorized>
        </AuthorizeView>
    </span>
    
    @code {
        [CascadingParameter] private Task<AuthenticationState> authTask { get; set; } = default!;
    
        [Inject] private AuthenticationStateProvider authState { get; set; } = default!;
    
        private ClaimsPrincipal user = new ClaimsPrincipal();
    
        protected async override Task OnInitializedAsync()
        {
            ArgumentNullException.ThrowIfNull(authTask);
            var state = await authTask;
            this.user = state.User;
            authState.AuthenticationStateChanged += this.OnUserChanged;
        }
    
        private async void OnUserChanged(Task<AuthenticationState> state)
            => await this.GetUser(state);
    
        private async Task GetUser(Task<AuthenticationState> state)
        {
            var authState = await state;
            this.user = authState.User;
            await this.InvokeAsync(this.StateHasChanged);
        }
    
        public void Dispose()
            => authState.AuthenticationStateChanged -= this.OnUserChanged;
    }
    

    The [slightly dated] demo project is here - Blazr.Demo.Authorization.

    Update based on the MRE Repo.

    Your code is pretty unconventional and left field so it took a while to start to read.

    Your immediate issue is in here:

    
            public async Task UpdateAuthenticationState(Utenti parUser)
            {
                ClaimsPrincipal claimsPrincipal;
                if (parUser != null)
                {
                    await _sessionStorage.SetAsync("user", parUser);
                    claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim>
                    {
                        new Claim("IdUser", parUser.IdUtente.ToString()),
                        new Claim("UserStatus", parUser.Stato),
                        new Claim(ClaimTypes.Name, parUser.Nome),
                        new Claim(ClaimTypes.Email, parUser.email),
                        new Claim(ClaimTypes.NameIdentifier, parUser.IdUtente.ToString())
                    }));
                }
                else
                {
                    await _sessionStorage.DeleteAsync("user");
                    claimsPrincipal = _anonymous;
                }
    
                NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(claimsPrincipal)));
            }
    

    If you refactor this code into something a little more readable, the problem becomes more obvious.

    
            public async Task _UpdateAuthenticationState(Utenti parUser)
            {
                ClaimsPrincipal claimsPrincipal;
                if (parUser != null)
                {
                    await _sessionStorage.SetAsync("user", parUser);
    
                    var claims = new List<Claim>
                    {
                        new Claim("IdUser", parUser.IdUtente.ToString()),
                        new Claim("UserStatus", parUser.Stato),
                        new Claim(ClaimTypes.Name, parUser.Nome),
                        new Claim(ClaimTypes.Email, parUser.email),
                        new Claim(ClaimTypes.NameIdentifier, parUser.IdUtente.ToString())
                    };
    
                    var claimsIdentity = new ClaimsIdentity(claims);
                    //var claimsIdentity = new ClaimsIdentity(claims, "CustomAuth");
    
                    claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
                }
                else
                {
                    await _sessionStorage.DeleteAsync("user");
    
                    claimsPrincipal = _anonymous;
                }
    
                NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(claimsPrincipal)));
            }
    

    You have provided no AuthenticationType, so the user is unauthorized.

    There are no prizes for writing hard to read, difficult to debug code. The compiler will refactor and optimize the hell out of it. You don't need to try to.

    The only loser is you. It's a lesson I've learnt the hard way!