Skip to content

Account security

The /admin dashboard and the OAuth consent login are protected by several independent layers. Regular MCP tokens (/mcp) are unaffected — this page is about human login.

Two-factor authentication (TOTP) — optional

Section titled “Two-factor authentication (TOTP) — optional”

Each user account may turn on time-based one-time-password 2FA (RFC 6238, compatible with Google Authenticator, Authy, 1Password, etc.). It is optional and per account — 2FA is not forced on anyone.

  • Turning it on. A user opens /admin/security (or goes straight to /admin/setup-2fa), scans the QR (or types the manual key), and confirms a 6-digit code. From then on that account uses 2FA.
  • Login is two-step once enabled. For an enrolled account, password comes first, then the 6-digit code at /admin/2fa; a correct password alone only yields a short-lived partial session that can reach nothing but the code page. An account without 2FA logs straight in.
  • Turning it off. The user goes to /admin/security and enters a current 6-digit code (or a backup code) to disable it — proof of possession, so a stolen session cannot silently strip the factor. (API: POST /admin/api/2fa/disable.)
  • Backup codes. Ten one-time codes are shown once at enrolment. Each works a single time in place of the authenticator if you lose your phone. Save them somewhere safe — they are not shown again.
  • The secret is encrypted at rest (AES-GCM with MASTER_TOKEN).

A super-admin opens /admin/users, finds the row, and clicks Reset 2FA. That removes the authenticator and revokes the user’s sessions; the user can re-enrol later. (API: POST /admin/api/users/:id/reset-2fa.)

Login attempts are rate-limited on two independent counters over a rolling 15-minute window:

  • 5 failures for one username → that account is locked out.
  • 20 failures from one IP → that IP is locked out.

A locked login is refused with HTTP 429 and a Retry-After header, before the password is even checked (so it also protects CPU). A successful login clears the username’s failure streak. Applies to both /admin/login and the OAuth consent login.

Passwords are PBKDF2-HMAC-SHA256, 600,000 iterations (OWASP 2023), with a random 16-byte salt. Hashes created at an older/lower cost are transparently re-hashed on the next successful login — no mass reset, no lockout.

Password policy (user-chosen passwords): ≥ 8 characters, ≥ 1 digit, ≥ 1 special character. Admin-generated provisioning passwords are random and high-entropy by construction.

Every state-changing /admin/* request (login, JSON APIs, actions) must be same-origin: a cross-site request carrying an attacker Origin/Referer is refused with HTTP 403. The session cookie is HttpOnly, Secure, SameSite=Strict, scoped to /admin.

Each session cookie carries a session epoch. Advancing a user’s stored epoch invalidates every existing cookie for that user at once. The epoch is bumped automatically on:

  • password change (self-service or an admin reset) — other sessions drop; the session that made the change is re-issued so it stays signed in;
  • 2FA reset;
  • Log out everywherePOST /admin/api/users/:id/logout-all (super-admin; also a button on the /admin/users row).

A revoked cookie is rejected with E_SESSION_REVOKED (401) / a redirect to login.

Sessions carry an issued-at timestamp and expire after 2 hours of inactivity, independent of the absolute cookie lifetime. Activity slide-refreshes the cookie (past the half-way mark), so an active user is never logged out mid-work; only an abandoned session ages out.

Instead of sharing the master token, grant a user account the Super-admin flag (/admin/users → create/edit → Super-admin, or is_super via the API). Such a user logs in with their own password (and 2FA if they turned it on) and gets full /admin privileges — with their own name in the audit log. The master token is kept as break-glass alongside them.

LayerControlRefusal
PasswordPBKDF2 600k + rehash-on-login
Brute force5/username, 20/IP per 15 min429 + Retry-After
CSRFsame-origin required403
2FAoptional TOTP + backup codes (per account)challenge when enabled
Sessionepoch revocationE_SESSION_REVOKED (401)
Idle2 h slidingredirect to login
Identityindividual super-admins (is_super)