-
Notifications
You must be signed in to change notification settings - Fork 0
Authentication and Security
BetterMind uses a custom JWT auth layer rather than a turnkey framework, giving full control over the two-factor challenge and role gating.
- On successful login the server signs a JWT and sets it as an httpOnly cookie named
token(not readable by JavaScript → mitigates XSS token theft). - The client never stores the token;
AuthContextlearns the current user by callingGET /api/auth/checkon mount. - Every protected Route Handler calls
verifyAuth(token)fromlib/authServer.js.
verifyAuth(token):
if no token → { authenticated: false }
if token in cache (TTL 5 min) → return cached result
jwt.verify(token, JWT_SECRET)
confirm user still exists in MongoDB
(if doctor) attach doctorId
cache + return { authenticated: true, user }
The 5-minute in-memory cache avoids a DB round-trip on every API call while still expiring quickly enough to reflect account changes.
POST /api/auth/login
│
├─ password invalid ───────────────► 401
│
├─ 2FA disabled ───► issue JWT cookie ───► { user }
│
└─ 2FA enabled ───► { requires2FA: true, userId } ◄── no JWT yet!
│
▼
/login/2fa challenge page
│
POST /api/auth/2fa/verify { userId, code }
│
code valid ───► issue JWT cookie ───► authenticated
The crucial property: the JWT is only issued after the TOTP code is verified, so a stolen password alone cannot create a session.
Implemented with otplib v13 (which requires explicit crypto plugins — the Noble crypto + Scure base32 plugins are wired up in lib/twoFactorAuth.js).
| Step | Endpoint | Result |
|---|---|---|
| Setup | GET /api/auth/2fa/setup |
Generates a secret + otpauth:// URI; the client renders it as a QR via qrcode
|
| Enable | POST /api/auth/2fa/setup |
Verifies the first 6-digit code, stores the secret, returns one-time recovery codes |
| Challenge | POST /api/auth/2fa/verify |
Used at login to validate a code (or recovery code) before issuing the JWT |
| Disable | POST /api/auth/2fa/disable |
Requires a valid current code |
The /settings/security page provides the full enable → QR scan → confirm → save-recovery-codes flow, and a confirm-to-disable flow.
Sensitive data is encrypted at rest using AES (lib/encryption.js) with a key from ENCRYPTION_KEY. This keeps sensitive fields unreadable even with direct database access.
A reusable limiter factory in lib/rateLimit.js throttles abuse-prone endpoints:
| Endpoint | Limit |
|---|---|
| Login | throttled |
| Register | 5 / 15 min |
| Chat | 30 / min |
| Assessment | 20 / hr |
Exceeding a limit returns HTTP 429.
Three roles — patient, doctor, admin — enforced on both layers:
-
Server: handlers check
user.roleafterverifyAuth. -
Client:
AuthContextexposesisDoctor(),isVerifiedDoctor(),isAdmin(),isPatient(), and pages redirect based on role.
Doctors additionally cannot accept appointments until an admin verifies their credentials (verified: true); rejected doctors see their rejectionReason.
| Threat | Mitigation |
|---|---|
| XSS token theft | httpOnly cookie; token never in JS-readable storage |
| Password-only compromise | TOTP 2FA gates JWT issuance |
| Brute force | Rate limiting + bcrypt |
| Privilege escalation | Server-side role checks on every protected route |
| Data-at-rest exposure | AES encryption of sensitive fields |
| Unverified practitioners | Admin verification workflow |