clearview/docs/superpowers/specs/2026-05-28-authentication-design.md

178 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/`.
```text
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.id` UUIDv4. No data is encoded in the cookie itself.
- On every authenticated request:
1. Read cookie → look up row in `sessions`.
2. If missing or `expires_at <= now()` → 401, delete cookie.
3. If `remember = false` → extend `expires_at = now() + 8h` (sliding).
4. If `remember = true` → leave `expires_at` untouched (fixed 30 days from creation).
5. Update `last_seen_at`.
- 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-required` returns `{"setup_required": true}` iff `COUNT(*) FROM users = 0`.
- `POST /api/auth/setup` accepts `{username, password}`, succeeds **only** when the table is empty, creates the user with `role='admin'`, immediately establishes a session (sets cookie), and writes a `setup` audit 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-required` returns true. Posts to `/api/auth/setup`.
- **`app.js`**:
- On boot, call `GET /api/auth/me`. On 401, redirect to `/login` (or `/setup` if `setup-required`).
- Wrap the `fetch` helper so any 401 response triggers a redirect to `/login`.
- Header shows `username (role)` + a **Logout** button that calls `POST /api/auth/logout` then redirects to `/login`.
- **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_audit` rows, 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
1. Browser `POST /api/auth/login` with `{username, password, remember}`.
2. Server fetches user by username. If not found or `is_active=false` → 401 + audit `login_fail` with the supplied username.
3. Verify password with Argon2id. On mismatch → 401 + audit `login_fail`.
4. Insert `sessions` row (`remember` flag determines TTL: 8h or 30d).
5. `Set-Cookie: clearview_session=<uuid>; HttpOnly; SameSite=Lax; Secure?; Path=/; Max-Age=<ttl>`.
6. Audit `login_ok`. Return `{username, role}`.
7. Browser then calls `GET /api/auth/me` and 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_user` and `require_admin` reject correctly.
- Setup endpoint: succeeds when empty, 409 when not empty.
- User CRUD endpoints: admin allowed, `user` role 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 `users` table is empty → users are redirected to `/setup` on first visit.
- No env vars are introduced for credentials. (An optional `CLEARVIEW_COOKIE_SECURE` toggle may be added so local-dev HTTP still works; default `true`.)
## 9. Work breakdown (units of work)
1. DB models + Alembic migration.
2. Auth core: hashing, policy, session create/lookup/refresh/revoke, audit helper.
3. Auth router: login / logout / me / setup-required / setup.
4. Users router + audit endpoint.
5. Apply `require_user` to existing routers; `require_admin` to user/audit routes.
6. Frontend: `login.html`, `setup.html`, fetch-wrapper 401 handling, header user-badge + logout.
7. Frontend: Users tab (CRUD + reset password) and Audit sub-view.
8. Tests for items 25.
9. Append entries to `changelog-develop.md` per 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.