Repository navigation
08 Authentication and Session Security
HealthBook AI (internal code name: BabyCare) uses email and password accounts with server-side sessions. The server generates a random session token and returns it once. It stores only a SHA-256 hash of the token. The web app on the same origin receives the token in an httpOnly cookie; the phone app and the cross-origin web deployment receive it as a bearer token. Every request for profile data then passes an ownership check before any record is read or written.
- Register and sign in accounts, with salted PBKDF2 password hashes.
- Issue, validate, expire and revoke sessions, storing only token hashes.
- Enforce the CSRF rule for cookie-authenticated writes and the origin check for browser writes.
- Rate-limit authentication routes per IP.
- Authorize each profile access as owner, editor caregiver or read-only viewer caregiver.
| Endpoint | Input | Output |
|---|---|---|
POST /api/auth/register |
email, password (12–200 chars), client = web or mobile (default mobile) |
201. Web: Set-Cookie plus {session: true, user}. Mobile: {token, user}. 409 if the email exists |
POST /api/auth/login |
same | 200 as above, or 401 "Incorrect email or password" |
POST /api/auth/logout |
session | Deletes this session's row and clears the cookie |
GET /api/me |
session | {id, email} |
Any other /api/...
|
Authorization: Bearer … or babycare_session cookie |
401 if missing, unknown or expired |
Emails are trimmed and lower-cased before lookup.
sequenceDiagram
autonumber
actor U as User
participant C as Web or mobile client
participant G as Guardrails middleware
participant A as FastAPI auth routes
participant DB as Database
U->>C: Enter email and password
C->>G: POST /api/auth/login
G->>G: Rate limit auth per IP, 30 per 60 s
G->>A: Forward request
A->>DB: Load user by normalised email
A->>A: PBKDF2-HMAC-SHA256, 390000 iterations, per-user salt
A->>A: hmac.compare_digest with stored hash
A->>A: secrets.token_urlsafe 36 bytes
A->>DB: Insert session with SHA-256 of token, expires in 14 days
A-->>C: Web gets httpOnly cookie, mobile gets bearer token
C->>G: GET /api/patients/42/medicines with cookie or bearer
G->>A: Forward request
A->>DB: Find session by SHA-256 of presented token
A->>A: Reject if missing or expired
A->>DB: own_patient checks owner or caregiver grant
alt Not owner and no grant
A-->>C: 404 Not found
else Viewer grant on a write
A-->>C: 403 Read-only caregiver access
else Allowed
A-->>C: 200 profile-scoped data
end
create_password() draws a 16-byte random salt (os.urandom). hash_password() runs hashlib.pbkdf2_hmac('sha256', password, salt, 390_000). The salt and hex digest are stored in users.password_salt / users.password_hash. Login compares digests with hmac.compare_digest. The minimum length is 12 characters, enforced by the request model on sign-up, login and password reset.
| Property | Value in code |
|---|---|
| Token | secrets.token_urlsafe(36) |
| Stored |
sessions.token_hash = sha256(token). The raw token is never persisted, and as_json() never serialises token_hash
|
| Lifetime |
SESSION_DAYS = 14. Expiry is fixed at issue time, with no sliding renewal |
| Validation |
require_user() looks up the hash and rejects a missing row ("Invalid session"), an expired row ("Session expired") or a missing user |
| Logout | Deletes only the current session row (user._session_hash) and calls delete_cookie
|
| Revoke all | A completed password reset deletes every session of the account (Forgot Password) |
Signed document URLs issued with POST /documents/{id}/signed-access are tied to the session. A test confirms they return 404 after logout.
| Same-origin web | Mobile app and cross-origin web | |
|---|---|---|
| Request | client: 'web' |
client: 'mobile' (default) |
| Transport | Cookie babycare_session: HttpOnly, SameSite=Strict, Secure when BABYCARE_ENV=production, Max-Age 14 days, Path=/
|
Authorization: Bearer <token> |
| Client storage | None readable by JS (only a "signed in" flag in sessionStorage) |
Mobile: expo-secure-store. Cross-origin web: sessionStorage
|
| Token in response body | No | Yes, once |
The cross-origin web deployment (static site on https://<project>.web.app, API on https://<api>.onrender.com) uses bearer mode because a strict same-site cookie would not be sent cross-site.
-
Cookie writes:
require_user()rejects any cookie-authenticated request whose method is not GET, HEAD or OPTIONS unless it carriesX-Requested-With: BabyCare(403 "CSRF check failed"). Browsers do not let a cross-site form set custom headers, andSameSite=Strictalready withholds the cookie on cross-site requests. Bearer requests are exempt because a browser never attaches a bearer header automatically. -
Origin check: for any
/api/write that has anOriginheader,secure_headersreturns 403 unless the origin is the API's own base URL or is listed inBABYCARE_CORS_ORIGINS. CORS is enabled only for those explicit origins.
guardrails.rate_allowed() keeps a fixed window per key, where the key is a SHA-256 of auth:<ip> or api:<ip>:
| Scope | Limit |
|---|---|
/api/auth/* (login, register, logout, password reset) |
30 requests per 60 s per IP |
Other /api/* (except /api/health) |
600 requests per 60 s per IP |
| Password reset request, per email | 3 per hour (in password_reset.py) |
When a limit is exceeded the API returns 429 with Retry-After: 60. The window is stored in the database table rate_windows by default. BABYCARE_RATE_LIMIT_STORE=memory switches to an in-process store for a single instance.
require_user() sets user._write_access to true for any non-GET/HEAD/OPTIONS request, except POSTs ending in /ask (record questions) or /signed-access (opening an original), which only read data. own_patient() then applies:
- Profile does not exist → 404.
- Caller is the owner → allowed.
- Caller has a
caregiver_accessrow → allowed for reads. A write with roleviewerreturns 403 "Read-only caregiver access". Roleeditormay write. - Otherwise → 404 (see Family Profile Isolation).
Sharing and account management (invitations, revoking caregivers) also call guardian(), which requires the caller to be the owner.
- Raw session and reset tokens never touch the database or logs. Lookups use the hash only.
- Every response sets
Cache-Control: no-store,X-Frame-Options: DENY,Referrer-Policy: no-referrer, a strict CSP and, in production, HSTS. - Known limitations: login skips hashing when the email is unknown, so response timing differs from a wrong password. Register returns 409 for an existing email. Rate limiting reduces, but does not remove, the enumeration risk from both. Expired session rows are rejected but not purged by these routes.
api/security.py (hash_password, create_password, issue_session, require_user, own_patient, own_resource), api/main.py (auth routes, Credentials), api/guardrails.py (secure_headers, rate_allowed), api/config.py (PRODUCTION), api/workflows.py (guardian), web/app.js (request), mobile/App.tsx. Tests: tests/test_api.py, tests/test_workflows.py (test_web_cookie_csrf_and_session_not_exposed, test_signed_file_expiry_and_session_revocation, test_read_only_invitation_and_immediate_revocation).
Backend Architecture · Web Application Architecture · Android Application Architecture · Family Profile Isolation · Forgot Password and Email Architecture · Security and Privacy Model
Overview
Platform
Identity and access
Medical records
- 10 Document Ingestion
- 11 OCR and Human Verification
- 13 Medicines and Administration
- 14 Vaccination Model
- 15 Labs, Growth, Longitudinal
- 16 Illness and Lifetime Health
Daily care
Output and sharing
Security
Engineering
- 26 Testing and Validation
- 27 Deployment and CI/CD
- 28 Technology Stack
- 29 Design Decisions
- 30 Limitations
- 31 Roadmap