feat(auth): password + session cookies, dual cookie/Bearer middleware (ADR-0001 Child A — refs #54)#58
Merged
Conversation
… (ADR-0001 Child A — refs #54) Add a FastAPI-side password + session-cookie surface that composes with the existing bearer-token auth, ahead of the Caddy reduction in Child B (#56). Wave 1 / additive — no existing edge-auth install is broken. New module ``hal0.api.auth.password``: - ``hash_password`` / ``verify_password`` (bcrypt cost 12, matching the format Caddy basic_auth uses, easing Child B's migration). - ``create_session_token`` / ``verify_session_token`` (HS256 JWT, key derived from ``HAL0_HOME/keyring``, generated atomically on first use, default TTL 7 days). ``TokenStore`` extended with a top-level ``password_hash`` field — ``get_password_hash`` / ``set_password_hash`` round-trip via the same atomic-write path the token rows use, so a single backup of tokens.toml covers both surfaces. New endpoints under ``/api/auth``: - ``POST /login`` — validate ``{username, password}`` and set the ``hal0_session`` cookie (HttpOnly, SameSite=Lax, Secure when TLS fronted via ``X-Forwarded-Proto`` or the request scheme). - ``POST /logout`` — clear the cookie (204). - ``POST /password`` — set or rotate. Allowed without auth iff ``password_hash is None`` (first-run claim ownership); requires writer scope thereafter. Min length 8. - ``GET /status`` — extended with ``password_set`` and ``auth_mode`` (``"open"`` | ``"password"``) so the wizard can branch without a second probe. Middleware extensions (``hal0.api.middleware.auth``): - ``require_token`` grows a cookie path: Bearer → session cookie → X-Forwarded-Email. A present-but-invalid cookie hard-fails with ``auth.invalid`` so an expired session prompts a clean re-login. - ``require_writer`` / ``require_admin`` enforce a CSRF tripwire when auth arrived via cookie *and* the verb is mutating: either ``X-Requested-With: XMLHttpRequest`` or ``X-CSRF-Token`` matching the first 16 chars of the session cookie. Bearer bypasses, matching the ADR's "Bearer can't be sent cross-origin without explicit opt-in" reasoning. GET/HEAD/OPTIONS are exempt (RFC 7231 §4.2.1 safe verbs). Tests at ``tests/api/test_password_auth.py`` cover the issue-spec scenarios: first-run set, second set without/with auth, login happy + wrong-creds, cookie+CSRF accepted, cookie-without-tripwire rejected, Bearer exemption, logout clears cookie, ``status`` payload reports ``password_set`` / ``auth_mode``. 20 new tests, full suite still green (856 passed). Hard-gated out of scope (per the brief): Caddyfile, PUBLIC_PATHS, ``--auth=basic`` installer flag, wizard UI, docs — all reserved for Children B (#56) and C (#57). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 17, 2026
thinmintdev
added a commit
that referenced
this pull request
May 17, 2026
…d C — refs #54) (#60) Wave 2, Child C of ADR-0001. Documentation pass for the auth collapse shipped in PRs #58 (Child A — FastAPI password + session cookies + dual cookie/Bearer middleware) and #59 (Child B — Caddyfile reduction + PUBLIC_PATHS deletion + --no-tls flag + HAL0_PUBLIC_HOST / HAL0_HARNESS_TLS rename). installer/README.md ------------------- Rewrites the auth section to describe the single FastAPI auth layer. Drops every reference to --auth=basic / HAL0_ADMIN_USER / HAL0_ADMIN_PASSWORD / HAL0_HOSTNAME / Caddy basic_auth / htpasswd. Documents the --no-tls flag (FastAPI binds 0.0.0.0:8080, reachable at http://<host>:8080/). Renames HAL0_HOSTNAME to HAL0_PUBLIC_HOST in the env-var table. Adds an "Upgrade notes (pre-v1)" subsection explaining that existing --auth=basic installs lose edge auth on upgrade and the two mitigations — set a password in the wizard, or --no-tls behind your own reverse proxy. Calls out the wizard's password-setup step (POST /api/auth/password, public on first run per Child A). PLAN.md ------- §1 "Auth + reverse proxy" rewritten to reflect the single FastAPI layer and a "Trust posture" subsection: hal0 defaults to open on the LAN; password auth is opt-in via the dashboard wizard; programmatic clients use Bearer tokens unchanged from #29. Drops the Caddy basic_auth / PUBLIC_PATHS prose; narrows Caddy's scope to TLS termination + reverse proxy. §10 (harness flags) renames HAL0_HARNESS_AUTH → HAL0_HARNESS_TLS to match what the harness scripts actually look for. docs/api-errors.md ------------------ Adds a brief note in the 401 section linking to ADR-0001 / PR #58 and naming the new endpoints (POST /api/auth/login, /api/auth/logout, /api/auth/password). The envelope shapes themselves are unchanged. tests/harness/FINDINGS.md ------------------------- Prepends "FIXED BY ARCHITECTURE REMOVAL (ADR-0001)" notes to the three historical entries that the auth collapse renders structurally unrepeatable: §10 (Caddy basic_auth swallows PUBLIC_PATHS — the original #28 critical bug, fixed in PR #49 and now historical because Caddy no longer has matchers or basicauth per PR #59), §16 (basic_auth password unrecoverable post-install — source of the #43 HITL decision, fixed by deletion because credential capture moved into the wizard per PRs #58 + #59), and §21 (/api/metrics/prometheus orphan in PUBLIC_PATHS — fixed by deletion because PUBLIC_PATHS is gone). The original report bodies are preserved verbatim below each note for historical reference. Re-run instructions updated to the new HAL0_HARNESS_TLS knob. README.md, tests/harness/README.md ---------------------------------- Sync the auth-posture summary in the repo root README and the harness opt-in flags table to match the new single-FastAPI model and the HAL0_HARNESS_TLS rename. These weren't called out in the spec but were left stale after Child B; updating them keeps the docs internally consistent. Closes #43 and #51 (per the parent ADR plan). Issue close comments follow the merge via the gh CLI; this PR body is the GH-semantics hook in case the manual close doesn't land. closes #43 closes #51 refs #54 refs #57 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
thinmintdev
added a commit
that referenced
this pull request
May 21, 2026
ADR-0001 (Collapse edge auth into FastAPI) is implemented. Child A (#58 — FastAPI password auth), Child B (#59 — Caddyfile reduction + --no-tls), and Child C (#60 — docs pass) all landed. Flips the header from Proposed → Accepted, records proposal/acceptance dates separately, names the implementing PRs, and appends an Outcome section summarizing what shipped against the original Decision. Adds #28 (the critical basic_auth ordering bug) to the closed-on-land list per tests/harness/FINDINGS.md §10. README.md and installer/README.md were already brought into line with the v1 single-FastAPI-layer reality in PR #60 — no further changes needed there.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Wave 1 of ADR-0001 — additive FastAPI password auth + signed session cookies, alongside the existing bearer-token surface. No existing edge-auth install is broken; the Caddyfile,
PUBLIC_PATHS, and--auth=basicinstaller flag are explicitly untouched and reserved for Child B (#56).Parent: #54 · This issue: #55 · ADR:
docs/adr/0001-collapse-edge-auth-into-fastapi.mdDoes not close #54 — Children B (#56) and C (#57) must also land before #54 can close.
New endpoints (all under
/api/auth)POST/login{username, password}and setshal0_sessioncookie (HttpOnly, SameSite=Lax, Secure-when-TLS)POST/logoutPOST/passwordGET/statuspassword_set: boolandauth_mode: "open" | "password"GET/loginMiddleware changes (
hal0.api.middleware.auth)require_tokengrows a session-cookie path: precedence is Bearer → cookie → X-Forwarded-Email. Present-but-invalid cookies hard-fail withauth.invalid(clean re-login path).require_writer/require_adminenforce a CSRF tripwire when auth came via cookie and the verb mutates: eitherX-Requested-With: XMLHttpRequestorX-CSRF-Tokenmatching the session cookie's first 16 chars. Bearer auth bypasses the check (per the ADR's threat model). GET/HEAD/OPTIONS are exempt (RFC 7231 §4.2.1).AuthIdentity.source = "session"plussession_tokenfield on the dataclass for the CSRF binding.CSRFRequired(auth.csrf_required, 403).Storage
TokenStoregains a top-levelpassword_hash: str | Nonefield.get_password_hash()/set_password_hash()round-trip through the existing atomic-write path so a singletokens.tomlbackup covers both surfaces.HAL0_HOME/etc/hal0/keyring(secrets.token_urlsafe(32), atomic write, mode0600).pyproject.toml:bcrypt>=4.0(cost 12, parity with Caddy basicauth format),PyJWT>=2.8(HS256).Tests
tests/api/test_password_auth.py— 20 new tests, all passing. Covers:X-Requested-Withwriter call → 200X-CSRF-Tokenwriter call → 200auth.csrf_requiredauth.invalidHttpOnly+SameSite=Laxalways presentSecureflag set whenX-Forwarded-Proto: https/api/auth/statusreportspassword_setandauth_modecorrectlyFull suite still green: 856 passed, 6 skipped (no regressions on token-only auth, middleware, scope matrix, or any other existing tests).
Out of scope (hard gates per the brief)
packaging/caddy/Caddyfile.template— Child BPUBLIC_PATHSfrozenset — Child B--auth=basicinstaller flag — Child BCI note
Hal0ai org Actions are currently billing-blocked, so this PR's CI will fail in ~2 seconds with no logs. Verified locally with:
Both green. The user will admin-merge.
Test plan
pytest tests/api/test_password_auth.py— greenpytest tests/— no regression on the existing 836 testsGET /api/auth/statusshowspassword_set: false, auth_mode: "open"curl -X POST /api/auth/password -d '{"password":"correcthorse"}'(no auth) succeeds oncePOST /api/auth/passwordwithout auth → 401POST /api/auth/login→ cookie issued,/api/auth/mereturnssource: "session"🤖 Generated with Claude Code