Storing JWT token in protected local storage returns null due to prerender issue and giving 401 error when calling API endpoint

5 hours ago 1
ARTICLE AD BOX

I am developing an Employee Management System in Blazor Server using PostgreSQL for the database and JWT for authentication. I am using Microsoft Identity tables for user data. I have created a login page that accepts an email and password, and I can authenticate the user and generate a token.

The problem occurs when I am trying to store the token in protected local storage so I can reuse it to attach it to the API header as a bearer token.

My current architecture is below.

WebPortal │ .gitignore │ readme.md │ WebPortal.slnLaunch.user │ WebPortal.slnx │ ├── WebPortal.API │ └── WebPortal.API.Authentication │ │ appsettings.Development.json │ │ appsettings.json │ │ Program.cs │ │ WeatherForecast.cs │ │ WebPortal.API.Authentication.csproj │ │ WebPortal.API.Authentication.csproj.user │ │ WebPortal.API.Authentication.http │ │ │ ├── Configuration │ │ │ AppConfiguration.cs │ │ │ CorsConfiguration.cs │ │ │ DatabaseConfiguration.cs │ │ │ IdentityConfiguration.cs │ │ │ JwtConfiguration.cs │ │ │ ServiceConfiguration.cs │ │ │ SwaggerConfiguration.cs │ │ │ ├── Controllers │ │ │ AuthenticationController.cs │ │ │ WeatherForecastController.cs │ │ │ └── Properties │ │ launchSettings.json │ ├── WebPortal.Core │ └── WebPortal.Core │ │ WebPortal.Core.csproj │ │ │ ├── Authorization │ │ │ INavigationAuthorizationService.cs │ │ │ IUserContext.cs │ │ │ NavigationAuthorizationService.cs │ │ │ UserContext.cs │ │ │ ├── Config │ │ │ ServiceCollectionExtensions.cs │ │ │ ├── Navigation │ │ │ IModule.cs │ │ │ NavigationMenuItem.cs │ │ │ ├── Policies │ │ │ NavigationPolicies.cs │ │ │ ├── RolePolicies │ │ │ IRolePolicyProvider.cs │ │ │ RolePolicyProvider.cs │ │ │ └── Security │ │ ModuleAssemblyAttribute.cs │ ├── WebPortal.Infrastructure │ └── WebPortal.Infrastructure.Authentication │ ├── DataConfiguration │ │ │ ApplicationDbContext.cs │ │ │ ├── Entities │ │ │ ApplicationUser.cs │ │ │ RefreshToken.cs │ │ │ RefreshToken.Extensions.cs │ │ │ RolePermissionsMapping.cs │ │ │ ├── Implementation │ │ │ AuthenticationRepository.cs │ │ │ UserRepository.cs │ │ │ └── Interfaces │ │ IAuthenticationRepository.cs │ │ IUserRepository.cs │ ├── WebPortal.Model │ └── WebPortal.Model.Authentication │ │ LoginRequestDTO.cs │ │ LoginResponseDTO.cs │ │ RefreshTokenDTO.cs │ │ RefreshTokenRequestDTO.cs │ │ UserDetailsDTO.cs │ │ WebPortal.Model.Authentication.csproj │ ├── WebPortal.Service │ └── WebPortal.Service.Authentication │ │ WebPortal.Service.Authentication.csproj │ │ │ ├── Implementation │ │ │ AuthService.cs │ │ │ └── Interfaces │ │ IAuthService.cs │ └── WebPortal.UI └── WebPortal.UI │ appsettings.Development.json │ appsettings.json │ Program.cs │ WebPortal.UI.csproj │ WebPortal.UI.csproj.user │ ├── Components │ │ App.razor │ │ Routes.razor │ │ _Imports.razor │ │ │ ├── Layout │ │ │ EmptyLayout.razor │ │ │ EmptyLayout.razor.css │ │ │ MainLayout.razor │ │ │ MainLayout.razor.css │ │ │ NavMenu.razor │ │ │ NavMenu.razor.css │ │ │ ReconnectModal.razor │ │ │ ReconnectModal.razor.css │ │ │ ReconnectModal.razor.js │ │ │ ├── Pages │ │ │ Counter.razor │ │ │ Error.razor │ │ │ Home.razor │ │ │ NotFound.razor │ │ │ Weather.razor │ │ │ │ │ └── Authentication │ │ │ Login.razor │ │ │ └── Shared │ ├── Configuration │ │ AppConfiguration.cs │ │ AuthConfiguration.cs │ │ HttpClientConfiguration.cs │ ├── Helpers │ │ AuthenticationSchemeHandler.cs │ │ JwtHttpClientHandler.cs │ │ SessionExpirationService.cs │ ├── Middleware │ │ TokenSeedMiddleware.cs │ ├── Properties │ │ launchSettings.json │ ├── Providers │ │ AuthStateProvider.cs │ │ TokenProvider.cs │ ├── Services │ └── Implementation │ │ AuthService.cs │ ├── State │ │ TokenStore.cs │ └── wwwroot │ favicon.png │ └── css │ app.css │ styles.css

I have different projects for their own functionality.

I put the logic for saving and retrieving the token in TokenProvider.cs in UI project. It saves the token in ProtectedLocalStorage.

The JwtClientHandler fetches token from TokenProvider and attaches it to header as bearer token.

Now the problem is here, when I try to fetch the token it throws error in TokenProvider.

InvalidOperationException: JavaScript interop calls cannot be issued at this time. This is because the component is being statically rendered. When prerendering is enabled, JavaScript interop calls can only be performed during the OnAfterRenderAsync lifecycle method.

After login user is moved to home page where there is a logout button. It calls the api endpoint for logout which is authorized with [Authorize] attribute and that is when I am facing this error because my endpoint needs the http call to have bearer token which it doesn't get.

One more thing which I forgot to add is that I am not using OnInitialized from Home page as I know JS interop needs to be used in OnAfterRenderAsync. Also I tried adding the prerender false parameter for Routes in App.razor as below but still it keeps trying to fetch data in prerender.

<Routes rendermode= new InteractiveServerRenderMode(prerender: false) />

I am attaching the code for TokenProvider, JwtHttpClientHandler below. Please help me on this.

TokenProvider.cs

public class TokenProvider { private const string TokenKey = "authToken"; private const string RefreshTokenKey = "refreshToken"; private readonly ProtectedLocalStorage _storage; public TokenProvider( ProtectedLocalStorage storage ) { _storage = storage; } public async Task SaveTokenAsync( string token, string refreshToken ) { await _storage.SetAsync( TokenKey, token ); await _storage.SetAsync( RefreshTokenKey, refreshToken ); } public async Task<string?> GetTokenAsync() { try { var result = await _storage.GetAsync<string>( TokenKey ); return result.Success ? result.Value : null; } catch(InvalidOperationException ) { return null; } catch( Exception ex ) { Console.WriteLine( $"GetTokenAsync failed: {ex.GetType().Name} - {ex.Message}" ); return null; } } public async Task<string?> GetRefreshTokenAsync() { try { var result = await _storage.GetAsync<string>( RefreshTokenKey ); return result.Success ? result.Value : null; } catch( InvalidOperationException ) { return null; } catch( Exception ex ) { Console.WriteLine( $"GetRefreshTokenAsync failed: {ex.GetType().Name} - {ex.Message}" ); return null; } } public async Task RemoveTokenAsync() { try { await _storage.DeleteAsync( TokenKey ); await _storage.DeleteAsync( RefreshTokenKey ); } catch { // custom exception middleware } } }

JwtHttpClientHandler.cs

public class JwtHttpClientHandler : DelegatingHandler { private readonly TokenProvider _tokenProvider; public JwtHttpClientHandler( TokenProvider tokenProvider ) { _tokenProvider = tokenProvider ?? throw new ArgumentNullException( nameof( tokenProvider ) ); } protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken ) { var token = await _tokenProvider.GetTokenAsync(); if( !string.IsNullOrWhiteSpace( token ) ) request.Headers.Authorization = new AuthenticationHeaderValue( "Bearer", token ); return await base.SendAsync( request, cancellationToken ); } }
Read Entire Article