Skip to content

Auth in Blazor

Key Points

  • AuthenticationStateProvider is the central abstraction. Provides current ClaimsPrincipal. Different impls per render mode.
  • Interactive Server: cookie auth typically. Auth happens server-side; principal flows to component via AuthenticationStateProvider.
  • Interactive WASM: token-based (PKCE OIDC). AuthenticationStateProvider reads from local store / cookies via BFF.
  • Auto mode: state must transfer from server prerender to WASM (use PersistentComponentState).
  • <AuthorizeView> + [Authorize] for declarative gating. <AuthorizeRouteView> at router for page-level.

Concepts (deep dive)

Setup (Interactive Server / Static SSR)

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie();

builder.Services.AddAuthorization();
builder.Services.AddCascadingAuthenticationState();

app.UseAuthentication();
app.UseAuthorization();
@inherits LayoutComponentBase

<AuthorizeView>
    <Authorized>Hello, @context.User.Identity?.Name!</Authorized>
    <NotAuthorized><a href="/login">Login</a></NotAuthorized>
</AuthorizeView>

Setup (Interactive WASM)

For standalone WASM with OIDC:

// Client/Program.cs
builder.Services.AddOidcAuthentication(o =>
{
    builder.Configuration.Bind("Auth", o.ProviderOptions);
    o.ProviderOptions.DefaultScopes.Add("openid");
    o.ProviderOptions.DefaultScopes.Add("api.read");
});
"Auth": {
  "Authority": "https://login.microsoftonline.com/{tenant}/v2.0",
  "ClientId": "...",
  "RedirectUri": "https://localhost:5001/authentication/login-callback",
  "PostLogoutRedirectUri": "https://localhost:5001/authentication/logout-callback",
  "ResponseType": "code"
}

<AuthorizeView>

<AuthorizeView Roles="Admin">
    <Authorized>You are admin</Authorized>
    <NotAuthorized>Not authorized</NotAuthorized>
    <Authorizing>Loading...</Authorizing>
</AuthorizeView>

<AuthorizeView Policy="MinimumAge">
    <Authorized>...</Authorized>
</AuthorizeView>

Per-page authorization

@page "/admin"
@attribute [Authorize(Roles = "Admin")]

<h1>Admin Panel</h1>

In App.razor / Routes.razor:

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="@routeData">
            <NotAuthorized>
                <p>Not authorized.</p>
            </NotAuthorized>
        </AuthorizeRouteView>
    </Found>
</Router>

AuthenticationStateProvider

@inject AuthenticationStateProvider Auth

@code {
    string? _user;

    protected override async Task OnInitializedAsync()
    {
        var state = await Auth.GetAuthenticationStateAsync();
        _user = state.User.Identity?.Name;
    }
}

Or use the cascading parameter:

<CascadingAuthenticationState>
    <App />
</CascadingAuthenticationState>

@* In any component: *@
[CascadingParameter] public Task<AuthenticationState>? AuthState { get; set; }

Token storage in WASM — Pitfalls

  • localStorage: vulnerable to XSS. Tokens stolen via injected script.
  • sessionStorage: same issue; lost on tab close.
  • HttpOnly cookies: best — JS can't read.
  • In-memory: lost on refresh; reasonable for short tasks.

For production WASM: use BFF (Backend-for-Frontend) pattern. SPA never holds the token; backend (cookie session) does. See JWT Validation Pitfalls.

BFF pattern with Blazor WASM

Browser (cookie session) → Blazor Host (bearer token) → API

User signs in via OIDC against Blazor Host (cookie). Host calls API server-to-server with bearer. WASM never sees the token.

Auto mode auth

AuthenticationState must flow: - Server prerender: cookie auth. - WASM: needs same identity.

Use PersistentComponentState to transfer:

// PersistedAuthState provider
[Inject] PersistentComponentState State { get; set; } = default!;

// Persist on server side
State.RegisterOnPersisting(() =>
{
    State.PersistAsJson("auth", currentUser);
    return Task.CompletedTask;
});

// Restore on WASM side
if (State.TryTakeFromJson<UserInfo>("auth", out var u)) /* ... */;

The Blazor template scaffolds this for Auto mode.

Calling secured APIs

// WASM
public class ApiClient(HttpClient http, IAccessTokenProvider tokens)
{
    public async Task<T> GetAsync<T>(string url)
    {
        var token = await tokens.RequestAccessToken();
        if (token.TryGetToken(out var accessToken))
            http.DefaultRequestHeaders.Authorization = new("Bearer", accessToken.Value);
        return await http.GetFromJsonAsync<T>(url) ?? throw new();
    }
}

Or AuthorizationMessageHandler:

builder.Services.AddHttpClient("api", c => c.BaseAddress = new("https://api/"))
    .AddHttpMessageHandler<AuthorizationMessageHandler>();

Auto-attaches token.

Logout

Server side:

await HttpContext.SignOutAsync();

WASM:

<a href="authentication/logout">Logout</a>

The OIDC handler navigates to IdP logout, clears cookie, redirects back.

Authorization policies

builder.Services.AddAuthorization(o =>
{
    o.AddPolicy("MinimumAge", p => p.RequireClaim("age", "18", "19", /* ... */));
});
<AuthorizeView Policy="MinimumAge">...</AuthorizeView>

See Authorization Policies & Claims.

Identity-relevant lifecycle

  • User logs in → cookie set / token stored → AuthenticationStateProvider notified → components re-render.
  • User logs out → state cleared → re-render.
  • Token expires → silent refresh or redirect to login.

Force re-render on auth change

public class CustomAuthProvider : AuthenticationStateProvider
{
    public override Task<AuthenticationState> GetAuthenticationStateAsync() { /* ... */ }

    public void NotifyChanged()
    {
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }
}

Common pitfalls

  • Storing tokens in localStorageXSS.
  • Mixing auth schemes between server and WASM in Auto mode.
  • No PersistentComponentState — re-auth roundtrip in Auto.
  • AuthorizeView Authorizing slot missing — flash of "not authorized".

Code: correct vs wrong

❌ Wrong: token in localStorage

localStorage.setItem('token', accessToken);

WASM sends with credentials; backend reads cookie.

❌ Wrong: page-level [Authorize] without router config

@page "/admin"
@attribute [Authorize]

Without <AuthorizeRouteView>, attribute ignored at routing. Add to App.razor.

✅ Correct: AuthorizeRouteView in router

<AuthorizeRouteView RouteData="@routeData" />

Design patterns for this topic

Pattern 1 — "BFF over SPA-direct OAuth"

  • Intent: SPA never holds tokens.

Pattern 2 — "AuthorizeView for declarative gating"

  • Intent: clean component-level UI hiding.

Pattern 3 — "PersistentComponentState for Auto"

  • Intent: seamless server→WASM transition.

Pattern 4 — "AuthorizationMessageHandler for outbound auth"

  • Intent: auto-attach token to API calls.

Pattern 5 — "Authorization policies"

  • Intent: centralized rules.

Pros & cons / trade-offs

Pattern Pros Cons
BFF Secure More moving parts
Cookie auth (Server) Simple Server state
OIDC + WASM (direct) Standalone Token-in-browser risks
Auto mode Best UX State transfer complexity

When to use / when to avoid

  • Use BFF for SPA + secure tokens.
  • Use cookie auth for Interactive Server / Static SSR.
  • Use OIDC + WASM for offline-capable PWA.
  • Avoid localStorage tokens.

Interview Q&A

Q1. AuthenticationStateProvider purpose? Central abstraction for current ClaimsPrincipal. Per-render-mode implementations.

Q2. Cookie vs token in Blazor? Cookie: server (Interactive Server, Static SSR). Token: WASM standalone.

Q3. BFF pattern? Backend holds tokens; SPA uses cookie session to backend. SPA never sees the token.

Q4. AuthorizeView vs [Authorize]? View: in-component declarative. Attribute: route/component-level via AuthorizeRouteView.

Q5. localStorage for tokens — risk? XSS reads it. HttpOnly cookies preferred.

Q6. Auto mode auth state? Persisted via PersistentComponentState from server prerender to WASM.

Q7. AuthorizationMessageHandler? Auto-attaches token to outbound HTTP calls.

Q8. Logout flow? SignOutAsync (server) or navigate to authentication/logout (WASM OIDC).

Q9. Policies in Blazor? Same as ASP.NET — AddAuthorization with named policies.

Q10. NotifyAuthenticationStateChanged? Force re-render when auth state changes (login/logout).

Q11. Authorizing slot in AuthorizeView? Show during auth check; avoids flash of NotAuthorized.

Q12. Multi-tenant Blazor auth? OIDC with dynamic authority per tenant; or claim-based tenant.


Gotchas / common mistakes

  • ⚠️ Tokens in localStorage.
  • ⚠️ No AuthorizeRouteView — page-level attributes ignored.
  • ⚠️ Auto mode without state transfer — re-auth.
  • ⚠️ Mixing schemes confusedly in Auto.

Further reading