Persist Blazor Authentication State Across Page Refreshes

Summary

An application intended to implement password-protected “rooms” failed to maintain the authenticated state of the user. The developer attempted to store the session state within an AddScoped service, but discovered that the state was lost upon every browser refresh. This resulted in a broken user experience where users were repeatedly prompted for credentials even after successfully entering them.

Root Cause

The fundamental issue stems from a misunderstanding of Service Lifetimes within the context of Blazor’s execution models (specifically Blazor WebAssembly or Blazor Server):

  • Scoped Service Lifecycle: In Blazor WebAssembly, a Scoped service lives for the duration of the user’s browser session (the tab). However, when a user hits “Refresh,” the entire WebAssembly runtime is re-initialized, effectively destroying the existing memory heap and creating a fresh instance of all services.
  • State Volatility: Memory-resident variables (in-memory state) are inherently volatile. They cannot survive a page reload because the client-side application instance is completely terminated and restarted by the browser.
  • The “Memory-Only” Fallacy: Treating a C# class instance as a persistent storage mechanism for authentication is equivalent to assuming data survives a power outage in a physical machine without a disk.

Why This Happens in Real Systems

This is a classic case of confusing Application State with Session State.

  • In-Memory State: Used for transient UI data (e.g., current selected tab, current text in an input box). It is lost on reload.
  • Session/Persistent State: Used for authentication and user identity. This must be backed by a persistent store (Cookies, LocalStorage, or a Database) so that the identity can be reconstituted after a process restart or a page refresh.

Real-World Impact

  • Degraded User Experience: Users are trapped in an “authentication loop” where they must re-authenticate every time they refresh the page or accidentally trigger a reload.
  • Increased Latency: The server or client must repeatedly execute authentication logic and UI transitions that should have been completed once.
  • Security Vulnerabilities: If engineers attempt to “fix” this by using insecure methods (like URL parameters for passwords), they introduce massive security holes.

Example or Code

// WRONG: This loses state on refresh
public class RoomAccessService
{
    public bool IsRoomUnlocked { get; set; }
}

// RIGHT: Use browser storage to persist state across refreshes
public class SecureRoomService
{
    private readonly IJSRuntime _jsRuntime;
    private const string StorageKey = "room_access_token";

    public SecureRoomService(IJSRuntime jsRuntime)
    {
        _jsRuntime = jsRuntime;
    }

    public async Task SetAuthenticated(string token)
    {
        await _jsRuntime.InvokeVoidAsync("localStorage.setItem", StorageKey, token);
    }

    public async Task IsAuthenticated()
    {
        var token = await _jsRuntime.InvokeAsync("localStorage.getItem", StorageKey);
        return!string.IsNullOrEmpty(token);
    }
}

How Senior Engineers Fix It

Senior engineers decouple identity from the application lifecycle. To solve this specific problem, we use Token-based Authentication or Cookie-based Authentication:

  • Persistence Layer: Instead of a boolean in a service, we use window.localStorage or window.sessionStorage via JavaScript Interop to store a non-sensitive identifier or a JWT (JSON Web Token).
  • OnInitializedAsync Pattern: In the Blazor component, we implement the OnInitializedAsync lifecycle method to check the persistent storage as soon as the component mounts.
  • State Container Pattern with Persistence: We implement a StateProvider that, upon initialization, attempts to “hydrate” its state from the browser’s local storage.
  • AuthenticationStateProvider: For robust ASP.NET Core integration, we would implement a custom AuthenticationStateProvider that reads the authentication status from a cookie or a token in the browser, ensuring the Blazor security framework recognizes the user immediately upon page load.

Why Juniors Miss It

  • Focus on Logic, Not Lifecycle: Juniors often focus on the logic (“If password is correct, set IsAuthorized = true“) and forget that the environment (the browser tab) is not a permanent container.
  • Over-reliance on In-Memory Variables: There is a tendency to treat C# objects like “global variables,” ignoring the fact that the entire application instance is ephemeral in client-side web development.
  • Abstracting Too Early: Juniors often try to solve the problem at the “Service” layer without understanding how the underlying browser-to-server communication actually works.

Leave a Comment