Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
106 lines
6.1 KiB
Markdown
106 lines
6.1 KiB
Markdown
# Browser authentication and Approval Engine transport
|
|
|
|
Implemented under `INFD-WP-0001-T08`. The service supplies sign-in and sign-out;
|
|
explicit owner configuration enables the protected memo and human-response
|
|
routes described in [protected-browser-review.md](protected-browser-review.md).
|
|
|
|
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 rollout remain pending. `/healthz` checks this process; `/readyz`
|
|
returns 503 without owner review configuration and recent audit delivery health.
|
|
Readiness is not a declaration of native policy/login/custody admission.
|
|
|
|
## 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 controller preserves
|
|
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. `review.py` now connects fresh PDP entitlement,
|
|
durable presentations, actor/version/acknowledgment guards, local responses,
|
|
the transactional outbox and the original-entry recovery rules. `runtime.py`
|
|
schedules bounded delivery and reconciliation. No consume route exists.
|
|
|
|
The legacy `evidence.Outbox` is an in-memory test double; `Store` supplies
|
|
durable atomic state/evidence. See [durable-review-evidence.md](durable-review-evidence.md).
|
|
Native policy/caller assignment, registered human login, deployment and
|
|
independent custody remain in T08. Disposable browser success does not close them.
|