Add verified browser login and human approval HTTP client
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
4e103f62a0
commit
0e48355b9f
16 changed files with 1365 additions and 39 deletions
107
docs/browser-authentication.md
Normal file
107
docs/browser-authentication.md
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# Browser authentication and Approval Engine transport
|
||||
|
||||
Implemented under `INFD-WP-0001-T08`. The service currently supplies sign-in and
|
||||
sign-out. It does not yet render memos or expose an approval-entry route.
|
||||
|
||||
Install and exercise from the repository:
|
||||
|
||||
```sh
|
||||
uv venv
|
||||
uv pip install -e '.[dev]'
|
||||
uv run python -m pytest -q
|
||||
INFD_APPROVAL_ENGINE_SOURCE=/home/worsch/approval-engine uv run python -m pytest -q
|
||||
uv run python tools/smoke_browser_shell.py
|
||||
INFD_KEYCAPE_ISSUER=https://kc.coulomb.social uv run informed-decision-web
|
||||
```
|
||||
|
||||
The component checks require the explicit Approval Engine source path. They use
|
||||
its actual JWT verifier and API with locally signed synthetic identity fixtures.
|
||||
They are not native human authentication or custody proof. The smoke script
|
||||
needs free loopback port 8080, checks the installed entrypoint and stops it. It
|
||||
never follows the authorization redirect or contacts KeyCape. `--receipt PATH`
|
||||
writes metadata-only results.
|
||||
|
||||
The entrypoint listens on `127.0.0.1:8080` for a local reverse proxy. The public
|
||||
origin/callback is fixed at `https://decisions.coulomb.social/auth/callback`;
|
||||
Host, forwarded headers and return URLs cannot replace it. A development HTTP
|
||||
listener does not replace this HTTPS callback registration. Deployment and
|
||||
registration remain pending. `/healthz` checks this process; `/readyz` returns
|
||||
503 because the protected approval path is incomplete.
|
||||
|
||||
## Identity boundary
|
||||
|
||||
`KeyCapeLogin` uses a configured HTTPS issuer with its `/authorize`, `/token`
|
||||
and `/jwks` endpoints. No discovery or JWT header can substitute an endpoint.
|
||||
It requests only `openid approval:read approval:approve`, with public-client
|
||||
S256 PKCE. State, a separate browser cookie and nonce are random; the callback
|
||||
consumes its five-minute pending state before token exchange. The access token
|
||||
never enters a browser cookie, HTML, URL or error message.
|
||||
|
||||
The verifier accepts RS256 only and checks both tokens against the issuer JWKS:
|
||||
ID-token audience is the client, access-token audience is `approval-engine`.
|
||||
Subject, tenant, provenance and assurance must agree. Nonce, time bounds, exact
|
||||
scopes, human principal, platform tenant and the published KeyCape MFA facts are
|
||||
required. JWKS refresh failure refuses login; expired cached keys are not a
|
||||
fallback. Login does not decide entitlement to view or approve anything.
|
||||
|
||||
KeyCape source `f9812ab3b2bfe8f0817185f44071e612264ec3ee`, specifically
|
||||
`src/internal/server/oidc/token.go`, is the provenance contract reviewed here:
|
||||
the code handler looks up the authenticated user and emits `human`; client
|
||||
credentials emit `service`. `tenant_source=directory` and `registration` stay
|
||||
distinct. Unknown/default provenance is refused in this flow. A verified
|
||||
registration-supplied tenant remains the bounded GH-DEC-2026-013 gap.
|
||||
|
||||
MFA `at` is preserved as authentication time. A successful login does not assert
|
||||
that MFA meets an action-specific freshness policy; the policy integration must
|
||||
use these facts for the action. No local approval verdict is inferred.
|
||||
|
||||
Sessions last at most fifteen minutes and never outlive either token. They are
|
||||
bounded to 1,024 entries, protected by a process lock, rotated on sign-in and
|
||||
deleted on sign-out/expiry. Restart requires reauthentication. This is a
|
||||
single-process design; replicas need a separate shared-session design. These
|
||||
sessions contain no approval current state or evidence. Cookies are Secure,
|
||||
HttpOnly, SameSite=Lax with the `__Host-` prefix. Sign-out requires POST, the
|
||||
exact origin and a session CSRF token. Responses prohibit caching and framing.
|
||||
The proxy must omit callback query strings, bearer headers and cookies from
|
||||
access logs. Waitress has no request-target access log enabled by this entrypoint.
|
||||
|
||||
## Approval transport boundary
|
||||
|
||||
`ApprovalHTTPClient` implements get-by-id and add-entry using the session's
|
||||
access token. Each read reaches the engine. It requires exact
|
||||
`binding.human_control=true` for this first factory integration and preserves
|
||||
the native `sha256:<64 hex>` act digest. A pre-entry read rejects an undeclared
|
||||
object before mutation. Identifiers cannot inject extra paths or query strings.
|
||||
|
||||
The default origin is HTTPS. The current in-cluster service can be wired with
|
||||
explicit `allow_internal_http=True` and a fixed `.svc`/`.svc.cluster.local`
|
||||
origin; that option also permits numeric loopback for component tests. Public
|
||||
HTTP origins are refused. Redirects are never followed, response bodies are
|
||||
limited to 256 KiB, and socket operations use a five-second timeout. No ambient
|
||||
proxy credentials are inherited.
|
||||
|
||||
POST bodies are empty objects: Approval Engine derives identity and evidence
|
||||
from its verified token and discards caller content. The adapter extracts
|
||||
`(approval_id, subject_id, approved_at)` from the matching actual human entry,
|
||||
never `updated_at` or local time. A `409 duplicate_approver` triggers a GET of
|
||||
that original entry and returns `duplicate=True`. The eventual UI must preserve
|
||||
the original presentation correlation; it cannot attach a new presentation to
|
||||
an old entry just because a duplicate exists. Other conflicts remain refusals.
|
||||
|
||||
A transport failure after POST is ambiguous: the engine may have committed.
|
||||
There is no automatic POST retry. Durable disposition processing must reconcile
|
||||
the current engine entry and its correlation before reporting success or
|
||||
offering a retry. A failed dependency must never be recorded as a human decline.
|
||||
|
||||
The client is an internal seam, not a sufficient binding flow. Before a browser
|
||||
route can call it, T08 must connect:
|
||||
|
||||
1. The access-engine entitlement decision before rendering a named memo.
|
||||
2. A durable presentation/version, actor match and required acknowledgments.
|
||||
3. Dispositions and a transactional evidence outbox, including return/discuss.
|
||||
4. Independent audit custody/delivery and entry-correlation reconciliation.
|
||||
5. Native registered KeyCape login and the deployed Approval Engine proof.
|
||||
|
||||
The browser currently exposes no memo or entry route and no consume capability.
|
||||
The current `evidence.Outbox` is in memory and cannot satisfy durable evidence
|
||||
requirements. These remaining items stay in the active T08 record.
|
||||
Loading…
Add table
Add a link
Reference in a new issue