informed-decision/docs/keycape-client-registration.md
tegwick 0e48355b9f Add verified browser login and human approval HTTP client
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
2026-09-10 22:05:17 +02:00

9 KiB

OIDC browser-client registration — informed-decision approver surface

Workplan task: INFD-WP-0001-T07 For: key-cape (KEY-WP-0013-T02, KEY-WP-0013-T05) 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. 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, approval-engine/docs/approver-surface-requirements.md


1. What this closes

On 2026-09-08 key-cape asked approval-engine for the human approver client's client_id and full callback URI. approval-engine correctly declined to invent them: it is a bearer-token resource server with no Ingress, no redirect endpoint, and no browser client. It recorded that the two strings belong to whichever browser-facing approver UI presents approval:approve tokens to it.

That component is this repository. These are the two strings.


2. The two strings

Field Value
client_id informed-decision-approver
Redirect URI https://decisions.coulomb.social/auth/callback

client_id is fixed and stable. It will not change.

The origin is settled. 2026-09-10: the operator assigned decisions.coulomb.social to this surface. It is not the decide.coulomb.social this document proposed — the assigned name is the one that counts, and it is recorded here rather than the proposal, because redirects match exactly at /authorize and a near-miss fails closed. That exactness is why approval-engine refused to guess in the first place, and it is the reason the proposed name is corrected here before submission rather than reconciled afterwards.

  • Assigned: decisions.coulomb.social, giving https://decisions.coulomb.social/auth/callback.
  • Consistent with the estate's existing pattern (forgejo.coulomb.social, hub.coulomb.social, kc.coulomb.social), Traefik ingressClassName, TLS secret per host.
  • DNS resolves to the cluster address as of 2026-09-10.
  • Delivered by railiance-apps: Ingress and issued certificate, applied 2026-09-10 (manifests/informed-decision-ingress.yaml).

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 verified chain. The condition this document set for its own submission — that the origin be real rather than plausible — is met. The path component /auth/callback was fixed ahead of the host, so nothing else in this registration moved.

The path is currently served by a placeholder while INFD-WP-0001-T08 is built. That does not affect the registration: key-cape matches the redirect URI as a string at /authorize and never fetches it.


3. Client shape

Property Value
Grant Authorization code
PKCE Required, S256
Client type Public
Redirect matching Exact
Scopes openid, approval:read, approval:approve
mfaRequired true

approval:read is required and was missing from the original specification. Without it this surface could submit an approval entry it was never able to display — raised by approval-engine, confirmed by key-cape, now in their published contract.

approval:consume is never requested. approval-engine refuses it for human principals, and consumption belongs to the PEP that causes the side effect (GH-DEC-2026-003).


4. Expected access token

Verified by approval-engine (RS256 against the issuer's /jwks):

Claim Value
aud approval-engine — the resource server, never the OAuth client id
tenant tenant:platformexact string equality, no alias or prefix handling
principal_type human
sub non-empty; becomes the approver identity in the entry
scope non-empty, containing the exact scope for the route
assurance a non-null JSON object — shape below
iss, exp, iat verified, 30s leeway

The access token is the evidence. id_token is never presented to approval-engine and appears nowhere in that codebase.

assurance

The shape is key-cape's, defined in their docs/approval-engine-auth-contract.mdlevel (aal1|aal2), methods, mfa, source, at. Cited here rather than restated, so it cannot drift.

Two properties matter to this surface and are recorded rather than assumed:

  • at is the authentication time, not the token mint time. key-cape fixed a defect here on 2026-09-09; the two differ by hours when a browser session is reused, and approval-engine stores this object verbatim as the only downstream record that MFA happened. Freshness is judged by comparing at, never by assuming it.
  • key-cape emits assurance on every human token and never empty. This surface nevertheless requires the object to carry authentication facts rather than relying on it always arriving populated — approval-engine accepts an empty object, so an empty one would produce an entry evidencing nothing.

5. Tenant provenance — declared, not assumed

This registration declares a tenant the directory record does not carry, and says so here rather than letting it read as a directory-asserted fact.

GH-DEC-2026-013 ruled directory-sourced tenancy the terminal state and granted key-cape's registration-bound shape (329e48f) as a declared bounded gap — admissible because its distinguishing case fails closed: registration and directory disagreeing refuses issuance with 403 tenant_binding rather than relabelling.

Nothing populates domain.User.Tenant for approver users. So a registration declaring tenant:platform reaches the token by the gap route by construction, not by accident. Gate House was explicit: do not declare a tenant the directory does not carry unless prepared to say so in the record.

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. 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 share one field here just because they share one upstream. See EvidenceModel.md §8c and PR-08.

6. What this surface does with the token

  • GET /v1/approvals/{id} and /claimapproval:read, to render.
  • POST /v1/approvals/{id}/entriesapproval:approve, to record a binding.
  • Never /consume.
  • Never any path containing check or ending /authorize — they 404 by design, and there is no "may this person approve" surface to call.

A 200 from approval-engine is not entitlement to view. A decision is obtained from access-engine before an approval is rendered to a person (PR-05) — noting that decision attributability is a declared open gap (GH-DEC-2026-010, FLEX-WP-0024) and this path is not described as validated while it stands.


7. Outstanding

# Item Owner
1 DNS A record + Ingress + TLS for decisions.coulomb.socialdone 2026-09-10, cert issued, host answers 200 railiance-apps / deployment
2 Submit the client contract to key-capedone, 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.