Blazor WebAssembly este o schimbare fundamentală față de tot ce am discutat în seria de până acum. Codul tău C# compilat rulează în browserul clientului, nu pe server. Asta înseamnă că regulile de securitate se schimbă semnificativ — unele lucruri devin mai simple, altele devin imposibile.
Cel mai important principiu al acestui articol: orice rulează în WASM poate fi inspectat, modificat și falsificat de utilizator. Blazor WASM este o aplicație client, nu un server de încredere. Securitatea reală trăiește pe server — WASM-ul o vizualizează și o respectă, dar nu o poate garanta singur.
1. Blazor WASM vs. Blazor Server — diferențe de securitate
Înainte de implementare, merită înțelese diferențele fundamentale dintre cele două modele Blazor din perspectiva securității:
| Aspect | Blazor Server | Blazor WebAssembly |
|---|---|---|
| Unde rulează codul | Server (SignalR) | Browser (WASM) |
| Acces la baza de date | Direct (EF Core, etc.) | Numai prin API HTTP |
| Secretele aplicației | Rămân pe server | Nu există secrete — totul e vizibil |
| Logica de autorizare | Pe server, de încredere | Pe client, doar UX — serverul validează |
| Token-uri de autentificare | Cookie HttpOnly, invizibil JS | În memorie sau localStorage, accesibil JS |
| Suprafața de atac XSS | Mai mică (token invizibil JS) | Mai mare (token accesibil din JS) |
Concluzia practică: în Blazor WASM, API-ul backend este ultima linie de apărare. Nu există scurtătură.
2. Autentificare cu OIDC în Blazor WASM
Blazor WASM folosește Authorization Code Flow cu PKCE pentru autentificare — exact ce am discutat în articolul despre OAuth2/OIDC, dar implementat pe client. Nu există client secret (e o aplicație publică), securitatea vine din PKCE.
2.1 Instalare pachete
dotnet add package Microsoft.AspNetCore.Components.WebAssembly.Authentication
2.2 Configurare în Program.cs (client)
// Program.cs — proiectul Blazor WASM client
builder.Services.AddOidcAuthentication(options =>
{
// Citit din wwwroot/appsettings.json
builder.Configuration.Bind("OidcConfiguration", options.ProviderOptions);
options.ProviderOptions.ResponseType = "code"; // Authorization Code Flow
options.ProviderOptions.DefaultScopes.Add("openid");
options.ProviderOptions.DefaultScopes.Add("profile");
options.ProviderOptions.DefaultScopes.Add("myapi");
});
2.3 wwwroot/appsettings.json
{
"OidcConfiguration": {
"Authority": "https://your-identityserver.com",
"ClientId": "blazor-wasm-client",
"PostLogoutRedirectUri": "https://localhost:5001/authentication/logout-callback",
"RedirectUri": "https://localhost:5001/authentication/login-callback",
"ResponseType": "code"
}
}
Atenție:
wwwroot/appsettings.jsoneste un fișier public — oricine poate descărca acest fișier din browser. Nu pune niciodată secrete, connection strings sau chei API aici. Doar configurare publică: Authority URL, ClientId, redirect URIs.
2.4 Pagina de autentificare — Authentication.razor
@page "/authentication/{action}"
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
<RemoteAuthenticatorView Action="@Action">
<LoggingIn>
<p>Se redirecționează spre autentificare...</p>
</LoggingIn>
<CompletingLoggingIn>
<p>Finalizare autentificare...</p>
</CompletingLoggingIn>
</RemoteAuthenticatorView>
@code {
[Parameter] public string? Action { get; set; }
}
2.5 App.razor — protecție globală cu CascadingAuthenticationState
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData"
DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
@if (!context.User.Identity?.IsAuthenticated ?? true)
{
<RedirectToLogin />
}
else
{
<p>Nu ai permisiunile necesare pentru această pagină.</p>
}
</NotAuthorized>
<Authorizing>
<p>Se verifică autentificarea...</p>
</Authorizing>
</AuthorizeRouteView>
</Found>
</Router>
</CascadingAuthenticationState>
3. Protecția rutelor și componentelor
3.1 [Authorize] pe pagini
@page "/dashboard"
@attribute [Authorize]
<h1>Dashboard</h1>
<p>Conținut vizibil doar utilizatorilor autentificați.</p>
@page "/admin"
@attribute [Authorize(Roles = "Admin")]
<h1>Panou Admin</h1>
3.2 AuthorizeView în componente
<AuthorizeView>
<Authorized>
<p>Bun venit, @context.User.Identity?.Name!</p>
<button @onclick="DoSomethingSecure">Acțiune securizată</button>
</Authorized>
<NotAuthorized>
<p>Te rugăm să te autentifici.</p>
</NotAuthorized>
</AuthorizeView>
<!-- Cu roluri -->
<AuthorizeView Roles="Admin,Manager">
<Authorized>
<button>Șterge utilizator</button>
</Authorized>
</AuthorizeView>
<!-- Cu policy -->
<AuthorizeView Policy="CanExportData">
<Authorized>
<button>Export CSV</button>
</Authorized>
</AuthorizeView>
Repetiție importantă:
[Authorize]șiAuthorizeViewîn Blazor WASM sunt exclusiv UI — ascund sau afișează elemente vizuale. Un utilizator care inspectează codul WASM poate ocoli aceste verificări. Autorizarea reală se face pe API.
4. Apeluri API securizate — IHttpClientFactory cu token automat
Blazor WASM oferă un mecanism elegant pentru a atașa automat access token-ul la apelurile HTTP, fără să te ocupi manual de fiecare request.
4.1 Configurare HttpClient cu token handler
// Program.cs
builder.Services.AddHttpClient("SecureApiClient",
client => client.BaseAddress = new Uri("https://api.myapp.com"))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
// Înregistrare și pentru injecție directă
builder.Services.AddScoped(sp =>
sp.GetRequiredService<IHttpClientFactory>()
.CreateClient("SecureApiClient"));
BaseAddressAuthorizationMessageHandler atașează automat Authorization: Bearer <token> la fiecare request spre base address — și gestionează refresh-ul token-ului transparent.
4.2 Utilizare în componente
@inject HttpClient Http
@inject NavigationManager Navigation
@code {
private List<ProductDto>? _products;
protected override async Task OnInitializedAsync()
{
try
{
_products = await Http.GetFromJsonAsync<List<ProductDto>>(
"api/products");
}
catch (AccessTokenNotAvailableException ex)
{
// Token-ul nu e disponibil — redirectăm la login
ex.Redirect();
}
catch (HttpRequestException ex) when
(ex.StatusCode == HttpStatusCode.Forbidden)
{
// Autentificat, dar fără permisiuni
Navigation.NavigateTo("/access-denied");
}
}
}
4.3 AuthorizationMessageHandler pentru API-uri multiple
Dacă aplicația ta apelează mai multe API-uri cu domenii diferite, BaseAddressAuthorizationMessageHandler nu e suficient — trimite token-ul doar la base address. Folosești AuthorizationMessageHandler cu URL-uri explicite:
builder.Services.AddHttpClient("ExternalApiClient",
client => client.BaseAddress = new Uri("https://external-api.com"))
.AddHttpMessageHandler(sp =>
sp.GetRequiredService<AuthorizationMessageHandler>()
.ConfigureHandler(
authorizedUrls: new[]
{
"https://external-api.com",
"https://another-api.com"
},
scopes: new[] { "myapi", "external-api" }));
5. Stocarea token-urilor — unde și de ce contează
Biblioteca Microsoft.AspNetCore.Components.WebAssembly.Authentication stochează token-urile în memorie (session storage intern, nu localStorage sau sessionStorage browser). Aceasta este alegerea corectă din perspectiva securității:
| Locație stocare | Persistent după refresh | Accesibil din JS | Vulnerabil la XSS | Recomandat |
|---|---|---|---|---|
| Memorie (implicit WASM) | Nu | Nu (izolat WASM) | Minim | ✅ Da |
sessionStorage |
Nu (tab curent) | Da | Da | ⚠️ Cu precauție |
localStorage |
Da | Da | Da — persistent! | ❌ Nu |
| Cookie HttpOnly | Da (configurat) | Nu | Nu | ✅ Da (Hosted WASM) |
Dezavantajul stocării în memorie: la refresh de pagină, utilizatorul e redirecționat la login sau se face un silent refresh prin iframe (dacă Authorization Server-ul suportă). Aceasta este o limitare acceptabilă față de riscul de securitate al localStorage.
6. Ce nu poți ascunde în Blazor WASM
Aceasta este secțiunea pe care mulți o ignoră și care produce cele mai grave greșeli de securitate în aplicații Blazor WASM.
Codul sursă compilat
Assembly-urile .NET compilate sunt descărcate în browser și pot fi inspectate cu orice decompilator .NET (ILSpy, dnSpy, dotPeek). Un utilizator determinat poate vedea:
- Logica de business din componente și servicii
- URL-urile API-urilor pe care le apelezi
- Structura modelelor de date
- Orice string hardcodat în cod
Ce înseamnă asta în practică
// NU FACE ASTA în Blazor WASM — cheia e vizibilă oricui
private const string ApiKey = "sk-live-abc123xyz";
// NU FACE ASTA — connection string vizibil
private const string ConnectionString =
"Server=prod-db;Database=MyApp;Password=secret";
// NU FACE ASTA — logica de prețuri manipulabilă pe client
private decimal CalculatePrice(Product p)
=> p.BasePrice * 0.8m; // reducere 20% — poate fi manipulată
// CORECT — totul sensibil rămâne pe server
// Clientul apelează API-ul, serverul calculează prețul
var price = await Http.GetFromJsonAsync<PriceDto>(
$"api/products/{productId}/price");
Verificări de autorizare în WASM
// NESIGUR — verificare de securitate pe client
@code {
private async Task DeleteUser(int userId)
{
// Această verificare poate fi ocolită
if (!await AuthService.IsAdminAsync())
return;
await Http.DeleteAsync($"api/users/{userId}");
// API-ul TREBUIE să verifice și el permisiunile
}
}
Verificarea din WASM e UX — previne click-uri accidentale. Verificarea din API e securitate — previne atacuri intenționate.
7. Securizarea API-ului backend — obligatoriu
Am discutat pe larg autorizarea API în articolele #4 (Role-based vs. Policy-based), #5 (Claims transformation) și #8 (IdentityServer/Keycloak). În contextul Blazor WASM, regulile se aplică fără excepție. Un reminder concis:
// API Controller — validare completă, indiferent de ce face clientul WASM
[ApiController]
[Route("api/[controller]")]
[Authorize] // autentificare obligatorie
public class OrdersController : ControllerBase
{
[HttpDelete("{id}")]
[Authorize(Policy = "CanDeleteOrders")] // autorizare granulară
public async Task<IActionResult> Delete(int id)
{
// Verificăm că resursa aparține utilizatorului curent
// Nu ne bazăm că Blazor a filtrat asta deja
var order = await _repo.GetByIdAsync(id);
if (order is null)
return NotFound();
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (order.UserId != userId)
return Forbid(); // utilizatorul nu deține această resursă
await _repo.DeleteAsync(id);
return NoContent();
}
}
8. Hosted Blazor WASM — avantaje de securitate
Blazor WASM poate fi găzduit de un server ASP.NET Core (Hosted mode). Asta deschide opțiuni de securitate suplimentare față de standalone:
Cookie-based auth cu BFF (Backend for Frontend)
Pattern-ul BFF mută gestionarea token-urilor complet pe server, eliminând expunerea lor în browser:
// Server project (ASP.NET Core host)
// Gestionează sesiunea OIDC și expune API-uri proxy spre downstream
builder.Services
.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
})
.AddOpenIdConnect(options =>
{
options.Authority = "https://your-idp.com";
options.ClientId = "bff-client";
options.ClientSecret = "secret"; // pe server, deci sigur
options.SaveTokens = true;
});
// Blazor WASM apelează server-ul local prin cookie HttpOnly
// Token-ul JWT nu ajunge niciodată în browser
BFF este pattern-ul recomandat pentru aplicații cu cerințe de securitate ridicate — elimină complet riscul de furt de token prin XSS.
9. Content Security Policy pentru Blazor WASM
CSP în Blazor WASM necesită atenție specială față de ce am văzut în articolul #12, deoarece WASM are cerințe specifice:
// Blazor WASM necesită 'wasm-unsafe-eval' pentru execuția WASM
// și 'unsafe-inline' poate fi necesar pentru Blazor internals
app.Use(async (context, next) =>
{
context.Response.Headers.Append(
"Content-Security-Policy",
string.Join("; ",
"default-src 'self'",
// WASM necesită acest flag
"script-src 'self' 'wasm-unsafe-eval'",
"style-src 'self' 'unsafe-inline'", // Blazor CSS
"img-src 'self' data:",
"connect-src 'self' https://your-idp.com https://api.myapp.com",
"frame-src 'self' https://your-idp.com", // OIDC silent refresh
"frame-ancestors 'none'"
));
await next();
});
Adaugă connect-src cu toate domeniile la care aplicația face fetch — Identity Provider, API backends, CDN-uri. Dacă lipsesc, request-urile vor fi blocate de browser fără eroare vizibilă în UI.
10. Probleme comune și soluțiile lor
| Problemă | Cauza probabilă | Soluție |
|---|---|---|
AccessTokenNotAvailableException |
Token expirat sau sesiune pierdută | Prinde excepția și apelează ex.Redirect() pentru re-autentificare |
| Redirect loop la login | Ruta de callback nu e definită sau Authentication.razor lipsește |
Verifică că pagina /authentication/{action} există și RedirectUri din config coincide cu ce e înregistrat la IdP |
| Token nu e trimis la API | BaseAddressAuthorizationMessageHandler nu e adăugat pe HttpClient |
Verifică înregistrarea din Program.cs; pentru domenii externe folosește AuthorizationMessageHandler |
| Claims lipsă în componentă | GetClaimsFromUserInfoEndpoint nu e activat sau claims nu sunt mapate |
Adaugă options.ProviderOptions.AdditionalProviderParameters.Add("claims", ...); sau configurează claims mapping pe IdP |
| CORS error la apel API | API-ul nu acceptă Origin-ul aplicației Blazor | Configurează CORS pe API cu originea exactă a aplicației Blazor; nu folosi wildcard în producție |
| Pagina protejată vizibilă înainte de redirect | AuthorizeRouteView nu are <Authorizing> configurat |
Adaugă template <Authorizing> cu loading spinner — previne flash de conținut neautorizat |
11. Checklist de producție
- ✅ Zero secrete în codul WASM sau în
wwwroot/appsettings.json - ✅ Toată logica de autorizare replicată pe API — WASM face doar UX
- ✅ Token-uri stocate în memorie, nu în
localStorage - ✅
BaseAddressAuthorizationMessageHandlerpe HttpClient-urile spre API - ✅
AccessTokenNotAvailableExceptionprins și redirectat corect în toate componentele cu apeluri API - ✅ CSP configurat cu
wasm-unsafe-evalșiconnect-srcexplicit pentru toate domeniile - ✅ CORS pe API restricționat la originea exactă a aplicației Blazor
- ✅ API verifică ownership-ul resursei (nu doar autentificarea) pentru operații CRUD
- ✅
<Authorizing>template configurat — previne flash de conținut înainte de verificarea autentificării - ✅ Considerat pattern BFF pentru aplicații cu cerințe de securitate ridicate
Concluzie
Blazor WebAssembly este o platformă excelentă pentru aplicații web moderne, dar vine cu un model de securitate fundamental diferit față de aplicații server-side. Principiul de bază nu se schimbă indiferent de tehnologie: nu ai încredere în client.
Blazor WASM face autentificarea și autorizarea vizuală convenabilă prin [Authorize] și AuthorizeView. Dar acestea sunt instrumente UX, nu gardieni de securitate. Gardienii reali sunt pe server — API-ul care validează fiecare request independent, fără să presupună că Blazor a verificat deja ceva.
Seria continuă cu Rate Limiting în .NET — un mecanism esențial pentru a proteja API-urile de abuz, indiferent de frontrunner-ul folosit.