Authentication Security
Sessions are stateless JWTs; there is no server-side session table. This trades revocation immediacy for horizontal scalability and eliminates a session store as an attack and exhaustion target. The consequences of that trade-off are stated explicitly below.
Credential Hashing
- bcrypt, cost factor 12. ~200-300ms/hash on current hardware, acceptable interactive latency, hostile to offline cracking. Cost is adaptive and can be raised without migrating existing hashes.
- Per-hash 128-bit salt defeats precomputation; identical passwords yield distinct digests.
- Verification via
bcrypt.compare()(constant-time over the digest). Plaintext is discarded after the auth request and never persisted, logged, or returned.
Password-strength enforcement is applied at the application layer at /api/auth/register. Organizational controls (reuse history, dictionary/compromised-password blocking, rotation cadence) belong in your IdP when SSO is in use.
JWT Session Tokens
- Algorithm: HS256. Header/payload/signature;
algis pinned server-side, do not accept a client-declared algorithm (noalg: none, no RS/HS confusion). - Claims:
userId,email,name,role,iat,exp. Theroleclaim drives RBAC; tampering invalidates the HMAC and the token is rejected. - Lifetime: 8h expiry. Every request re-verifies signature and
exp; expiry yields 401 and a client redirect to login. - Transport: issued in the response body (not a cookie) and presented as
Authorization: Bearer <token>. Because no ambient cookie carries the session, the API is not CSRF-exploitable.
Revocation Semantics (state this to auditors)
Inactivity timeout: independent of the token's absolute lifetime, the client enforces the Idle timeout setting (Settings → Session, default 10 minutes) as an inactivity window. Interaction (pointer, keyboard, scroll) refreshes the window; once it elapses — checked periodically and again the moment the tab regains focus after a laptop wake — the app disconnects the user's database connections, clears the session, and returns to the sign-in screen with an explanatory notice.
Statelessness means an already-issued token remains valid until exp. Deactivating a user blocks new logins immediately but does not retroactively kill an outstanding token; rotating JWT_SECRET invalidates all live tokens at once (a blunt, global revocation). Plan break-glass around these two levers: fast global cut via secret rotation, per-user revocation bounded by the 8h window. If sub-8h per-principal revocation is a hard requirement, front the app with your IdP's session controls.
Token Storage Trade-off
The bearer token lives in localStorage. This is an explicit, defensible choice given the header-based scheme:
- Header-carried tokens are structurally immune to CSRF (no ambient credential).
- The exposure that remains is XSS-driven exfiltration, mitigated, not eliminated, by CSP, the strict CORS allowlist, and the bounded token lifetime.
- An httpOnly cookie would blunt XSS exfiltration but reintroduce CSRF and its token-management burden. The net risk was judged comparable; the header model was kept for its CSRF properties and API ergonomics.
The load-bearing dependency is therefore XSS prevention. Preserving the CSP and treating any new sink for untrusted HTML/script as a security-relevant change is the mitigating discipline.
Multi-Factor Authentication
- TOTP (RFC 6238) via
otpauth; enrollment QR rendered withqrcode. Flow:/api/auth/mfa/setup→/api/auth/mfa/enable. - The TOTP seed is encrypted at rest under
EncryptionService. - 10 single-use backup codes, stored as SHA-256 hashes, a lost database yields no usable recovery codes.
RBAC & Separation of Duties
Exactly one role per principal, enforced by RBAC middleware and license-tier route gating:
| Role | Author scripts | Approve | Execute | Manage releases | Manage users / config |
|---|---|---|---|---|---|
| Admin | Yes | Yes | Yes | Yes | Yes |
| Developer | Yes | No | No | No | No |
| Reviewer | No | Yes | No | No | No |
| Deployer | No | No | Yes | Yes | No |
| Viewer | No | No | No | No | No |
The workflow enforces a two-person rule for anything reaching production: author (Developer) ≠ approver (Reviewer) ≠ executor (Deployer). Gating is applied at the role and license-tier boundary (route/feature level) rather than as exhaustive per-action server checks, model Admin as a break-glass identity, not a daily driver, and provision role-specific accounts for routine work.
Brute-Force & Enumeration Resistance
- Per-IP rate limiting on auth endpoints (general window default 2000/15min; tunable via
GENERAL_RATE_LIMIT_MAX). - bcrypt's ~200-300ms verification caps guess throughput independent of the limiter.
- Uniform failure responses regardless of account existence, no username oracle.
SSO Federation (Enterprise)
IdentityProviderService brokers SAML, OIDC, and LDAP, plus first-class providers (Azure AD, Okta, Google, AWS Cognito). Federated deployments should delegate password policy, MFA, conditional access, and session revocation to the IdP and treat the local credential path as break-glass only. IdP federation routes are Enterprise-tier gated.