🛠 Series: ERP RFQ ModulePart 3 of 8

"Blazor WASM has no server session. The JWT is the session — treat it like one."

Blazor WASM runs entirely in the browser. There is no server-side session. Cookie authentication does not apply — the browser handles cookies, not the WASM runtime. We store the JWT in sessionStorage and build a custom AuthenticationStateProvider that reads it.

Part of the Green Concrete ERP project. View full project page →

Why Blazor WASM Cannot Use Cookie Auth

ASP.NET Core cookie authentication sets an encrypted cookie on the server and validates it on each request. In Blazor WASM, the application runs in the browser — there is no server-side session to validate against. The API is a separate process (GreenConcrete.WebApi). The Blazor app needs a stateless authentication mechanism: JWT.

sessionStorage (not localStorage) is the right choice for enterprise: it expires when the browser tab closes, so a manager who walks away from their desk does not leave an active session on a shared computer.

Custom AuthenticationStateProvider

public class JwtAuthStateProvider : AuthenticationStateProvider
{
    private readonly ISessionStorageService _session;
    private readonly HttpClient _http;

    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        var token = await _session.GetItemAsync<string>("jwt");
        if (string.IsNullOrEmpty(token))
            return Anon();
        var claims = ParseClaimsFromJwt(token);
        _http.DefaultRequestHeaders.Authorization =
            new AuthenticationHeaderValue("Bearer", token);
        var identity = new ClaimsIdentity(claims, "jwt");
        return new AuthenticationState(new ClaimsPrincipal(identity));
    }

    public async Task LoginAsync(string token)
    {
        await _session.SetItemAsync("jwt", token);
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }

    public async Task LogoutAsync()
    {
        await _session.RemoveItemAsync("jwt");
        _http.DefaultRequestHeaders.Authorization = null;
        NotifyAuthenticationStateChanged(Task.FromResult(Anon()));
    }

    private static AuthenticationState Anon() =>
        new(new ClaimsPrincipal(new ClaimsIdentity()));
}

Protected Routes + Role-Based UI

// App.razor — all routes protected, unauthenticated redirects to /login
<CascadingAuthenticationState>
  <Router AppAssembly="typeof(App).Assembly">
    <Found Context="routeData">
      <AuthorizeRouteView RouteData="routeData"
        NotAuthorized="@(() => NavManager.NavigateTo("/login"))" />
    </Found>
  </Router>
</CascadingAuthenticationState>

// NavMenu.razor — Manager-only nav item
<AuthorizeView Roles="Manager,Admin">
  <NavLink href="approvals">Approve Quotations</NavLink>
</AuthorizeView>

// Test users seeded in database:
// ahmed / ahmed123 — Salesperson (no Approve link)
// khalid / khalid123 — Manager (Approve link visible)
// admin / admin123 — Admin (all permissions)

🎯 Quick Check

Q1: Why use sessionStorage instead of localStorage for the JWT?

Show Answer

sessionStorage expires when the browser tab closes — correct for enterprise security where users share computers. localStorage persists across browser restarts, which means a session could remain active indefinitely on a shared machine. For most enterprise applications, sessionStorage is the safer choice.

Q2: If AuthorizeView hides the Approve link from Salespeople, why do we still need [Authorize(Roles="Manager")] on the API controller?

Show Answer

Because the UI is client-side — a salesperson can modify the JavaScript or directly call the API endpoint. The API must enforce authorization independently of what the UI shows. Never trust the client to enforce security. AuthorizeView is a UX convenience; the API authorization is the real security boundary.

Q3: What does NotifyAuthenticationStateChanged do?

Show Answer

It tells the CascadingAuthenticationState to re-evaluate the current user and propagate the change to all components that use AuthorizeView or inject AuthenticationStateProvider. After calling LoginAsync or LogoutAsync, the entire component tree immediately reflects the new authentication state without requiring a page reload.

Key Takeaways

  • Blazor WASM uses JWT + sessionStorage — cookie auth does not apply in a WASM runtime
  • Custom AuthenticationStateProvider reads the JWT from sessionStorage and sets the Authorization header on the HttpClient
  • sessionStorage expires when the tab closes — correct for enterprise shared-computer scenarios
  • AuthorizeRouteView protects navigation; [Authorize(Roles)] on the API enforces the real security boundary
  • NotifyAuthenticationStateChanged propagates login/logout to all components instantly