C# Blazor .NET 8 localstorage deleted on page refresh F5

Kinga 0 Reputation points
2024-10-17T08:40:02.0933333+00:00

Hi

In Blazor .NET7 I use localstorage to save a JWT for all my users claims etc. Works perfectly.

Upgraded to .NET8 the JWT still works, but, if you refresh your browser page (F5) the localstorage is deleted and I get a 401 unauthorized. In .NET7 this did not happen it refreshed the page correctly with the localstorage in-tact.

Thanks

.NET
.NET
Microsoft Technologies based on the .NET software framework.
3,873 questions
Blazor
Blazor
A free and open-source web framework that enables developers to create web apps using C# and HTML being developed by Microsoft.
1,582 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
10,951 questions
{count} votes

1 answer

Sort by: Most helpful
  1. Tiny Wang-MSFT 2,721 Reputation points Microsoft Vendor
    2024-10-17T12:31:01.8866667+00:00

    Hi Kinga, I had a test in my side, but everything worked well, please allow me to share my sample with you. I created a new .net 8 blazor web app with InteractiveServer render mode. Then I followed this document to add custom authentication state provider for my blazor application. In the meantime, I also added jwt token logic into it to simulate your requirement.

    My Program.cs, like what you can see, I injected Blazored.LocalStorage service.

    using Blazored.LocalStorage;
    using BlazorWebAppCustomAuth;
    using BlazorWebAppCustomAuth.Components;
    using Microsoft.AspNetCore.Components.Authorization;
    var builder = WebApplication.CreateBuilder(args);
    // Add services to the container.
    builder.Services.AddRazorComponents()
        .AddInteractiveServerComponents();
    builder.Services.AddCascadingAuthenticationState();
    builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
    builder.Services.AddBlazoredLocalStorage();
    var app = builder.Build();
    // Configure the HTTP request pipeline.
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Error", createScopeForErrors: true);
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseAntiforgery();
    app.MapRazorComponents<App>()
        .AddInteractiveServerRenderMode();
    app.Run();
    

    And here's my custom auth state provider.

    using Blazored.LocalStorage;
    using Microsoft.AspNetCore.Components;
    using Microsoft.AspNetCore.Components.Authorization;
    using Microsoft.IdentityModel.Tokens;
    using System.IdentityModel.Tokens.Jwt;
    using System.Security.Claims;
    using System.Text;
    namespace BlazorWebAppCustomAuth
    {
        public class CustomAuthStateProvider : AuthenticationStateProvider
        {
            private readonly ILocalStorageService _localStorageService;
            private ClaimsPrincipal _anonymous = new ClaimsPrincipal(new ClaimsIdentity());
            private AuthenticationState _cachedAuthState;
    
            public CustomAuthStateProvider(ILocalStorageService localStorageService)
            {
                _localStorageService = localStorageService;
            }
    
            public override async Task<AuthenticationState> GetAuthenticationStateAsync()
            {
                var savedToken = await _localStorageService.GetItemAsync<string>("authToken");
                if (string.IsNullOrWhiteSpace(savedToken))
                {
                    return new AuthenticationState(_anonymous);
                }
                else {
                    var handler = new JwtSecurityTokenHandler();
                    var tokenContent = handler.ReadJwtToken(savedToken);
                    var claims = tokenContent.Claims;
                    var identitify = claims.Where(x => x.Type == "name").FirstOrDefault().Value;
                    var identity = new ClaimsIdentity(
                    [
                        new Claim(ClaimTypes.Name, identitify),
                    ], "Custom Authentication");
                    var user = new ClaimsPrincipal(identity);
                    return await Task.FromResult(new AuthenticationState(user));
                } 
            }
    
            public async Task AuthenticateUserAsync(string userIdentifier)
            {
                var identity = new ClaimsIdentity(
                [
                    new Claim(ClaimTypes.Name, userIdentifier),
                ], "Custom Authentication");
                var token = generateJwt(userIdentifier);
                await _localStorageService.SetItemAsync("authToken", token);
                var user = new ClaimsPrincipal(identity);
                NotifyAuthenticationStateChanged(
                    Task.FromResult(new AuthenticationState(user)));
            }
    
            public string generateJwt(string userIdentifier)
            {
                var claims = new List<Claim>
                {
                    new Claim(JwtRegisteredClaimNames.Name, userIdentifier),
                    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                    new Claim("role","admin")
                };
                var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("this is my custom Secret key for authentication"));
                var token = new JwtSecurityToken(
                    issuer: "Test.com",
                    audience: "Test.com",
                    expires: DateTime.Now.AddHours(3),
                    claims: claims,
                    signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
                    );
                var accessToken = new JwtSecurityTokenHandler().WriteToken(token);
                return accessToken;
            }
        }
    }
    

    In App.razor component, I changed the Routes component to be non-prerendering by <Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" /> otherwise I will get InvalidOperationException on line var savedToken = await _localStorageService.GetItemAsync<string>("authToken"); because JavaScript interop calls can only be performed during the OnAfterRenderAsync lifecycle when prerendering is enabled.

    Here's my Routes.razor

    <Router AppAssembly="typeof(Program).Assembly">
        <Found Context="routeData">
            @* <RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
            <FocusOnNavigate RouteData="routeData" Selector="h1" /> *@
            <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
        </Found>
    </Router>
    

    And I created a Login.razor component to authenticate user. The SignIn method will trigger the

    AuthenticateUserAsync to store the token. And after signing in, I can refresh the page to trigger the GetAuthenticationStateAsyncmethod to check the jwt token.

    @page "/login"
    @inject AuthenticationStateProvider AuthenticationStateProvider
    @rendermode InteractiveServer
    
    <input @bind="userIdentifier" />
    <button @onclick="SignIn">Sign in</button>
    
    <AuthorizeView>
        <Authorized>
            <p>Hello, @context.User.Identity?.Name!</p>
        </Authorized>
        <NotAuthorized>
            <p>You're not authorized.</p>
        </NotAuthorized>
    </AuthorizeView>
    
    @code {
        public string userIdentifier = string.Empty;
        private void SignIn()
        {
            ((CustomAuthStateProvider)AuthenticationStateProvider).AuthenticateUserAsync(userIdentifier);
        }
    }
    

    Everything worked well in my side.

    User's image


    If the answer is the right solution, please click "Accept Answer" and kindly upvote it. If you have extra questions about this answer, please click "Comment".

    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.

    Best regards,

    Tiny


Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.