Web Security¶
Package: Asdamir.Web · Namespace: Asdamir.Web.Security
Introduction¶
Asdamir.Web hardens Blazor/HTTP apps with a set of composable middleware and services: a Content-Security-Policy nonce, security headers, request rate limiting, ASP.NET Core Data Protection, route authorization, audit logging and auto-logout.
Registration¶
builder.Services.AddFrameworkSecurity(builder.Configuration);
...
app.UseSecurityHeaders(); // HSTS, X-Content-Type-Options, frame options, etc.
app.UseCspNonce(); // per-request CSP nonce (no inline-script holes)
app.UseRateLimiting(); // per-key request throttling
app.UseAuditLogging(); // capture audited requests
app.UseNoCache(); // no-store on sensitive responses
Add only what you need — each middleware has its own Use… extension.
Building blocks¶
| Concern | What it provides |
|---|---|
| CSP nonce | ICspNonceProvider + middleware; pages emit a per-request nonce so scripts are allow-listed without unsafe-inline |
| Security headers | SecurityHeadersOptions — HSTS, X-Content-Type-Options, frame/referrer/permissions policies |
| Rate limiting | IRateLimitService (in-memory or DB-backed for scale-out) + [RateLimit] attribute + middleware |
| Data Protection | DataProtectionService — encrypt/decrypt tokens (e.g. email/refresh) with keyed purposes |
| Route authorization | blocks unauthorized navigation before render; decisions are cached and audited |
| Auto-logout | idle detection + session activity tracking + session-warning dialog |
| Code analysis | AddSecurityAnalysis() runs static security checks (optionally at startup) |
CSP nonce example¶
Rate limiting¶
[RateLimit(3, 300)] // limit, windowSeconds — e.g. ForgotPassword
public async Task<IActionResult> ForgotPassword(...) { ... }
IRateLimitService is a fixed-window limiter. The default InMemoryRateLimitService is per-process —
fine for a single instance, but behind a load balancer each replica counts independently (a brute-force
attacker gets N × limit). For scale-out, back it with a shared store so counters aggregate
across instances. AppManagement does this in Dapper mode: a SqlRateLimitService (counters in
dbo.RateLimitCounters, atomic via the dbo.RateLimit_TryAcquire proc) guards the login / 2FA endpoints
so the limit holds across all API instances. Swap the registered IRateLimitService to choose per-host
vs. shared.
Data Protection keys (cookies + antiforgery)¶
Auth cookies and antiforgery tokens are encrypted with ASP.NET Data Protection keys. By default
that key ring is per-host and ephemeral in containers — every restart silently logs users out and
breaks antiforgery, and separate instances can't read each other's cookies (scale-out). Call
AddFrameworkDataProtection(builder.Configuration) on each UI host to fix that:
// appsettings.json (host)
"DataProtection": {
// Stable name so all instances share one key ring (the default discriminator is the content-root
// path, which differs per deployment/container and would isolate the keys).
"ApplicationName": "MyApp",
// A durable, shared directory — a persistent / mounted volume in production. Empty = the ASP.NET
// default location (fine for single-host dev). Filesystem keeps this UI-tier-friendly (no DB).
"KeyPath": "/var/asdamir/dpkeys"
}
The generated app's Server and AppManagement's AdminConsole already call it; set KeyPath to a shared
volume for restart-safe, multi-instance auth. (To encrypt the keys at rest or use a DB/Redis ring,
swap the provider in AddFrameworkDataProtection.)