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.
|
||||
58
docs/evidence/2026-09-10-browser-authentication.json
Normal file
58
docs/evidence/2026-09-10-browser-authentication.json
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
{
|
||||
"schema": "helixforge.browser-authentication-source.v1",
|
||||
"observed_at": "2026-09-10T19:20:43.362423+00:00",
|
||||
"status": "passed",
|
||||
"implementation_base": "4e103f62a0e68d9a273c0abec76ee6ab35db2531",
|
||||
"contracts": {
|
||||
"key-cape": "f9812ab3b2bfe8f0817185f44071e612264ec3ee",
|
||||
"approval-engine": "2fdb01d42aa721f3b44f46abe018e53a7813e916"
|
||||
},
|
||||
"validation": {
|
||||
"tests_passed": 206,
|
||||
"tests_failed": 0,
|
||||
"tests_skipped": 0,
|
||||
"existing_tests": 100,
|
||||
"real_engine_component_tests": 3,
|
||||
"command": "INFD_APPROVAL_ENGINE_SOURCE=/home/worsch/approval-engine /tmp/hfact-browser-20260910/.venv/bin/python -m pytest -q",
|
||||
"identity_evidence": "locally signed synthetic fixture, validated by login and actual Approval Engine JWT verifier; not a native KeyCape human login",
|
||||
"local_http_smoke": {
|
||||
"status": "passed",
|
||||
"checks": {
|
||||
"installed_entrypoint_health": true,
|
||||
"fixed_issuer_authorization_redirect": true,
|
||||
"exact_registered_callback_and_scopes": true,
|
||||
"pkce_s256_and_browser_cookie": true,
|
||||
"incomplete_approval_path_not_ready": true,
|
||||
"binding_route_unavailable": true
|
||||
},
|
||||
"issuer_contacted": false,
|
||||
"native_human_login": false,
|
||||
"model_calls": 0
|
||||
}
|
||||
},
|
||||
"source_sha256": {
|
||||
"informed_decision/approval_http.py": "14187ab81f0ce2d9d61ec548ff4da8563d7d1d6084f3d91dddfba9cc90d1864d",
|
||||
"informed_decision/http_transport.py": "d861f9cea798af7afeffb78b93b353ade0037b4536cf44b6e71ef7d6dc794940",
|
||||
"informed_decision/oidc.py": "f68d0145e0eb08e46fbbaeaf21b34bfaf51b4fdddc247e46d57c20a19f692134",
|
||||
"informed_decision/web.py": "c4eb8f828e38437aec305796adf30b947207a2d34b2e5fc9c63ed9d5eff99729",
|
||||
"informed_decision/provenance.py": "de02c6bf162865ba9619d74cfd68dd0a5d7f81e6c9e427f67f848cb2c2214f83",
|
||||
"pyproject.toml": "8e8e008035077e409c883fb1198387b24c4c63b37a3d695c8740a237cd3258d9",
|
||||
"tests/test_approval_component.py": "eea6cd523b81fb99478b72666a50ea46d5c031b7459241e0522a2606534ecbbc",
|
||||
"tests/test_approval_http.py": "81bdd8aaa0c039d6e9c37ba030a87956adf132fa2391882f18120f8ec578b4fd",
|
||||
"tests/test_browser_auth.py": "01f4088968eea87c58ac0b30806111f1367349e6eaf55f1d3c59499e6c68b590",
|
||||
"tests/test_http_transport.py": "314f7210748a548adebedb7c63cbc9b2b5790db00c082ef1fff2664e871e350e",
|
||||
"tools/smoke_browser_shell.py": "373524286e0bd475b6c6afa974f1836f0f12a39b041e8b4682c42bdd91ebc56d"
|
||||
},
|
||||
"native_human_login_proven": false,
|
||||
"browser_approval_route_available": false,
|
||||
"deployed": false,
|
||||
"factory_attempts": 0,
|
||||
"paid_model_calls": 0,
|
||||
"remaining_owner_record": "INFD-WP-0001-T08",
|
||||
"remaining": [
|
||||
"durable presentation/disposition plus transactional evidence outbox",
|
||||
"entitlement before render and actor/version/ack guards in browser path",
|
||||
"independent audit custody/delivery and duplicate/ambiguous entry reconciliation",
|
||||
"native client registration/login and deployed Approval Engine proof"
|
||||
]
|
||||
}
|
||||
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
**Workplan task:** `INFD-WP-0001-T07`
|
||||
**For:** `key-cape` (`KEY-WP-0013-T02`, `KEY-WP-0013-T05`)
|
||||
**Status:** **draft — not yet submitted.** One input outstanding: the deployed
|
||||
origin (§2). Everything else is fixed and stable.
|
||||
**Status:** **submitted 2026-09-10** (`INFD-WP-0001-T07` done). The
|
||||
origin is live. Registration rollout and a native human token accepted by
|
||||
Approval Engine remain T08; the browser login implementation is described in
|
||||
[browser-authentication.md](browser-authentication.md).
|
||||
**Rulings:** `GH-DEC-2026-012`, `GH-DEC-2026-013`
|
||||
**Contracts cited:** `key-cape/docs/approval-engine-auth-contract.md`,
|
||||
`key-cape/docs/tenant-claim-contract.md`,
|
||||
|
|
@ -47,11 +49,10 @@ than reconciled afterwards.
|
|||
`hub.coulomb.social`, `kc.coulomb.social`), Traefik `ingressClassName`, TLS
|
||||
secret per host.
|
||||
- **DNS resolves** to the cluster address as of 2026-09-10.
|
||||
- **Still requires, in `railiance-apps` rather than here:** an Ingress manifest
|
||||
and an issued certificate. Manifest written 2026-09-10
|
||||
(`manifests/informed-decision-ingress.yaml`); not yet applied.
|
||||
- **Delivered by `railiance-apps`:** Ingress and issued certificate, applied
|
||||
2026-09-10 (`manifests/informed-decision-ingress.yaml`).
|
||||
|
||||
**The host answers. This document is ready to submit.** 2026-09-10 14:32 UTC:
|
||||
**The host answers. This document has been submitted.** 2026-09-10 14:32 UTC:
|
||||
`railiance-apps` applied the Ingress and cert-manager issued a Let's Encrypt
|
||||
certificate for `decisions.coulomb.social`;
|
||||
`GET https://decisions.coulomb.social/auth/callback` returns `200` over a
|
||||
|
|
@ -146,11 +147,12 @@ Consequences accepted here:
|
|||
1. **This is transitional.** When the directory carries tenants, the same
|
||||
`key-cape` code stops supplying and starts enforcing agreement, with no second
|
||||
migration. This registration is expected to survive that unchanged.
|
||||
2. **The claim is stored with its provenance.** `key-cape` emits `tenant` as a
|
||||
bare string, so a consumer cannot distinguish a directory-asserted tenant from
|
||||
a registration-supplied one. Until the claim carries its own provenance
|
||||
(`GH-DEC-2026-013` §5, second field deliberately undesigned), this surface
|
||||
records which route the value arrived by. See `PR-09`.
|
||||
2. **The claim is stored with its provenance.** Current `key-cape` emits
|
||||
`tenant_source`: `directory`, `registration`, or `default`. The browser
|
||||
adapter preserves the first two as distinct routes and refuses missing,
|
||||
unknown or default provenance for this platform approver flow. It does not
|
||||
relabel registration-supplied tenancy as a fact asserted by the directory.
|
||||
See `PR-09` and `key-cape/src/internal/server/oidc/token.go`.
|
||||
3. **The tenant claim is never used as the act-scope.** `binding.target` is the
|
||||
act-scope and is committed inside `view_hash`. Two different facts — which
|
||||
scope this act enters, and which tenant this person belongs to — must not
|
||||
|
|
@ -180,5 +182,17 @@ while it stands.
|
|||
| # | Item | Owner |
|
||||
| --- | --- | --- |
|
||||
| 1 | ~~DNS A record + Ingress + TLS for `decisions.coulomb.social`~~ — **done 2026-09-10**, cert issued, host answers 200 | `railiance-apps` / deployment |
|
||||
| 2 | Submit this document to `key-cape`, closing `KEY-WP-0013-T02` | this repo, once 1 lands |
|
||||
| 3 | Prove a token issued against the registration is accepted by `approval-engine` | `T08` |
|
||||
| 2 | ~~Submit the client contract to `key-cape`~~ — **done**, source T07 return | this repo |
|
||||
| 3 | Deploy/admit the registration and prove a native human token is accepted by `approval-engine` | `T08`, `KEY-WP-0013-T05`, `APPROVAL-WP-0002-T01` |
|
||||
|
||||
## 8. Humanity provenance correction — 2026-09-10
|
||||
|
||||
The earlier assertion that `principal_type: human` came from registration was
|
||||
incorrect for the current KeyCape implementation (`f9812ab`). Its code exchange
|
||||
consumes a user-authenticated PKCE session, looks up that user and emits the
|
||||
literal `human`; its client-credentials handler emits `service`.
|
||||
`oidc.py` verifies the fixed issuer, signature, audiences, nonce, paired subject,
|
||||
exact scopes, tenant provenance and MFA facts before assigning the
|
||||
`authentication-derived` route. The generic provenance guard continues refusing
|
||||
registration-only assertions. This is a checked source contract, not a claim
|
||||
that the current deployed issuer or a native human login has been verified here.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue