Skip to content

[ADR-0001 / Child A] FastAPI password auth + session cookies #55

@thinmintdev

Description

@thinmintdev

Parent: #54 · ADR: docs/adr/0001-collapse-edge-auth-into-fastapi.md

Wave 1 / additive. Doesn't break any existing edge-auth install. Lands before Child B (Caddy reduction).

What to build

A new hal0.api.auth.password module + middleware extensions:

Storage

  • Extend the existing auth store (currently the JSON token file in HAL0_HOME) with a password_hash: str | None field. One file, not two.
  • Bcrypt cost factor 12.

Endpoints

  • POST /api/auth/login — body {username, password}; on success sets a signed session cookie (HttpOnly, SameSite=Lax, Secure only when TLS terminated). Returns {user, scope}. 401 on bad creds.
  • POST /api/auth/logout — clears the session cookie. 204.
  • POST /api/auth/password — body {password}; sets/rotates the password. Requires writer scope when a password exists; allowed without auth when password_hash is None (the first-run "claim ownership" path).
  • GET /api/auth/status — already exists; extend payload with password_set: bool, auth_mode: "open" | "password".

Middleware

  • Session cookie validates identically to a Bearer token in require_token / require_writer. Browser UI uses cookie, programmatic clients keep using Bearer. The middleware should look at cookie first, fall back to Authorization: Bearer ….
  • Cookie payload is a signed token (HS256 with a secret derived from HAL0_HOME/keyring — generate if missing). Treats expiry consistently with how token expiry is handled today.
  • CSRF: when the cookie is used to authenticate a writer route, require either an X-Requested-With: XMLHttpRequest header or a CSRF token. Bearer auth bypasses (Bearer can't be sent cross-origin from a browser without explicit opt-in).

Tests

  • tests/api/test_password_auth.py — login happy/sad path, set-password first-run vs rotate, logout, cookie/Bearer interchangeability against an existing writer-protected route, CSRF rejection on cookie-only writer call.

Don't touch

  • The Caddyfile or the --auth=basic install flag — that's Child B's job.
  • PUBLIC_PATHS — that's Child B's job too.
  • The wizard UI — that's wired up in Child B alongside the Caddy/install changes.

Acceptance

  • pytest tests/api/test_password_auth.py green.
  • Full suite still green (no regression on token-only auth).
  • GET /api/auth/status returns password_set: false, auth_mode: "open" on a fresh install.
  • A user can curl -X POST /api/auth/password -d '{"password":"..."}' once (no auth, since none set), then subsequent POST /api/auth/password requires either login cookie or writer Bearer.

Branch / PR

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions