Blazor lets one C# codebase run on the server, in the browser through WebAssembly, or both, and teams often forget that the security model changes completely depending on where a component renders. In interactive server rendering, component code runs on the server and the browser holds a SignalR connection; in WebAssembly the compiled assemblies are downloaded to the user, who can read and change them. An [Authorize] attribute or an AuthorizeView that is a real control on the server is only a display hint in the browser. This guide explains how identity and authorisation actually work across Blazor render modes, where tokens and secrets belong, how to harden circuits and the SignalR channel, and what to log. It is for .NET developers, application security engineers and reviewers assessing Blazor Web Apps, Blazor Server and standalone Blazor WebAssembly applications, and it builds on the ASP.NET Core Security Guide rather than repeating it.
Key takeaways
- Client-side authorisation is not a control. Microsoft's Blazor documentation states that authorisation checks in client-side code can be bypassed; every rule must also be enforced in a server endpoint or server-rendered component.
- Interactive Auto has two enforcement points. The component's
[Authorize]protects the server render path and the web API protects the WebAssembly path; securing one does not secure the other. - Nothing secret ships in WebAssembly. Connection strings, client secrets, API keys and private business logic in a WASM assembly are public; call your own server API, which holds the credentials.
- Prefer a backend for frontend for tokens. Keeping access and refresh tokens server side in the authentication cookie (OIDC with
SaveTokens) removes them from browser JavaScript and storage. - Treat every circuit event as untrusted input. Event arguments, JS interop results and calls into .NET from JavaScript can carry any payload, and server memory and connections are shared across all users.
How identity and access work in Blazor
Blazor in .NET 8 and later uses per-component render modes. Where a component runs decides what its security checks are worth.
| Render mode | Where code runs | How the user is identified | What enforces access |
|---|---|---|---|
| Static server-side rendering (SSR) | Server, per HTTP request | Normal ASP.NET Core authentication from HttpContext.User (cookie, OIDC) | [Authorize] on the routable component, endpoint policies, the authentication middleware redirect (for example LoginPath) |
| Interactive Server | Server, inside a SignalR circuit | Authentication state captured when the circuit's WebSocket connects, held for the connection's lifetime and re-evaluated on reconnect | [Authorize], checks in event handlers and services, revalidation of the circuit |
| Interactive WebAssembly | Browser, .NET runtime in WebAssembly | Serialised state from the server (PersistentAuthenticationStateProvider) or tokens from Microsoft.AspNetCore.Components.WebAssembly.Authentication | Only the server API the client calls. Client checks are user experience only |
| Interactive Auto | Server first, then browser once the runtime is cached | Both of the above | Both the component (server path) and the API (client path) |
Server-side authentication state
- Circuit identity: navigation inside a circuit does not send cookies, so the circuit keeps the principal it received at connect time. A user whose account is disabled stays signed in to an open circuit until it is revalidated.
- Revalidation:
RevalidatingServerAuthenticationStateProviderrechecks the principal on aRevalidationInterval. Microsoft documents a default of 30 minutes for Identity or cookie authentication. Returning false fromValidateAuthenticationStateAsyncmarks the circuit unauthenticated but does not invalidate the authentication cookie, so a full page load can sign the user back in until the cookie is rejected as well. - Forced sign-out with ASP.NET Core Identity: shorten the interval in
IdentityRevalidatingAuthenticationStateProvider, setSecurityStampValidatorOptions.ValidationIntervaljust inside it, and callUserManager<TUser>.UpdateSecurityStampAsyncon disable, password reset or role removal. Without Identity, check a data store inCookieAuthenticationEvents.OnValidatePrincipaland callRejectPrincipal. - Do not validate in navigation: Microsoft advises against a custom
NavigationManagerfor authentication checks; use a customAuthenticationStateProvider. - Identity UI belongs in Razor Pages:
SignInManager<TUser>andUserManager<TUser>are not supported inside Razor components, so sign-in, registration and account management pages run as static SSR or Razor Pages.
WebAssembly authentication state
- Serialised state: a Blazor Web App flows the server principal to the client with
AddAuthenticationStateSerializationon the server andAddAuthenticationStateDeserializationon the client. TheSerializeAllClaimsoption controls whether every claim or only name and role claims travel; send only what the UI needs, because the client can read all of it. - Fixed for the session: client authentication state is fixed for the lifetime of the WebAssembly app; sign-in and sign-out require a full page reload, which matters when roles change mid-session.
- Standalone WASM with OIDC: the authentication library supports only the authorisation code flow with PKCE. The implicit grant is not supported. Microsoft notes that refresh tokens cannot be secured in a WebAssembly app and recommends short lifetimes, rotation and a maximum lifetime that forces interactive sign-in.
- Fallback policy:
AuthorizationOptions.FallbackPolicywithRequireAuthenticatedUseris not supported in WebAssembly. Apply@attribute [Authorize]in_Imports.razorand mark theAuthenticationcomponent[AllowAnonymous]so the sign-in redirect works.
What AuthorizeView does and does not do
AuthorizeView and role-gated NavLink elements decide what is rendered. They do not stop a user calling an event handler, navigating to a route or calling an API directly. Put the check in the handler, the service and the endpoint, and load protected data from an authorised server call instead of embedding it in the client. Use the cascading Task<AuthenticationState> parameter for procedural decisions. See the Broken Access Control Prevention Guide.
Admin roles and privileged access
- Policy, not role strings in markup: define named authorisation policies (
AddAuthorization(o => o.AddPolicy("CanApprovePayments", ...))) and apply them to components with[Authorize(Policy = "...")]and to endpoints with.RequireAuthorization("...").AuthorizeView Roles="Admin, Superuser"means either role; requiring both needs nested views, which is easy to get wrong, so prefer a policy. - Resource-based checks on the server: for "may this user edit this record", inject
IAuthorizationServiceinto the server service and callAuthorizeAsync(user, resource, policy). A WASM client that sends a record ID must never decide this itself. See the OWASP API Top 10 API1 Broken Object Level Authorization Guide. - Separate admin surfaces: admin pages that only work as Interactive Server or static SSR keep admin logic and data off the client entirely. If an admin area must be WebAssembly, assume its code is read by every authenticated user and keep the logic in the API.
- Step-up for dangerous actions: require a recent authentication or a stronger method for role changes, payouts and exports, enforced by a policy that checks an authentication time or method claim from the identity provider.
- Revoke quickly: removing a role in the directory does not reach an open circuit or a running WASM app until revalidation or reload. Pair role removal with a security stamp update and a revalidation interval measured in minutes for privileged users.
Integrations, API tokens and service accounts
| Credential | Wrong place | Right place |
|---|---|---|
| Database connection string | WASM wwwroot/appsettings.json, client assembly | Server only, ideally a managed identity with no password at all |
| OIDC client secret | Client project, source control | Server configuration from Secret Manager in development and a vault in staging and production |
| Third-party API key | Client HttpClient default headers | Server API or YARP forwarder that adds the key server side |
| User access and refresh tokens | Browser storage readable by script | Server, in the authentication cookie's properties (SaveTokens = true), attached to outgoing calls by a DelegatingHandler |
| SignalR hub access | Anonymous hub | [Authorize] on the hub; standalone WASM passes the token through options.AccessTokenProvider |
- Backend for frontend: Microsoft's Blazor Web App OIDC sample keeps tokens on the server, refreshes them non-interactively using the
offline_accessscope, and either calls downstream APIs from server code or forwards client calls with YARPMapForwarder, attaching the user's token. The browser sees only a same-site cookie. See the OAuth for Browser-Based Applications and the BFF Pattern Guide. - Separate registrations: Microsoft recommends separate identity provider registrations for the client and the server API. Scope the client registration to the API only and do not enable implicit grant token issuance on either.
- Claims mapping: set
MapInboundClaims = falseon the OIDC handler for most providers so claim names are not rewritten, and base policies on the issuer's real claim names. - Environment variables: Microsoft's guidance is to avoid environment variables for sensitive data outside local development and to use managed identities for Azure services. See the Secrets Management Guide.
- Machine callers: background jobs and server-to-server calls from the Blazor host use their own workload identity with the narrowest scopes, never a user's saved token or a shared admin key. See the Service Account Security Guide.
Logging and detection
Detailed circuit errors go to server logs while the client receives a generic message, so the server log is the only place many Blazor attacks show up.
| Signal | Source | Why it matters |
|---|---|---|
| Authorisation failures on API endpoints called by WASM | ASP.NET Core authorisation logs, API gateway | A user probing endpoints the UI hides; repeated 403s on admin routes from a non-admin |
| Circuit exceptions and disconnects | Server logs for the circuit host | Malformed event payloads, interop abuse or resource exhaustion attempts |
| Rejected antiforgery and cross-origin posts (400) | Antiforgery middleware | CSRF attempts against SSR forms |
| Concurrent circuits per user and per IP | Custom CircuitHandler metrics | Connection flooding, since there is no per-user limit by default |
| Token refresh failures and reuse | Identity provider sign-in logs | Stolen refresh tokens in standalone WASM apps |
| Sign-in, role and security stamp changes | Identity store audit | Evidence that revocation reached active sessions |
Use a CircuitHandler to record circuit open and close events with the user identifier, and never log tokens or full claims sets. See the Secure Logging Vocabulary and Application Security Events Guide.
Metrics that show the controls work:
- Share of WASM-called API endpoints with an explicit authorisation policy (target 100%).
- Number of secrets or keys found by scanning published
wwwrootand_frameworkoutput (target zero). - Time from account disable to loss of access in open circuits (should be at or under the revalidation interval).
- Peak concurrent circuits per user against the configured limit.
- Count of components using
AddMarkupContentorMarkupStringwith non-constant input.
Hardening baseline
- Inventory render modes. List every routable component and its render mode; flag any WebAssembly or Auto component that touches sensitive data, and confirm the data arrives from an authorised API.
- Enforce on the server first. Add
[Authorize]or a policy to every server-rendered routable component and.RequireAuthorization()to every API endpoint; then add client checks for usability. Test each endpoint with a low-privilege token. - Configure antiforgery. The header-based protection added by the template inspects
Sec-Fetch-SiteandOriginon unsafe methods; for token-based protection callUseAntiforgeryafterUseRouting,UseAuthentication,UseAuthorizationandUseHttpsRedirection. Never setDisableCsrfProtectionwithout an alternative. See the CSRF Prevention Guide. - Tune circuits. Review
CircuitOptions:DisconnectedCircuitMaxRetained,DisconnectedCircuitRetentionPeriod,JSInteropDefaultCallTimeout(documented default one minute) andMaxBufferedUnacknowledgedRenderBatches, plusHubConnectionContextOptions.MaximumReceiveMessageSize. Require authentication before a circuit starts where the app allows, and cap circuits per user. - Restrict origins. Blazor's SignalR endpoint can be reached cross-origin by default; restrict it with SignalR CORS configuration or
DisableCorsAttribute. See the CORS Misconfiguration Guide. - Render safely. Razor expressions encode output; do not pass untrusted strings to
AddMarkupContentorMarkupString, never write user data throughinnerHTMLin interop, and add a restrictive Content Security Policy. See the Content Security Policy Guide. - Block framing and side channels. Set CSP
frame-ancestors(andX-Frame-Optionsfor older clients). Microsoft warns that WebSocket compression enables CRIME and BREACH style attacks, so do not render secrets or personal data in the same render batch as attacker-controlled content, and keep the default iframe block when compression is on. - Validate redirects. Do not pass user input to
NavigationManager.NavigateTowithout an allowlist. See the Open Redirect Guide. - Disable detailed errors outside development and serve the app only over HTTPS with HSTS.
- Roll back safely. Ship authorisation changes behind configuration so a policy that locks out legitimate users can be relaxed without redeploying the client, and stage them through a pilot group first.
Common misconfigurations
- Hidden menu equals protected page: a role check removes the
NavLink, but the routable component has no[Authorize], so typing the URL loads it. - API trusts the WASM client: the client checks
IsInRole("Admin")before callingDELETE /api/users/{id}, and the endpoint has no policy. Anyone with a valid session can call it. This is the pattern ASVS 5.0 requirement V8.3.1 targets: authorisation must sit at a trusted service layer, not in code the consumer controls. - Auto render secured once: the team secures the API used by the WebAssembly path but the server render path calls the service directly with no component attribute, or the reverse.
- Secrets in the client bundle: a key placed in
wwwroot/appsettings.jsonor a constant in a shared class library is published inside the downloadable assemblies. - Singleton user state: a singleton service caches per-user data with keys a user can influence, leaking one user's data into another user's circuit.
- Revocation assumed instant: an account is disabled during an incident but its circuit lives on for the default 30-minute revalidation window, and the cookie keeps working.
- Interop trusted: values returned by
IJSRuntime.InvokeAsyncor methods exposed with[JSInvokable]are used without validation, although any page script can call them.
Framework mapping
| Blazor control | NIST SP 800-53 Rev. 5 | NIST CSF 2.0 | OWASP ASVS 5.0 | ISO/IEC 27001:2022 Annex A |
|---|---|---|---|---|
| Server-side authorisation on components and APIs | AC-3, AC-6 | PR.AA-05 | V8.3.1 | A.8.3, A.8.26 |
| Server-side input validation of events and interop | SI-10 | PR.PS-06 | V2.2.2 | A.8.28 |
| Tokens and secrets kept off the client | IA-5, SC-28 | PR.AA-05 | V14.3.3 | A.5.17, A.8.24 |
| Circuit revalidation and session control | SC-23 | PR.AA-05 | n/a | A.8.5 |
| HTTPS for the SignalR channel | SC-8 | PR.DS-02 | n/a | A.8.24 |
| Circuit and authorisation logging | AU-2 | DE.CM-09 | n/a | A.8.15 |
Relevant ATT&CK techniques for threat modelling are T1190 Exploit Public-Facing Application, T1539 Steal Web Session Cookie and T1552.001 Credentials In Files (secrets left in published client files). CIS Controls v8.1 Control 16, Application Software Security, covers the secure development practices here.
Where identity fits
Blazor apps hold more non-human credentials than they appear to: the OIDC client secret, the data protection key ring that protects the authentication cookie, downstream API keys and the managed identity of the host. The data protection keys deserve special care in a web farm, because anyone who can read them can forge cookies; persist them to protected storage and encrypt them at rest. Inventory each credential, give it an owner and a rotation date, and prefer workload identity federation or managed identities over stored secrets. See the .NET Security Guide.
Practitioner checklist
- Every routable server-rendered component carries
[Authorize]or a named policy, and every API endpoint uses.RequireAuthorization(). - No authorisation decision relies only on
AuthorizeView, hidden links or client-side role checks. - Published client output (
wwwroot,_framework) is scanned for secrets in CI and contains none. - User tokens are held server side in a BFF design, or standalone WASM uses PKCE, short-lived rotating refresh tokens and a maximum session lifetime.
- Serialised claims sent to the client are limited to what the UI needs.
- Revalidation interval and security stamp validation are set so disabled privileged users lose circuit access within minutes.
- Antiforgery is configured in the correct middleware order and SignalR origins are restricted.
CircuitOptionsand hub message size limits are reviewed, and circuits per user are capped and monitored.- No untrusted input reaches
AddMarkupContent,MarkupString,innerHTMLorNavigateTo; CSP andframe-ancestorsare set. - Detailed errors are disabled in production and circuit events are logged without tokens.
Standards and references
- Microsoft Learn: ASP.NET Core Blazor authentication and authorization
- Microsoft Learn: Threat mitigation guidance for ASP.NET Core Blazor interactive server-side rendering
- Microsoft Learn: Secure ASP.NET Core Blazor WebAssembly
- Microsoft Learn: Secure an ASP.NET Core Blazor Web App with OpenID Connect
- OWASP: Application Security Verification Standard 5.0
- NIST: SP 800-53 Rev. 5
- NIST: Cybersecurity Framework 2.0
Related NHI Mgmt Group resources: ASP.NET Core Security Guide · OAuth for Browser-Based Applications and the BFF Pattern Guide · Client-Side Security Guide · Broken Access Control Prevention Guide