API Security & Token Hardening
Enterprise-grade API security architecture: 4-layer defense-in-depth, bearer tokens, tenant isolation, rate limiting, CSRF, and CSP.
On this page
SveltyCMS employs a zero-trust architecture for API access — authentication alone is insufficient. Every API request passes through 4 independent security layers that re-validate permissions at each boundary.
1. Defense-in-Depth Architecture
Request → Layer 1 (Middleware) → Layer 2 (Dispatcher) → Layer 3 (Handler) → Layer 4 (Page Action) → Response
| Layer | What It Checks | Failure Mode |
|---|---|---|
| Layer 1 — Middleware | CSP, CORS, HSTS, X-Frame-Options, firewall patterns, body size (10MB), rate limiting, __Host- cookie prefix, CSRF |
403/429 before any business logic runs |
| Layer 2 — Dispatcher | ENDPOINT_PERMISSIONS mapping, SCIM blocking, admin fast-path |
403 for any unmapped namespace |
| Layer 3 — Handler | Admin verification for mutations, media:write/media:delete checks |
403 on unauthorized mutations |
| Layer 4 — Page Action | requireCollectionBuilderPermission(), inline RBAC checks |
403 before state change |
Fail-Closed Default: If a permission mapping is missing or ambiguous, the dispatcher returns 403 — never 200 with leaked data.
2. Token Architecture
2.1 Website Tokens (API Keys)
Designed for machine-to-machine communication. Stateless secrets bound to a specific tenantId and role.
| Property | Detail |
|---|---|
| Generation | CSPRNG with rejection sampling (zero modulo bias) |
| Storage | SHA-256 hashed before persistence — plaintext never stored |
| Types | content-api (read/restricted), admin-api (full access) |
| Expiry | Configurable per-token with automatic invalidation |
| Consumption | Atomic one-time token support via dbAdapter.system.websiteTokens.consume() — prevents TOCTOU race conditions |
| Lookup | Single-pass getByToken — no redundant round-trips |
2.2 Session & CSRF Cookies
Browser-based authentication and mutation security rely on hardened cookie structures:
| Cookie | Name (Prod / Dev) | Attributes | Purpose |
|---|---|---|---|
| Session Cookie | __Host-auth_sessions / auth_sessions |
httpOnly: true, secure: true, sameSite: "strict", path: "/" |
Holds the 32-character CSPRNG session token. Inaccessible to JavaScript (XSS mitigation). |
| CSRF Cookie | __Host-csrf_token / csrf_token |
httpOnly: false, secure: true, sameSite: "strict", path: "/" |
256-bit CSPRNG token read by client fetch utilities to attach the X-CSRF-Token header. |
__Host-Prefix Contract (RFC 6265bis): In HTTPS environments, cookies use the__Host-prefix, requiringsecure: true,path: "/", and forbiddingdomaindelegation. This guarantees subdomains cannot plant, spoof, or overwrite cookies.- Cookie Deletion Parity:
clearSessionCookiessets identical cookie attributes (path: "/",sameSite: "strict",secure: true,httpOnly: true) upon logout or invalidation across all prefix variants (__Host-,__Secure-, plain), ensuring complete cookie removal across all browsers. - Session Lifecycle & Invalidation:
- Auto-rotated upon login and session renewal.
- Password change or logout triggers cascading invalidation across all devices.
- 3-layer caching: Memory (L1) → Redis (L2) → Database (L3) with credential-free memory caching.
3. Authentication Flow
3.1 Bearer Token Validation
1. Extract token from Authorization: Bearer <token> header
2. Single DB query: dbAdapter.system.websiteTokens.getByToken
3. Normalize legacy tokens → default to content-api
4. Create Virtual User context (no full User document hydration)
5. Verify tenantId match → reject mismatched tokens
locals.user = {
_id: `token:${token._id}`,
role: token.type === "admin-api" ? "admin" : "guest",
tenantId: token.tenantId ?? locals.tenantId,
isApiToken: true,
};
3.2 GraphQL Authentication
GraphQL requests pass through the same middleware pipeline as REST:
- HTTP entry:
handleRequest()rejects unauthenticated requests (401) before schema execution - Validation plugin: Query depth limit (8), alias limit (15)
- Introspection: Explicitly blocked in production via
NoSchemaIntrospectionCustomRule - Subscriptions: Context handler validates sessions before WebSocket upgrade
3.3 SCIM 2.0 Authentication
SCIM endpoints (/api/scim/v2/Users, /Groups, /Bulk) use Bearer token authentication with additional tenant-scoped RBAC checks:
- SCIM tokens require
system:adminor explicit SCIM provisioning permissions - Bulk operations are validated for cross-tenant leakage before execution
- PATCH operations use RFC 7644-compliant filter evaluation
4. Multi-Tenant Hardening
4.1 Orphaned Token Protection
If a token’s owner is deleted (tenantId: null), the system isolates the token to the current request’s tenant context — preventing cross-tenant privilege escalation.
4.2 Tenant Isolation Checks
Every Bearer-token request is verified against the environment’s tenantId. Mismatched tokens are rejected with 403 and an audit log event.
4.3 Batch Operation Guard
batch-module.ts rejects coalesced operations containing mixed tenant IDs — preventing cross-tenant data leakage in shared-DB deployments.
5. Rate Limiting & DoS Prevention
| Mechanism | Location | Detail |
|---|---|---|
| Login brute-force | handle-authentication.ts |
handle-rate-limit.ts (RateLimiter) — 5 attempts → 15-min lockout (HTTP 423) |
| API rate limit | handle-rate-limit.ts |
Per-IP + per-tenant fixed window; commerce lane isolated from admin mutations (X-RateLimit-Lane) |
| WAF endpoint limits | SecurityResponseService |
rate-limiter-flexible per path; /api/commerce is 60/min (prefix-matched) |
| Body size limit | api-handler.ts |
10MB cap — returns 413 PAYLOAD_TOO_LARGE before parsing |
| Memory load shedding | hooks.server.ts |
Rejects mutation traffic (503) when heap > 90% |
| Retry-After header | RFC 6585 compliant | All 429 responses include Retry-After: 60 |
6. CSP & Security Headers
All API responses include mandatory security headers:
| Header | Value | Purpose |
|---|---|---|
Content-Security-Policy |
script-src 'self' (production API routes) |
Blocks inline script execution |
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
Enforces HTTPS |
X-Frame-Options |
DENY |
Prevents clickjacking |
X-Content-Type-Options |
nosniff |
Prevents MIME sniffing |
Cross-Origin-Opener-Policy |
same-origin |
Spectre/Meltdown protection |
Cross-Origin-Embedder-Policy |
require-corp |
Cross-origin isolation |
Cross-Origin-Resource-Policy |
same-origin |
Resource isolation |
Referrer-Policy |
strict-origin-when-cross-origin |
Referrer leakage control |
GraphQL Playground CSP: The relaxed CSP (
unsafe-inline,unsafe-eval) is only applied in non-production environments. InNODE_ENV=production, GraphQL endpoints use the same strict CSP as REST APIs.
7. CSRF Protection & Request Verification Architecture
SveltyCMS enforces a hardened Double-Submit Cookie Pattern combined with multi-layer origin and referer verification on all mutating requests (POST, PUT, PATCH, DELETE).
7.1 The Double-Submit Handshake
Client Request (Mutation)
├─ Cookie: __Host-csrf_token=<token_A> (or csrf_token in dev)
├─ Header: X-CSRF-Token: <token_A>
└─ Header: Origin / Referer: https://example.com
│
▼
[1] Safe-Method Bypass (GET / HEAD / OPTIONS) ───► 0µs cost, CSRF skipped
│
[2] Token-Bound Bypass (Bearer / API Key / SCIM) ──► CSRF skipped (stateless)
│
[3] Same-Origin Fast-Path (Origin/Referer === Host) ──► Validated in nanoseconds
│
[4] Constant-Time Token Comparison ──────────────► XOR accumulator (no timing leak)
│
[5] Single-Use Token Rotation ───────────────────► Invalidate old token, issue fresh cookie
- Token Generation: 256-bit CSPRNG token (
crypto.getRandomValues()) placed in thecsrf_token/__Host-csrf_tokencookie (sameSite: "strict",httpOnly: false,path: "/"). - Double Submission: Browser clients extract the cookie value and submit it in the
X-CSRF-TokenHTTP header on every mutating API request (clientJsonHeaders()). - Constant-Time Comparison:
validateCsrfTokenvalidates the header token against the cookie using a bitwise XOR accumulator:let result = 0; for (let i = 0; i < cookieToken.length; i++) { result |= cookieToken.charCodeAt(i) ^ tokenToValidate.charCodeAt(i); } return result === 0; - Single-Use Rotation: On every successful mutation, the server immediately invalidates the consumed CSRF token and issues a fresh token in the response cookie, preventing replay attacks.
- Same-Origin Fast-Path: Same-origin requests (
originUrl.host === hostorrefererUrl.host === host) pass verification via nanosecond string comparison. - Stateless Exemption: Machine-to-machine requests utilizing Bearer tokens, API Keys, or SCIM credentials are automatically exempt from cookie CSRF (they do not rely on ambient browser cookies).
7.2 Check Parity: Read Paths vs. Mutation Paths
To ensure optimal throughput and prevent performance bottlenecks, SveltyCMS maintains strict check parity without duplicate or redundant operations:
| Execution Stage | Read Paths (findById, find, list) |
Mutation Paths (create, update, bulk) |
|---|---|---|
| CSRF Verification | 0 checks (0µs overhead) — Safe HTTP verbs (GET/HEAD) bypass CSRF. |
1 check at the API gateway layer (validateCsrfForRequest). |
| Authentication & RBAC | Single check in handleAuthentication + handleAuthorization (cached in locals). |
Single check in middleware + endpoint permission mapping (_checkEndpointPermission). |
| Schema Resolution | Single lookup from hot in-memory LRU (schemaOf). |
Single lookup from hot in-memory LRU (schemaOf). |
| Payload Preparation | Skipped on read. | Single unified pass (prepareWritePayload) combining sanitization, type validation, and write guards. |
| Database Execution | Single direct prepared query (findById / findOne). |
Single atomic prepared statement (insert / update with RETURNING). |
| Post-Write Processing | None. | Detached / non-blocking background execution (schedulePostWrite) — does not delay response. |
8. Performance Benchmarks
All metrics self-measured via bun test tests/benchmarks/ — reproducible on any machine.
| Metric | p95 | Adapter |
|---|---|---|
| Bearer token validation | 0.06 ms | SQLite (Local SDK) |
| Bearer token validation | 0.37 ms | MariaDB |
| Session validation (L1 cache hit) | <0.001 ms | Memory |
| Session validation (L3 DB fallback) | 1.78 ms | PostgreSQL |
| Full auth pipeline (middleware → RBAC) | 1.61 ms | All adapters |
| API request (auth + RBAC + response) | 1.16 ms | SQLite |
| Concurrent throughput | 1,054 RPS | MariaDB |
Reproduce:
bun test tests/benchmarks/auth-performance.test.ts
9. Audit & Compliance
- Crypto-chained logs: Every API mutation is logged with SHA-256 chained audit entries — tamper-evident verification
- security.txt: RFC 9116 standard at
/.well-known/security.txt - EU Directive 2006/114/EC: All competitive comparisons use verifiable public data
- Incident response: Full runbook
- 0 published CVEs — private reporting via GitHub Security Advisories