soul.demarkus.io:6309/journal/2026-06-11.md/v1 draft reader meta

2026-06-11 — Broker confidential web-client support

Implemented plan Step 1 from /demarkus-library/plans/phase-1b-web-sso.md on branch feature/phase-1b-broker-web-client (demarkus repo, broker).

What landed

  • webClients registry in broker config: clientID, clientSecretHash (sha256-hex, normalized + shape-validated at load), exact-match https redirectURIs (no loopback, no userinfo, no fragment), optional name.
  • /oauth/authorize branches on registry membership: registered client → allowlist exact match; unregistered → existing loopback-only path untouched.
  • authorization_code grant requires client auth (Basic per RFC 6749 §2.3.1 with form-urlencoded halves, or client_secret_post) when the presented client_id is registered. Verified constant-time before Redeem so a bad secret never burns the code. 401 invalid_client + WWW-Authenticate: Basic.
  • Refresh tokens minted through a confidential exchange carry clientID (omitempty — old Secret records decode as unbound); the refresh grant requires the bound client to authenticate. Deregistered client → bound tokens stop minting.
  • Discovery now advertises token_endpoint_auth_methods_supported: ["none","client_secret_basic","client_secret_post"].
  • New client_auth.go (parse + verify + 401 helper), web_client_test.go (~7 test funcs incl. code-preserved-after-401 pin), ADR at docs/adr/0001-broker-confidential-web-clients.md (first repo ADR).

Deviations from the plan (reasoned, recorded in the ADR)

  • No Confidential stamp in the auth-code store. Redundant: Redeem already binds client_id constant-time and the registry is immutable for the process lifetime, so registry lookup of the presented client_id is authoritative. Checking the secret pre-Redeem also preserves the store's keep-code-on-client-error semantics.
  • No per-client scopes field. Scopes are declared-not-enforced broker-wide; the field would be dead config (conventions: question opt-in knobs).

Gotcha

deviceTokenRefresh verifies client binding after refreshStore.Refresh (the record is the source of the requirement), so a failed client auth still bumps LastUsedAt. Deliberate: observability metadata only, no token minted. Documented in the handler comment.

Follow-ups

  • Helm chart values surface for webClients (config is additive; chart not touched in this change).
  • /token/revoke does not require client auth for bound tokens — RFC 7009 would suggest it; revocation-by-holder is fail-safe so left as is.
  • Tests + bash pre-commit.sh green. Fritz to commit + PR.
trail
  1. soul.demarkus.io:6309 v1