Skip to content

Security

Defense-in-depth security architecture.

Overview

flowchart TB
    subgraph Edge["Edge Protection"]
        A[HTTPS/TLS]
        B[Security Headers]
        C[Rate Limiting]
    end

    subgraph Auth["Authentication"]
        D[JWT Tokens]
        E[Refresh Tokens]
        F[Permission Version]
    end

    subgraph Bot["Bot Protection"]
        G[CAPTCHA]
        H[Honeypot]
        I[Blocked Domains]
    end

    subgraph Data["Data Protection"]
        J[Input Validation]
        K[Output Encoding]
        L[SQL Injection Prevention]
    end

    Edge --> Auth
    Auth --> Bot
    Bot --> Data

Security Headers

Applied via SecurityHeadersMiddleware:

Header Value Purpose
Content-Security-Policy default-src 'none'; frame-ancestors 'none' XSS/framing prevention (API responses; Swagger and Hangfire UIs excluded)
X-Content-Type-Options nosniff MIME sniffing prevention
X-Frame-Options DENY Clickjacking prevention (Hangfire paths use CSP frame-ancestors with the allowed origin instead)
Strict-Transport-Security max-age=31536000; includeSubDomains HTTPS enforcement (non-development only)
Referrer-Policy strict-origin-when-cross-origin Referrer control
Permissions-Policy geolocation=(), camera=(), microphone=() Feature restrictions
Cross-Origin-Opener-Policy same-origin Window isolation
Cross-Origin-Resource-Policy same-origin Resource sharing

Rate Limiting

Per-endpoint rate limiting via [RateLimit] attribute protects against abuse.

See Middleware for details.

Bot Protection

CAPTCHA (Turnstile)

Cloudflare Turnstile protects public forms:

  • Registration
  • Password reset request (forgot password)

Invisible challenge for most users.

Honeypot Fields

Hidden form fields that bots fill:

<input
  name="website"
  style={{ display: "none" }}
  tabIndex={-1}
  autoComplete="off"
/>

Requests with filled honeypot are rejected.

Blocked Email Domains

Disposable email domains are blocked at registration.

Configured via Security__Account__BlockedEmailDomains (array in appsettings.json).

Token Security

Access Tokens (JWT)

  • Short-lived (15 minutes default)
  • Contains user ID, role, group ID, permission version
  • Transmitted via Authorization header
  • Validated on every request

Refresh Tokens

  • Long-lived (1-30 days)
  • Stored in HTTP-only cookie — SameSite=None + Secure on HTTPS (frontend and API run on different origins), SameSite=Lax on local HTTP
  • Single-use (rotated on refresh)
  • Stored hashed in database

Permission Version

When a group's permissions change (e.g., enabled modules):

  1. The group's PermissionsVersion increments
  2. Existing access tokens carry a stale PermsVersion claim
  3. PermsVersionMiddleware compares the claim against the current version (cached) and rejects stale tokens with 401 and error code auth.policy_changed
  4. Admins are exempt from the check
  5. Client refreshes token automatically to pick up new permissions

Input Validation

All input validated via FluentValidation:

  • Type checking
  • Length limits
  • Format validation (email, URL)
  • Business rule validation

See Validation Pattern.

SQL Injection Prevention

  • EF Core parameterized queries
  • No raw SQL with user input
  • DbContext abstractions

Secrets Management

Secret Storage
JWT signing key Environment variable
Database connection Environment variable
API keys Environment variable
User passwords bcrypt hash in database

Never logged, never in source control.

Audit Trail

All entities track:

  • CreatedByUserId
  • CreatedDate
  • UpdatedByUserId
  • UpdatedDate

Soft delete preserves data with DeletedDate.