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.
- One password rule wherever a password is set (creating a user, an administrator's reset, account setup, a reset link, the first administrator): at least Min Password Length characters (Settings → Session & Security; 12 by default and never fewer), with an uppercase and a lowercase letter, a digit and a symbol unless Enforce Password Complexity is off. The setup and reset pages state the rule in effect. Changing your own password needs your current one.
- Verification via
bcrypt.compare()(constant-time over the digest). Plaintext is discarded after the auth request and never persisted, logged, or returned. - Sign-in does not reveal accounts. An unknown email and a wrong password both answer Invalid email or password, and an unknown email still costs a bcrypt comparison, so neither the answer nor its timing tells which addresses have accounts. The lockout and single sign-on answers appear only after the right password.
The server applies the rule wherever a password is set; the browser checks it first only to explain it. 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. Tampering invalidates the HMAC and the token is rejected. The role used for access is the account's current role, read on every request, not the one in the token. - 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 Idle Timeout (Settings → Session & Security, default 10 minutes) as an inactivity window; Auto-Logout on Inactivity off turns it off for everyone. 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.
Per-user revocation is immediate. Every request looks up the account behind the token. Deactivating or deleting a user ends their sessions on their next request (401 SESSION_ENDED, and the browser returns to sign-in with the reason); a role change applies to the next request; an administrator's password reset, or a password reset through a setup or reset link, ends every session issued before it. Rotating JWT_SECRET still ends all sessions at once.
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. Enrollment:/api/auth/mfa/setup→/api/auth/mfa/enable. - Sign-in is two steps: the password returns a 5-minute challenge that no other endpoint accepts (it is signed with a key derived separately from session tokens), and the session is issued only after
/api/auth/mfa/verifyaccepts a TOTP or backup code. The sign-in screen asks for the code. - Wrong codes count toward the same lockout as wrong passwords (Max Login Attempts and Lockout Duration in Settings → Session & Security; 5 attempts and 15 minutes by default), and a correct password does not reset the count for an MFA user until the code is verified.
- Turning MFA off requires a current code.
- 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
Every API route except sign-in, license status, health checks and client error reporting requires a signed-in user. That includes all of /api/connections, /api/db-devops/*, /api/ai/* and /api/notifications. Requests without a valid session token get 401.
Each user has exactly one built-in role: Admin, Release Manager, Approver or User. The server enforces these checks, and in DB DevOps a non-admin also needs the section the change belongs to (Configuration, Change Management, Schema or Governance) in one of their roles, or the change is refused with 403 SECTION_REQUIRED; see Roles & Access Control.
| Action | Who can do it |
|---|---|
| Author and edit migration scripts; request an approval | Any signed-in user |
| Approve or reject a script; sign, reject or sign off an approval or release | Approver, Release Manager or Admin |
| Withdraw a signature from a pending request | The signer, or Admin |
| Revoke an approved request | Approver, Release Manager or Admin |
| Create or edit a release; delete a script; run, complete, fail or roll back an execution | Release Manager or Admin |
| Delete a release or an approval request | Admin |
| Add or edit a DB DevOps connection (including bulk upload and import into DB DevOps) | Release Manager or Admin |
| Add, edit, rename or delete environments and groupings | Release Manager or Admin |
| Delete a DB DevOps connection | Admin |
| View and test DB DevOps connections | Any signed-in user |
| IDE connections | Their owner and admins, or everyone when the connection is shared. Anyone else gets 404 wherever the connection is named by its id: listing, connecting, the Object Explorer, queries, AI, Schema Ops and seed data |
| Create, edit, deactivate or delete users; roles; settings; AI provider configuration; identity provider configuration; data-protection export and erasure | Admin |
| Change a password | Admin for any user, or the user for their own account |
| Notifications and notification preferences | Always the signed-in user's own |
Self-approval. Admins choose, under Settings, Approval, whether people may approve their own work (Allow Self-Approval, on by default so a one-person install keeps working). When it is off, the server refuses with 403 and the reason: approving a script you wrote, signing an approval request you made or one for a script you wrote or a release you created, and signing off a release you created. The matching buttons are shown disabled, with the reason on hover. The switch is stored on the server and every change to it is audited under the signed-in admin.
A request that fails a role check gets 403 Insufficient permissions. Who did something is always taken from the signed-in session: script authors, approvers, rejecters, approval requesters, signers, release creators and execution runners are recorded as the signed-in user, whatever the request body says, so the audit trail can't be attributed to someone else. A signed-in user can still approve their own script if their role allows it. For a strict two-person rule on production, give authors the User role and approvers the Approver role, and treat Admin as a break-glass identity rather than a daily driver.
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
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 works with any active license.