key-cape/workplans/KEY-WP-0028-browser-client-field-validation.md
tegwick 04931387f5 Record the stale-claim correction alongside the others
A message to railiance-platform listed the upstream issuer pin as outstanding
when the committed receipt showed it done ten hours earlier. Corrected to them
directly; recorded here with the other two instances because the pattern is the
point, not the individual slip. docs/evidence/ is the authoritative artifact for
live proofs in this repository, and reading it is cheaper than the corrections
it prevents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 713576@bnt-lap001
Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
2026-09-09 16:41:10 +02:00

3.9 KiB

id type title domain repo status owner topic_slug created updated state_hub_workstream_id
KEY-WP-0028 workplan Reject service-identity fields that a browser client silently ignores infotech key-cape finished claude browser-client-field-validation 2026-09-09 2026-09-09 6d0cb4fd-17da-5466-8011-5ade1ac46171

serviceSubject and roles are read only on the client_credentials path. On a browser client they are accepted at startup and then ignored: the subject and roles come from the directory user. A registration that looks effective and is not fails later, somewhere else, as a downstream rejection rather than as a registration defect.

tokenLifetime was already rejected this way for non-service clients, so the rule exists — it was just incomplete.

Reject the ignored fields

id: KEY-WP-0028-T01
status: done
priority: medium
state_hub_task_id: "e4bce8c7-0e60-55e8-ad32-a3a0c4e46044"

Config validation now rejects serviceSubject and roles on any client that does not use client_credentials, naming where the value actually comes from. Nothing in config/dev-config.yaml, the example fixture or the live deployment sets either field on a browser client, so this breaks no existing configuration — checked against all three rather than assumed.

Re-verified against sso/keycape-config on 2026-09-09, after a peer raised that this validation fails closed at startup and a rollout is imminent, so a stray field would surface as KeyCape failing to boot. The deployed config now holds six clients: demo-app, netkingdom-bootstrap-console and openbao-admin are browser clients carrying neither field, while rapp-qonto-client, secrets-engine-approval and approval-engine-operator carry both legitimately on the client_credentials path. The rule rejects none of them. Kept as errors rather than downgraded to warnings on that evidence: a warning for a field that does nothing is the state this change exists to end.

tenant is deliberately excluded from the rule, and a test pins that. A browser client may declare a tenant; humanTenant resolves it against the directory and refuses issuance when the two disagree (KEY-WP-0013-T05). An earlier version of this change rejected tenant too, which would have made that feature unusable.

Correction

id: KEY-WP-0028-T02
status: done
priority: medium
state_hub_task_id: "5ab951f8-2f9c-522c-b6ea-7f6d572cd618"

This work started from KEY-WP-0013-T05's blocker paragraph, which described a human token as unable to carry tenant:platform. That was true when written and had already been fixed in a concurrent session by the time this task began; the paragraph was read as current state rather than as a dated record.

Two consequences, both corrected within the session:

  • the tenant rejection was removed before it was committed, and a test now asserts a browser client carrying tenant: tenant:platform validates cleanly;
  • a hub decision (0145ab57) recording the tenant question as open and unimplemented was withdrawn with a rationale pointing at the implementation. Nothing was built on it.

A third instance followed the same day, outside this task: a message to railiance-platform (d361bf28) listed the upstream issuer pin as outstanding when docs/evidence/2026-09-09-upstream-issuer-pin.json showed it done ten hours earlier — check_before.issuer_matches: false, then pinned to https://auth.coulomb.social. Corrected in dd827b9f. The concern itself was sound; only the claim that it was still owed was wrong, and the two are worth separating rather than letting the correct half excuse the other.

The lesson worth keeping: in a repository where several sessions work at once, prose ages faster than the code and the receipts it describes. docs/evidence/ is the authoritative artifact for anything claiming a live proof here. Read it before writing "outstanding", "unproven" or "not done" — that check is cheaper than any of the corrections it would have prevented.