DEV Community

Cover image for What broke my JWT flow in Blazor Server and how I fixed it
Prithwiraj Das
Prithwiraj Das

Posted on

What broke my JWT flow in Blazor Server and how I fixed it

Three things I got wrong building my first production Blazor Server app, coming from a background in Angular and traditional ASP.NET.

Originally published on Medium


The one thing you need to understand first

In a standard HTTP app, every user interaction is a new request — new pipeline, new HttpContext, new everything. Blazor Server works differently: the initial page load is an HTTP request, but after that, everything runs over a persistent WebSocket connection (SignalR). There are no further HTTP requests.

This means HttpContext is only reliably available during that first request — once the SignalR circuit is established, it is either null or stale.

Microsoft's documentation puts it plainly:

"IHttpContextAccessor generally should be avoided with interactive rendering because a valid HttpContext isn't always available."

That one sentence explains everything below.


Issue 1 — User metadata cached at the wrong time

What I had:

My UserMetadataService loaded user auth data in the constructor using Task.Run(...).Wait(). Load once, reuse everywhere:

public class UserMetadataService  
{  
    private readonly IHttpContextAccessor _authState;  
    public IUserAuthData userAuthData { get; private set; }  

    public UserMetadataService(IHttpContextAccessor authState)  
    {  
        _authState = authState;  
        Task.Run(LoadUserMetaData).Wait();  
    }  

    private async Task LoadUserMetaData()  
    {  
        var user = _authState.HttpContext?.User;  
        userAuthData = user != null ? new UserAuthData(user.Claims) : null;  
    }  
}
Enter fullscreen mode Exit fullscreen mode

This worked on first load. But for users whose session context needed to be read after circuit establishment — role-based access checks, tenant resolution — the cached value was stale, and for some users it was null entirely.

What fixed it:

Converting userAuthData from a cached property to a computed one:

public sealed class UserMetadataService(IHttpContextAccessor authState)  
{  
    public IUserAuthData userAuthData  
    {  
        get  
        {  
            var user = authState.HttpContext?.User;  
            return user != null ? new UserAuthData(user.Claims) : null;  
        }  
    }  
}
Enter fullscreen mode Exit fullscreen mode

Issue 2 — Reading HttpContext too late in TokenService

This one took longer to find because it only failed intermittently and left almost nothing useful in the logs.

What I had:

My TokenService held a reference to IHttpContextAccessor and read HttpContext inside RefreshTokenAsync() — called whenever a new JWT was needed for an outgoing API request:

public class TokenService : ITokenService  
{  
    private readonly IHttpContextAccessor _httpContextAccessor;  

    public async Task RefreshTokenAsync()  
    {  
        var httpContext = _httpContextAccessor.HttpContext;  
        if (httpContext?.User?.Identity?.IsAuthenticated != true)  
        {  
            _logger.LogWarning("User is not authenticated");  
            _cachedToken = null;  
            _navigationManager.NavigateTo(_navigationManager.Uri, forceLoad: true);  
            return;  
        }  

        var user = httpContext.User;  
        var tokenDescriptor = new SecurityTokenDescriptor  
        {  
            Subject = new ClaimsIdentity(user.Claims.ToArray()),  
            IssuedAt = DateTime.UtcNow,  
            Expires = DateTime.UtcNow.AddMinutes(3),  
        };  
    }  
}
Enter fullscreen mode Exit fullscreen mode

The constructor runs during initial circuit setup, before SignalR takes over. That is the one reliable window to read from HttpContext in a Blazor Server service. Reading it anywhere else — from a method, a timer, a component event — is not safe.


Issue 3 — JwtTokenForwardHandler extending the wrong base class

What I had:

public class JwtTokenForwardHandler : HttpClientHandler { }  

// In Program.cs  
.ConfigurePrimaryHttpMessageHandler(serviceProvider =>  
    serviceProvider.GetRequiredService<JwtTokenForwardHandler>());
Enter fullscreen mode Exit fullscreen mode

HttpClientHandler is the primary transport handler — it owns the socket and the connection pool. Registering it via ConfigurePrimaryHttpMessageHandler tells IHttpClientFactory to treat it as the transport layer, which means it gets pooled and shared across requests. Any user-specific state on that handler can persist across request boundaries.

What fixed it:

public class JwtTokenForwardHandler : DelegatingHandler  
{  
    private readonly ITokenService _tokenService;  

    public JwtTokenForwardHandler(ITokenService tokenService)  
    {  
        _tokenService = tokenService;  
    }  

    protected override async Task<HttpResponseMessage> SendAsync(  
        HttpRequestMessage request,  
        CancellationToken cancellationToken)  
    {  
        var token = await _tokenService.GetCurrentTokenAsync();  
        request.Headers.Authorization =  
            new AuthenticationHeaderValue("Bearer", token);  
        return await base.SendAsync(request, cancellationToken);  
    }  
}  

// In Program.cs  
.AddHttpMessageHandler<JwtTokenForwardHandler>();
Enter fullscreen mode Exit fullscreen mode

A DelegatingHandler is outgoing middleware — it sits in front of the transport layer, adds the token per call inside SendAsync, and passes control down the chain. No state is stored on the handler instance.


What I took away from this

All three of these patterns felt correct when I wrote them. They were correct in every HTTP-based context I had used them in. The shift Blazor Server requires is not obvious from reading the API surface — it only becomes clear once you understand that the circuit model fundamentally changes when HttpContext exists and what service lifetimes mean in that environment.

References

Top comments (0)