RO EN

Secure Blazor WebAssembly — securitate când codul rulează în browser

Secure Blazor WebAssembly — securitate când codul rulează în browser
Doru Bulubașa
18 May 2026

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.json este 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] și AuthorizeView î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
  • BaseAddressAuthorizationMessageHandler pe HttpClient-urile spre API
  • AccessTokenNotAvailableException prins și redirectat corect în toate componentele cu apeluri API
  • ✅ CSP configurat cu wasm-unsafe-eval și connect-src explicit 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.