9.2 KiB
Clearview — Authentication Design
Date: 2026-05-28 Status: Approved (brainstorming phase) Scope: Add an authentication layer to the existing Clearview FastAPI + static-frontend application.
1. Goal
Restrict access to the Clearview UI and API to a small group of named administrators. No self-service registration, no public exposure of endpoints, no multi-tenant user isolation.
2. Requirements
| # | Requirement |
|---|---|
| R1 | All existing API routes (/api/tenants/*, /api/jobs/*, /api/onboarding/*) require an authenticated session. |
| R2 | Two roles: admin and user. Only admin can manage users and view the audit log. Both can use the scanner UI. |
| R3 | Server-side sessions using an opaque session ID stored in an HttpOnly cookie. |
| R4 | First admin is created via a one-time initial-setup page that is reachable only while the users table is empty. No env-var fallback. |
| R5 | Password hashing with Argon2id (default parameters via argon2-cffi). |
| R6 | Password policy: minimum 12 characters, at least one letter and one digit. Validated server-side. |
| R7 | Session TTL: 8 hours sliding by default; 30 days fixed when "remember me" is checked at login. |
| R8 | Audit log persisted in DB and viewable in the UI by admins. |
| R9 | Existing changelog convention applies: append entries to changelog-develop.md per change. |
Explicitly out of scope for v1: rate limiting / login lockout, password reset via email, MFA, SSO, fine-grained permissions beyond admin / user.
3. Architecture
3.1 Backend module layout
A new package clearview_app/auth/ with focused modules:
| File | Purpose |
|---|---|
auth/__init__.py |
Package marker. |
auth/models.py |
SQLAlchemy models: User, Session, AuthAudit. |
auth/security.py |
Argon2id hashing, password policy validation, session-ID generation. |
auth/sessions.py |
Create / lookup / refresh / revoke session records; expiry handling. |
auth/audit.py |
Single record(event, user_id, ip, detail) helper. |
auth/dependencies.py |
FastAPI dependencies: current_session, require_user, require_admin. |
auth/router.py |
POST /api/auth/login, POST /api/auth/logout, GET /api/auth/me, GET /api/auth/setup-required, POST /api/auth/setup. |
auth/users_router.py |
Admin-only: GET/POST/PATCH/DELETE /api/users, POST /api/users/{id}/reset-password, GET /api/audit. |
main.py wires the new routers before the static-files mount and applies Depends(require_user) to the three existing routers (tenants, jobs, onboarding).
3.2 Database
Three new tables, added via a new Alembic migration in clearview_app/migrations/versions/.
users
id int pk
username text unique not null
password_hash text not null -- Argon2id encoded string
role text not null -- 'admin' | 'user'
is_active bool not null default true
created_at timestamptz not null default now()
updated_at timestamptz not null default now()
sessions
id uuid pk -- opaque session id, stored in cookie
user_id int not null fk -> users.id on delete cascade
created_at timestamptz not null default now()
expires_at timestamptz not null
last_seen_at timestamptz not null default now()
ip text
user_agent text
remember bool not null default false
auth_audit
id bigserial pk
ts timestamptz not null default now()
user_id int null fk -> users.id on delete set null
event text not null -- see event list below
ip text
detail jsonb -- e.g. {"username": "alice"} on login_fail
Audit events: login_ok, login_fail, logout, user_create, user_update, user_delete, password_reset, setup.
3.3 Session handling
- Cookie name:
clearview_session. Flags:HttpOnly,SameSite=Lax,Secure(set when request is HTTPS; configurable for local-dev HTTP). - Cookie value: the
sessions.idUUIDv4. No data is encoded in the cookie itself. - On every authenticated request:
- Read cookie → look up row in
sessions. - If missing or
expires_at <= now()→ 401, delete cookie. - If
remember = false→ extendexpires_at = now() + 8h(sliding). - If
remember = true→ leaveexpires_atuntouched (fixed 30 days from creation). - Update
last_seen_at.
- Read cookie → look up row in
- Logout: delete the session row and clear the cookie.
- Cleanup: a lightweight purge of expired sessions runs at login time and is also exposed as a periodic task in the existing worker (cheap query:
DELETE FROM sessions WHERE expires_at < now()).
3.4 Initial-setup flow
GET /api/auth/setup-requiredreturns{"setup_required": true}iffCOUNT(*) FROM users = 0.POST /api/auth/setupaccepts{username, password}, succeeds only when the table is empty, creates the user withrole='admin', immediately establishes a session (sets cookie), and writes asetupaudit row.- Once any user exists, both endpoints return 409 /
setup_required=false.
3.5 Password policy
- Server-side check before hashing or updating:
len(pw) >= 12 and any(c.isalpha() for c in pw) and any(c.isdigit() for c in pw). - Hashing:
argon2.PasswordHasher()with library defaults; the encoded string includes salt + parameters, so future tuning is non-breaking.
4. Frontend
The frontend is the existing static site/ (index.html, app.js, styles.css). Changes:
- New
login.html— standalone page, no app-shell. Form with username, password, "remember me" checkbox. Posts to/api/auth/login, then redirects to/. - New
setup.html— same shell as login, used when/api/auth/setup-requiredreturns true. Posts to/api/auth/setup. app.js:- On boot, call
GET /api/auth/me. On 401, redirect to/login(or/setupifsetup-required). - Wrap the
fetchhelper so any 401 response triggers a redirect to/login. - Header shows
username (role)+ a Logout button that callsPOST /api/auth/logoutthen redirects to/login.
- On boot, call
- New "Users" sidebar tab (admin-only): list / add / edit (username, role, active) / delete users, and a Reset password action that opens a modal.
- New "Audit" sub-view under Users (admin-only): paged table of
auth_auditrows, newest first, with event-type filter. - Non-admin users do not see the Users tab; the backend also enforces this independently (defence in depth).
5. Data flow — successful login
- Browser
POST /api/auth/loginwith{username, password, remember}. - Server fetches user by username. If not found or
is_active=false→ 401 + auditlogin_failwith the supplied username. - Verify password with Argon2id. On mismatch → 401 + audit
login_fail. - Insert
sessionsrow (rememberflag determines TTL: 8h or 30d). Set-Cookie: clearview_session=<uuid>; HttpOnly; SameSite=Lax; Secure?; Path=/; Max-Age=<ttl>.- Audit
login_ok. Return{username, role}. - Browser then calls
GET /api/auth/meand renders the app.
6. Error handling
| Situation | Response |
|---|---|
| Missing / invalid / expired session cookie on protected endpoint | 401 Unauthorized, cookie cleared. |
Authenticated user hitting an admin-only endpoint |
403 Forbidden. |
| Setup endpoint called when users already exist | 409 Conflict. |
| Password policy violation | 400 Bad Request with a single human-readable message. |
| Argon2 verification raising any exception | Treated as failed login (no info leak). |
7. Testing
Pytest cases (added in the existing test layout, mirroring current patterns):
- Password hashing round-trip; policy validator edge cases.
- Login success, wrong password, unknown user, inactive user.
- Session expiry: sliding 8h refresh vs fixed 30d remember.
- Logout invalidates the session.
require_userandrequire_adminreject correctly.- Setup endpoint: succeeds when empty, 409 when not empty.
- User CRUD endpoints: admin allowed,
userrole gets 403. - Audit rows are written for each event.
8. Migration & compatibility
- Single forward Alembic migration adds the three tables. No changes to existing tables.
- First deploy on an existing install: the
userstable is empty → users are redirected to/setupon first visit. - No env vars are introduced for credentials. (An optional
CLEARVIEW_COOKIE_SECUREtoggle may be added so local-dev HTTP still works; defaulttrue.)
9. Work breakdown (units of work)
- DB models + Alembic migration.
- Auth core: hashing, policy, session create/lookup/refresh/revoke, audit helper.
- Auth router: login / logout / me / setup-required / setup.
- Users router + audit endpoint.
- Apply
require_userto existing routers;require_adminto user/audit routes. - Frontend:
login.html,setup.html, fetch-wrapper 401 handling, header user-badge + logout. - Frontend: Users tab (CRUD + reset password) and Audit sub-view.
- Tests for items 2–5.
- Append entries to
changelog-develop.mdper change.
10. Open items deferred to v2
- Rate limiting / brute-force lockout.
- Email-based password reset.
- MFA / SSO via Microsoft Entra (would reuse existing tenant app-registration plumbing).
- Per-tenant data scoping for non-admin users.