Security¶
This document describes BimZone's security architecture, controls, and responsible disclosure process.
Authentication¶
JWT + rotating refresh tokens
Access tokens are short-lived JSON Web Tokens (15-minute expiry, HS256 signed with JWT_SECRET). Long-lived refresh tokens (30-day expiry, REFRESH_TOKEN_TTL_DAYS) are stored as a SHA-256 hash in the database and delivered via HttpOnly; SameSite=Strict; Secure cookies (with the __Secure- prefix in production, scoped to Path=/api/auth). Every token rotation issues a new refresh token and revokes the previous one, carrying a shared token-family id forward across the rotation chain. A fixed 30-second reuse-grace window absorbs parallel in-flight requests (duplicated bootstrap calls, or multiple tabs racing the same cookie). Reuse-detection: a refresh token that was already rotated (revoked, outside the grace window) but is replayed while still unexpired is treated as a stolen/leaked token — the entire token family is immediately revoked (back-dated past the grace window) and the session cleared, forcing a fresh login for both attacker and victim. Audited as auth.refresh_reuse_detected.
TOTP MFA
TOTP two-factor authentication (RFC 6238, 30-second window) is supported for all accounts. Workspace administrators can enforce MFA for all members, and enforcement is server-authoritative: a member of an MFA-enforced workspace who has not yet enrolled receives no usable session on login — only a short-lived, limited-scope setup grant (typ:"mfa-setup") that can drive enrolment and nothing else; the real session is issued only once TOTP is active. The password step alone likewise never yields a bearer credential — the MFA challenge token (typ:"mfa") is structurally distinct and is rejected by the access-token verifier. Recovery codes (eight single-use codes, SHA-256 hashed) are provided at enrolment.
HIBP password check
Passwords are validated against the HaveIBeenPwned k-anonymity API at registration and password change; compromised passwords are rejected with a clear error message. Passwords are hashed with argon2id (memoryCost 19456 KiB, timeCost 2, parallelism 1).
Account lockout
Five consecutive failed login attempts trigger a 15-minute account lockout. The lockout timestamp is stored server-side; client-supplied timestamps are never trusted.
Invitation-only access
Self-registration is disabled. All access to the platform is granted through workspace invitations issued by a workspace admin or owner. Invitation tokens are single-use with a configurable expiry and carry a pre-assigned workspace and optional project role.
SSO (SAML 2.0 / OAuth)
Google and Microsoft OAuth 2.0 are supported as identity providers. A SAML 2.0 service-provider integration is also available (SP metadata at /api/auth/saml/metadata). Both flows preserve the invitation-only access model — users must be invited before they can authenticate via SSO.
Authorization¶
Four-tier project RBAC
Access is enforced server-side on every request. There are four project roles:
| Role | Capabilities |
|---|---|
admin |
Full project access |
upload_engineer |
Upload models, create issues/RFIs |
viewer |
Read-only access to all project data |
inspector |
Create photo-linked issues, run inspections |
Workspace owners and workspace admins implicitly hold admin rights on all projects in their workspace. Roles are checked in the route handler using requireProjectRole; the role is never derived from the client payload.
Server-authoritative checks
Authorization is never delegated to the client. Every mutation verifies the caller's current database-resident role before acting. There is no capability field in the JWT that could be spoofed by a modified token.
Data protection¶
Signed storage URLs
Files (IFC models, point clouds, photos, screenshots) are never served from publicly addressable paths. Each download URL is HMAC-signed with a configurable TTL (default 15 minutes) and bound to the specific storage key; the signature is verified server-side before the file is streamed.
Parameterized queries
All database access goes through Drizzle ORM, which uses parameterized queries exclusively. Raw SQL fragments used for aggregate expressions are typed and reviewed. User-supplied strings are never interpolated into SQL.
Input validation
Every API endpoint uses Zod schemas to validate and coerce request payloads before they reach business logic. Unexpected fields are stripped. Type-safe schema validation is enforced at compile time and at runtime.
HTTP hardening
Helmet is applied globally, setting X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Strict-Transport-Security, and a restrictive Referrer-Policy. A Content Security Policy is enforced at the reverse-proxy / nginx layer in production (see nginx.conf).
Rate limiting
@fastify/rate-limit is applied globally (configurable via RATE_LIMIT_MAX and RATE_LIMIT_WINDOW). Authentication endpoints carry a tighter per-IP rate limit. When REDIS_URL is configured the limiter uses the shared Redis store so the limit is enforced across all nodes; without Redis it degrades to an in-memory per-node limiter (byte-identical to a single-node deployment).
Audit log
Every create, update, delete and access action records: workspace ID, project ID, actor user ID and email, IP address, user-agent, action type, entity type and ID, and a details JSONB payload. The audit log is append-only and never modified. A scheduled retention job prunes records older than the workspace-configured retention period.
Infrastructure and deployment¶
On-premises / self-hosted option
BimZone ships as a single Node.js process (apps/server) and a static web bundle (apps/web/dist). All state lives in a PostgreSQL database and a pluggable object-storage backend (local filesystem or any S3-compatible API: AWS S3, MinIO, Cloudflare R2, DigitalOcean Spaces, Backblaze B2). Organizations that require full data sovereignty can run the entire stack on their own infrastructure with no data leaving their network.
Secret management
All secrets (JWT_SECRET, JWT_REFRESH_SECRET, STORAGE_*) are loaded from environment variables at process start. The repository contains no secrets. .env files are .gitignored.
Transport security
HTTPS with TLS 1.2+ is required in production. The development server runs over plain HTTP on localhost only. Refresh token cookies carry the Secure flag (and the __Secure- prefix) and are not transmitted over plain HTTP; the server refuses to start with NODE_ENV=production and a plain-http:// PUBLIC_URL. TLS is terminated either at an upstream load balancer / cloud edge (the default nginx.conf runs on :80 behind it) or by nginx itself using apps/web/nginx.tls.conf (TLS 1.2/1.3, HSTS, HTTP→HTTPS redirect). The API also emits HSTS via Helmet as defence-in-depth.
Compliance roadmap¶
The following items are on the roadmap and are planned before General Availability. They are not yet attained.
- SOC 2 Type II — controls mapping and evidence collection in progress; formal audit engagement planned pre-GA.
- Penetration test — an independent black-box and grey-box penetration test is planned before the first production customer deployment.
- ISO 27001 — under evaluation for enterprise and government deployments.
Responsible disclosure¶
If you discover a security vulnerability in BimZone, please report it privately to the workspace operator or platform administrator. Do not open a public GitHub issue for security findings. Allow reasonable time for a patch before any public disclosure. We aim to acknowledge all reports within 72 hours and to provide a remediation timeline within 7 days.
For the hosted / SaaS deployment, contact the platform operator directly through the support channel provided during onboarding.