docs(19.4): phase verification report
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
---
|
||||
phase: 19.4-oidc-provider-on-wristband
|
||||
verified: 2026-10-19T21:10:00Z
|
||||
status: human_needed
|
||||
score: 15/15 must-haves verified
|
||||
covered_files:
|
||||
- .planning/phases/19.4-oidc-provider-on-wristband/19.4-01-PLAN.md
|
||||
- .planning/phases/19.4-oidc-provider-on-wristband/19.4-01-SUMMARY.md
|
||||
- .planning/phases/19.4-oidc-provider-on-wristband/19.4-02-PLAN.md
|
||||
- .planning/phases/19.4-oidc-provider-on-wristband/19.4-02-SUMMARY.md
|
||||
- .planning/phases/19.4-oidc-provider-on-wristband/19.4-03-PLAN.md
|
||||
- .planning/phases/19.4-oidc-provider-on-wristband/19.4-03-SUMMARY.md
|
||||
- .planning/phases/19.4-oidc-provider-on-wristband/19.4-04-PLAN.md
|
||||
- .planning/phases/19.4-oidc-provider-on-wristband/19.4-04-SUMMARY.md
|
||||
- README.md
|
||||
- docs/services/oauth-server.md
|
||||
- docs/services/parity-testing.md
|
||||
- modules/tide/README.md
|
||||
- modules/tide/capture_test.go
|
||||
- modules/tide/variables.go
|
||||
- modules/wristband/README.md
|
||||
- modules/wristband/authorize.go
|
||||
- modules/wristband/authorize_test.go
|
||||
- modules/wristband/client_issue.go
|
||||
- modules/wristband/consent_test.go
|
||||
- modules/wristband/example_test.go
|
||||
- modules/wristband/oidc_authorize.go
|
||||
- modules/wristband/oidc_authorize_smoke_test.go
|
||||
- modules/wristband/oidc_authorize_test.go
|
||||
- modules/wristband/oidc_consent.go
|
||||
- modules/wristband/oidc_consent_test.go
|
||||
- modules/wristband/oidc_discovery.go
|
||||
- modules/wristband/oidc_discovery_test.go
|
||||
- modules/wristband/oidc_fakes_test.go
|
||||
- modules/wristband/oidc_flavour_test.go
|
||||
- modules/wristband/oidc_keys.go
|
||||
- modules/wristband/oidc_keys_test.go
|
||||
- modules/wristband/oidc_options.go
|
||||
- modules/wristband/oidc_redirect.go
|
||||
- modules/wristband/oidc_redirect_test.go
|
||||
- modules/wristband/oidc_refresh_test.go
|
||||
- modules/wristband/oidc_security_test.go
|
||||
- modules/wristband/oidc_stores.go
|
||||
- modules/wristband/oidc_subject.go
|
||||
- modules/wristband/oidc_subject_test.go
|
||||
- modules/wristband/oidc_token.go
|
||||
- modules/wristband/oidc_token_smoke_test.go
|
||||
- modules/wristband/oidc_token_test.go
|
||||
- modules/wristband/oidc_tracer_test.go
|
||||
- modules/wristband/oidc_userinfo.go
|
||||
- modules/wristband/oidc_userinfo_test.go
|
||||
- modules/wristband/register.go
|
||||
- modules/wristband/registration_test.go
|
||||
- modules/wristband/server.go
|
||||
- modules/wristband/server_test.go
|
||||
- modules/wristband/stores.go
|
||||
- modules/wristband/token.go
|
||||
- modules/wristband/token_test.go
|
||||
- scripts/check-phase19.4.sh
|
||||
covered_digest: "v2:sha256:a3c1754c6cdf2bbf236657e6a4391c25f7f8ea05b97056c3acc1403f03ccc2db"
|
||||
behavior_unverified: 0
|
||||
overrides_applied: 0
|
||||
deferred:
|
||||
- truth: "D-14 PHP parity recording: TestPHPParityReplay and TestIDTokenShapeParity green against recordings of the PHP Golem15.OAuthServer plugin"
|
||||
addressed_in: "WavePath cutover phase (not yet on the roadmap; preliminary task)"
|
||||
evidence: "User decision 2026-10-19, recorded in 19.4-CONTEXT.md D-14 amendment and the amended 19.4-03/19.4-04 must-haves (commit 3386cbe); todo .planning/todos/pending/2026-10-19-record-php-oidc-parity-against-wavepath.md (g15office govfaqir). The phase-19.4 D-14 truth is the fail-closed harness, which is verified."
|
||||
human_verification:
|
||||
- test: "Run the plugin in a local application (or keep the test harness server alive), set a site name, a logo and an accent colour in row 1 of golem15_oauthserver_settings, open an authorize URL for a test client, then open the inactive-client error page. Compare both side by side with the PHP consent.htm and error.htm pages in a browser."
|
||||
expected: "Same structure and copy as PHP, branding (name, logo, colours, font, custom CSS) applied, error heading in the fixed danger colour #B91C1C."
|
||||
why_human: "TestConsentPageBrandingSmoke and TestConsentPageSecurity prove the branding values reach the HTML and cannot inject markup; no test judges visual likeness to the PHP pages (D-03/D-14 say HTML need not be byte-identical)."
|
||||
- test: "Open the Clients and Branding screens in the embedded admin SPA as an administrator with manage_clients and manage_settings: create a client, check the one-time secret reveal callout, the scopes taglist, the authorized-users table on the form, the regenerate-secret danger button, and the Settings link to Branding with the logo upload."
|
||||
expected: "Both screens render usably; the secret shows once and not on reload; regenerate asks for confirmation."
|
||||
why_human: "TestClientsAdminSmoke and TestBrandingAdminSmoke drive cabana's JSON admin API only; no browser test looks at how the SPA renders these partials and widgets."
|
||||
- test: "Review the LLM-judge verdicts on the 11 judgment-tier prohibitions in the 'Prohibitions' section of this report and confirm or reject each."
|
||||
expected: "Each prohibition held as stated (the verifier found evidence for all 11)."
|
||||
why_human: "Judgment-tier prohibitions are non-authoritative when judged by the verifier and must not be absorbed silently into a pass (ADR-550 D4)."
|
||||
---
|
||||
|
||||
# Phase 19.4: OIDC provider on wristband Verification Report
|
||||
|
||||
**Phase Goal:** `wristband` is a full OpenID Connect provider that existing relying parties can switch to without noticing: discovery, JWKS with asymmetric signing and key rotation, `id_token`, userinfo, resumable authorization with account switching, a consent store and pairwise subjects. The existing issuer, signing keys and client secrets carry over.
|
||||
**Verified:** 2026-10-19T21:10:00Z
|
||||
**Status:** human_needed
|
||||
**Re-verification:** No, initial verification
|
||||
|
||||
Repositories checked: summercms.go (`modules/wristband`, `modules/tide`, `scripts/check-phase19.4.sh`, `docs/`), `../sm-oauthserver-plugin` (13 commits, clean tree, not pushed), sm-user-plugin at `../fonoteka.go/plugins/golem15/user` (4 phase commits b82d85f, ee47440, 80b436d, 9335e80; ahead 7 / behind 1 of origin, not pushed), and `../fonoteka.go` (687e25a: explicit Resource).
|
||||
|
||||
**MVP mode note:** ROADMAP marks the phase `Mode: mvp`, but its goal is not in user-story form, and `/msd mvp-phase 19.4` was never run. Every plan records this and does not invent a story. The orchestrator asked for verification against the ROADMAP success criteria, so this report uses standard goal-backward verification. A relying-party flow table stands in for the User Flow Coverage table.
|
||||
|
||||
## Goal Achievement
|
||||
|
||||
### Relying-party flow coverage
|
||||
|
||||
| Step a relying party takes | Expected | Evidence | Status |
|
||||
| --- | --- | --- | --- |
|
||||
| Fetch discovery | PHP field set and order, `max-age=3600, public`, public and pairwise subjects, algorithms from the ring | `oidc_discovery.go` (ordered struct, algs from `PublishedKeys`); `TestDiscoveryFields`, `TestDiscoveryEndpoint`, go-oidc `NewProvider` in `TestRelyingPartyInterop` | ✓ |
|
||||
| Fetch JWKS | active plus retired-in-grace keys, RSA and EC JWKs with `use`/`alg`/`kid` | `jwkFor` in `oidc_keys.go`; `KeyRing.PublishedKeys` grace filter; `TestJWKSShapes`, `TestMixedRingJWKS`, `TestRotateAndPruneSmoke` | ✓ |
|
||||
| Authorize, log in, consent | login handoff with only `redirect=`, resume, one-shot consent token, silent or incremental consent, switch account | `oidc_authorize.go` / `oidc_consent.go`; `TestLoginHandoffAndSwitchSmoke`, `TestResumeAndSilentConsent`, `TestSwitchAccount`, `TestConsentCSRF`, `TestIncrementalConsent` | ✓ |
|
||||
| Exchange the code | client_secret_basic and post, PKCE S256, burn-then-check, id_token with `aud` string, `nonce`, `at_hash`, signed by the active kid | `oidc_token.go` `redeemCode`, `signIDToken`; go-oidc `Verify` plus `VerifyAccessToken` on RS256 and ES256 rings | ✓ |
|
||||
| Read userinfo | same `sub` as the id_token, uniform 401 | `oidc_userinfo.go`; `TestUserInfoUniform401`, `TestUserInfoEndpoint`, interop `provider.UserInfo` | ✓ |
|
||||
| Keep working after cutover | imported kid, secret, live access token, codes still work | `console/import_winter.go`, `console/import_keys.go`; `TestImportWinterCutoverSmoke` | ✓ |
|
||||
|
||||
### Observable Truths
|
||||
|
||||
| # | Truth | Status | Evidence |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | SC-1 / D-16: a decision note settles hand-written wristband vs zitadel/oidc and names the JOSE/JWT dependency before code lands | ✓ VERIFIED | `.planning/notes/2026-10-19-oidc-on-wristband-golang-jwt.md` (756a97c) names golang-jwt/jwt/v5, coreos/go-oidc/v3 v3.21.0, x/oauth2 v0.37.0 and rejects zitadel. It is an ancestor of 659b42a, the first commit adding `oidc_keys.go`. STACK.md marks the zitadel row Superseded. The framework `go.mod`/`go.sum` are unchanged since 95051d2. |
|
||||
| 2 | SC-2: discovery, JWKS, userinfo and id_token in the token response work with RS256 or ES256 keys; rotation publishes old and new key together | ✓ VERIFIED | `oidc_discovery.go`, `oidc_keys.go` (RS256/ES256 method derived from the key type, std-lib JWKs), `oidc_token.go`, `oidc_userinfo.go`; plugin `classes/keyring.go` (grace window). `TestRelyingPartyInterop` (go-oidc, RS256 and ES256 subtests), `TestMixedKeyRingSmoke`, `TestES256Ring`, `TestRotateAndPruneSmoke`, `TestRelyingPartyFlow` (rotation within grace) all pass in the gate's docker stage, re-run by this verifier. |
|
||||
| 3 | SC-3: resume, switch account, consent list and revocation, pairwise and public subjects; a scheduled prune removes expired codes, tokens and requests | ✓ VERIFIED | `Resume`, `SwitchAccount`, `ConsentRequest`/`DecideConsent` in wristband; `controllers/authorizations.go` (GET list, DELETE with a `FOR UPDATE` lock, 404 `not_found`); `oidc_subject.go` `SubjectSector` plus `SubjectFor`; `schedule.go` `oauthserver:prune` with `pact.Daily()`, asserted through `p.(pact.HasSchedule)` in `TestScheduleSmoke`; `console/prune.go` (six kinds). Tests: `TestRevokeAuthorization`, `TestAuthorizationsSmoke`, `TestPairwiseSubjects`, `TestSubjectStability`, `TestPruneExpired`, `TestLoginHandoffAndSwitchSmoke`. |
|
||||
| 4 | SC-4 / D-11 / D-19: an existing issuer, its signing keys (original kids) and client secrets import so relying parties keep working; the phase has a security review | ✓ VERIFIED | `console/import_winter.go` (six tables, ids, hashes, setval, refuse without --force, dry run, missing-user abort); `console/import_keys.go` reads PHP `keys.json` + `{kid}.key/.pub`. I checked the format against `wavepath.org/.../classes/KeyRing.php:20` and its `gmdate('c')` timestamps; the import keeps each kid, encrypts with `lagoon.NewEncrypted` and keeps retired state. Hash scheme identical (PHP `hash('sha256')` hex, same `oas_cs_`/`oas_ac_`/`oas_at_` prefixes). `TestImportWinterCutoverSmoke`: old access token reads userinfo, a pre-cutover id_token verifies against the Go JWKS, the old secret authenticates, the new id_token carries the imported kid. `19.4-SECURITY-REVIEW.md` covers ASVS L1 and T-19.4-01..39 plus SC, none open. |
|
||||
| 5 | SC-5: wristband README and docs updated; new code has unit tests delivered in the last plan | ✓ VERIFIED | README summary row equals the module README sentence (diff empty); `docs/services/oauth-server.md` has the "OpenID Connect provider" section; `TestDocsTree` and `docs:build --check` pass. Plan 04 commits (dd16746, 34c124a, 1651ba0, 9335e80) hold tests, the method map and the gate only, with no production code. |
|
||||
| 6 | D-01 / D-06: one wristband engine, two `*Server` flavours; the plugin publishes them as one `*Servers`; wristband imports no ORM, no framework module, no application | ✓ VERIFIED | Flavour dispatch at `authorize.go:35`, `token.go:67`, `register.go:61`, `server.go:179`; `servers.go:147-155` builds `NewOIDCServer` + `SetOIDCBackend` and `NewServer` + `SetBackend`. `go list -deps ./modules/wristband` shows no gorm or summercms module; the application-name grep over non-test wristband files and the docs page is empty. |
|
||||
| 7 | D-09: existing `Backend`/`Tx` source-compatible; fonoteka compiles and passes with only an explicit Resource; `DefaultOptions().Resource` is empty | ✓ VERIFIED | fonoteka `oauth_store.go` unchanged since 10.2; fonoteka 687e25a touches only `plugin.go` (line 157) and `oauth_resource_test.go`. The gate's docker stage ran 37 fonoteka OAuth tests including `TestOAuthResourceExplicit` and `TestPhase08Coverage`: all pass. `TestDefaultOptionsResourceEmpty` passes. |
|
||||
| 8 | D-07 / D-08: kind isolation; MCP refuses openid and oidc-kind clients; OIDC accepts only oidc kind; DCR never yields identity scopes | ✓ VERIFIED | `authorize.go:64`, `token.go:197` (Kind oidc counts as unknown); plugin stores filter `kind` in SQL. `TestKindIsolation`, `TestKindIsolationSmoke`, `TestMCPRefusesOpenID`, `TestMCPStoreSmoke` pass. |
|
||||
| 9 | D-10 / D-12: keys in the DB, private PEM `lagoon.Encrypted` and `json:"-"`, lazy first key, one active key under races, RS256 default, ES256 configurable | ✓ VERIFIED | `classes/keyring.go` (`generateFirst` ON CONFLICT on the partial index, silent logger); the migration holds `oas_keys_one_active ... WHERE active`. `TestKeyRingFirstKeyRaceSmoke`, `TestKeyRingMultiInstanceRace`, `TestKeyRingPreviousAppKey`, `TestKeyRing` pass. |
|
||||
| 10 | D-03 / D-04: branded server-rendered consent and error pages with sanitised branding and a one-shot token; redirect consent mode with JSON endpoints; anonymous users get the login URL with only `redirect=` | ✓ VERIFIED (visual likeness → human item 1) | `views/consent.html.tmpl` (hidden `request_id`/`consent_token`, deny first), `classes/branding.go` sanitisers to `template.CSS`, `controllers/consent_api.go`; `TestConsentPageBrandingSmoke`, `TestConsentPageSecurity`, `TestConsentRedirectMode`, `TestLoginHandoffAndSwitchSmoke`. |
|
||||
| 11 | D-05: sm-user-plugin changes additive only; claims from its users; cascade on user.Deleting / user.Deactivating | ✓ VERIFIED | Every removed non-test line since c077029 is inside `applyAvatar`'s body or the `bulk("deactivate", ...)` entry, so no exported declaration was removed. This is stronger than the gate's grep, which misses var, const and field removals (IN-09). `RequestUser`, `EndRequestSession`, `AvatarURL`, `DeactivatingEventName`, `FireDeactivatingEvent` exist. `TestRequestUser`, `TestEndRequestSession`, `TestAvatarURL`, `TestDeactivatingEvent`, `TestUserDeleteCascade`, `TestClaimBuilder` pass. |
|
||||
| 12 | D-15: random UUIDv4 subjects per (user, sector), public by default, pairwise opt-in with `ErrSectorRequired`; userinfo sub equals id_token sub | ✓ VERIFIED | `oidc_subject.go`; store `SubjectFor` ON CONFLICT then select; `TestOIDCSubjectIsRandomSmoke`, `TestPairwiseSubjectSmoke`, `TestPairwiseSubjects`. |
|
||||
| 13 | D-17: refresh off by default; opt-in per client with rotation and lineage revocation | ✓ VERIFIED | `oidc_token.go:286`, `:303-425`; `TestRefreshOptIn`, `TestOIDCRefreshOptInSmoke`. Note: code-replay revocation misses rotated successors (WR-01, warning; see below). |
|
||||
| 14 | D-18: PHP throttle buckets and keys; discovery and JWKS unthrottled | ✓ VERIFIED | `buckets.go:44` (30/20/60/120 per minute, PHP key prefixes); `routes.go` (body cap before `throttle:oauth-token`, none on discovery/JWKS); `TestBuckets`, `TestBucketKeysSmoke`, `TestTokenLimitsSmoke`. |
|
||||
| 15 | D-14 (amended 2026-10-19): the parity harness ships and fails closed; recordings deferred to the WavePath cutover; NOT RUN does not hold the verdict | ✓ VERIFIED | Ten flow files, `rules.yaml`, `deviations.yaml` (11 reasons, all citing a D-number), runbook. This verifier ran the parity tests: they SKIP with "no PHP recordings" by default; under `PHASE194_REQUIRE_PARITY=1` both FAIL (exit 1). No flow holds `response:`, and no `.vars` file is tracked. The gate prints "NOT RUN parity: D-14 unproven (... deferred, see .planning/todos/pending/2026-10-19-record-php-oidc-parity-against-wavepath.md)". The todo exists (g15office govfaqir). PHP method map: 171 of 171 methods mapped (146 ported, 25 adapted, 0 not-applicable), enforced by `TestPHPMethodMap`. |
|
||||
|
||||
**Score:** 15/15 truths verified (0 present, behavior-unverified)
|
||||
|
||||
All behavior-dependent truths (consent one-shot burn, code burn-then-check and replay revocation, refresh rotation and lineage, key-race single active key, cascade, rotation grace) are exercised by named tests the gate requires and that passed in this verifier's own `--all` run.
|
||||
|
||||
### Deferred Items
|
||||
|
||||
| # | Item | Addressed In | Evidence |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | PHP parity recording and a green `TestPHPParityReplay` / `TestIDTokenShapeParity` | WavePath cutover phase (preliminary task, not yet on the roadmap) | User decision 2026-10-19; CONTEXT D-14 amendment; amended plan 03/04 must-haves (3386cbe); todo `2026-10-19-record-php-oidc-parity-against-wavepath.md`. The user ruled that this does not block the phase. It is not a gap or a human item. |
|
||||
|
||||
### Required Artifacts
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
| --- | --- | --- | --- |
|
||||
| `.planning/notes/2026-10-19-oidc-on-wristband-golang-jwt.md` | SC-1 decision note | ✓ VERIFIED | all dependency names present; precedes code |
|
||||
| `modules/wristband/oidc_options.go` | NewOIDCServer, SetOIDCBackend, OIDCHost, ConsentView, IssueOIDCClientCredentials | ✓ VERIFIED | used by plugin `servers.go`, `controllers/clients.go` |
|
||||
| `modules/wristband/oidc_stores.go` | OIDCBackend, OIDCTx, records | ✓ VERIFIED | implemented by plugin `classes/oidc_store.go` |
|
||||
| `modules/wristband/oidc_keys.go` | KeyStore, JWK, id_token signing, PEM parsing | ✓ VERIFIED | `jwt.MapClaims`; implemented by `classes/keyring.go` |
|
||||
| `modules/wristband/oidc_authorize.go` / `oidc_consent.go` / `oidc_token.go` / `oidc_userinfo.go` / `oidc_subject.go` / `oidc_redirect.go` / `oidc_discovery.go` | protocol | ✓ VERIFIED | 6,915 lines across the oidc_*.go files; mounted by plugin routes |
|
||||
| `../sm-oauthserver-plugin/plugin.go`, `servers.go`, `routes.go`, `buckets.go`, `schedule.go` | plugin wiring | ✓ VERIFIED | capability methods present; Requires golem15.user |
|
||||
| `../sm-oauthserver-plugin/classes/{oidc_store,mcp_store,keyring,host,claims,branding,cascade,reveal}.go` | stores, key ring, pages, claims | ✓ VERIFIED | listeners registered at `plugin.go:93-94` |
|
||||
| `../sm-oauthserver-plugin/updates/2026101900{01,02,03}_*.go` | raw SQL migrations | ✓ VERIFIED | `WHERE active` partial index present |
|
||||
| `../sm-oauthserver-plugin/console/{import_winter,import_keys,import_settings,rotate_keys,prune}.go` | import, rotate, prune | ✓ VERIFIED | registered in `Commands()` |
|
||||
| `../sm-oauthserver-plugin/controllers/{clients,branding,authorizations,consent_api}.go` + YAML | admin and JSON APIs | ✓ VERIFIED | `regenerate_secret` Danger action; SingletonRecord branding |
|
||||
| `../sm-oauthserver-plugin/views/{consent,error}.html.tmpl` | branded pages | ✓ VERIFIED | embedded and executed by `classes/host.go` |
|
||||
| `../sm-oauthserver-plugin/interop_test.go` | go-oidc interop | ✓ VERIFIED | RS256 and ES256 subtests; go-oidc only in `_test.go` |
|
||||
| `../sm-oauthserver-plugin/parity_test.go` + `testdata/parity/**` | fail-closed harness | ✓ VERIFIED | see truth 15 |
|
||||
| `../sm-oauthserver-plugin/testdata/PHP_METHOD_MAP.md`, `php_methods.txt`, `php_method_map_test.go` | case-by-case map | ✓ VERIFIED | 171/171 |
|
||||
| `scripts/check-phase19.4.sh` | fail-closed gate | ✓ VERIFIED | `--all` exit 0 on this host |
|
||||
| `.planning/phases/19.4-.../19.4-SECURITY-REVIEW.md` | ASVS L1 review | ✓ VERIFIED | every threat verified, fixed (test pin) or accepted |
|
||||
| sm-user-plugin `classes/session_user.go`, `avatar.go`, `events.go`, `controllers/registration.go` | additive exports | ✓ VERIFIED | used by plugin `host.go`, `claims.go`, `cascade.go` |
|
||||
|
||||
### Key Link Verification
|
||||
|
||||
| From | To | Via | Status | Details |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `authorize.go` | `oidc_authorize.go` | flavour dispatch before MCP code | WIRED | `authorize.go:35` |
|
||||
| `oidc_token.go` | `oidc_keys.go` | `ActiveSigningKey` → `signIDToken` | WIRED | interop verifies the signature |
|
||||
| `oidc_discovery.go` | KeyStore | `PublishedKeys` | WIRED | `publishedKeys()` |
|
||||
| fonoteka `plugin.go` | wristband `Options.Resource` | `oauthOpts.Resource =` | WIRED | `plugin.go:157` |
|
||||
| plugin `servers.go` | `NewOIDCServer`/`SetOIDCBackend` | Boot | WIRED | boot fails on error |
|
||||
| plugin `classes/host.go` | sm-user `RequestUser` / `EndRequestSession` | OIDCHost | WIRED | handoff and switch smokes |
|
||||
| plugin `plugin.go` | sm-user `DeletingEvent` / `DeactivatingEvent` | `app.Events.Listen` | WIRED | `plugin.go:93-94`; `TestUserDeleteCascade` |
|
||||
| plugin `routes.go` | `buckets.go` | `throttle:oauth-*` | WIRED | `TestTokenLimitsSmoke` gets a 429 after 20 attempts |
|
||||
| plugin `schedule.go` | `console/prune.go` | `pact.Daily()` via `HasSchedule` | WIRED | asserted through the interface |
|
||||
| plugin `console/import_keys.go` | `models/signing_key.go` | `lagoon.NewEncrypted` | WIRED | `import_keys.go:246` |
|
||||
| plugin `controllers/clients.go` | `wristband.IssueOIDCClientCredentials` | FormBeforeCreate | WIRED | `TestClientsAdminSmoke` |
|
||||
| `scripts/check-phase19.4.sh` | `TestPHPMethodMap` | plugin stage | WIRED | passes in the gate |
|
||||
|
||||
### Data-Flow Trace (Level 4)
|
||||
|
||||
| Artifact | Data Variable | Source | Produces Real Data | Status |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| JWKS / discovery algs | published keys | `golem15_oauthserver_keys` (active OR retired within grace) | yes (Postgres in tests) | ✓ FLOWING |
|
||||
| id_token claims | identity claims | sm-user `users` + user groups + optional userfriends tables (`classes/claims.go`) | yes | ✓ FLOWING (WR-06: the communities read-failure fallback is not safe inside the Postgres transaction) |
|
||||
| consent page | client, scopes, branding | `golem15_oauthserver_clients`, auth request, settings row 1 | yes | ✓ FLOWING |
|
||||
| authorizations list | consents joined to clients | `golem15_oauthserver_consents` | yes | ✓ FLOWING |
|
||||
|
||||
### Behavioral Spot-Checks
|
||||
|
||||
| Behavior | Command | Result | Status |
|
||||
| --- | --- | --- | --- |
|
||||
| Full phase gate across four repositories, Docker included | `bash scripts/check-phase19.4.sh --all` | exit 0. PASS self-test, go (42 wristband + 1 tide), plugin (8), userplugin, fonoteka (3), docs, docker (41 plugin + 3 user plugin + 37 fonoteka OAuth). NOT RUN parity (deferred). | ✓ PASS |
|
||||
| Parity skips without recordings | `go test -short -run '^(TestPHPParityReplay\|TestIDTokenShapeParity)$' -v .` (plugin) | both `--- SKIP` "no PHP recordings" | ✓ PASS |
|
||||
| Parity fails closed | same with `PHASE194_REQUIRE_PARITY=1` | both `--- FAIL`, exit 1 | ✓ PASS |
|
||||
| Decision note precedes code | `git merge-base --is-ancestor 756a97c 659b42a` | ancestor | ✓ PASS |
|
||||
| wristband purity | `go list -deps ./modules/wristband \| grep -E 'gorm.io\|summercms/modules/'` | no match | ✓ PASS |
|
||||
| Method map complete | row count vs `php_methods.txt` vs live PHP tree | 171 / 171 / 171 | ✓ PASS |
|
||||
|
||||
### Probe Execution
|
||||
|
||||
No `scripts/*/tests/probe-*.sh` exists or is declared by the plans; the phase gate script stands in for probes and was run above. Step 7c: no probes to run.
|
||||
|
||||
### Requirements Coverage
|
||||
|
||||
The phase IDs are local (CONTEXT and plan success criteria); `.planning/REQUIREMENTS.md` maps nothing to Phase 19.4 (ROADMAP "Requirements: TBD"), so there are no orphaned REQUIREMENTS.md IDs.
|
||||
|
||||
| Requirement | Source Plan | Description | Status | Evidence |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| SC-1 | 01, 04 | Decision note before code, names the JOSE dependency | ✓ SATISFIED | truth 1 |
|
||||
| SC-2 | 01, 02, 04 | Discovery, JWKS, userinfo, id_token on RS256/ES256; rotation publishes both | ✓ SATISFIED | truth 2 |
|
||||
| SC-3 | 01, 02, 03, 04 | Resume, switch account, consent list/revoke, subjects, scheduled prune | ✓ SATISFIED | truth 3 |
|
||||
| SC-4 | 03, 04 | Import of issuer, keys, secrets; security review | ✓ SATISFIED | truth 4 |
|
||||
| SC-5 | 01, 04 | README and docs; unit tests in the last plan | ✓ SATISFIED | truth 5 |
|
||||
| D-01 | 01, 02 | Protocol in wristband, storage/UI in the new plugin | ✓ SATISFIED | truth 6 |
|
||||
| D-02 | 02, 04 | Plugin stores serve MCP and OIDC | ✓ SATISFIED | `classes/mcp_store.go`; `TestMCPStore`, `TestMCPStoreSmoke` |
|
||||
| D-03 | 02, 03, 04 | Server-rendered branded pages, CSRF, redirect mode | ✓ SATISFIED (visual → human 1) | truth 10 |
|
||||
| D-04 | 01, 02 | Login handoff, cookie user, switch account | ✓ SATISFIED | truth 10 |
|
||||
| D-05 | 02, 04 | Requires sm-user-plugin; additive changes only | ✓ SATISFIED | truth 11 |
|
||||
| D-06 | 01, 02 | Two Servers, one engine | ✓ SATISFIED | truth 6 |
|
||||
| D-07 | 01, 04 | MCP cannot request openid | ✓ SATISFIED | truth 8 |
|
||||
| D-08 | 01, 02, 03, 04 | Client kind; each Server accepts only its kind | ✓ SATISFIED (admin SPA → human 2) | truth 8 |
|
||||
| D-09 | 01, 04 | Additive interfaces; fonoteka unchanged | ✓ SATISFIED | truth 7 |
|
||||
| D-10 | 02, 04 | DB key ring, encrypted private PEM, lazy first key | ✓ SATISFIED | truth 9 |
|
||||
| D-11 | 03, 04 | Import keeps original kids and retired state | ✓ SATISFIED | truth 4 |
|
||||
| D-12 | 01, 02, 04 | RS256 default, ES256 configurable, mixed JWKS | ✓ SATISFIED | truth 2 |
|
||||
| D-13 | 03, 04 | rotate-keys, grace window, opt-in scheduled rotation | ✓ SATISFIED | `console/rotate_keys.go`, `schedule.go`; `TestRotateAndPruneSmoke`, `TestRotateKeysCommand`, `TestScheduleConfig` |
|
||||
| D-14 | 01, 03, 04 | Parity bar (amended: harness fail-closed, recording deferred) | ✓ SATISFIED (as amended) | truth 15; recording deferred |
|
||||
| D-15 | 01, 02, 03, 04 | Public default, pairwise opt-in | ✓ SATISFIED | truth 12 |
|
||||
| D-16 | 01, 02 | Library decision; real RP interop test | ✓ SATISFIED | truths 1, 2 |
|
||||
| D-17 | 01, 02, 03, 04 | Opt-in refresh per client | ✓ SATISFIED | truth 13 (WR-01, WR-02 warnings) |
|
||||
| D-18 | 02, 04 | PHP throttle buckets and keys | ✓ SATISFIED | truth 14 |
|
||||
| D-19 | 02, 03, 04 | Import of the six tables; hashes copied as-is; issuer from config | ✓ SATISFIED | truth 4; `servers.go:71-75` |
|
||||
| Folded todo | 01 | Neutral `DefaultOptions().Resource` | ✓ SATISFIED | truth 7; todo moved to `done/` |
|
||||
|
||||
### Prohibitions (judgment tier, LLM-judge verdicts, non-authoritative)
|
||||
|
||||
Each verdict below is unverified-prohibition: human review recommended (human item 3).
|
||||
|
||||
| # | Plan | Prohibition | Verdict | Evidence |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | 01 | wristband imports no ORM, framework module or application; no non-test file, README or docs page names a consuming application | held | `go list -deps` clean; name grep empty; gate docs stage "127 files checked" |
|
||||
| 2 | 01 | MCP wire bytes unchanged; old tests pass with only explicit-resource edits | held | pre-existing test diffs only swap application literals for neutral ones; `TestPhase08Coverage` and 37 fonoteka OAuth tests pass; MCP encoders untouched (`git diff 95051d2` removes no encoder line) |
|
||||
| 3 | 01 | Private keys, codes, tokens and secrets never logged or in error strings | held | `TestOIDCLogsCarryNoCredentials`, `TestImportKeyDirErrorsNameNoKey`; security review V7 sweep |
|
||||
| 4 | 02 | No sm-user-plugin exported signature changes; DeactivateUsers unchanged | held | full removed-line diff since c077029 (truth 11) |
|
||||
| 5 | 02 | Signing-key model never in cabana; private PEM never leaves the key ring | held | `TestAdminSurfaceHidesCredentials`; no `SigningKey` in `controllers/` or `admin.go` |
|
||||
| 6 | 02 | Nothing pushed, tagged or deployed; no app pointer bumped | held | plugin remote has no repository yet; user plugin ahead 7 with no remote branch containing b82d85f; summercms.go ahead 31, no tag contains 659b42a; fonoteka pointers modified but uncommitted (another session) |
|
||||
| 7 | 03 | No private key, plaintext secret, session token or vars file committed | held | `TestParityFixturesHoldNoSecrets`; no `.vars` tracked; secret-pattern grep over testdata clean |
|
||||
| 8 | 03 | Signing-key model never in a cabana controller, form or list | held | same as 5 |
|
||||
| 9 | 03 | Nothing pushed, tagged or deployed | held | same as 6 |
|
||||
| 10 | 04 | No production behaviour change in plan 04 except defect fixes | held | plan 04 commits touch only `_test.go`, testdata, the parity runbook text and the gate |
|
||||
| 11 | 04 | Nothing pushed or tagged; release waits for the user | held | same as 6 |
|
||||
|
||||
### Anti-Patterns Found
|
||||
|
||||
| File | Line | Pattern | Severity | Impact |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| (all phase files in three repos) | n/a | TBD / FIXME / XXX / TODO / placeholder | none found | no debt markers |
|
||||
| `sm-oauthserver-plugin/classes/oidc_store.go` | 301-312 | WR-01: code replay leaves rotated refresh successors alive | ⚠️ Warning | only with `refresh.enabled: true` (default off, Go-only) |
|
||||
| `sm-oauthserver-plugin/classes/host.go`, `lang/*` | 271-285 | WR-02: consent page never lists `offline_access` | ⚠️ Warning | only with refresh enabled and an OfflineAccess client |
|
||||
| `sm-oauthserver-plugin/console/import_winter.go` | 216-275 | WR-03: `--force` re-import leaves refresh tokens and wipes MCP clients | ⚠️ Warning | the first import into empty tables (the cutover path) is unaffected |
|
||||
| `sm-oauthserver-plugin/controllers/clients.go` | 330-352 | WR-04: imported clients with legacy redirect URIs cannot be saved in the admin | ⚠️ Warning | authorize keeps working; admin edit blocked |
|
||||
| `modules/wristband/oidc_subject.go`, plugin `controllers/clients.go` | 160-188 / 366-384 | WR-05: editing a pairwise client's sector silently changes every sub | ⚠️ Warning | pairwise is Go-only and opt-in; admin action |
|
||||
| `sm-oauthserver-plugin/classes/claims.go` | 181-190 | WR-06: communities read failure aborts the Postgres transaction (500 instead of `[]`) | ⚠️ Warning | only on a transient failure of the optional read (confirmed in code: it runs on the transaction handle with no savepoint) |
|
||||
| `scripts/check-phase19.4.sh` | 436-438 | IN-09: additive check greps only func/type removals | ℹ️ Info | the verifier's full diff found no removals of any kind |
|
||||
|
||||
**Do the review warnings break a success criterion?** No. WR-01, WR-02 and WR-05 are in Go-only, default-off or opt-in features (refresh per D-17, pairwise per D-15) that the PHP relying parties do not use. The "switch without noticing" contract and SC-2/SC-3 hold on the default path. WR-03 and WR-04 affect a forced re-import and admin editing of legacy clients, not the SC-4 cutover. A first import into empty tables keeps relying parties working (`TestImportWinterCutoverSmoke`). WR-06 needs a transient failure of an optional query. All six stay open in `19.4-REVIEW-DISPOSITION.md`. They should be fixed before an application enables refresh or pairwise subjects, but they are not goal failures. WR-01 is the most security-relevant: an attacker holding an intercepted code who refreshed once keeps a 30-day lineage. Fix it before anyone turns `refresh.enabled` on.
|
||||
|
||||
Pre-existing and out of scope (`deferred-items.md`): fonoteka `TestPhase09SecurityRoutes` allowlist misses cabana routes from other phases, and two tide test files are not gofmt-clean. Neither involves this phase's code.
|
||||
|
||||
### Human Verification Required
|
||||
|
||||
### 1. Consent and error pages against PHP
|
||||
|
||||
**Test:** With the plugin running locally, set a site name, a logo and an accent colour in row 1 of `golem15_oauthserver_settings`, open an authorize URL for a test client and the inactive-client error page, and compare both side by side with PHP's `consent.htm` / `error.htm` in a browser.
|
||||
**Expected:** Same structure and copy, branding applied, error heading in the fixed danger colour.
|
||||
**Why human:** Tests prove branding reaches the HTML safely; none judges visual likeness.
|
||||
|
||||
### 2. Clients and Branding admin screens in the SPA
|
||||
|
||||
**Test:** Open both screens in the embedded admin SPA: create a client, check the one-time secret reveal, the scopes taglist, the authorized-users table, the regenerate danger button, and the Settings link to Branding with a logo upload.
|
||||
**Expected:** Both render usably; the secret appears once.
|
||||
**Why human:** The admin smokes drive the JSON API only.
|
||||
|
||||
### 3. Judgment-tier prohibitions
|
||||
|
||||
**Test:** Review the 11 LLM-judge verdicts in the Prohibitions table.
|
||||
**Expected:** Confirm each held.
|
||||
**Why human:** Verifier verdicts on judgment-tier prohibitions are non-authoritative.
|
||||
|
||||
Not listed on purpose: the PHP parity recording. The user deferred it to the WavePath cutover (D-14 amendment), and it does not hold this verdict.
|
||||
|
||||
### Gaps Summary
|
||||
|
||||
No gaps. Every ROADMAP success criterion, every CONTEXT decision D-01..D-19 and the folded todo is implemented, wired and pinned by named tests. Those tests pass in this verifier's own run of `scripts/check-phase19.4.sh --all` on a Docker host (all stages PASS; parity NOT RUN as amended). The status is `human_needed` only because of two visual checks (D-03/D-08) and the review of judgment-tier prohibitions. The six open review warnings are real defects in opt-in or admin-edge paths. None breaks a success criterion, and WR-01 should be fixed before refresh is enabled anywhere.
|
||||
|
||||
---
|
||||
|
||||
_Verified: 2026-10-19T21:10:00Z_
|
||||
_Verifier: Claude (msd-verifier)_
|
||||
Reference in New Issue
Block a user