fix(08): revise plans based on checker feedback
This commit is contained in:
@@ -223,7 +223,7 @@ Plans:
|
||||
4. The guarded outbound fetch helper rejects a non-allow-listed host and enforces a byte cap and timeout on a user-supplied cover URL fetch (manual cover URL, Discogs cover).
|
||||
5. OpenAPI is generated from swaggo/swag annotations on handlers and `openapi-typescript` produces valid TypeScript types from it; CORS and JSON body-size limits match the PHP deployment.
|
||||
|
||||
**Plans**: 6 plans
|
||||
**Plans**: 10 plans
|
||||
|
||||
Plans:
|
||||
**Wave 1** *(parallel)*
|
||||
@@ -325,27 +325,40 @@ Plans:
|
||||
|
||||
**Wave 1**
|
||||
|
||||
- [ ] 08-01-PLAN.md — Discover and dynamically register clients through the real app and corrected OAuth schema
|
||||
- [ ] 08-01-PLAN.md — Define and prove the app-agnostic metadata and DCR engine
|
||||
|
||||
**Wave 2** *(blocked on 08-01)*
|
||||
|
||||
- [ ] 08-02-PLAN.md — Complete S256 authorize, JWT consent, and atomic authorization-code exchange
|
||||
- [ ] 08-02-PLAN.md — Correct OAuth schema and implement transaction-scoped Postgres stores
|
||||
|
||||
**Wave 3** *(blocked on 08-02)*
|
||||
|
||||
- [ ] 08-03-PLAN.md — Rotate refresh grants, kill replayed lineages, and manage connected apps
|
||||
- [ ] 08-03-PLAN.md — Mount persistent metadata and DCR on the assembled raw route surface
|
||||
|
||||
**Wave 4** *(blocked on 08-03)*
|
||||
|
||||
- [ ] 08-04-PLAN.md — Provision confidential clients and serve the MCP personal-token bootstrap
|
||||
- [ ] 08-04-PLAN.md — Implement ordered authorize validation and atomic PKCE code exchange
|
||||
|
||||
**Wave 5** *(blocked on 08-04)*
|
||||
|
||||
- [ ] 08-05-PLAN.md — Replay PHP OAuth flows and run the unchanged real MCP lifecycle
|
||||
- [ ] 08-05-PLAN.md — Wire JWT consent and prove the unchanged Nuxt UI contract
|
||||
|
||||
**Wave 6** *(blocked on 08-05; blocking security checkpoint)*
|
||||
**Wave 6** *(blocked on 08-05)*
|
||||
|
||||
- [ ] 08-06-PLAN.md — Close 103-method coverage, independent security review, and final phase gate
|
||||
- [ ] 08-06-PLAN.md — Rotate refresh grants, kill replayed lineages, and manage connected apps
|
||||
|
||||
**Wave 7** *(parallel; blocked on 08-06)*
|
||||
|
||||
- [ ] 08-07-PLAN.md — Provision confidential clients through the exact operator command
|
||||
- [ ] 08-08-PLAN.md — Serve the MCP personal-token bootstrap on the existing token surface
|
||||
|
||||
**Wave 8** *(blocked on 08-07 and 08-08)*
|
||||
|
||||
- [ ] 08-09-PLAN.md — Replay PHP OAuth flows and run the unchanged real MCP lifecycle through the pre-security gate
|
||||
|
||||
**Wave 9** *(blocked on 08-09; blocking security checkpoint)*
|
||||
|
||||
- [ ] 08-10-PLAN.md — Close 103-method coverage, independent security review, and the final fail-closed gate
|
||||
|
||||
### Phase 9: Backend admin authentication and schema pipeline
|
||||
|
||||
@@ -483,7 +496,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 →
|
||||
| 5. Data layer full fidelity | 6/6 | Complete | 2026-09-18 |
|
||||
| 6. HTTP routing, auth groups and rate limiting | 14/14 | Complete | 2026-09-21 |
|
||||
| 7. User plugin and authentication | 8/8 | Complete | 2026-09-23 |
|
||||
| 8. OAuth2.1 authorization server | 0/TBD | Not started | - |
|
||||
| 8. OAuth2.1 authorization server | 0/10 | Not started | - |
|
||||
| 9. Backend admin authentication and schema pipeline | 0/TBD | Not started | - |
|
||||
| 10. Admin Vue SPA | 0/TBD | Not started | - |
|
||||
| 11. Jobs, realtime and search infrastructure | 0/TBD | Not started | - |
|
||||
|
||||
@@ -10,55 +10,39 @@ files_modified:
|
||||
- wristband/crypto.go
|
||||
- wristband/register.go
|
||||
- wristband/registration_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/models/oauth_client.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/models/oauth_auth_code.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/updates/12_oauth_schema_correction.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/updates/oauth_schema_correction_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/config/config.yaml
|
||||
- ../fonoteka.go/config/app.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/plugin.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/routes.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/oauth_registration_test.go
|
||||
- scripts/check-phase8-red.sh
|
||||
autonomous: true
|
||||
requirements: [AUTH-05, AUTH-06, AUTH-07]
|
||||
requirements: [AUTH-05, AUTH-06]
|
||||
must_haves:
|
||||
truths:
|
||||
- "An unauthenticated connector can fetch the exact RFC 8414 metadata document and dynamically register a public or confidential client."
|
||||
- "Registration is JSON-only, rate-limited, bounded to 64 KiB, and emits bare PHP-compatible success and error bodies."
|
||||
- "Public clients, multiple pending requests, and the required OAuth lookup indexes persist correctly in Postgres."
|
||||
- "D-01: The app-agnostic standard-library wristband package serves exact RFC 8414 metadata and validates RFC 7591 registration without zitadel/oidc."
|
||||
- "D-06: PHP-minimal response shapes and configurable metadata fields are wristband defaults with no response hooks."
|
||||
- "D-02: Registration is JSON-only, and D-21: its body is bounded at 64 KiB before decoding with endpoint-native errors."
|
||||
- "D-04: Client-secret checks use constant-time fixed transforms and DCR cap/sweep behavior is deterministic under concurrency."
|
||||
artifacts:
|
||||
- path: "wristband/server.go"
|
||||
provides: "App-agnostic OAuth options and RFC 8414 metadata handler"
|
||||
exports: ["Options", "Server", "New"]
|
||||
provides: "Options, exact metadata writer, and app-agnostic server contract"
|
||||
- path: "wristband/register.go"
|
||||
provides: "RFC 7591 validation, client issuance, cap, sweep, and bounded JSON handler"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go"
|
||||
provides: "GORM-backed transaction-scoped wristband store"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/updates/12_oauth_schema_correction.go"
|
||||
provides: "Additive nullability and index correction"
|
||||
provides: "RFC 7591 validation, issuance, cap, sweep, and bounded handler"
|
||||
- path: "scripts/check-phase8-red.sh"
|
||||
provides: "Fail-closed RED verifier rejecting syntax/setup/missing-test failures"
|
||||
key_links:
|
||||
- from: "../fonoteka.go/plugins/golem15/fonoteka/plugin.go"
|
||||
to: "wristband.New"
|
||||
via: "Boot constructs the server from app config and the GORM backend"
|
||||
pattern: "wristband\\.New"
|
||||
- from: "../fonoteka.go/plugins/golem15/fonoteka/routes.go"
|
||||
to: "wristband.Server"
|
||||
via: "raw metadata and register handlers, with register throttle"
|
||||
pattern: "GroupRaw|fonoteka-oauth-register"
|
||||
- from: "wristband/register.go"
|
||||
to: "wristband.Backend.WithinTx"
|
||||
via: "serialized sweep, cap check, and create"
|
||||
pattern: "WithinTx"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Deliver the first real OAuth vertical slice: an unchanged connector can discover this authorization server and register a client against persistent Postgres state.
|
||||
Define and implement the framework-only discovery and dynamic-registration contract before app persistence or routing.
|
||||
|
||||
Purpose: Establish the exact raw wire contract and correct data representation that every later grant flow uses, while honoring D-01's direct standard-library implementation instead of zitadel/oidc.
|
||||
Output: The `wristband` metadata/DCR surface, corrected OAuth schema and models, GORM store adapter, app configuration, boot wiring, and raw routes.
|
||||
Purpose: Keep D-01's protocol engine small and app-agnostic while making the RED phase executable and diagnostic.
|
||||
Output: `wristband` metadata/DCR contracts, deterministic tests, crypto helpers, and the shared RED verifier.
|
||||
</objective>
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** fonoteka-mcp or ChatGPT connector, **I want to** discover and register with SummerCMS using the same OAuth contract as the PHP backend, **so that** I can begin the unchanged PKCE connection flow.
|
||||
**As a** connector implementer, **I want to** exercise discovery and registration against a deterministic OAuth engine, **so that** the app adapter can persist and mount an already proven wire contract.
|
||||
|
||||
<execution_context>
|
||||
@/home/jin/.codex/get-shit-done/workflows/execute-plan.md
|
||||
@@ -73,118 +57,38 @@ Output: The `wristband` metadata/DCR surface, corrected OAuth schema and models,
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-RESEARCH.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-PATTERNS.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
|
||||
|
||||
<interfaces>
|
||||
Existing routing contract from `pact/capabilities.go` and `surf/router.go`:
|
||||
- `Router.GroupRaw(prefix string, middleware []string, fn func(Router))`
|
||||
- `Router.Get/Post/Delete(path string, handler http.HandlerFunc, middleware ...string)`
|
||||
|
||||
Existing application records:
|
||||
- `models.OAuthClient` owns client id, optional secret hash, redirect/grant/auth-method JSON, registration IP, consent/revocation, and scope ceiling.
|
||||
- `models.OAuthAuthCode` must represent a pre-consent row with nullable request/code/user fields.
|
||||
- `Plugin.Buckets()` already exposes `fonoteka-oauth-register` at 30 requests/minute keyed by trusted client IP.
|
||||
|
||||
New framework contract established by this plan:
|
||||
- `wristband.Options` carries issuer, endpoint paths, scopes/auth methods, resource, consent URL builder, TTLs, DCR cap/stale age, and `RegisterMaxBytes: 65536`.
|
||||
- `wristband.Backend.WithinTx(context.Context, func(wristband.Tx) error) error` is the only mutation boundary; `Tx` composes client/code/refresh/token-issuer operations without importing GORM.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Specify the discovery and registration path with failing tests</name>
|
||||
<files>wristband/registration_test.go, ../fonoteka.go/plugins/golem15/fonoteka/updates/oauth_schema_correction_test.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_registration_test.go</files>
|
||||
<read_first>
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-RESEARCH.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
|
||||
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/postgres_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/plugin_boot_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/updates/11_secrets_slice.go
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/OAuthMetadataController.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/OAuthRegisterController.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthMetadataTest.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthRegisterTest.php
|
||||
</read_first>
|
||||
<name>Task 1: Create compiling RED discovery and registration contracts</name>
|
||||
<files>wristband/server.go, wristband/stores.go, wristband/registration_test.go, scripts/check-phase8-red.sh</files>
|
||||
<behavior>
|
||||
- Metadata returns the exact unwrapped 11-field document, field order, content type, and recorded cache header.
|
||||
- JSON DCR creates public `none` and confidential `client_secret_post`/`client_secret_basic` clients; a secret is returned once and only its SHA-256 hex is stored.
|
||||
- Non-JSON, invalid URI/auth/grant/response metadata, more than five URIs, cap overflow, and a body larger than 65,536 bytes return exact endpoint-native errors.
|
||||
- Concurrent registrations cannot cross the configured client cap; artisan clients with null registration IP are not swept.
|
||||
- The additive migration permits null public-client/pending fields, creates named operational indexes, and refuses rollback when null lifecycle rows would be lost.
|
||||
- Metadata is the exact unwrapped 11-field document with recorded order, content type, and cache header.
|
||||
- Public and confidential DCR validate PHP-compatible URI, grant, response, auth-method, cap, sweep, and 65,536-byte rules.
|
||||
- Test failures use `PHASE8_RED:registration` only for missing behavior; syntax, build, setup, missing-test, panic, and unrelated failures are rejected.
|
||||
</behavior>
|
||||
<action>Per D-18 and the MVP test-first rule, add failing framework, real-Postgres, and assembled-route tests before production code. Use deterministic clock/random readers in wristband tests. Assert bytes and headers before decoding JSON. Include named failures for T-08-DCR-FLOOD, T-08-SECRET-TIMING, T-08-SURFACE, and T-08-REQUEST-LEAK. Prove the raw route table has no JWT, `inv_token`, `inv.scope`, body-limit, or house middleware, while `/register` carries only `throttle:fonoteka-oauth-register`. Commit the RED tests separately; do not weaken assertions to make current code pass.</action>
|
||||
<action>D-06: define the exported options, typed records, transaction-scoped Backend/Tx contracts, handler signatures, and deterministic clock/random seams with compiling stubs. D-18: add behavior tests that compile and intentionally fail through `PHASE8_RED:registration` assertions. Create `scripts/check-phase8-red.sh` to run the supplied command, require a nonzero result and the requested marker, and fail if output contains `build failed`, `setup failed`, `syntax error`, `no tests to run`, `no test files`, or a panic. Include named T-08-DCR-FLOOD, T-08-SECRET-TIMING, and T-08-REQUEST-LEAK cases. Commit RED separately.</action>
|
||||
<verify>
|
||||
<automated>test -f wristband/registration_test.go && cd ../fonoteka.go && test -f plugins/golem15/fonoteka/oauth_registration_test.go</automated>
|
||||
<automated>scripts/check-phase8-red.sh registration go test ./wristband -run 'Test(Metadata|Register|Registration)' -count=1</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `rg -n 'Test(Metadata|Register|RegistrationBodyLimit|RegistrationCap|OAuthSchema)' wristband/registration_test.go ../fonoteka.go/plugins/golem15/fonoteka/{oauth_registration_test.go,updates/oauth_schema_correction_test.go,classes/auth/oauth_store_test.go}` finds named tests for every behavior above.
|
||||
- At least one focused test fails because the wristband production package or raw routes do not yet exist; the failure is implementation-related, not a syntax error.
|
||||
- Test source contains literal assertions for `65536`, `Basic realm=`, `Cache-Control`, and the absence of house/auth middleware.
|
||||
</acceptance_criteria>
|
||||
<done>The executable happy-path and adversarial discovery/DCR contract exists in RED state, including schema, concurrency, body-bound, header, and surface-isolation coverage.</done>
|
||||
<done>The tests compile, the named tests execute, and the verifier accepts only the expected missing-behavior RED marker.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Implement wristband discovery, DCR, cryptography, and persistent storage</name>
|
||||
<files>wristband/server.go, wristband/stores.go, wristband/crypto.go, wristband/register.go, ../fonoteka.go/plugins/golem15/fonoteka/models/oauth_client.go, ../fonoteka.go/plugins/golem15/fonoteka/models/oauth_auth_code.go, ../fonoteka.go/plugins/golem15/fonoteka/updates/12_oauth_schema_correction.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go</files>
|
||||
<read_first>
|
||||
wristband/registration_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/updates/oauth_schema_correction_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/models/oauth_client.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/models/oauth_auth_code.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/models/oauth_refresh_token.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/updates/11_secrets_slice.go
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-PATTERNS.md
|
||||
</read_first>
|
||||
<name>Task 2: Implement exact metadata, DCR, bounds, and cryptography</name>
|
||||
<files>wristband/server.go, wristband/stores.go, wristband/crypto.go, wristband/register.go, wristband/registration_test.go</files>
|
||||
<behavior>
|
||||
- Fixed-length SHA-256 transforms use `crypto/subtle.ConstantTimeCompare`; raw client secrets are never persisted or logged.
|
||||
- DCR validates the exact PHP allow-list and URI rules, serializes cap/sweep/create atomically, and strips control characters from a maximum-120-character client name.
|
||||
- The GORM adapter applies `FOR UPDATE` only in app code and every transaction-scoped method uses the callback's `*gorm.DB`.
|
||||
- Fixed SHA-256 transforms use `crypto/subtle.ConstantTimeCompare`; raw client secrets are returned once and never persisted/logged.
|
||||
- Sweep, cap check, and create occur within one backend transaction; concurrent registrations cannot cross the cap.
|
||||
- Oversized and malformed registration input returns exact `invalid_client_metadata` bytes without a house envelope.
|
||||
</behavior>
|
||||
<action>Implement the app-agnostic `wristband` package per D-01, D-03, D-05, D-06, D-07, D-17, and D-21. Use `crypto/rand`, `sha256`, `subtle.ConstantTimeCompare`, `base64.RawURLEncoding`, `encoding/json`, and `net/http`; add no dependency. Define typed records and a transaction-scoped backend, a local no-newline JSON writer, exact metadata, and RFC 7591 registration. Enforce the 64 KiB bound with `http.MaxBytesReader` before decoding and map overflow to `invalid_client_metadata`. Create client IDs from 16 random bytes and secrets from 32; store only SHA-256 hex. Correct the two model files to pointer fields and add a new gormigrate step that drops the four `NOT NULL` constraints, adds the PHP-equivalent operational indexes idempotently, and refuses down migration when null rows exist. Implement atomic client cap/sweep/create in the GORM adapter; do not import app/GORM code from wristband.</action>
|
||||
<action>D-01: use only `crypto/rand`, `crypto/sha256`, `crypto/subtle`, `encoding/base64`, `encoding/json`, `net/http`, and `net/url`; add no dependency. D-03: model configurable TTLs, cap 200, stale age 24h, issuer/resource/endpoints, and `RegisterMaxBytes: 65536`. D-05: keep all protocol rules in wristband and import no fonoteka/GORM code. D-06: use a local no-newline exact JSON writer with no response hooks. D-07: use only the transaction-scoped interfaces. D-17: expose expired-row and unconsented-client sweep operations without timers/goroutines. D-21: apply `http.MaxBytesReader` before JSON decode. Strip control characters and cap names at 120 characters.</action>
|
||||
<verify>
|
||||
<automated>go test ./wristband -run 'Test(Metadata|Register|Registration)' -count=1 && cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/classes/auth ./plugins/golem15/fonoteka/updates -run 'TestOAuth' -count=1</automated>
|
||||
<automated>go test ./wristband -run 'Test(Metadata|Register|Registration)' -count=1</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `go list -deps ./wristband | rg 'fonoteka|gorm.io'` returns no matches.
|
||||
- `rg -n 'subtle\.ConstantTimeCompare' wristband/crypto.go` finds the fixed-transform comparison and `rg -n 'client_secret_hash.*\*string|request_id.*\*string|code_hash.*\*string|user_id.*\*uint' ../fonoteka.go/plugins/golem15/fonoteka/models/oauth_{client,auth_code}.go` finds all nullable model corrections.
|
||||
- Focused framework and real-Postgres tests pass, including concurrent cap enforcement and rollback refusal.
|
||||
- No raw request id, code, verifier, client secret, access token, or refresh token is written through a logging call in `wristband/`.
|
||||
</acceptance_criteria>
|
||||
<done>The framework protocol/storage contracts and corrected Postgres schema make public/confidential registration safe, durable, bounded, and byte-compatible.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Mount the configured metadata and DCR slice in the real app</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/config/config.yaml, ../fonoteka.go/config/app.yaml, ../fonoteka.go/plugins/golem15/fonoteka/plugin.go, ../fonoteka.go/plugins/golem15/fonoteka/routes.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_registration_test.go</files>
|
||||
<read_first>
|
||||
../fonoteka.go/plugins/golem15/fonoteka/oauth_registration_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/plugin.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/routes.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/config/config.yaml
|
||||
../fonoteka.go/config/app.yaml
|
||||
surf/router.go
|
||||
.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-03-SUMMARY.md
|
||||
</read_first>
|
||||
<behavior>
|
||||
- `GET /.well-known/oauth-authorization-server` and `POST /oauth/mcp/register` run through the assembled app, not a hand-built router.
|
||||
- Defaults are pending/code 600 seconds, access 3600 seconds, refresh 30 days, DCR cap 200, stale-client age 24 hours, resource `https://mcp.plytarium.com/mcp`, and registration maximum 65,536 bytes.
|
||||
- `/register` is throttled per trusted client IP and no `oauth` guard is registered.
|
||||
</behavior>
|
||||
<action>Per D-03, D-09, D-10, and D-12, add plugin config keys under `plugins.golem15.fonoteka.oauth.*`, keep `app.url` as the issuer source with its trailing slash trimmed once, construct the GORM backend and wristband server in `Plugin.Boot`, and retain it for route/command factories. Mount metadata and register inside the existing raw group; pass `throttle:fonoteka-oauth-register` only to register. Do not attach the Phase 6 body limiter or add an `oauth` guard. Preserve the backend token-surface 401 contract and do not add RFC 9728 metadata/challenges owned by fonoteka-mcp.</action>
|
||||
<verify>
|
||||
<automated>cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestOAuth(Metadata|Register|RawRoute|Config)' -count=1</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Assembled-route tests return 200 metadata and 201 DCR with the exact unwrapped bodies and expected headers.
|
||||
- Route-table assertions show `/oauth/mcp/register` has `throttle:fonoteka-oauth-register` and neither RFC route has JWT, `inv_token`, `inv.scope`, or house middleware.
|
||||
- `rg -n 'Register\(.*oauth|"oauth"' ../fonoteka.go/plugins/golem15/fonoteka` finds no new guard registration.
|
||||
- `go vet ./... && go test ./...` passes in both `summercms.go` and `../fonoteka.go`.
|
||||
</acceptance_criteria>
|
||||
<done>A real connector can discover and register against the assembled Go application with exact PHP-compatible routing, configuration, persistence, limits, and security boundaries.</done>
|
||||
<done>Framework discovery and DCR tests pass with exact bytes, atomic cap behavior, bounded decoding, hash-only persistence, and no app-tier imports.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
@@ -194,32 +98,28 @@ New framework contract established by this plan:
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Internet connector → raw OAuth routes | Unauthenticated metadata and attacker-controlled JSON cross into the authorization server. |
|
||||
| wristband → app Backend | App-agnostic protocol state crosses into transaction-scoped Postgres persistence. |
|
||||
| app config → public metadata | Deployment-controlled issuer/resource/endpoints become client trust anchors. |
|
||||
| Connector → wristband | Untrusted metadata/DCR requests cross into protocol parsing. |
|
||||
| wristband → Backend | Protocol state crosses into an app-provided transaction. |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-08-DCR-FLOOD | Denial of Service | `register.go`, client store | mitigate | 64 KiB pre-decode cap, existing per-IP limiter, atomic cap 200, and 24-hour unconsented sweep with concurrency tests. |
|
||||
| T-08-SECRET-TIMING | Information Disclosure | `crypto.go`, client authentication helper | mitigate | Compare fixed SHA-256 hex transforms only through `crypto/subtle.ConstantTimeCompare`; source and behavior tests reject direct equality. |
|
||||
| T-08-REQUEST-LEAK | Information Disclosure | handlers, logs, fixtures | mitigate | No secret/handle logging, hash-only persistence, one-time secret response, log-capture/source scans. |
|
||||
| T-08-SURFACE | Elevation of Privilege | raw route registration | mitigate | Route-table test proves raw routes have only their named throttle and no auth/house middleware. |
|
||||
| T-08-SC | Tampering | dependencies | mitigate | No package install occurs; fail if a new module dependency appears without a package-legitimacy audit. |
|
||||
| T-08-DCR-FLOOD | Denial of Service | register handler/store | mitigate | 64 KiB cap, serialized client cap, stale sweep, concurrency tests. |
|
||||
| T-08-SECRET-TIMING | Information Disclosure | crypto/client secret | mitigate | Fixed SHA-256 transforms and `subtle.ConstantTimeCompare`. |
|
||||
| T-08-REQUEST-LEAK | Information Disclosure | handler/tests | mitigate | Hash-only records and no sensitive-value logging. |
|
||||
| T-08-SC | Tampering | dependencies | mitigate | No package install; stdlib-only import audit. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `go test ./wristband -count=1`
|
||||
- `cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestOAuth(Metadata|Register|Schema|Store|RawRoute)' -count=1`
|
||||
- `go vet ./... && go test ./...` passes in each repository.
|
||||
- `go list -deps ./wristband | rg 'fonoteka|gorm.io'` returns no matches.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- The exact metadata document and RFC 7591 responses are reachable on the assembled app without an envelope.
|
||||
- Public and confidential clients persist with hash-only secrets; concurrent cap enforcement cannot create client 201.
|
||||
- The corrected schema represents public clients and multiple pending requests and includes the required indexes.
|
||||
- Registration rejects over-64-KiB and non-JSON requests through endpoint-native errors and the named limiter is present.
|
||||
- Exact metadata and DCR behavior is green in a self-contained framework package.
|
||||
- RED verification cannot pass on mere file presence, compile errors, missing tests, or unrelated failures.
|
||||
- DCR is bounded, concurrency-safe, and secret-safe before app integration.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
||||
@@ -5,60 +5,37 @@ type: execute
|
||||
wave: 2
|
||||
depends_on: [08-01]
|
||||
files_modified:
|
||||
- wristband/authorize.go
|
||||
- wristband/token.go
|
||||
- wristband/authorize_test.go
|
||||
- wristband/token_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/models/oauth_client.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/models/oauth_auth_code.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/updates/12_oauth_schema_correction.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/updates/oauth_schema_correction_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_token_issuer.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/api_token_manager.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/plugin.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/routes.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/oauth_connect_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go
|
||||
autonomous: true
|
||||
requirements: [AUTH-05, AUTH-06, AUTH-07]
|
||||
requirements: [AUTH-05, AUTH-07]
|
||||
must_haves:
|
||||
truths:
|
||||
- "A registered client can start an authorization request only with an exact redirect URI, S256 PKCE, valid resource, and ceiling-bounded scopes."
|
||||
- "A JWT-authenticated user can inspect, allow, or deny the pending request using the unchanged Nuxt consent screen contract."
|
||||
- "An allowed request yields a single-use authorization code that exchanges atomically for an ordinary `inv_` access token and refresh token."
|
||||
- "D-07: Public clients and multiple pending authorization requests persist through one transaction-scoped GORM adapter."
|
||||
- "D-17: Expiry sweeps delete only expired lifecycle rows and retain unexpired replay evidence."
|
||||
artifacts:
|
||||
- path: "wristband/authorize.go"
|
||||
provides: "Authorize validation, ordered redirects, pending-state creation, scope/resource/PKCE policy"
|
||||
- path: "wristband/token.go"
|
||||
provides: "Authorization-code token exchange and exact RFC 6749 responses"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go"
|
||||
provides: "JWT consent show/allow/deny payloads for unchanged Nuxt"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_token_issuer.go"
|
||||
provides: "Transaction-bound `inv_` access-token mint/revoke adapter"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/updates/12_oauth_schema_correction.go"
|
||||
provides: "Additive nullability and index correction with safe rollback refusal"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go"
|
||||
provides: "GORM transaction-scoped wristband backend"
|
||||
key_links:
|
||||
- from: "wristband/token.go"
|
||||
to: "wristband.Backend.WithinTx"
|
||||
via: "lock, consume code, mint access token, and create refresh token atomically"
|
||||
- from: "oauth_store.go"
|
||||
to: "wristband.Backend"
|
||||
via: "WithinTx callback whose methods all use callback *gorm.DB"
|
||||
pattern: "WithinTx"
|
||||
- from: "oauth_consent_controller.go"
|
||||
to: "wristband.Server"
|
||||
via: "issue-code and deny operations rather than direct OAuth row mutation"
|
||||
pattern: "IssueCode|Deny"
|
||||
- from: "oauth_token_issuer.go"
|
||||
to: "api_token_manager.go"
|
||||
via: "configured-prefix personal-token mint with oauth_client_id stamp"
|
||||
pattern: "MintPersonalToken"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Deliver the interactive authorization-code vertical slice from a connector's PKCE authorize request through Nuxt consent to one atomic token exchange.
|
||||
Make the existing Postgres schema and app store faithfully represent wristband's client and pending-request state.
|
||||
|
||||
Purpose: Make the unchanged browser and connector complete the primary OAuth flow with exact redirect, body, header, scope, tenant, and token semantics.
|
||||
Output: Authorize/token framework handlers, consent controllers, transaction-bound token issuer, store transitions, app routes, and end-to-end tests.
|
||||
Purpose: Separate persistence correctness from protocol and route wiring so nullability, indexes, locking, cap serialization, and sweep semantics are independently verifiable.
|
||||
Output: Corrected models, additive migration, GORM backend, and real-Postgres tests.
|
||||
</objective>
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** Płytarium user connecting a chat application, **I want to** review its requested permissions and approve or deny them, **so that** only an exact PKCE-bound grant can access my active collection.
|
||||
|
||||
<execution_context>
|
||||
@/home/jin/.codex/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jin/.codex/get-shit-done/templates/summary.md
|
||||
@@ -70,123 +47,39 @@ Output: Authorize/token framework handlers, consent controllers, transaction-bou
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-RESEARCH.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-PATTERNS.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-UI-SPEC.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-01-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
From Plan 08-01:
|
||||
- `wristband.Server` owns configured RFC handlers and app-agnostic operations.
|
||||
- `wristband.Backend.WithinTx(ctx, func(Tx) error) error` provides one atomic store/token boundary.
|
||||
|
||||
Existing app interfaces:
|
||||
- `auth.MintPersonalToken(userID uint, name string, scopes []string, expiresAt *time.Time, collectionIDs []uint) (string, *models.ApiToken, error)`.
|
||||
- `classes.ResolveActiveCollection(ctx, db, userID)` returns the trusted collection and persists fallback context.
|
||||
- `bouncer.User(ctx)` supplies the JWT principal; app controllers write exact JSON through `wire.WriteJSON`.
|
||||
|
||||
Locked consent payload:
|
||||
- GET returns `data{client_name,redirect_host,scopes_requested,collection_name,expires_at}`.
|
||||
- POST consent/deny returns `data{redirect_to}` with ordered RFC3986 query parameters.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Specify the complete PKCE consent and code-exchange path</name>
|
||||
<files>wristband/authorize_test.go, wristband/token_test.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller_test.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_connect_test.go</files>
|
||||
<read_first>
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-UI-SPEC.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
|
||||
wristband/server.go
|
||||
wristband/stores.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/token_api_controller.go
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/OAuthAuthorizeController.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/OAuthTokenController.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/OAuthConsentController.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/classes/auth/OAuthCodeManager.php
|
||||
</read_first>
|
||||
<name>Task 1: Specify schema and store behavior in compiling RED tests</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/updates/oauth_schema_correction_test.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go</files>
|
||||
<behavior>
|
||||
- Unknown client and unregistered redirect return local plain-text 400 with no `Location`; later failures redirect only to the exact registered URI in PHP parameter order.
|
||||
- S256 is mandatory; challenge/verifier syntax and lengths are enforced; wrong/missing/plain PKCE never yields a token.
|
||||
- Scope ceiling truncates data scopes while keeping `offline_access` protocol-only; consent cannot add scopes or collection IDs.
|
||||
- Pending requests are JWT owner-bound and single-use; stale, consumed, or foreign handles share the exact 404.
|
||||
- Code exchange locks and consumes the code once, compares S256 in constant time, mints `inv_`, stamps `oauth_client_id`, and returns exact no-store/no-cache token bytes.
|
||||
- Public client secret, pending request id/code hash/user id nullability, named indexes, and rollback refusal are proven on real Postgres.
|
||||
- Two pending requests coexist; atomic cap/sweep/create cannot exceed the configured cap under contention.
|
||||
- Failures emit `PHASE8_RED:persistence` only for absent persistence behavior.
|
||||
</behavior>
|
||||
<action>Per D-02, D-04, D-08, D-12, and D-18, add RED unit and assembled real-Postgres tests for the full public-client happy path plus T-08-PKCE, T-08-CODE-REPLAY, T-08-OPEN-REDIRECT, T-08-SCOPE-CEILING, T-08-CROSS-USER, T-08-REQUEST-LEAK, and T-08-SURFACE. Assert exact query parameter order and RFC 3986 `%20`, body bytes, content types, `Cache-Control`, `Pragma`, and `WWW-Authenticate: Basic realm="OAuth"` on invalid confidential-client authentication. Prove JSON `/token` is `invalid_request`, form body values override query values, and Basic credentials override form credentials.</action>
|
||||
<action>D-18: add real-Postgres tests using the existing auth TestMain harness. Compile them against the interfaces from 08-01; use `PHASE8_RED:persistence` assertions for intentionally missing migration/store behavior, and do not use undefined symbols as RED. Include T-08-DCR-FLOOD and transaction-handle tests that detect accidental use of the outer DB.</action>
|
||||
<verify>
|
||||
<automated>test -f wristband/authorize_test.go && test -f wristband/token_test.go && cd ../fonoteka.go && test -f plugins/golem15/fonoteka/oauth_connect_test.go</automated>
|
||||
<automated>scripts/check-phase8-red.sh persistence bash -lc "cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/classes/auth ./plugins/golem15/fonoteka/updates -run 'TestOAuth(Schema|Store|RegistrationCap)' -count=1"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Named tests cover authorize validation order, ordered redirects, consent show/allow/deny, PKCE syntax/mismatch, sequential and concurrent code replay, token parser asymmetry, Basic precedence, exact headers, and cross-user denial.
|
||||
- The assembled happy-path test begins at `GET /oauth/mcp/authorize`, uses JWT consent endpoints, and finishes at `POST /oauth/mcp/token` with an `inv_` access token.
|
||||
- Focused tests fail on absent implementation while compiling against the interfaces produced by 08-01.
|
||||
</acceptance_criteria>
|
||||
<done>The RED suite proves the exact end-to-end connection contract and every high-severity boundary before implementation.</done>
|
||||
<done>The real-Postgres RED suite compiles, runs named tests, and fails only through the persistence marker.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Implement authorize validation and atomic authorization-code exchange</name>
|
||||
<files>wristband/authorize.go, wristband/token.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_token_issuer.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/api_token_manager.go</files>
|
||||
<read_first>
|
||||
wristband/authorize_test.go
|
||||
wristband/token_test.go
|
||||
wristband/server.go
|
||||
wristband/stores.go
|
||||
wristband/crypto.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/api_token_manager.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/token_guard.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/models/api_token.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/models/oauth_auth_code.go
|
||||
</read_first>
|
||||
<name>Task 2: Correct OAuth schema and implement the transaction-scoped store</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/models/oauth_client.go, ../fonoteka.go/plugins/golem15/fonoteka/models/oauth_auth_code.go, ../fonoteka.go/plugins/golem15/fonoteka/updates/12_oauth_schema_correction.go, ../fonoteka.go/plugins/golem15/fonoteka/updates/oauth_schema_correction_test.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go</files>
|
||||
<behavior>
|
||||
- Authorize reads query only and validates client then redirect before any redirect-capable error.
|
||||
- Ordered redirect encoder retains existing query, appends with `&`, preserves field order, and encodes spaces as `%20`.
|
||||
- Code exchange executes code lock/consume, token mint/persist, oauth-client stamp, and refresh-row creation in one transaction.
|
||||
- Four lifecycle fields are pointers and database nullable; PHP-equivalent operational indexes exist.
|
||||
- Down migration refuses when null lifecycle rows would be lost.
|
||||
- Every store mutation uses the callback transaction and app-tier `FOR UPDATE` where required.
|
||||
</behavior>
|
||||
<action>Implement D-02, D-03, D-04, D-05, D-06, D-07, D-11, and D-17. Add authorize validation in locked order: usable client, exact redirect, response type, S256 method/challenge, requested/ceiling scopes, optional exact resource, then pending creation. Build redirects from ordered pairs rather than `url.Values.Encode`. Add token parsing with JSON rejection then `ParseForm`; authenticate `none`, post, or Basic using fixed SHA-256 transforms and constant-time comparison. Exchange codes under a store row lock; compare S256 through `subtle.ConstantTimeCompare`, mark the code used, mint/persist an access token, set `oauth_client_id`, and create the refresh record atomically. Make the personal-token prefix config-backed at the app adapter while preserving `inv_` as Płytarium's configured value. Run the expiry sweep on token entry but delete only expired rows.</action>
|
||||
<action>D-07: correct `client_secret_hash`, `request_id`, `code_hash`, and `user_id` model fields to pointers and implement the wristband Backend/Tx adapter without importing GORM into wristband. Add a new gormigrate step rather than editing applied history; drop four NOT NULL constraints, create named indexes idempotently, and fail rollback if null rows exist. Implement atomic DCR sweep/cap/create, exact expired-row sweep, and row-lock-capable lifecycle methods using only the callback `*gorm.DB`. D-17: retain unexpired rotated/revoked refresh rows.</action>
|
||||
<verify>
|
||||
<automated>go test ./wristband -run 'Test(Authorize|Token|PKCE|Code)' -count=1 && cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/classes/auth -run 'TestOAuth(Code|Issuer)' -count=1</automated>
|
||||
<automated>cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/classes/auth ./plugins/golem15/fonoteka/updates -run 'TestOAuth(Schema|Store|RegistrationCap|Sweep)' -count=1</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Framework tests prove local 400s have no `Location`, trusted errors have ordered `error,error_description,iss,state`, and successful consent redirects use `code,iss,state`.
|
||||
- Sequential and synchronized concurrent code exchanges produce exactly one usable token grant.
|
||||
- `rg -n 'subtle\.ConstantTimeCompare' wristband` finds both client-secret and PKCE comparisons; no direct equality compares presented secrets/verifiers.
|
||||
- Issued access tokens start with configured `inv_`, store only SHA-256 hash, carry exact scopes/collection IDs/expiry, and have `oauth_client_id` set.
|
||||
</acceptance_criteria>
|
||||
<done>The framework can safely create pending authorization state and atomically exchange one PKCE-bound code for the existing personal-token model.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Wire JWT consent and the raw authorize/token routes</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller_test.go, ../fonoteka.go/plugins/golem15/fonoteka/plugin.go, ../fonoteka.go/plugins/golem15/fonoteka/routes.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_connect_test.go</files>
|
||||
<read_first>
|
||||
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/oauth_connect_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/token_api_controller.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/classes/active_collection.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/plugin.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/routes.go
|
||||
/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/app/pages/connect.vue
|
||||
/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/app/stores/fonoteka.ts
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Consent show returns sanitized client name, host-only redirect, ordered mintable scopes, server-resolved collection name, and ISO expiry.
|
||||
- Allow grants only submitted ∩ requested ∩ ceiling ∩ mintable scopes and server-derived collection ids; deny consumes the request and returns ordered `access_denied,iss,state`.
|
||||
- Raw authorize/token endpoints remain unenveloped and token alone receives `throttle:fonoteka-oauth-token`; JWT management routes never appear under personal-token auth.
|
||||
</behavior>
|
||||
<action>Per D-08, D-09, D-10, D-12 and the UI-SPEC, implement GET request, POST consent, and POST deny inside the established JWT+locale+must-change-password group. Use `bouncer.User`, `ResolveActiveCollection`, `lagoon.Validate`, Vue-safe plain strings, and wristband operations; never accept collection IDs or extra scopes from the request. Return the exact UI payload/status shapes and collapse absent, stale, consumed, and foreign handles to `{"error":"Request not found"}` 404. Mount authorize and token on the raw group; token gets only `throttle:fonoteka-oauth-token`. Preserve no house middleware, no `oauth` guard, and no backend RFC 9728 challenge.</action>
|
||||
<verify>
|
||||
<automated>cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestOAuth(Authorize|Consent|Deny|CodeExchange|Surface)' -count=1</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- The assembled PKCE happy-path test passes from authorize through JWT consent and token exchange against real Postgres.
|
||||
- Consent tests prove empty/no-longer-grantable scope intersection is exact 422 and foreign/stale/used handles are indistinguishable exact 404.
|
||||
- Route-table tests prove four raw RFC routes and three JWT consent routes occupy only their locked auth surfaces.
|
||||
- `git -C /media/nvme/dev/golem15/fonoteka/vue-fonoteka-app status --short` shows no Phase 8 changes.
|
||||
</acceptance_criteria>
|
||||
<done>The unchanged Nuxt consent page and a registered PKCE client complete one secure authorization-code flow through the assembled Go app.</done>
|
||||
<done>Postgres can persist public clients and concurrent pending requests, exposes required indexes, serializes DCR cap enforcement, and sweeps only expired rows.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
@@ -196,35 +89,26 @@ Locked consent payload:
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Connector → authorize/token | Untrusted query/form/Basic credentials attempt to create or redeem grants. |
|
||||
| Browser JWT principal → consent API | Authenticated but untrusted scope selection crosses tenant/ownership boundaries. |
|
||||
| wristband transaction → personal-token store | A one-time authorization code becomes durable access/refresh credentials. |
|
||||
| wristband records → GORM | App-agnostic state crosses into persistent rows and locks. |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-08-PKCE | Spoofing/Elevation | authorize and code exchange | mitigate | Mandatory S256 syntax plus constant-time computed challenge comparison; missing/plain/wrong tests. |
|
||||
| T-08-CODE-REPLAY | Spoofing | code exchange/store | mitigate | `FOR UPDATE`, one transaction, used stamp, sequential and concurrent single-winner tests. |
|
||||
| T-08-OPEN-REDIRECT | Spoofing/Disclosure | authorize/consent redirects | mitigate | Validate usable client and exact redirect before constructing any `Location`; ordered trusted-URI tests. |
|
||||
| T-08-SECRET-TIMING | Information Disclosure | token client auth and PKCE | mitigate | Constant-time fixed-transform comparisons only. |
|
||||
| T-08-SCOPE-CEILING | Elevation | authorize and consent | mitigate | Requested ∩ ceiling before display; submitted ∩ pending ∩ mintable at consent; collection IDs server-derived. |
|
||||
| T-08-CROSS-USER | Elevation | consent endpoints | mitigate | Principal-scoped pending lookup/consume and indistinguishable 404s. |
|
||||
| T-08-REQUEST-LEAK | Information Disclosure | browser/logging | mitigate | Opaque short-lived handle, no value logging, host-only UI payload, no-referrer client behavior retained unchanged. |
|
||||
| T-08-SURFACE | Elevation | routes | mitigate | Assembled route-table isolation across raw, JWT, and personal-token groups. |
|
||||
| T-08-SC | Tampering | dependencies | mitigate | No new dependency; standard-library implementation enforced by import tests. |
|
||||
| T-08-DCR-FLOOD | Denial of Service | client store | mitigate | Transactionally serialized cap/sweep/create with contention test. |
|
||||
| T-08-CODE-REPLAY | Spoofing | auth-code store | mitigate | App-tier row-lock methods and single transaction handle. |
|
||||
| T-08-REFRESH-REPLAY | Spoofing/Elevation | refresh store | mitigate | Preserve replay evidence until expiry and expose locked traversal. |
|
||||
| T-08-SC | Tampering | dependencies | mitigate | Existing GORM/Postgres only; no install. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `go test ./wristband -run 'Test(Authorize|Token|PKCE|Code)' -count=1`
|
||||
- `cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestOAuth(Authorize|Consent|Deny|CodeExchange|Surface)' -count=1`
|
||||
- `go vet ./... && go test ./...` passes in each repository.
|
||||
- Focused migration/store Postgres tests pass.
|
||||
- `rg -n 'clause.Locking' ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go` finds app-tier locks only.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- A registered client completes authorize → JWT consent → code exchange and receives an `inv_` access token plus refresh token.
|
||||
- PKCE, exact redirect, scope ceiling, ownership, parser precedence, single use, and exact raw response headers are all failing-when-broken tests.
|
||||
- The existing consent UI receives every locked field and status without any Nuxt change.
|
||||
- Schema and store can represent every client/pending/code/refresh lifecycle state.
|
||||
- DCR cap and single-use operations have real-Postgres concurrency evidence.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
||||
@@ -5,55 +5,38 @@ type: execute
|
||||
wave: 3
|
||||
depends_on: [08-02]
|
||||
files_modified:
|
||||
- wristband/token.go
|
||||
- wristband/token_test.go
|
||||
- wristband/stores.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/config/config.yaml
|
||||
- ../fonoteka.go/config/app.yaml
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/plugin.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/routes.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/oauth_lifecycle_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/oauth_registration_test.go
|
||||
autonomous: true
|
||||
requirements: [AUTH-05, AUTH-06, AUTH-07]
|
||||
must_haves:
|
||||
truths:
|
||||
- "A valid refresh token rotates to a new access/refresh pair while revoking the prior access token."
|
||||
- "Replaying a spent refresh token commits revocation of the whole lineage, leaving no usable branch."
|
||||
- "A user sees only their live connected OAuth apps and can revoke one access token plus its refresh lineage atomically."
|
||||
- "D-03: The assembled app exposes configured PHP-default TTLs, caps, issuer, resource, consent URL, and registration bound."
|
||||
- "D-09: Metadata and registration are raw routes and registration alone carries its named throttle."
|
||||
- "D-10: No oauth guard is registered; OAuth access remains on inv_token."
|
||||
- "D-12: Backend challenge ownership stays unchanged and RFC 9728 behavior remains in fonoteka-mcp."
|
||||
artifacts:
|
||||
- path: "wristband/token.go"
|
||||
provides: "Refresh grant rotation, replay detection, and committed lineage kill"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go"
|
||||
provides: "Row-locked refresh traversal, revoke, and expiry sweep storage"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller.go"
|
||||
provides: "Owner-scoped connected-app list and revoke handlers"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/plugin.go"
|
||||
provides: "Configured store-backed wristband server construction"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/routes.go"
|
||||
provides: "Raw metadata and register route mounting"
|
||||
key_links:
|
||||
- from: "wristband/token.go"
|
||||
to: "oauth_store.go"
|
||||
via: "transaction outcome commits lineage kill before returning invalid_grant"
|
||||
pattern: "WithinTx"
|
||||
- from: "connected_app_controller.go"
|
||||
to: "wristband.Server"
|
||||
via: "cascade revoke operation rather than direct refresh-row deletion"
|
||||
pattern: "Revoke"
|
||||
- from: "connected_app_controller.go"
|
||||
to: "token_api_controller.go"
|
||||
via: "reuse of positive allow-list token serializer"
|
||||
pattern: "serializeToken"
|
||||
- from: "plugin.go"
|
||||
to: "wristband.New"
|
||||
via: "configured Options and GORM backend"
|
||||
pattern: "wristband\\.New"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Deliver the durable OAuth lifecycle slice: safe refresh rotation/replay handling and user-visible connected-app listing/revocation.
|
||||
Mount the proven discovery/DCR engine on the real application with persistent state and exact raw-route isolation.
|
||||
|
||||
Purpose: Ensure stolen or replayed refresh tokens cannot create surviving branches and the unchanged Settings UI controls the same grant lineage.
|
||||
Output: Refresh-grant state machine, row-locked store operations, connected-app controllers/routes, and concurrency-backed lifecycle tests.
|
||||
Purpose: Deliver the first connector-visible vertical outcome without mixing schema work into protocol implementation.
|
||||
Output: OAuth config, boot wiring, raw routes, and assembled Postgres-backed tests.
|
||||
</objective>
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** connected-app user, **I want to** refresh access safely and revoke applications from Settings, **so that** replayed or revoked credentials immediately lose access.
|
||||
|
||||
<execution_context>
|
||||
@/home/jin/.codex/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jin/.codex/get-shit-done/templates/summary.md
|
||||
@@ -64,115 +47,39 @@ Output: Refresh-grant state machine, row-locked store operations, connected-app
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-RESEARCH.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-PATTERNS.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-UI-SPEC.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-02-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
From Plan 08-02:
|
||||
- Token handler already dispatches `authorization_code` and recognizes the configured refresh grant.
|
||||
- `wristband.Tx` provides row-lock-capable refresh/code stores and the transaction-bound access-token issuer.
|
||||
- OAuth access tokens are `models.ApiToken` rows distinguished by non-null `OAuthClientID`.
|
||||
|
||||
Existing serializer:
|
||||
- `serializeToken(gdb, *models.ApiToken) map[string]any` is the required positive allow-list for connected-app responses.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Specify rotation, replay, list, and revoke as one lifecycle</name>
|
||||
<files>wristband/token_test.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller_test.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_lifecycle_test.go</files>
|
||||
<read_first>
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-UI-SPEC.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
|
||||
wristband/token.go
|
||||
wristband/stores.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/token_api_controller.go
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/classes/auth/OAuthCodeManager.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/ConnectedAppController.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/security/OAuthRefreshRotationTest.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/security/OAuthRevocationTest.php
|
||||
</read_first>
|
||||
<name>Task 1: Specify assembled discovery and registration in RED</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/oauth_registration_test.go</files>
|
||||
<behavior>
|
||||
- Normal refresh revokes the old access token, marks predecessor `rotated_to_id`, and returns a new same-scope/same-collection pair.
|
||||
- Sequential or concurrent spent-token replay returns `invalid_grant` only after the complete lineage and current access token are durably revoked.
|
||||
- Expiry sweep removes only expired pending/code/refresh rows and retains unexpired rotated/revoked refresh rows as replay evidence.
|
||||
- Connected-app list is newest-first, owner-only, live OAuth tokens only, with manual count separate and no secret/client-id fields.
|
||||
- Revoke of an owned OAuth token kills its refresh lineage; foreign, missing, and manual token IDs share the exact 404.
|
||||
- Assembled routes return exact metadata and persistent public/confidential DCR responses.
|
||||
- Route table rejects JWT, inv_token, inv.scope, body-limit, and house middleware; register has only its named throttle.
|
||||
- Failures use `PHASE8_RED:registration-app`, not compile/setup/missing-test failure.
|
||||
</behavior>
|
||||
<action>Per D-04, D-08, D-16, D-17, and D-18, extend the RED suite with deterministic in-memory tests and synchronized real-Postgres contention tests for T-08-REFRESH-REPLAY, T-08-CROSS-USER, T-08-SCOPE-CEILING, T-08-REQUEST-LEAK, and T-08-SURFACE. Include an assembled lifecycle that starts with a grant from 08-02, refreshes, replays the spent predecessor, verifies the new branch and access token are dead, creates another grant, lists it, revokes it, and proves refresh afterward fails. Assert exact UI response allow-lists and 404 bytes.</action>
|
||||
<action>D-18: add an assembled-router real-Postgres test against existing boot seams. Assert bytes and headers before decoding, exact configured defaults, route isolation, and no oauth guard. Keep the test compiling against 08-01/08-02 contracts and mark only absent app wiring with `PHASE8_RED:registration-app`.</action>
|
||||
<verify>
|
||||
<automated>rg -n 'TestOAuth(Refresh|Replay|Connected|Revoke|Sweep)' wristband/token_test.go ../fonoteka.go/plugins/golem15/fonoteka/{oauth_lifecycle_test.go,classes/auth/oauth_store_test.go,controllers/api/connected_app_controller_test.go}</automated>
|
||||
<automated>scripts/check-phase8-red.sh registration-app bash -lc "cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestOAuth(Metadata|Register|RawRoute|Config)' -count=1"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Tests include sequential replay, a barrier-synchronized double refresh, committed lineage kill, expiry retention, owner isolation, manual-token exclusion, list ordering, and post-revoke refresh failure.
|
||||
- The replay test explicitly reloads database rows after the `invalid_grant` response and asserts revoked lineage/access state, preventing rollback-hidden false positives.
|
||||
- Tests fail on missing refresh/connected-app implementation while all 08-02 happy-path tests remain green.
|
||||
</acceptance_criteria>
|
||||
<done>The RED lifecycle suite detects branch survival, rollback of replay revocation, ownership leaks, serialization leaks, and route misplacement.</done>
|
||||
<done>Assembled tests execute and fail solely because config/boot/routes are not wired.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Implement refresh rotation, committed replay kill, and exact sweeps</name>
|
||||
<files>wristband/token.go, wristband/stores.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go</files>
|
||||
<read_first>
|
||||
wristband/token_test.go
|
||||
wristband/token.go
|
||||
wristband/stores.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/models/oauth_refresh_token.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/models/api_token.go
|
||||
</read_first>
|
||||
<name>Task 2: Configure, boot, and route persistent discovery and DCR</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/config/config.yaml, ../fonoteka.go/config/app.yaml, ../fonoteka.go/plugins/golem15/fonoteka/plugin.go, ../fonoteka.go/plugins/golem15/fonoteka/routes.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_registration_test.go</files>
|
||||
<behavior>
|
||||
- Presented refresh lookup uses SHA-256 hash and a row lock inside the single transaction boundary.
|
||||
- Replay revocation returns success from the transaction callback, then maps the recorded outcome to `invalid_grant` outside it.
|
||||
- Rotation keeps predecessor/successor relationships and unexpired evidence rows.
|
||||
- Defaults are pending/code 600s, access 3600s, refresh 30 days, DCR cap 200, stale age 24h, resource URL, and 65,536-byte register maximum.
|
||||
- Issuer trims the app URL once; metadata and registration use the actual GORM backend.
|
||||
</behavior>
|
||||
<action>Implement D-04, D-05, D-07, and D-17's refresh branch in wristband and the GORM adapter. Authenticate the client using the same Basic-over-form rule, hash the presented refresh secret, lock its row, reject expired/revoked/wrong-client grants, and rotate atomically by revoking the old access token, minting/persisting its successor, creating the next refresh secret/hash, and linking `rotated_to_id`. If a spent token is presented, traverse and revoke the whole lineage and associated access tokens, return nil from the transaction so the kill commits, then return `invalid_grant` from the handler. Keep the old scopes, collection IDs, offline flag, and client binding. Sweep only rows whose `expires_at` is past; do not delete unexpired replay evidence.</action>
|
||||
<action>D-03: add `plugins.golem15.fonoteka.oauth.*` defaults and use `app.url` as issuer. Construct the backend and wristband server in Plugin.Boot and retain it for later route/command factories. D-09: mount metadata and register inside the existing raw group; pass `throttle:fonoteka-oauth-register` only to register. D-10: register no oauth guard. D-12: preserve the exact backend personal-token 401 and do not add protected-resource metadata or rich Bearer challenges.</action>
|
||||
<verify>
|
||||
<automated>go test ./wristband -run 'Test(Refresh|Replay|Sweep)' -count=1 && cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/classes/auth -run 'TestOAuth(Refresh|Replay|Sweep)' -count=1</automated>
|
||||
<automated>cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestOAuth(Metadata|Register|RawRoute|Config)' -count=1</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Normal refresh and sequential/concurrent replay tests pass under real Postgres.
|
||||
- A spent-token replay leaves every lineage refresh row and its live access token revoked after the response transaction commits.
|
||||
- `go test -race ./wristband` passes and the app contention test produces a single usable branch.
|
||||
- Sweep tests prove expired rows are removed and unexpired rotated/revoked rows remain.
|
||||
</acceptance_criteria>
|
||||
<done>Refresh rotation is atomic, preserves replay evidence, and commits whole-lineage revocation before emitting the protocol error.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Expose connected-app list and atomic revoke to the unchanged Settings UI</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller_test.go, ../fonoteka.go/plugins/golem15/fonoteka/routes.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_lifecycle_test.go</files>
|
||||
<read_first>
|
||||
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/oauth_lifecycle_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/token_api_controller.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/routes.go
|
||||
/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/app/components/settings/ConnectedAppsManager.vue
|
||||
/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/app/stores/fonoteka.ts
|
||||
</read_first>
|
||||
<behavior>
|
||||
- List returns `data` and numeric `manual_tokens_count`, filters to owner/live/OAuth tokens, and orders created_at descending.
|
||||
- Every row is `serializeToken` plus sanitized client name and contains no raw credential, hash, OAuth client id, redirect URI, or other-user data.
|
||||
- Revoke completes access-token and refresh-lineage revocation before returning `{"data":{"revoked":true}}`.
|
||||
</behavior>
|
||||
<action>Per D-08 and the UI-SPEC, add GET and DELETE connected-app controllers in the JWT group. Reuse `serializeToken`; append only the sanitized/truncated client name, initialize collection/scope arrays as arrays, count live manual tokens separately, and order OAuth tokens newest first. Scope every query by `bouncer.User`. For DELETE, require an owned OAuth token, invoke the wristband lineage-revoke operation in the same committed transaction, and collapse missing/foreign/manual IDs to exact `{"error":"Token not found"}` 404. Mount only under `/_fonoteka/api/v1/oauth`; do not expose these routes on the personal-token or raw groups.</action>
|
||||
<verify>
|
||||
<automated>cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestOAuth(ConnectedApps|Revoke|Lifecycle|Surface)' -count=1</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Empty, populated, manual-count, newest-first, foreign/manual 404, and successful atomic revoke tests pass with exact bytes.
|
||||
- The lifecycle test proves the revoked app disappears on the next list and its refresh token returns `invalid_grant`.
|
||||
- JSON assertions reject `token`, `token_hash`, `oauth_client_id`, `client_secret`, `refresh_token`, `request_id`, and `redirect_uris` anywhere in list output.
|
||||
- The Nuxt repository remains unchanged.
|
||||
</acceptance_criteria>
|
||||
<done>The existing Settings → Integrations UI can list and revoke only the current user's connected applications, and revoke kills the entire grant lineage.</done>
|
||||
<done>An unchanged connector can discover and dynamically register against the assembled app with persistent Postgres state and exact route boundaries.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
@@ -182,32 +89,26 @@ Existing serializer:
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Refresh credential → token endpoint | A bearer-like long-lived credential requests a new grant branch. |
|
||||
| JWT principal → connected-app API | User-controlled ids request listing/revocation of durable credentials. |
|
||||
| Transaction outcome → OAuth error | Security revocation must commit even though the protocol response is an error. |
|
||||
| Internet → raw routes | Unauthenticated protocol traffic enters the assembled app. |
|
||||
| Config → public metadata | Deployment values become client trust anchors. |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-08-REFRESH-REPLAY | Spoofing/Elevation | refresh state machine | mitigate | Row lock, rotation chain, single-winner concurrency, commit lineage kill before `invalid_grant`. |
|
||||
| T-08-CROSS-USER | Elevation | connected-app controllers | mitigate | Owner-scoped reads/deletes and indistinguishable missing/foreign/manual 404. |
|
||||
| T-08-SCOPE-CEILING | Elevation | refresh rotation | mitigate | Copy only stored granted scopes/collection ids; refresh cannot add request-provided authority. |
|
||||
| T-08-REQUEST-LEAK | Information Disclosure | list response/logs | mitigate | Positive allow-list serializer and explicit forbidden-field tests. |
|
||||
| T-08-SURFACE | Elevation | route groups | mitigate | JWT-only management route inspection; token endpoint remains raw. |
|
||||
| T-08-SC | Tampering | dependencies | mitigate | No install; standard library and existing GORM only. |
|
||||
| T-08-DCR-FLOOD | Denial of Service | register route | mitigate | Named per-IP limiter plus framework body/cap controls. |
|
||||
| T-08-SURFACE | Elevation | route groups | mitigate | Assembled route-table test for exact middleware. |
|
||||
| T-08-SC | Tampering | dependencies | mitigate | No new package. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `go test ./wristband -run 'Test(Refresh|Replay|Sweep)' -count=1`
|
||||
- `cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestOAuth(Refresh|Replay|ConnectedApps|Revoke|Lifecycle|Surface)' -count=1`
|
||||
- `cd ../fonoteka.go && go test -race ./plugins/golem15/fonoteka/classes/auth ./plugins/golem15/fonoteka`
|
||||
- Focused assembled discovery/DCR tests pass.
|
||||
- `go vet ./... && go test ./...` passes in both repositories at the wave boundary.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Refresh rotation has exactly one usable successor and replay durably kills the entire lineage.
|
||||
- Connected-app output matches the UI contract and cannot disclose secrets or other users.
|
||||
- Revocation removes the app from the list and invalidates both access and refresh credentials before success is returned.
|
||||
- Metadata and DCR are reachable through the real app with exact PHP-compatible responses.
|
||||
- Public/confidential clients persist and raw routes remain isolated.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
||||
@@ -5,52 +5,40 @@ type: execute
|
||||
wave: 4
|
||||
depends_on: [08-03]
|
||||
files_modified:
|
||||
- bonfire/command.go
|
||||
- bonfire/root.go
|
||||
- bonfire/output_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/console/oauth_client.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/console/oauth_client_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/plugin.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/routes.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/oauth_tools_test.go
|
||||
- wristband/authorize.go
|
||||
- wristband/token.go
|
||||
- wristband/authorize_test.go
|
||||
- wristband/token_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_token_issuer.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/api_token_manager.go
|
||||
autonomous: true
|
||||
requirements: [AUTH-05, AUTH-07]
|
||||
requirements: [AUTH-05, AUTH-06]
|
||||
must_haves:
|
||||
truths:
|
||||
- "An operator can issue, amend, and list confidential OAuth clients with repeated redirect/scope flags, while a client secret is printed only at creation."
|
||||
- "The unchanged fonoteka-mcp process can authenticate its issued `inv_` token at `GET /api/v1/fonoteka/me` and receive the exact bootstrap payload."
|
||||
- "Invalid personal tokens keep the existing exact `Invalid token` body and gain no backend Bearer/resource-metadata challenge."
|
||||
- "D-02: Authorize reads query only and token rejects JSON before ParseForm body-over-query parsing."
|
||||
- "D-04: S256/client-secret comparisons are constant-time and code replay has one winner."
|
||||
- "D-05: Wristband owns authorize, PKCE, scope/resource policy, and atomic code exchange."
|
||||
- "D-11: Issued access tokens retain the configured inv_ prefix."
|
||||
artifacts:
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/console/oauth_client.go"
|
||||
provides: "Exact `fonoteka:oauth-client` operator command"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller.go"
|
||||
provides: "Personal-token MCP bootstrap endpoint"
|
||||
- path: "bonfire/command.go"
|
||||
provides: "Typed repeatable string-slice flag contract"
|
||||
- path: "wristband/authorize.go"
|
||||
provides: "Ordered validation, redirects, PKCE, resource and scope policy"
|
||||
- path: "wristband/token.go"
|
||||
provides: "Client authentication and atomic authorization-code exchange"
|
||||
key_links:
|
||||
- from: "oauth_client.go"
|
||||
to: "wristband client issuance helper"
|
||||
via: "same validation/hash/one-time-secret path as DCR"
|
||||
pattern: "wristband"
|
||||
- from: "me_token_controller.go"
|
||||
to: "bouncer.Credential"
|
||||
via: "reuse matched `*models.ApiToken` without re-parsing bearer input"
|
||||
pattern: "Credential"
|
||||
- from: "wristband/token.go"
|
||||
to: "oauth_token_issuer.go"
|
||||
via: "single transaction-bound Tx"
|
||||
pattern: "WithinTx"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Deliver the operator and MCP bootstrap slice required for confidential-client coverage and unchanged real-MCP startup.
|
||||
Implement the protocol core from authorize validation through one atomic authorization-code exchange.
|
||||
|
||||
Purpose: Preserve the PHP management command and satisfy the actual MCP client's pre-tool-call `/me` dependency without broadening the profile API.
|
||||
Output: Repeatable bonfire flags, app OAuth client command, personal-token `/me` endpoint, route wiring, and integration tests.
|
||||
Purpose: Prove PKCE, redirect, parser, scope, client-auth, and code-replay rules independently of the browser controller.
|
||||
Output: Authorize/token handlers, issuer adapter, store transitions, and deterministic/concurrent tests.
|
||||
</objective>
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As an** operator and unchanged fonoteka-mcp client, **I want to** provision a confidential client and bootstrap an issued personal token, **so that** managed connectors and MCP tools can use the same secure OAuth server.
|
||||
|
||||
<execution_context>
|
||||
@/home/jin/.codex/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jin/.codex/get-shit-done/templates/summary.md
|
||||
@@ -61,115 +49,39 @@ Output: Repeatable bonfire flags, app OAuth client command, personal-token `/me`
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-PATTERNS.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-UI-SPEC.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-RESEARCH.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-03-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
Existing bonfire contract:
|
||||
- `Command{ Name, Description, Flags []Flag, Args []Arg, Run func(context.Context, Input, Output) error }`.
|
||||
- `Input.Flag(name) (string, bool)` handles scalar flags; this plan adds an explicit string-slice shape without changing scalar callers.
|
||||
|
||||
Existing auth context:
|
||||
- `TokenGuard.AuthenticateCredential` places the matched `*models.ApiToken` in `bouncer.Credential` and its owner in `bouncer.User`.
|
||||
- Personal-token route middleware order is `inv_token`, `throttle:fonoteka-api-token`, `inv.scope:read`.
|
||||
|
||||
Locked `/me` response:
|
||||
- `data{scopes,collection_ids,user_id,name}` with arrays, not null, and no other profile/token fields.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Specify operator provisioning and real MCP bootstrap contracts</name>
|
||||
<files>bonfire/output_test.go, ../fonoteka.go/plugins/golem15/fonoteka/console/oauth_client_test.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller_test.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_tools_test.go</files>
|
||||
<read_first>
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
|
||||
bonfire/command.go
|
||||
bonfire/root.go
|
||||
bonfire/output_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/token_guard.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_locale_controller.go
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/console/IssueOAuthClient.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/MeTokenController.php
|
||||
/media/nvme/dev/golem15/fonoteka/fonoteka-mcp/src/http.ts
|
||||
</read_first>
|
||||
<name>Task 1: Specify authorize and code exchange with executable RED tests</name>
|
||||
<files>wristband/authorize.go, wristband/token.go, wristband/authorize_test.go, wristband/token_test.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_token_issuer.go</files>
|
||||
<behavior>
|
||||
- Repeated `--redirect-uri` and `--scope` values arrive as ordered slices without breaking existing scalar/bare flags.
|
||||
- Create prints `client_id=`, `client_secret=`, and the non-recoverable warning once; `--list` never prints a secret or hash.
|
||||
- Explicit `--client-id` updates the intended client through validated store operations; scope ceiling is enforced by later authorization.
|
||||
- A valid read-scoped `inv_` token gets exact `/me` data; absent/invalid/wrong-scope tokens keep existing 401/403 bytes and headers.
|
||||
- Unknown client/unregistered redirect are local 400 without Location; later errors use exact ordered RFC3986 redirects.
|
||||
- S256 is mandatory; parser precedence, Basic override, cache headers, and exact challenge are tested.
|
||||
- Tests compile and fail only through `PHASE8_RED:authorize-token`.
|
||||
</behavior>
|
||||
<action>Per D-11, D-12, D-18, D-19, and D-20, add RED tests for repeatable bonfire flags, every command mode/output line, secret non-recovery, and assembled `/me` behavior. Use the real command root and assembled surf router. Assert the MCP-required fields exactly and reject any added user/profile/credential fields. Add T-08-SECRET-TIMING, T-08-SCOPE-CEILING, T-08-REQUEST-LEAK, and T-08-SURFACE regressions: command issuance uses hash-only storage; list output never leaks; `/me` is personal-token-only; invalid token response is exactly `{"error":"Invalid token"}` with no `WWW-Authenticate` or protected-resource metadata header.</action>
|
||||
<action>D-18: extend the existing interfaces with compiling authorize/token stubs and add deterministic unit plus real-store adapter tests. Use explicit `PHASE8_RED:authorize-token` assertions for absent behavior. Reject syntax/build/setup/missing-test failures through the shared RED verifier. Cover T-08-PKCE, CODE-REPLAY, OPEN-REDIRECT, SECRET-TIMING, SCOPE-CEILING, and REQUEST-LEAK, including synchronized concurrent code exchange.</action>
|
||||
<verify>
|
||||
<automated>rg -n 'Test(Repeatable|OAuthClientCommand|MeToken|TokenSurface)' bonfire/output_test.go ../fonoteka.go/plugins/golem15/fonoteka/{console/oauth_client_test.go,controllers/api/me_token_controller_test.go,oauth_tools_test.go}</automated>
|
||||
<automated>scripts/check-phase8-red.sh authorize-token go test ./wristband -run 'Test(Authorize|Token|PKCE|Code)' -count=1</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- RED tests cover repeated flags, create/update/list, one-time secret, scope ceiling, exact `/me`, invalid token, missing scope, and route isolation.
|
||||
- Command tests assert the exact warning and prove `--list` output contains neither `client_secret=` nor the stored secret hash.
|
||||
- `/me` tests use the existing `inv_token` guard and `bouncer.Credential`, not direct controller context injection.
|
||||
</acceptance_criteria>
|
||||
<done>The operator and MCP bootstrap contracts are executable and fail only on the absent production behavior.</done>
|
||||
<done>Named tests compile and execute, with the verifier accepting only the intended missing-behavior marker.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Add repeatable flags and the exact OAuth client command</name>
|
||||
<files>bonfire/command.go, bonfire/root.go, bonfire/output_test.go, ../fonoteka.go/plugins/golem15/fonoteka/console/oauth_client.go, ../fonoteka.go/plugins/golem15/fonoteka/console/oauth_client_test.go, ../fonoteka.go/plugins/golem15/fonoteka/plugin.go</files>
|
||||
<read_first>
|
||||
bonfire/output_test.go
|
||||
bonfire/command.go
|
||||
bonfire/root.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/console/oauth_client_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/plugin.go
|
||||
../fonoteka.go/plugins/golem15/user/plugin.go
|
||||
wristband/register.go
|
||||
wristband/stores.go
|
||||
</read_first>
|
||||
<name>Task 2: Implement ordered authorize and atomic code exchange</name>
|
||||
<files>wristband/authorize.go, wristband/token.go, wristband/authorize_test.go, wristband/token_test.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_token_issuer.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/api_token_manager.go</files>
|
||||
<behavior>
|
||||
- `Flag` explicitly distinguishes scalar and repeated string values; `Input` exposes both without parsing `os.Args`.
|
||||
- Command create/update/list shares wristband validation and issuance, including constant-time-secret-ready hash storage and one-time raw secret.
|
||||
- Validation order is usable client, exact redirect, response type, S256 method/challenge, scope ceiling, resource, pending creation.
|
||||
- Code lock/consume, access-token mint/stamp, and refresh creation commit atomically once.
|
||||
</behavior>
|
||||
<action>Extend bonfire with an explicit string-slice flag kind and `Input.Flags(name) ([]string, bool)` while preserving every existing `Flag` call and bare/scalar behavior; wire Cobra `StringSlice`/`StringSliceP` only for that kind. Implement `fonoteka:oauth-client` per D-19 with optional name, repeated `--redirect-uri`/`--scope`, scalar `--auth-method`, `--client-id`, and bare `--list`. Keep it thin over wristband's client validation/issuing helper and the app ClientStore; artisan clients have null `registration_ip`. Print the exact creation lines and warning; never recover or print secrets in list/update. Register through the plugin command capability and include sanitized names, redirects, ceiling, auth method, and revocation state in list output.</action>
|
||||
<action>D-02: implement endpoint-specific parsing and reject JSON token calls before ParseForm. D-03: apply configured TTL/resource. D-04: compare fixed transforms with `subtle.ConstantTimeCompare`. D-05 and D-06: keep state/policy and exact raw responses in wristband. D-07: exchange under one app transaction/row lock. Build redirects from ordered pairs, never `url.Values.Encode`. D-11: make the app adapter prefix config-backed while retaining `inv_`. D-17: run expired-only sweep on token entry. Preserve exact `Basic realm="OAuth"`, no-store, and no-cache headers.</action>
|
||||
<verify>
|
||||
<automated>go test ./bonfire -run 'Test.*Flag' -count=1 && cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run TestOAuthClientCommand -count=1</automated>
|
||||
<automated>go test ./wristband -run 'Test(Authorize|Token|PKCE|Code)' -count=1 && cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/classes/auth -run 'TestOAuth(Code|Issuer)' -count=1</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Existing bonfire test suite stays green and a repeated flag preserves both ordered values.
|
||||
- The command accepts the locked signature, validates through wristband, and passes create/update/list real-store tests.
|
||||
- Create output contains one client id, one client secret, and one warning; list/update output contains no raw secret or hash.
|
||||
- No `os.Args` access exists in the app command.
|
||||
</acceptance_criteria>
|
||||
<done>Operators can safely provision confidential or ceiling-bounded clients with the exact PHP command contract.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Serve the MCP token bootstrap payload on the existing personal-token surface</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller_test.go, ../fonoteka.go/plugins/golem15/fonoteka/routes.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_tools_test.go</files>
|
||||
<read_first>
|
||||
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/oauth_tools_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/token_guard.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_locale_controller.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/routes.go
|
||||
/media/nvme/dev/golem15/fonoteka/fonoteka-mcp/src/http.ts
|
||||
</read_first>
|
||||
<behavior>
|
||||
- `/api/v1/fonoteka/me` reads the matched credential and principal and returns exactly scopes, collection_ids, user_id, and name.
|
||||
- It inherits the personal-token middleware order and exact deny responses; it performs no second bearer parse or DB token lookup.
|
||||
</behavior>
|
||||
<action>Implement D-20's exact token bootstrap handler using `bouncer.Credential` asserted as `*models.ApiToken` and `bouncer.User`. Initialize scopes and collection_ids to arrays, preserve nullable token name as PHP does, and emit only the four locked fields through `wire.WriteJSON`. Mount GET `/me` in the existing `/api/v1/fonoteka` group after `inv_token`, `throttle:fonoteka-api-token`, and `inv.scope:read`. Do not add profile fields or move the route into the JWT/raw groups. Per D-12, leave the backend invalid-token response and headers untouched.</action>
|
||||
<verify>
|
||||
<automated>cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'Test(MeToken|TokenSurface|OAuthTools)' -count=1</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Valid access token returns exact `data.scopes`, `data.collection_ids`, `data.user_id`, and `data.name`, with arrays never null.
|
||||
- Missing/invalid token is exact 401 `{"error":"Invalid token"}` without `WWW-Authenticate`; missing read scope is the existing exact 403.
|
||||
- Route-table test finds `/api/v1/fonoteka/me` only under the personal-token group with all three existing middleware in order.
|
||||
- `go vet ./... && go test ./...` passes in both repositories.
|
||||
</acceptance_criteria>
|
||||
<done>The real MCP process can bootstrap from its OAuth-issued `inv_` token without any client change or backend header drift.</done>
|
||||
<done>One exact PKCE-bound code produces one inv_ access/refresh grant, and all validation/parser/replay failures are exact and tested.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
@@ -179,31 +91,29 @@ Locked `/me` response:
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Operator CLI → OAuth client store | Trusted operator input can create recoverable-once confidential credentials. |
|
||||
| Bearer header → personal-token `/me` | Untrusted bearer input crosses the established token guard and scope ceiling. |
|
||||
| Stored credential → command/API output | Secret-bearing records must be reduced to positive allow-list output. |
|
||||
| Connector → authorize/token | Untrusted query/form/Basic input requests or redeems authority. |
|
||||
| Tx → access-token store | One-time code state becomes durable credentials. |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-08-SECRET-TIMING | Information Disclosure | issued confidential client | mitigate | Command reuses wristband hash/validation path; raw secret returned once, hash only stored. |
|
||||
| T-08-SCOPE-CEILING | Elevation | command and `/me` | mitigate | Validated command ceiling and existing `inv.scope:read`; output reflects stored granted scopes only. |
|
||||
| T-08-REQUEST-LEAK | Information Disclosure | CLI/list/API output | mitigate | List never prints secret/hash; `/me` positive allow-list; output-source tests. |
|
||||
| T-08-SURFACE | Elevation | `/me` route | mitigate | Existing personal-token group and route-table proof; backend challenge remains unchanged. |
|
||||
| T-08-SC | Tampering | Cobra dependency use | mitigate | Use already-pinned Cobra; no new package install. |
|
||||
| T-08-PKCE | Spoofing/Elevation | authorize/token | mitigate | Mandatory S256 syntax and constant-time comparison. |
|
||||
| T-08-CODE-REPLAY | Spoofing | exchange | mitigate | Row lock, single transaction, concurrent one-winner test. |
|
||||
| T-08-OPEN-REDIRECT | Spoofing/Disclosure | authorize | mitigate | Exact redirect validation before any redirect. |
|
||||
| T-08-SECRET-TIMING | Information Disclosure | client auth | mitigate | Fixed transforms and constant-time compare. |
|
||||
| T-08-SCOPE-CEILING | Elevation | authorize | mitigate | Requested ∩ client ceiling before persistence. |
|
||||
| T-08-SC | Tampering | dependencies | mitigate | Standard library and existing app services only. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `go test ./bonfire -count=1`
|
||||
- `cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'Test(OAuthClientCommand|MeToken|TokenSurface|OAuthTools)' -count=1`
|
||||
- `go vet ./... && go test ./...` passes in each repository.
|
||||
- Focused wristband and issuer/store tests pass.
|
||||
- `go test -race ./wristband` passes at wave boundary.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- The command exactly supports create/update/list and repeated flags without leaking an existing secret.
|
||||
- An OAuth-issued `inv_` token receives the exact MCP `/me` bootstrap payload.
|
||||
- Invalid-token and route-surface behavior remains byte-identical to Phase 6/7.
|
||||
- Authorize and token protocol behavior is exact, concurrency-safe, and app-agnostic.
|
||||
- The configured personal-token issuer produces the unchanged inv_ wire format.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
||||
@@ -5,49 +5,39 @@ type: execute
|
||||
wave: 5
|
||||
depends_on: [08-04]
|
||||
files_modified:
|
||||
- ../fonoteka.go/parity/oauth_flow_test.go
|
||||
- ../fonoteka.go/parity/capture_clients.mjs
|
||||
- ../fonoteka.go/parity/capture-rules.yaml
|
||||
- ../fonoteka.go/parity/fixtures/mcp/mcp-lifecycle.yaml
|
||||
- ../fonoteka.go/parity/manifest.yaml
|
||||
- ../fonoteka.go/parity/check_corpus.go
|
||||
- scripts/check-phase8.sh
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/plugin.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/routes.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/oauth_connect_test.go
|
||||
- scripts/check-phase8-ui.mjs
|
||||
autonomous: true
|
||||
requirements: [AUTH-05, AUTH-06, AUTH-07]
|
||||
must_haves:
|
||||
truths:
|
||||
- "All four raw OAuth routes and five JWT OAuth management routes replay their recorded PHP contracts against Go and count as ported only after passing."
|
||||
- "A clean recorded lifecycle proves DCR, authorize, consent, token, refresh, replay kill, list, revoke, post-revoke failure, deny, confidential Basic auth, and scope ceiling."
|
||||
- "The unchanged real fonoteka-mcp process completes discovery, DCR, PKCE, JWT consent, token bootstrap, an MCP tool call, and refresh against the Go backend."
|
||||
- "D-08: JWT consent returns exact unchanged-Nuxt payloads and derives scopes and collection ids server-side."
|
||||
- "D-09: Authorize/token remain raw while consent routes remain in the JWT group."
|
||||
- "Invalid handles make no request, login uses the closed return-path allow-list, and English/Polish consent copy resolves."
|
||||
- "Consent state, keyboard/focus, 44px target, and mobile stacking matrices are proven without changing Nuxt source."
|
||||
artifacts:
|
||||
- path: "../fonoteka.go/parity/oauth_flow_test.go"
|
||||
provides: "Projected existing flows and full lifecycle replay"
|
||||
- path: "../fonoteka.go/parity/fixtures/mcp/mcp-lifecycle.yaml"
|
||||
provides: "Secret-scrubbed PHP lifecycle source-of-truth fixture"
|
||||
- path: "scripts/check-phase8.sh"
|
||||
provides: "Two-repository, parity, secret, Postgres, real-MCP phase gate"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go"
|
||||
provides: "Owner-bound consent show/allow/deny API"
|
||||
- path: "scripts/check-phase8-ui.mjs"
|
||||
provides: "Read-only browser harness for the approved UI-SPEC"
|
||||
key_links:
|
||||
- from: "oauth_flow_test.go"
|
||||
to: "newConfiguredTarget"
|
||||
via: "replay through the assembled Go app and real Postgres"
|
||||
pattern: "newConfiguredTarget"
|
||||
- from: "scripts/check-phase8.sh"
|
||||
to: "/media/nvme/dev/golem15/fonoteka/fonoteka-mcp"
|
||||
via: "three configured URLs and scripted MCP SDK lifecycle"
|
||||
pattern: "FONOTEKA_(API_URL|MCP_PUBLIC_URL|MCP_AUTH_SERVER)"
|
||||
- from: "scripts/check-phase8-ui.mjs"
|
||||
to: "/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app"
|
||||
via: "existing Playwright dependency, mocked backend responses, and no source writes"
|
||||
pattern: "playwright"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Prove the completed server through recorded PHP parity and the unchanged real MCP client rather than only implementation-local tests.
|
||||
Wire browser consent and prove the complete approved UI contract against the unchanged Nuxt checkout.
|
||||
|
||||
Purpose: Turn exact route bytes, lifecycle security semantics, protected-resource discovery ownership, and actual SDK compatibility into one repeatable acceptance gate.
|
||||
Output: Lifecycle fixture/capture policy, projected replay tests, nine ported manifest entries, and `scripts/check-phase8.sh`.
|
||||
Purpose: Separate browser-facing API/state compatibility from protocol internals and make preservation objectively executable.
|
||||
Output: JWT consent controllers/routes, assembled flow tests, and an external read-only browser/UI gate.
|
||||
</objective>
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As an** unchanged MCP client, **I want to** complete the full OAuth lifecycle against Go exactly as I did against PHP, **so that** discovery, tool use, refresh, replay defense, and revocation are proven together.
|
||||
|
||||
<execution_context>
|
||||
@/home/jin/.codex/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jin/.codex/get-shit-done/templates/summary.md
|
||||
@@ -58,114 +48,57 @@ Output: Lifecycle fixture/capture policy, projected replay tests, nine ported ma
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-RESEARCH.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-UI-SPEC.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-04-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
Existing parity seams:
|
||||
- `newConfiguredTarget(t, db)` boots the same assembled handler used by the app.
|
||||
- `tide.LoadFlow`, `tide.OpenStore`, and `tide.ReplayFlow` execute captured request/response sequences with private variables.
|
||||
- `parity/manifest.yaml` status becomes `ported` only when the selected replay subtest passes.
|
||||
|
||||
Unchanged MCP inputs:
|
||||
- `FONOTEKA_API_URL` points to the Go app personal-token API.
|
||||
- `FONOTEKA_MCP_PUBLIC_URL` points to the MCP resource server.
|
||||
- `FONOTEKA_MCP_AUTH_SERVER` points to the Go authorization server.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Specify recorded and real-client OAuth acceptance before changing fixtures</name>
|
||||
<files>../fonoteka.go/parity/oauth_flow_test.go, scripts/check-phase8.sh</files>
|
||||
<read_first>
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
|
||||
../fonoteka.go/parity/nuxt_flow_test.go
|
||||
../fonoteka.go/parity/parity_test.go
|
||||
../fonoteka.go/parity/fixtures/mcp/mcp-oauth.yaml
|
||||
../fonoteka.go/parity/fixtures/mcp/mcp-tools.yaml
|
||||
../fonoteka.go/parity/manifest.yaml
|
||||
../fonoteka.go/parity/capture_clients.mjs
|
||||
scripts/check-phase3.sh
|
||||
/media/nvme/dev/golem15/fonoteka/fonoteka-mcp/src/http.ts
|
||||
/media/nvme/dev/golem15/fonoteka/fonoteka-mcp/src/config.ts
|
||||
</read_first>
|
||||
<name>Task 1: Specify assembled consent and route behavior in executable RED</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller_test.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_connect_test.go</files>
|
||||
<behavior>
|
||||
- Existing broad flows are projected to named OAuth/MCP prerequisite steps and fail if an expected step disappears; unrelated later-phase calls are not replayed.
|
||||
- The clean lifecycle flow is required and every terminal security action is asserted before routes can be marked ported.
|
||||
- The gate starts real Postgres, Go app, and unchanged Node MCP; it verifies MCP-owned protected-resource metadata and Bearer hint separately from backend-owned metadata and Basic invalid-client challenge.
|
||||
- Show/allow/deny cover exact 200/404/422 payloads, host-only redirect, canonical scopes, active collection, ownership, and ordered redirects.
|
||||
- Tests compile and fail only through `PHASE8_RED:consent`.
|
||||
</behavior>
|
||||
<action>Per D-12, D-13, D-14, D-15, D-16, D-18, and the MVP test-first rule, create failing parity tests and the fail-closed gate skeleton before recording/changing status. Project `mcp-oauth` and `mcp-tools` by stable named step IDs and require the exact OAuth prerequisites plus `/me` and one tool call. Require full `mcp-lifecycle`. In the gate, declare stages for both repo vet/test/race, real-Postgres app boot, real MCP startup with all three environment variables, resource-server discovery/401 challenge, backend metadata/DCR/PKCE/login/consent/token, `/me`, MCP tool call, refresh/replay/revoke, and corpus/secret checks. Plan 08-06 adds the security-review validation stage after its review artifact exists. Do not edit or patch the MCP checkout.</action>
|
||||
<action>D-18: add controller and assembled PKCE flow tests against 08-04 interfaces. Use `PHASE8_RED:consent` only for absent controller/route behavior and reject syntax/setup/missing-test failures via the RED verifier. Cover T-08-CROSS-USER, SCOPE-CEILING, REQUEST-LEAK, and SURFACE, including duplicate action single-use.</action>
|
||||
<verify>
|
||||
<automated>test -f ../fonoteka.go/parity/oauth_flow_test.go && test -x scripts/check-phase8.sh</automated>
|
||||
<automated>scripts/check-phase8-red.sh consent bash -lc "cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestOAuth(Authorize|Consent|Deny|CodeExchange|Surface)' -count=1"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `oauth_flow_test.go` names projections for `mcp-oauth`, `mcp-tools`, and the complete `mcp-lifecycle`, and missing expected steps fail.
|
||||
- `scripts/check-phase8.sh` uses `set -euo pipefail`, fail-closed dependency/Docker checks, cleanup traps, and all three MCP environment variables.
|
||||
- The gate distinguishes MCP RFC 9728 metadata/rich Bearer challenge from backend exact Basic invalid-client challenge and unchanged token-surface 401.
|
||||
- Tests/gate fail because the lifecycle fixture/status/evidence is not yet complete, not because of shell or Go syntax errors.
|
||||
</acceptance_criteria>
|
||||
<done>The acceptance harness demands the exact recorded and real-client lifecycle before any route can be claimed ported.</done>
|
||||
<done>Consent tests compile, execute, and fail only on the intended missing behavior.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Record, scrub, replay, and promote the complete OAuth lifecycle</name>
|
||||
<files>../fonoteka.go/parity/capture_clients.mjs, ../fonoteka.go/parity/capture-rules.yaml, ../fonoteka.go/parity/fixtures/mcp/mcp-lifecycle.yaml, ../fonoteka.go/parity/oauth_flow_test.go, ../fonoteka.go/parity/manifest.yaml, ../fonoteka.go/parity/check_corpus.go</files>
|
||||
<read_first>
|
||||
../fonoteka.go/parity/oauth_flow_test.go
|
||||
../fonoteka.go/parity/capture_clients.mjs
|
||||
../fonoteka.go/parity/capture-rules.yaml
|
||||
../fonoteka.go/parity/php_parity.sh
|
||||
../fonoteka.go/parity/fixtures/mcp/mcp-oauth.yaml
|
||||
../fonoteka.go/parity/fixtures/mcp/mcp-tools.yaml
|
||||
../fonoteka.go/parity/manifest.yaml
|
||||
tide/flow.go
|
||||
tide/replay.go
|
||||
</read_first>
|
||||
<name>Task 2: Implement owner-bound JWT consent and raw route wiring</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller_test.go, ../fonoteka.go/plugins/golem15/fonoteka/plugin.go, ../fonoteka.go/plugins/golem15/fonoteka/routes.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_connect_test.go</files>
|
||||
<behavior>
|
||||
- Lifecycle records DCR → authorize → consent → token → refresh → spent-token replay → list → revoke → refresh failure → deny, plus confidential Basic and ceiling/invalid-scope cases.
|
||||
- Every request id, code, verifier, client secret, access token, and refresh token is represented only by a typed placeholder in committed fixtures; private vars are mode 0600.
|
||||
- Nine manifest entries become ported only after their exact route replay passes; pending never increments passing.
|
||||
- Show returns sanitized client, host-only redirect, ordered mintable scopes, active collection name, and ISO expiry.
|
||||
- Allow grants submitted ∩ requested ∩ ceiling ∩ mintable; deny consumes pending state; both return nonblank ordered redirect_to.
|
||||
</behavior>
|
||||
<action>Extend the existing capture script/rules and use the Phase 2 isolated-PHP process to record D-16's lifecycle. Issue the confidential client through `fonoteka:oauth-client`; exercise scope ceiling truncation and invalid-scope redirect with `client_secret_basic`. Capture all secret-bearing values with explicit pkce/token/credential categories into the private store, confirm both vars files are 0600, and commit only symbolic variable references. Add full and projected replays through `newConfiguredTarget` with real Postgres. After each of the four raw and five JWT route subtests passes, change only those manifest entries to `status: ported`; keep honest corpus accounting.</action>
|
||||
<action>D-08: implement show/allow/deny in the JWT+locale+must-change-password group using `bouncer.User`, `ResolveActiveCollection`, `lagoon.Validate`, and wristband operations; never accept collection IDs or extra scopes. Collapse missing/stale/used/foreign handles to exact 404 and empty/no-longer-grantable intersection to exact 422. D-09: mount authorize/token in raw routes and only token receives its throttle. D-10 and D-12: add no oauth guard, house middleware, backend Bearer challenge, or RFC 9728 document.</action>
|
||||
<verify>
|
||||
<automated>cd ../fonoteka.go && go test ./parity -run 'TestOAuthFlows|TestParityCorpus|TestParityContract' -count=1</automated>
|
||||
<automated>cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestOAuth(Authorize|Consent|Deny|CodeExchange|Surface)' -count=1</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `mcp-lifecycle.yaml` contains the locked sequence and placeholder references, not recoverable credential values.
|
||||
- `go run ./parity/check_corpus.go --manifest parity/manifest.yaml --fixtures parity/fixtures --check-secrets` exits 0.
|
||||
- All nine OAuth manifest entries are `ported`; replay reports them passing with zero failing and does not count any pending route as passing.
|
||||
- Existing `mcp-oauth`/`mcp-tools` projections fail if a required named step is removed and ignore only explicitly enumerated later-phase steps.
|
||||
</acceptance_criteria>
|
||||
<done>The Go app passes the PHP-recorded OAuth route and lifecycle contracts without committing live secrets.</done>
|
||||
<done>The unchanged consent client can inspect, allow, or deny one owner-bound request and complete exact PKCE code exchange.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Complete the real unchanged-MCP phase gate</name>
|
||||
<files>scripts/check-phase8.sh</files>
|
||||
<name>Task 3: Prove the approved consent and connected-app UI contract read-only</name>
|
||||
<files>scripts/check-phase8-ui.mjs</files>
|
||||
<read_first>
|
||||
scripts/check-phase8.sh
|
||||
scripts/check-phase3.sh
|
||||
../fonoteka.go/parity/oauth_flow_test.go
|
||||
/media/nvme/dev/golem15/fonoteka/fonoteka-mcp/package.json
|
||||
/media/nvme/dev/golem15/fonoteka/fonoteka-mcp/src/http.ts
|
||||
/media/nvme/dev/golem15/fonoteka/fonoteka-mcp/src/config.ts
|
||||
/media/nvme/dev/golem15/fonoteka/fonoteka-mcp/src/install.ts
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-UI-SPEC.md
|
||||
/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/package.json
|
||||
/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/app/pages/connect.vue
|
||||
/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/app/components/fonoteka/ConsentScopePicker.vue
|
||||
/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/app/components/fonoteka/ConnectedAppsManager.vue
|
||||
/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/app/stores/fonoteka.ts
|
||||
</read_first>
|
||||
<action>Finish D-14's executable gate using the existing gate family and installed Node MCP dependencies. Allocate loopback ports, start disposable Postgres and the assembled Go app, start the unchanged MCP with its three URLs pointed at the test services, and drive the actual SDK discovery/DCR/PKCE flow. Obtain a JWT only through the app's real login route, consent through the JWT API, exchange, call an MCP tool after `/me` bootstrap, refresh, replay/revoke, and assert failures. Verify MCP emits the rich Bearer `resource_metadata` challenge and protected-resource document; verify the backend emits only exact Basic on token invalid-client and unchanged no-challenge token-surface 401. Run both modules' vet/test/race, parity/corpus/secret checks, and require a verified security-review artifact stage to be satisfiable by 08-06. Preserve cleanup on success, error, and interruption; never print secrets.</action>
|
||||
<action>Create a read-only harness outside the Nuxt checkout using its already-installed Playwright runtime. First run `pnpm verify:oauth-return-path` and `pnpm verify:oauth-i18n`. Then boot the unchanged app and intercept API responses to prove: invalid/missing/repeated-first-invalid handles send no oauth/request call; logged-out entry preserves only a validated localized return path; 200, 404, network/error, empty-scope, allow-pending, deny-pending, and one-redirect outcomes render correctly; connected-app error/empty/manual-count/populated/cancel/revoke-pending/success/failure/identical-404 states render correctly. Assert keyboard order, visible 2px focus, dialog trap/Escape/restore, native disabled semantics, checkbox/revoke targets at least 44px, narrow/mobile stacking and no horizontal overflow, and both English/Polish strings with no raw keys. Snapshot the OAuth-related Nuxt paths before/after and fail on any diff; do not write fixtures, snapshots, generated files, or source inside Nuxt.</action>
|
||||
<verify>
|
||||
<automated>scripts/check-phase8.sh</automated>
|
||||
<automated>cd /media/nvme/dev/golem15/fonoteka/vue-fonoteka-app && pnpm verify:oauth-return-path && pnpm verify:oauth-i18n && cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && node scripts/check-phase8-ui.mjs --focused</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- The gate starts the real unchanged MCP checkout and completes metadata, DCR, PKCE, JWT consent, token, `/me`, one MCP tool call, and refresh against the Go backend.
|
||||
- The gate proves spent refresh replay and connected-app revoke kill the lineage and later access/refresh attempts fail.
|
||||
- Both repositories pass `go vet ./...`, `go test ./...`, and `go test -race ./...`; corpus and secret scans exit 0.
|
||||
- `git -C /media/nvme/dev/golem15/fonoteka/fonoteka-mcp status --short` and the Nuxt equivalent show no Phase 8 diff.
|
||||
</acceptance_criteria>
|
||||
<done>The actual connector stack, including RFC 9728 resource-server behavior, runs unchanged through the complete Go authorization lifecycle.</done>
|
||||
<done>The full UI-SPEC state/accessibility/return-path/i18n matrix passes against unchanged Nuxt files, and the harness is callable from the final gate.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
@@ -175,36 +108,29 @@ Unchanged MCP inputs:
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| PHP capture → committed fixtures | Live credentials must become typed placeholders before entering git. |
|
||||
| Scripted SDK → Go backend/MCP | External client parsing and redirects exercise public network-facing contracts. |
|
||||
| Gate process → child services | Secrets and cleanup state cross shell/Node/Go process boundaries. |
|
||||
| Browser JWT principal → consent API | Authenticated input crosses ownership/scope/tenant boundaries. |
|
||||
| Backend data → unchanged Nuxt | Untrusted names and protocol state select rendered UI states. |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-08-PKCE | Spoofing/Elevation | real SDK flow | mitigate | Actual SDK S256 authorize/exchange plus wrong-verifier rejection in gate. |
|
||||
| T-08-CODE-REPLAY | Spoofing | lifecycle replay | mitigate | Recorded and real repeated code/spent state fail. |
|
||||
| T-08-REFRESH-REPLAY | Spoofing/Elevation | lifecycle replay/gate | mitigate | Spent replay kills lineage; post-replay DB/API evidence required. |
|
||||
| T-08-OPEN-REDIRECT | Spoofing/Disclosure | recorded authorize cases | mitigate | PHP fixture and Go replay assert local errors versus trusted ordered redirects. |
|
||||
| T-08-SECRET-TIMING | Information Disclosure | confidential Basic flow | mitigate | Actual confidential flow uses the constant-time implementation; final source audit in 08-06. |
|
||||
| T-08-SCOPE-CEILING | Elevation | confidential lifecycle | mitigate | Recorded ceiling truncation and invalid-scope redirect. |
|
||||
| T-08-CROSS-USER | Elevation | consent/list/revoke replay | mitigate | JWT ownership cases and indistinguishable 404 replay. |
|
||||
| T-08-REQUEST-LEAK | Information Disclosure | fixtures/logs | mitigate | Capture categories, 0600 stores, placeholder-only fixtures, check-secrets and quiet gate. |
|
||||
| T-08-DCR-FLOOD | Denial of Service | register route | mitigate | Recorded native errors plus focused rate/body/cap tests run by gate. |
|
||||
| T-08-SURFACE | Elevation | MCP/backend boundary | mitigate | Gate asserts correct RFC 9728 ownership and exact backend challenges. |
|
||||
| T-08-SC | Tampering | reused Node dependencies | mitigate | No install; use checked-in lockfile/node_modules and dependency preflight. |
|
||||
| T-08-SCOPE-CEILING | Elevation | consent | mitigate | Four-way scope intersection and server-derived collection. |
|
||||
| T-08-CROSS-USER | Elevation | consent | mitigate | Principal-bound lookup/consume and indistinguishable 404. |
|
||||
| T-08-REQUEST-LEAK | Information Disclosure | browser/errors | mitigate | Opaque handle, no-referrer behavior, no request on invalid handle. |
|
||||
| T-08-SURFACE | Elevation | route groups | mitigate | Raw/JWT isolation and browser contract harness. |
|
||||
| T-08-SC | Tampering | Playwright reuse | mitigate | Reuse installed locked dependency; no package install. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `cd ../fonoteka.go && go test ./parity -run 'TestOAuthFlows|TestParityCorpus|TestParityContract' -count=1`
|
||||
- `scripts/check-phase8.sh`
|
||||
- Focused consent/app tests pass under 30 seconds where possible.
|
||||
- UI harness runs at the wave boundary and is invoked again by the final gate.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Nine OAuth routes and the full lifecycle replay pass against Go with no leaked fixture secret.
|
||||
- The real unchanged MCP discovers, authorizes, initializes, executes a tool, refreshes, and observes replay/revoke failure.
|
||||
- Resource-server and authorization-server header ownership is proven exactly, not conflated.
|
||||
- Consent API matches every unchanged client state selector.
|
||||
- Return-path, no-invalid-request, i18n, state, accessibility, and responsive matrices have runnable evidence.
|
||||
- Nuxt source remains unchanged.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
||||
@@ -5,48 +5,54 @@ type: execute
|
||||
wave: 6
|
||||
depends_on: [08-05]
|
||||
files_modified:
|
||||
- wristband/phase08_coverage_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/phase08_coverage_test.go
|
||||
- ../fonoteka.go/parity/oauth_audit_test.go
|
||||
- scripts/check-phase8.sh
|
||||
- .planning/phases/08-oauth2-1-authorization-server/08-PHP-TEST-MAP.md
|
||||
- .planning/phases/08-oauth2-1-authorization-server/08-SECURITY-REVIEW.md
|
||||
- .planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
|
||||
autonomous: false
|
||||
- wristband/token.go
|
||||
- wristband/token_test.go
|
||||
- wristband/stores.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/routes.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/oauth_lifecycle_test.go
|
||||
autonomous: true
|
||||
requirements: [AUTH-05, AUTH-06, AUTH-07]
|
||||
must_haves:
|
||||
truths:
|
||||
- "Every PHP OAuth functional/security test method maps to a named passing Go test or subtest, and both repositories pass vet/test/race."
|
||||
- "Every T-08 threat maps to a failing-when-broken test with zero open high-severity findings."
|
||||
- "The phase gate refuses missing tests, route parity, secret scans, unchanged-client evidence, or an unverified security review."
|
||||
- "D-04: A valid refresh token rotates to a new access/refresh pair while revoking the prior access token."
|
||||
- "D-17: Replaying a spent refresh token commits revocation of the whole lineage and unexpired evidence remains, leaving no usable branch."
|
||||
- "D-08: A user sees only their live connected OAuth apps and can revoke one access token plus its refresh lineage atomically."
|
||||
artifacts:
|
||||
- path: ".planning/phases/08-oauth2-1-authorization-server/08-PHP-TEST-MAP.md"
|
||||
provides: "Auditable one-to-one map of all 103 PHP methods to Go evidence"
|
||||
- path: ".planning/phases/08-oauth2-1-authorization-server/08-SECURITY-REVIEW.md"
|
||||
provides: "ASVS L1 threat disposition and executed evidence"
|
||||
- path: ".planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md"
|
||||
provides: "Nyquist-complete task/status and gate sign-off"
|
||||
- path: "wristband/token.go"
|
||||
provides: "Refresh grant rotation, replay detection, and committed lineage kill"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go"
|
||||
provides: "Row-locked refresh traversal, revoke, and expiry sweep storage"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller.go"
|
||||
provides: "Owner-scoped connected-app list and revoke handlers"
|
||||
key_links:
|
||||
- from: "08-SECURITY-REVIEW.md"
|
||||
to: "named Go tests"
|
||||
via: "file:TestName evidence for every mitigated threat"
|
||||
pattern: "T-08-"
|
||||
- from: "scripts/check-phase8.sh"
|
||||
to: "08-SECURITY-REVIEW.md"
|
||||
via: "fail-closed verified/zero-open audit check"
|
||||
pattern: "08-SECURITY-REVIEW"
|
||||
- from: "wristband/token.go"
|
||||
to: "oauth_store.go"
|
||||
via: "transaction outcome commits lineage kill before returning invalid_grant"
|
||||
pattern: "WithinTx"
|
||||
- from: "connected_app_controller.go"
|
||||
to: "wristband.Server"
|
||||
via: "cascade revoke operation rather than direct refresh-row deletion"
|
||||
pattern: "Revoke"
|
||||
- from: "connected_app_controller.go"
|
||||
to: "token_api_controller.go"
|
||||
via: "reuse of positive allow-list token serializer"
|
||||
pattern: "serializeToken"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Close Phase 8 with complete unit/security coverage, an independent security-review agent pass, and one fail-closed verification gate.
|
||||
Deliver the durable OAuth lifecycle slice: safe refresh rotation/replay handling and user-visible connected-app listing/revocation.
|
||||
|
||||
Purpose: Demonstrate that the exact OAuth implementation is not merely functional but resistant to every identified high-severity replay, redirect, timing, scope, ownership, leakage, flooding, and surface threat.
|
||||
Output: Coverage tests, 103-method audit map, verified security review, signed validation strategy, and final gate.
|
||||
Purpose: Ensure stolen or replayed refresh tokens cannot create surviving branches and the unchanged Settings UI controls the same grant lineage.
|
||||
Output: Refresh-grant state machine, row-locked store operations, connected-app controllers/routes, and concurrency-backed lifecycle tests.
|
||||
</objective>
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** Płytarium operator, **I want to** rely on independently reviewed OAuth behavior and complete regression evidence, **so that** unchanged connectors can be enabled without accepting an unproven high-severity security risk.
|
||||
**As a** connected-app user, **I want to** refresh access safely and revoke applications from Settings, **so that** replayed or revoked credentials immediately lose access.
|
||||
|
||||
<execution_context>
|
||||
@/home/jin/.codex/get-shit-done/workflows/execute-plan.md
|
||||
@@ -59,126 +65,114 @@ Output: Coverage tests, 103-method audit map, verified security review, signed v
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-RESEARCH.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-PATTERNS.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-UI-SPEC.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-05-SUMMARY.md
|
||||
@.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-SECURITY-REVIEW.md
|
||||
|
||||
<interfaces>
|
||||
Security-review evidence contract:
|
||||
- One register row per `T-08-*` threat with category, component, disposition, mitigation, and exact `file:TestName` evidence.
|
||||
- Frontmatter reports total/closed/open and status; completion requires `status: verified`, `threats_open: 0`, and no unmitigated HIGH.
|
||||
From Plan 08-05:
|
||||
- Token handler already dispatches `authorization_code` and recognizes the configured refresh grant.
|
||||
- `wristband.Tx` provides row-lock-capable refresh/code stores and the transaction-bound access-token issuer.
|
||||
- OAuth access tokens are `models.ApiToken` rows distinguished by non-null `OAuthClientID`.
|
||||
|
||||
PHP test inventory contract:
|
||||
- 103 methods: authorize 11, client-command 8, metadata 2, migration 7, register 10, token 10, consent/scope 7, refresh rotation 10, revocation 8, surface isolation 30.
|
||||
Existing serializer:
|
||||
- `serializeToken(gdb, *models.ApiToken) map[string]any` is the required positive allow-list for connected-app responses.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Close the 103-method PHP audit and Phase 8 coverage gaps</name>
|
||||
<files>wristband/phase08_coverage_test.go, ../fonoteka.go/plugins/golem15/fonoteka/phase08_coverage_test.go, ../fonoteka.go/parity/oauth_audit_test.go, .planning/phases/08-oauth2-1-authorization-server/08-PHP-TEST-MAP.md</files>
|
||||
<name>Task 1: Specify rotation, replay, list, and revoke as one lifecycle</name>
|
||||
<files>wristband/token_test.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller_test.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_lifecycle_test.go</files>
|
||||
<read_first>
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-UI-SPEC.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
|
||||
wristband/registration_test.go
|
||||
wristband/authorize_test.go
|
||||
wristband/token_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/oauth_registration_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/oauth_connect_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/oauth_lifecycle_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/oauth_tools_test.go
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthAuthorizeTest.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthClientCommandTest.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthMetadataTest.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthMigrationTest.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthRegisterTest.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthTokenTest.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/security/OAuthConsentScopeCeilingTest.php
|
||||
wristband/token.go
|
||||
wristband/stores.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/token_api_controller.go
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/classes/auth/OAuthCodeManager.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/ConnectedAppController.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/security/OAuthRefreshRotationTest.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/security/OAuthRevocationTest.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/security/TokenSurfaceIsolationTest.php
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Every PHP method is listed once with its source class, behavior, and a named Go test/subtest that actually runs.
|
||||
- Coverage tests exercise error branches, encoding failures, nil/misconfigured dependencies, clock/entropy errors, parser edges, and route/config drift not already covered.
|
||||
- Audit fails if a mapped Go test is renamed/missing or if any PHP method is unmapped/duplicated.
|
||||
- Normal refresh revokes the old access token, marks predecessor `rotated_to_id`, and returns a new same-scope/same-collection pair.
|
||||
- Sequential or concurrent spent-token replay returns `invalid_grant` only after the complete lineage and current access token are durably revoked.
|
||||
- Expiry sweep removes only expired pending/code/refresh rows and retains unexpired rotated/revoked refresh rows as replay evidence.
|
||||
- Connected-app list is newest-first, owner-only, live OAuth tokens only, with manual count separate and no secret/client-id fields.
|
||||
- Revoke of an owned OAuth token kills its refresh lineage; foreign, missing, and manual token IDs share the exact 404.
|
||||
</behavior>
|
||||
<action>Per D-18 and the repository rule that unit coverage is the last plan, enumerate all 103 PHP methods into `08-PHP-TEST-MAP.md`, map each to existing Phase 8 tests, and add focused coverage tests only where no named evidence exists. Add an executable audit that parses the inventory/map and Go test list so counts alone cannot hide missing or duplicate mappings. Close framework handler/store branches, app boot/config/route/controller/command branches, parity projections, and every exact response/header path. Do not replace behavior assertions with coverage-only calls or map one broad test to methods whose distinct assertions are absent.</action>
|
||||
<action>D-04: and D-18: extend the RED suite with deterministic in-memory tests and synchronized real-Postgres contention tests for T-08-REFRESH-REPLAY, T-08-CROSS-USER, T-08-SCOPE-CEILING, T-08-REQUEST-LEAK, and T-08-SURFACE. D-16: cover the lifecycle sequence later recorded by parity. D-17: prove exact sweep retention. Include an assembled lifecycle that starts with the grant from 08-05, refreshes, replays the spent predecessor, verifies the new branch and access token are dead, creates another grant, lists it, revokes it, and proves refresh afterward fails. Use compiling stubs and separate `PHASE8_RED:lifecycle-framework` and `PHASE8_RED:lifecycle-app` assertions; reject syntax/build/setup/missing-test failures through the shared RED verifier. Assert exact UI response allow-lists and 404 bytes.</action>
|
||||
<verify>
|
||||
<automated>go test ./wristband -count=1 && cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... ./parity -run 'Test(OAuth|Phase08|PHPTestMap|TokenSurface|MeToken)' -count=1</automated>
|
||||
<automated>scripts/check-phase8-red.sh lifecycle-framework go test ./wristband -run 'Test(Refresh|Replay|Sweep)' -count=1 && scripts/check-phase8-red.sh lifecycle-app bash -lc "cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestOAuth(Refresh|Replay|Connected|Revoke|Sweep)' -count=1"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- The map contains exactly 103 unique PHP method rows distributed 11/8/2/7/10/10/7/10/8/30 by source suite.
|
||||
- The executable audit confirms every mapped Go `TestName[/subtest]` exists and executes; missing or duplicate rows make it fail.
|
||||
- Both repositories pass full `go vet ./...`, `go test ./...`, and `go test -race ./...`, including nested plugin modules.
|
||||
- Coverage additions retain exact byte/header/concurrency assertions for security branches.
|
||||
- Tests include sequential replay, a barrier-synchronized double refresh, committed lineage kill, expiry retention, owner isolation, manual-token exclusion, list ordering, and post-revoke refresh failure.
|
||||
- The replay test explicitly reloads database rows after the `invalid_grant` response and asserts revoked lineage/access state, preventing rollback-hidden false positives.
|
||||
- Tests fail on missing refresh/connected-app implementation while all 08-02 happy-path tests remain green.
|
||||
</acceptance_criteria>
|
||||
<done>All PHP OAuth behavior has one-to-one named Go evidence and the phase's code paths are covered by meaningful regression tests.</done>
|
||||
<done>The RED lifecycle suite detects branch survival, rollback of replay revocation, ownership leaks, serialization leaks, and route misplacement.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Run the mandated security-review agent and close every high-severity finding</name>
|
||||
<files>.planning/phases/08-oauth2-1-authorization-server/08-SECURITY-REVIEW.md, .planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md, scripts/check-phase8.sh</files>
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Implement refresh rotation, committed replay kill, and exact sweeps</name>
|
||||
<files>wristband/token.go, wristband/stores.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go</files>
|
||||
<read_first>
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-RESEARCH.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-PHP-TEST-MAP.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
|
||||
.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-SECURITY-REVIEW.md
|
||||
scripts/check-phase8.sh
|
||||
wristband/server.go
|
||||
wristband/authorize.go
|
||||
wristband/token_test.go
|
||||
wristband/token.go
|
||||
wristband/register.go
|
||||
wristband/crypto.go
|
||||
wristband/stores.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/routes.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/models/oauth_refresh_token.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/models/api_token.go
|
||||
</read_first>
|
||||
<action>Invoke the `gsd-security-auditor` security-review agent against all Phase 8 production/test changes and the locked threat map, explicitly requiring OWASP ASVS L1 review of T-08-PKCE, CODE-REPLAY, REFRESH-REPLAY, OPEN-REDIRECT, SECRET-TIMING, SCOPE-CEILING, CROSS-USER, REQUEST-LEAK, DCR-FLOOD, SURFACE, and supply-chain status. Write `08-SECURITY-REVIEW.md` in the Phase 6 format with trust boundaries, complete STRIDE register, severity, disposition, mitigation, and executed `file:TestName` evidence. If the agent finds any HIGH issue, stop sign-off, implement the narrow fix and failing regression in the owning Phase 8 file, rerun the focused and full gates, and re-run the auditor until no HIGH remains. Then update VALIDATION task IDs/statuses, set `nyquist_compliant: true` and `wave_0_complete: true`, and add a fail-closed verified/zero-open security-review check to `check-phase8.sh`.</action>
|
||||
<behavior>
|
||||
- Presented refresh lookup uses SHA-256 hash and a row lock inside the single transaction boundary.
|
||||
- Replay revocation returns success from the transaction callback, then maps the recorded outcome to `invalid_grant` outside it.
|
||||
- Rotation keeps predecessor/successor relationships and unexpired evidence rows.
|
||||
</behavior>
|
||||
<action>Implement D-04, D-05, D-07, and D-17's refresh branch in wristband and the GORM adapter. Authenticate the client using the same Basic-over-form rule, hash the presented refresh secret, lock its row, reject expired/revoked/wrong-client grants, and rotate atomically by revoking the old access token, minting/persisting its successor, creating the next refresh secret/hash, and linking `rotated_to_id`. If a spent token is presented, traverse and revoke the whole lineage and associated access tokens, return nil from the transaction so the kill commits, then return `invalid_grant` from the handler. Keep the old scopes, collection IDs, offline flag, and client binding. Sweep only rows whose `expires_at` is past; do not delete unexpired replay evidence.</action>
|
||||
<verify>
|
||||
<automated>scripts/check-phase8.sh</automated>
|
||||
<automated>go test ./wristband -run 'Test(Refresh|Replay|Sweep)' -count=1 && cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/classes/auth -run 'TestOAuth(Refresh|Replay|Sweep)' -count=1</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `08-SECURITY-REVIEW.md` has `status: verified`, `threats_open: 0`, and one evidence-backed disposition for every named T-08 threat plus T-08-SC.
|
||||
- Every HIGH finding is mitigated by a named failing-when-broken test; no HIGH is accepted, deferred, or omitted.
|
||||
- Static evidence finds `crypto/subtle.ConstantTimeCompare` for client secret and PKCE, row locks for code/refresh, committed replay kill, 64 KiB register cap, raw/JWT/personal route isolation, and no sensitive-value logging.
|
||||
- `08-VALIDATION.md` maps final plan/task IDs, all required test/gate files exist, all statuses are green, and both Nyquist flags are true.
|
||||
- `scripts/check-phase8.sh` exits nonzero if the review is missing, unverified, has a nonzero open count, or lacks any required T-08 row.
|
||||
- Normal refresh and sequential/concurrent replay tests pass under real Postgres.
|
||||
- A spent-token replay leaves every lineage refresh row and its live access token revoked after the response transaction commits.
|
||||
- `go test -race ./wristband` passes and the app contention test produces a single usable branch.
|
||||
- Sweep tests prove expired rows are removed and unexpired rotated/revoked rows remain.
|
||||
</acceptance_criteria>
|
||||
<done>An independent security agent has reviewed the implemented phase, all high-severity findings are closed with executable evidence, and the final gate enforces the review.</done>
|
||||
<done>Refresh rotation is atomic, preserves replay evidence, and commits whole-lineage revocation before emitting the protocol error.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking-human">
|
||||
<name>Task 3: Approve the OAuth security and unchanged-client evidence</name>
|
||||
<files>.planning/phases/08-oauth2-1-authorization-server/08-SECURITY-REVIEW.md, .planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md</files>
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Expose connected-app list and atomic revoke to the unchanged Settings UI</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller_test.go, ../fonoteka.go/plugins/golem15/fonoteka/routes.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_lifecycle_test.go</files>
|
||||
<read_first>
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-SECURITY-REVIEW.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-PHP-TEST-MAP.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-05-SUMMARY.md
|
||||
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/oauth_lifecycle_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/token_api_controller.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/routes.go
|
||||
/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/app/components/fonoteka/ConnectedAppsManager.vue
|
||||
/media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/app/stores/fonoteka.ts
|
||||
</read_first>
|
||||
<action>Present the completed automated evidence after the security-review agent has produced zero open high-severity findings. Do not ask the user to rerun automation; show the exact gate result, threat totals, 103-method audit result, nine-route parity result, real-MCP lifecycle result, and unchanged Nuxt/MCP worktree checks. Block completion if any displayed result is missing or non-green.</action>
|
||||
<behavior>
|
||||
- List returns `data` and numeric `manual_tokens_count`, filters to owner/live/OAuth tokens, and orders created_at descending.
|
||||
- Every row is `serializeToken` plus sanitized client name and contains no raw credential, hash, OAuth client id, redirect URI, or other-user data.
|
||||
- Revoke completes access-token and refresh-lineage revocation before returning `{"data":{"revoked":true}}`.
|
||||
</behavior>
|
||||
<action>Per D-08 and the UI-SPEC, add GET and DELETE connected-app controllers in the JWT group. Reuse `serializeToken`; append only the sanitized/truncated client name, initialize collection/scope arrays as arrays, count live manual tokens separately, and order OAuth tokens newest first. Scope every query by `bouncer.User`. For DELETE, require an owned OAuth token, invoke the wristband lineage-revoke operation in the same committed transaction, and collapse missing/foreign/manual IDs to exact `{"error":"Token not found"}` 404. Mount only under `/_fonoteka/api/v1/oauth`; do not expose these routes on the personal-token or raw groups.</action>
|
||||
<verify>
|
||||
<automated>scripts/check-phase8.sh</automated>
|
||||
<automated>cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestOAuth(ConnectedApps|Revoke|Lifecycle|Surface)' -count=1</automated>
|
||||
</verify>
|
||||
<what-built>Direct standard-library OAuth authorization server with DCR, S256 PKCE, JWT consent, authorization-code and rotating-refresh grants, connected-app revocation, operator client command, MCP bootstrap endpoint, PHP parity, and unchanged real-MCP proof.</what-built>
|
||||
<how-to-verify>
|
||||
1. Review `08-SECURITY-REVIEW.md`; expect `status: verified`, zero open threats, and named test evidence for every T-08 row.
|
||||
2. Review the recorded gate transcript; expect both repositories' vet/test/race, 103/103 PHP method map, nine OAuth route replays, secret scan, and real MCP lifecycle to be green.
|
||||
3. Confirm the Nuxt and fonoteka-mcp repositories have no Phase 8 source diff.
|
||||
</how-to-verify>
|
||||
<acceptance_criteria>
|
||||
- Human approval occurs only after zero open HIGH findings and a passing `scripts/check-phase8.sh` result are shown.
|
||||
- The evidence explicitly includes exact Basic `WWW-Authenticate` at backend invalid-client, unchanged no-challenge token 401, and MCP-owned rich Bearer/resource-metadata behavior.
|
||||
- Rejection includes the failing threat/test/gate identifier so remediation is deterministic.
|
||||
- Empty, populated, manual-count, newest-first, foreign/manual 404, and successful atomic revoke tests pass with exact bytes.
|
||||
- The lifecycle test proves the revoked app disappears on the next list and its refresh token returns `invalid_grant`.
|
||||
- JSON assertions reject `token`, `token_hash`, `oauth_client_id`, `client_secret`, `refresh_token`, `request_id`, and `redirect_uris` anywhere in list output.
|
||||
- The Nuxt repository remains unchanged.
|
||||
</acceptance_criteria>
|
||||
<resume-signal>Type "approved" to close Phase 8, or provide the failed threat/test/gate identifier.</resume-signal>
|
||||
<done>The user has accepted the complete automated OAuth compatibility and security evidence.</done>
|
||||
<done>The existing Settings → Integrations UI can list and revoke only the current user's connected applications, and revoke kills the entire grant lineage.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
@@ -188,39 +182,32 @@ PHP test inventory contract:
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Phase implementation → independent auditor | Claims must be supported by executable evidence, not implementation intent. |
|
||||
| Test inventory → completion status | Missing/renamed tests or unmapped PHP methods must fail closed. |
|
||||
| Security report → phase gate | Stale, missing, or open findings must prevent sign-off. |
|
||||
| Refresh credential → token endpoint | A bearer-like long-lived credential requests a new grant branch. |
|
||||
| JWT principal → connected-app API | User-controlled ids request listing/revocation of durable credentials. |
|
||||
| Transaction outcome → OAuth error | Security revocation must commit even though the protocol response is an error. |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-08-PKCE | Spoofing/Elevation | authorize/exchange | mitigate | Independent audit plus missing/plain/wrong/syntax/constant-time tests. |
|
||||
| T-08-CODE-REPLAY | Spoofing | code transaction | mitigate | Sequential/concurrent single-winner tests and row-lock source evidence. |
|
||||
| T-08-REFRESH-REPLAY | Spoofing/Elevation | refresh transaction | mitigate | Branch-concurrency and post-error persisted lineage-kill evidence. |
|
||||
| T-08-OPEN-REDIRECT | Spoofing/Disclosure | redirect construction | mitigate | Exact allow-list/validation-order and no-Location tests. |
|
||||
| T-08-SECRET-TIMING | Information Disclosure | crypto/client auth | mitigate | `subtle.ConstantTimeCompare` source gate and invalid-secret behavior tests. |
|
||||
| T-08-SCOPE-CEILING | Elevation | authorize/consent/refresh | mitigate | End-to-end requested/submitted/ceiling/mintable and server-derived tenant proofs. |
|
||||
| T-08-CROSS-USER | Elevation | consent/connected apps | mitigate | Foreign ownership tests with indistinguishable 404s. |
|
||||
| T-08-REQUEST-LEAK | Information Disclosure | logs/fixtures/output | mitigate | Log capture, source scan, fixture secret scan, positive output allow-lists. |
|
||||
| T-08-DCR-FLOOD | Denial of Service | register | mitigate | 64 KiB cap, limiter, atomic client cap, sweep, concurrency evidence. |
|
||||
| T-08-SURFACE | Elevation | route/MCP boundary | mitigate | Assembled route table and real client header-ownership gate. |
|
||||
| T-08-SC | Tampering | package supply chain | mitigate | No added package; module-diff and package-audit checks. |
|
||||
| T-08-REFRESH-REPLAY | Spoofing/Elevation | refresh state machine | mitigate | Row lock, rotation chain, single-winner concurrency, commit lineage kill before `invalid_grant`. |
|
||||
| T-08-CROSS-USER | Elevation | connected-app controllers | mitigate | Owner-scoped reads/deletes and indistinguishable missing/foreign/manual 404. |
|
||||
| T-08-SCOPE-CEILING | Elevation | refresh rotation | mitigate | Copy only stored granted scopes/collection ids; refresh cannot add request-provided authority. |
|
||||
| T-08-REQUEST-LEAK | Information Disclosure | list response/logs | mitigate | Positive allow-list serializer and explicit forbidden-field tests. |
|
||||
| T-08-SURFACE | Elevation | route groups | mitigate | JWT-only management route inspection; token endpoint remains raw. |
|
||||
| T-08-SC | Tampering | dependencies | mitigate | No install; standard library and existing GORM only. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `go vet ./... && go test ./... && go test -race ./...`
|
||||
- `cd ../fonoteka.go && go vet ./... && go test ./... && go test -race ./...`
|
||||
- `scripts/check-phase8.sh`
|
||||
- Human approval after independent security review reports zero open HIGH findings.
|
||||
- `go test ./wristband -run 'Test(Refresh|Replay|Sweep)' -count=1`
|
||||
- `cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestOAuth(Refresh|Replay|ConnectedApps|Revoke|Lifecycle|Surface)' -count=1`
|
||||
- `cd ../fonoteka.go && go test -race ./plugins/golem15/fonoteka/classes/auth ./plugins/golem15/fonoteka`
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- All 103 PHP methods map uniquely to named passing Go tests/subtests.
|
||||
- All T-08 threats have explicit dispositions and executable evidence; zero high-severity findings remain open.
|
||||
- Nyquist validation, parity, secret scan, and unchanged real-MCP lifecycle are green in the final gate.
|
||||
- The blocking human security checkpoint is approved.
|
||||
- Refresh rotation has exactly one usable successor and replay durably kills the entire lineage.
|
||||
- Connected-app output matches the UI contract and cannot disclose secrets or other users.
|
||||
- Revocation removes the app from the list and invalidates both access and refresh credentials before success is returned.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
||||
113
.planning/phases/08-oauth2-1-authorization-server/08-07-PLAN.md
Normal file
113
.planning/phases/08-oauth2-1-authorization-server/08-07-PLAN.md
Normal file
@@ -0,0 +1,113 @@
|
||||
---
|
||||
phase: 08-oauth2-1-authorization-server
|
||||
plan: 07
|
||||
type: execute
|
||||
wave: 7
|
||||
depends_on: [08-06]
|
||||
files_modified:
|
||||
- bonfire/command.go
|
||||
- bonfire/root.go
|
||||
- bonfire/output_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/console/oauth_client.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/console/oauth_client_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/plugin.go
|
||||
autonomous: true
|
||||
requirements: [AUTH-05, AUTH-07]
|
||||
must_haves:
|
||||
truths:
|
||||
- "D-19: Operators can create, update, and list OAuth clients with repeatable flags and one-time secret output."
|
||||
- "D-04: Command issuance stores only a hash and never leaks secret material through list/update output."
|
||||
artifacts:
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/console/oauth_client.go"
|
||||
provides: "Exact fonoteka:oauth-client command"
|
||||
- path: "bonfire/command.go"
|
||||
provides: "Typed repeatable string-slice flag contract"
|
||||
key_links:
|
||||
- from: "oauth_client.go"
|
||||
to: "wristband client issuance"
|
||||
via: "shared validation/hash/one-time-secret path"
|
||||
pattern: "wristband"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Provision confidential and ceiling-bounded OAuth clients through the exact app command.
|
||||
|
||||
Purpose: Deliver operator management separately from the personal-token MCP bootstrap surface.
|
||||
Output: Repeatable bonfire flags, client command, plugin registration, and command tests.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/jin/.codex/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jin/.codex/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-06-SUMMARY.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Specify repeatable flags and command output in executable RED</name>
|
||||
<files>bonfire/output_test.go, ../fonoteka.go/plugins/golem15/fonoteka/console/oauth_client_test.go</files>
|
||||
<behavior>
|
||||
- Repeated redirect/scope flags preserve order without breaking scalar/bare flags.
|
||||
- Create prints id, secret, warning once; update/list never reveal secret/hash.
|
||||
- Tests compile and fail only through separate `PHASE8_RED:bonfire-flags` and `PHASE8_RED:oauth-command` markers.
|
||||
</behavior>
|
||||
<action>D-18: and D-19: add real command-root tests for create/update/list, exact lines, one-time secret, scope ceiling, and non-recovery. Define compiling flag/command seams first; mark only missing bonfire behavior with `PHASE8_RED:bonfire-flags` and missing app-command behavior with `PHASE8_RED:oauth-command`. Use the shared verifier to reject syntax/setup/missing tests.</action>
|
||||
<verify>
|
||||
<automated>scripts/check-phase8-red.sh bonfire-flags go test ./bonfire -run 'Test.*Flag' -count=1 && scripts/check-phase8-red.sh oauth-command bash -lc "cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run TestOAuthClientCommand -count=1"</automated>
|
||||
</verify>
|
||||
<done>RED command tests execute and fail only for absent repeatable-flag/command behavior.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Add repeatable flags and exact OAuth client command</name>
|
||||
<files>bonfire/command.go, bonfire/root.go, bonfire/output_test.go, ../fonoteka.go/plugins/golem15/fonoteka/console/oauth_client.go, ../fonoteka.go/plugins/golem15/fonoteka/console/oauth_client_test.go, ../fonoteka.go/plugins/golem15/fonoteka/plugin.go</files>
|
||||
<behavior>
|
||||
- Flag/Input distinguish scalar and repeated values; existing callers remain compatible.
|
||||
- Create/update/list share wristband validation/issuance and artisan clients have null registration_ip.
|
||||
</behavior>
|
||||
<action>D-19: extend bonfire with explicit string-slice flags and `Input.Flags(name)`, using Cobra StringSlice only for that kind. Implement the exact name/redirect-uri/scope/auth-method/client-id/list signature thinly over wristband and ClientStore; never parse os.Args. Print the exact creation lines/warning and never recover or print secrets on list/update. Register through plugin command capability.</action>
|
||||
<verify>
|
||||
<automated>go test ./bonfire -run 'Test.*Flag' -count=1 && cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run TestOAuthClientCommand -count=1</automated>
|
||||
</verify>
|
||||
<done>Operators can safely provision and inspect OAuth clients with exact repeatable flags and no secret recovery.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Operator CLI → client store | Trusted input creates recoverable-once credentials. |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-08-SECRET-TIMING | Information Disclosure | issued client | mitigate | Shared hash/validation path and one-time secret. |
|
||||
| T-08-SCOPE-CEILING | Elevation | command | mitigate | Validated stored ceiling used by authorize. |
|
||||
| T-08-REQUEST-LEAK | Information Disclosure | output | mitigate | Exact positive output and secret/hash rejection tests. |
|
||||
| T-08-SC | Tampering | Cobra | mitigate | Existing pinned dependency only. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- Bonfire and command tests pass.
|
||||
- List/update output contains no client secret or hash.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Exact create/update/list command behavior is runnable and secret-safe.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/08-oauth2-1-authorization-server/08-07-SUMMARY.md` when done.
|
||||
</output>
|
||||
110
.planning/phases/08-oauth2-1-authorization-server/08-08-PLAN.md
Normal file
110
.planning/phases/08-oauth2-1-authorization-server/08-08-PLAN.md
Normal file
@@ -0,0 +1,110 @@
|
||||
---
|
||||
phase: 08-oauth2-1-authorization-server
|
||||
plan: 08
|
||||
type: execute
|
||||
wave: 7
|
||||
depends_on: [08-06]
|
||||
files_modified:
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/routes.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/oauth_tools_test.go
|
||||
autonomous: true
|
||||
requirements: [AUTH-07]
|
||||
must_haves:
|
||||
truths:
|
||||
- "D-20: The unchanged fonoteka-mcp receives exact scopes, collection_ids, user_id, and name from personal-token GET /me."
|
||||
- "D-12: Invalid personal tokens retain exact Invalid token bytes and no backend Bearer/resource-metadata challenge."
|
||||
artifacts:
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller.go"
|
||||
provides: "Minimal MCP bootstrap endpoint"
|
||||
key_links:
|
||||
- from: "me_token_controller.go"
|
||||
to: "bouncer.Credential"
|
||||
via: "reuse matched ApiToken without reparsing bearer input"
|
||||
pattern: "Credential"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Serve the exact personal-token bootstrap needed by the unchanged MCP process.
|
||||
|
||||
Purpose: Deliver the approved D-20 prerequisite as an isolated auth-surface slice that can execute in parallel with operator provisioning.
|
||||
Output: `/api/v1/fonoteka/me`, route isolation, and assembled tests.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/jin/.codex/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jin/.codex/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-UI-SPEC.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-06-SUMMARY.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Specify exact MCP bootstrap and token-surface behavior in RED</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller_test.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_tools_test.go</files>
|
||||
<behavior>
|
||||
- Valid read-scoped inv_ token returns exactly four fields; arrays are never null.
|
||||
- Missing/invalid/wrong-scope tokens preserve existing exact 401/403 bytes and headers.
|
||||
- Tests compile and fail only through `PHASE8_RED:mcp-me`.
|
||||
</behavior>
|
||||
<action>D-18 and D-20: use the assembled surf router and existing inv_token guard, not direct controller injection. Add exact positive/negative payload and route-isolation tests; mark only missing `/me` behavior with `PHASE8_RED:mcp-me` and reject syntax/setup/missing-test failures via the shared verifier.</action>
|
||||
<verify>
|
||||
<automated>scripts/check-phase8-red.sh mcp-me bash -lc "cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'Test(MeToken|TokenSurface|OAuthTools)' -count=1"</automated>
|
||||
</verify>
|
||||
<done>The assembled RED tests run through the real guard and fail only on absent `/me` behavior.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Mount exact personal-token MCP bootstrap</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller_test.go, ../fonoteka.go/plugins/golem15/fonoteka/routes.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_tools_test.go</files>
|
||||
<behavior>
|
||||
- Handler reads matched ApiToken and principal, emits only exact four fields, and performs no second lookup.
|
||||
- Route inherits inv_token, throttle, inv.scope:read in order and appears nowhere else.
|
||||
</behavior>
|
||||
<action>D-20: implement the exact handler using `bouncer.Credential` and `bouncer.User`, initialize arrays, preserve nullable name, and emit only locked fields through wire.WriteJSON. Mount GET `/me` in the existing personal-token group after its three middleware. D-12: leave invalid-token bytes/headers unchanged and add no RFC 9728 or Bearer challenge.</action>
|
||||
<verify>
|
||||
<automated>cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'Test(MeToken|TokenSurface|OAuthTools)' -count=1</automated>
|
||||
</verify>
|
||||
<done>The unchanged MCP process can bootstrap from an issued inv_ token without profile-surface expansion or header drift.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Bearer header → personal-token /me | Untrusted bearer input crosses existing token and scope guards. |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-08-SCOPE-CEILING | Elevation | /me | mitigate | Existing inv.scope:read middleware. |
|
||||
| T-08-REQUEST-LEAK | Information Disclosure | response | mitigate | Four-field positive allow-list. |
|
||||
| T-08-SURFACE | Elevation | routes | mitigate | Personal-token-only route-table proof. |
|
||||
| T-08-SC | Tampering | dependencies | mitigate | No install. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- Focused `/me` and token-surface tests pass.
|
||||
- Route table shows exact middleware order and no JWT/raw duplicate.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Real MCP bootstrap payload is exact and token failures remain unchanged.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/08-oauth2-1-authorization-server/08-08-SUMMARY.md` when done.
|
||||
</output>
|
||||
214
.planning/phases/08-oauth2-1-authorization-server/08-09-PLAN.md
Normal file
214
.planning/phases/08-oauth2-1-authorization-server/08-09-PLAN.md
Normal file
@@ -0,0 +1,214 @@
|
||||
---
|
||||
phase: 08-oauth2-1-authorization-server
|
||||
plan: 09
|
||||
type: execute
|
||||
wave: 8
|
||||
depends_on: [08-07, 08-08]
|
||||
files_modified:
|
||||
- ../fonoteka.go/parity/oauth_flow_test.go
|
||||
- ../fonoteka.go/parity/capture_clients.mjs
|
||||
- ../fonoteka.go/parity/capture-rules.yaml
|
||||
- ../fonoteka.go/parity/fixtures/mcp/mcp-lifecycle.yaml
|
||||
- ../fonoteka.go/parity/manifest.yaml
|
||||
- ../fonoteka.go/parity/check_corpus.go
|
||||
- scripts/check-phase8.sh
|
||||
autonomous: true
|
||||
requirements: [AUTH-05, AUTH-06, AUTH-07]
|
||||
must_haves:
|
||||
truths:
|
||||
- "D-13: All four raw OAuth routes and five JWT OAuth management routes replay their recorded PHP contracts against Go and count as ported only after passing."
|
||||
- "D-16: A clean recorded lifecycle proves DCR, authorize, consent, token, refresh, replay kill, list, revoke, post-revoke failure, deny, confidential Basic auth, and scope ceiling."
|
||||
- "D-14: The unchanged real fonoteka-mcp process completes discovery, DCR, PKCE, JWT consent, token bootstrap, an MCP tool call, and refresh against the Go backend."
|
||||
- "D-15: Live vendor connects remain excluded; automated DCR-plus-PKCE evidence is the Phase 8 acceptance boundary."
|
||||
artifacts:
|
||||
- path: "../fonoteka.go/parity/oauth_flow_test.go"
|
||||
provides: "Projected existing flows and full lifecycle replay"
|
||||
- path: "../fonoteka.go/parity/fixtures/mcp/mcp-lifecycle.yaml"
|
||||
provides: "Secret-scrubbed PHP lifecycle source-of-truth fixture"
|
||||
- path: "scripts/check-phase8.sh"
|
||||
provides: "Two-repository, parity, secret, Postgres, real-MCP phase gate"
|
||||
key_links:
|
||||
- from: "oauth_flow_test.go"
|
||||
to: "newConfiguredTarget"
|
||||
via: "replay through the assembled Go app and real Postgres"
|
||||
pattern: "newConfiguredTarget"
|
||||
- from: "scripts/check-phase8.sh"
|
||||
to: "/media/nvme/dev/golem15/fonoteka/fonoteka-mcp"
|
||||
via: "three configured URLs and scripted MCP SDK lifecycle"
|
||||
pattern: "FONOTEKA_(API_URL|MCP_PUBLIC_URL|MCP_AUTH_SERVER)"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Prove the completed server through recorded PHP parity and the unchanged real MCP client rather than only implementation-local tests.
|
||||
|
||||
Purpose: Turn exact route bytes, lifecycle security semantics, protected-resource discovery ownership, and actual SDK compatibility into one repeatable acceptance gate.
|
||||
Output: Lifecycle fixture/capture policy, projected replay tests, nine ported manifest entries, and `scripts/check-phase8.sh`.
|
||||
</objective>
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As an** unchanged MCP client, **I want to** complete the full OAuth lifecycle against Go exactly as I did against PHP, **so that** discovery, tool use, refresh, replay defense, and revocation are proven together.
|
||||
|
||||
<execution_context>
|
||||
@/home/jin/.codex/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jin/.codex/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-RESEARCH.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-07-SUMMARY.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-08-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
Existing parity seams:
|
||||
- `newConfiguredTarget(t, db)` boots the same assembled handler used by the app.
|
||||
- `tide.LoadFlow`, `tide.OpenStore`, and `tide.ReplayFlow` execute captured request/response sequences with private variables.
|
||||
- `parity/manifest.yaml` status becomes `ported` only when the selected replay subtest passes.
|
||||
|
||||
Unchanged MCP inputs:
|
||||
- `FONOTEKA_API_URL` points to the Go app personal-token API.
|
||||
- `FONOTEKA_MCP_PUBLIC_URL` points to the MCP resource server.
|
||||
- `FONOTEKA_MCP_AUTH_SERVER` points to the Go authorization server.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Specify recorded and real-client OAuth acceptance before changing fixtures</name>
|
||||
<files>../fonoteka.go/parity/oauth_flow_test.go, scripts/check-phase8.sh</files>
|
||||
<read_first>
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
|
||||
../fonoteka.go/parity/nuxt_flow_test.go
|
||||
../fonoteka.go/parity/parity_test.go
|
||||
../fonoteka.go/parity/fixtures/mcp/mcp-oauth.yaml
|
||||
../fonoteka.go/parity/fixtures/mcp/mcp-tools.yaml
|
||||
../fonoteka.go/parity/manifest.yaml
|
||||
../fonoteka.go/parity/capture_clients.mjs
|
||||
scripts/check-phase3.sh
|
||||
/media/nvme/dev/golem15/fonoteka/fonoteka-mcp/src/http.ts
|
||||
/media/nvme/dev/golem15/fonoteka/fonoteka-mcp/src/config.ts
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Existing broad flows are projected to named OAuth/MCP prerequisite steps and fail if an expected step disappears; unrelated later-phase calls are not replayed.
|
||||
- The clean lifecycle flow is required and every terminal security action is asserted before routes can be marked ported.
|
||||
- The gate starts real Postgres, Go app, and unchanged Node MCP; it verifies MCP-owned protected-resource metadata and Bearer hint separately from backend-owned metadata and Basic invalid-client challenge.
|
||||
</behavior>
|
||||
<action>D-12: distinguish backend Basic/no-challenge responses from MCP RFC 9728 behavior. D-13: project `mcp-oauth` and `mcp-tools` by stable named step IDs. D-14: declare real Postgres/app/MCP stages with all three environment variables. D-15: keep live vendor connects excluded. D-16: require the full clean lifecycle. D-18: create compiling failing parity tests and a fail-closed gate skeleton before changing fixtures/status. Use `PHASE8_RED:parity-gate` and the shared RED verifier so shell/Go syntax, missing tests, setup failures, and unrelated failures cannot satisfy RED. Plan 08-10 alone adds the security-review validation stage after the review exists. Do not edit or patch MCP or Nuxt.</action>
|
||||
<verify>
|
||||
<automated>scripts/check-phase8-red.sh parity-gate bash -lc "cd ../fonoteka.go && go test ./parity -run 'TestOAuthFlows' -count=1"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `oauth_flow_test.go` names projections for `mcp-oauth`, `mcp-tools`, and the complete `mcp-lifecycle`, and missing expected steps fail.
|
||||
- `scripts/check-phase8.sh` uses `set -euo pipefail`, fail-closed dependency/Docker checks, cleanup traps, and all three MCP environment variables.
|
||||
- The gate distinguishes MCP RFC 9728 metadata/rich Bearer challenge from backend exact Basic invalid-client challenge and unchanged token-surface 401.
|
||||
- Tests/gate fail because the lifecycle fixture/status/evidence is not yet complete, not because of shell or Go syntax errors.
|
||||
</acceptance_criteria>
|
||||
<done>The acceptance harness demands the exact recorded and real-client lifecycle before any route can be claimed ported.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Record, scrub, replay, and promote the complete OAuth lifecycle</name>
|
||||
<files>../fonoteka.go/parity/capture_clients.mjs, ../fonoteka.go/parity/capture-rules.yaml, ../fonoteka.go/parity/fixtures/mcp/mcp-lifecycle.yaml, ../fonoteka.go/parity/oauth_flow_test.go, ../fonoteka.go/parity/manifest.yaml, ../fonoteka.go/parity/check_corpus.go</files>
|
||||
<read_first>
|
||||
../fonoteka.go/parity/oauth_flow_test.go
|
||||
../fonoteka.go/parity/capture_clients.mjs
|
||||
../fonoteka.go/parity/capture-rules.yaml
|
||||
../fonoteka.go/parity/php_parity.sh
|
||||
../fonoteka.go/parity/fixtures/mcp/mcp-oauth.yaml
|
||||
../fonoteka.go/parity/fixtures/mcp/mcp-tools.yaml
|
||||
../fonoteka.go/parity/manifest.yaml
|
||||
tide/flow.go
|
||||
tide/replay.go
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Lifecycle records DCR → authorize → consent → token → refresh → spent-token replay → list → revoke → refresh failure → deny, plus confidential Basic and ceiling/invalid-scope cases.
|
||||
- Every request id, code, verifier, client secret, access token, and refresh token is represented only by a typed placeholder in committed fixtures; private vars are mode 0600.
|
||||
- Nine manifest entries become ported only after their exact route replay passes; pending never increments passing.
|
||||
</behavior>
|
||||
<action>D-16: extend the existing capture script/rules and use the Phase 2 isolated-PHP process to record the locked lifecycle. Issue the confidential client through `fonoteka:oauth-client`; exercise scope ceiling truncation and invalid-scope redirect with `client_secret_basic`. Capture all secret-bearing values with explicit pkce/token/credential categories into the private store, confirm both vars files are 0600, and commit only symbolic variable references. Add full and projected replays through `newConfiguredTarget` with real Postgres. After each of the four raw and five JWT route subtests passes, change only those manifest entries to `status: ported`; keep honest corpus accounting.</action>
|
||||
<verify>
|
||||
<automated>cd ../fonoteka.go && go test ./parity -run 'TestOAuthFlows|TestParityCorpus|TestParityContract' -count=1</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `mcp-lifecycle.yaml` contains the locked sequence and placeholder references, not recoverable credential values.
|
||||
- `go run ./parity/check_corpus.go --manifest parity/manifest.yaml --fixtures parity/fixtures --check-secrets` exits 0.
|
||||
- All nine OAuth manifest entries are `ported`; replay reports them passing with zero failing and does not count any pending route as passing.
|
||||
- Existing `mcp-oauth`/`mcp-tools` projections fail if a required named step is removed and ignore only explicitly enumerated later-phase steps.
|
||||
</acceptance_criteria>
|
||||
<done>The Go app passes the PHP-recorded OAuth route and lifecycle contracts without committing live secrets.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Complete the real unchanged-MCP phase gate</name>
|
||||
<files>scripts/check-phase8.sh</files>
|
||||
<read_first>
|
||||
scripts/check-phase8.sh
|
||||
scripts/check-phase3.sh
|
||||
../fonoteka.go/parity/oauth_flow_test.go
|
||||
/media/nvme/dev/golem15/fonoteka/fonoteka-mcp/package.json
|
||||
/media/nvme/dev/golem15/fonoteka/fonoteka-mcp/src/http.ts
|
||||
/media/nvme/dev/golem15/fonoteka/fonoteka-mcp/src/config.ts
|
||||
/media/nvme/dev/golem15/fonoteka/fonoteka-mcp/src/install.ts
|
||||
</read_first>
|
||||
<action>D-14: finish the executable pre-security gate using the existing gate family and installed Node MCP dependencies. Add `--core-smoke` for focused syntax/dependency/service lifecycle feedback and `--pre-security` for the complete wave-boundary gate. Allocate loopback ports, start disposable Postgres and the assembled Go app, start unchanged MCP with all three URLs, and drive SDK discovery/DCR/PKCE/login/consent/token, `/me`, tool, refresh, replay, and revoke. Verify RFC 9728 ownership separately from exact backend Basic/no-challenge responses. Run both modules' vet/test/race, parity/corpus/secret checks, `scripts/check-phase8-ui.mjs`, and unchanged Nuxt/MCP path diffs. Do not require `08-SECURITY-REVIEW.md` in either mode; Plan 08-10 adds the final fail-closed review stage. Preserve cleanup and never print secrets.</action>
|
||||
<verify>
|
||||
<automated>scripts/check-phase8.sh --core-smoke</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- The gate starts the real unchanged MCP checkout and completes metadata, DCR, PKCE, JWT consent, token, `/me`, one MCP tool call, and refresh against the Go backend.
|
||||
- The gate proves spent refresh replay and connected-app revoke kill the lineage and later access/refresh attempts fail.
|
||||
- Both repositories pass `go vet ./...`, `go test ./...`, and `go test -race ./...`; corpus and secret scans exit 0.
|
||||
- `git -C /media/nvme/dev/golem15/fonoteka/fonoteka-mcp status --short` and the Nuxt equivalent show no Phase 8 diff.
|
||||
</acceptance_criteria>
|
||||
<done>The actual connector stack, including RFC 9728 resource-server behavior, runs unchanged through the complete Go authorization lifecycle.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| PHP capture → committed fixtures | Live credentials must become typed placeholders before entering git. |
|
||||
| Scripted SDK → Go backend/MCP | External client parsing and redirects exercise public network-facing contracts. |
|
||||
| Gate process → child services | Secrets and cleanup state cross shell/Node/Go process boundaries. |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-08-PKCE | Spoofing/Elevation | real SDK flow | mitigate | Actual SDK S256 authorize/exchange plus wrong-verifier rejection in gate. |
|
||||
| T-08-CODE-REPLAY | Spoofing | lifecycle replay | mitigate | Recorded and real repeated code/spent state fail. |
|
||||
| T-08-REFRESH-REPLAY | Spoofing/Elevation | lifecycle replay/gate | mitigate | Spent replay kills lineage; post-replay DB/API evidence required. |
|
||||
| T-08-OPEN-REDIRECT | Spoofing/Disclosure | recorded authorize cases | mitigate | PHP fixture and Go replay assert local errors versus trusted ordered redirects. |
|
||||
| T-08-SECRET-TIMING | Information Disclosure | confidential Basic flow | mitigate | Actual confidential flow uses the constant-time implementation; final source audit in 08-06. |
|
||||
| T-08-SCOPE-CEILING | Elevation | confidential lifecycle | mitigate | Recorded ceiling truncation and invalid-scope redirect. |
|
||||
| T-08-CROSS-USER | Elevation | consent/list/revoke replay | mitigate | JWT ownership cases and indistinguishable 404 replay. |
|
||||
| T-08-REQUEST-LEAK | Information Disclosure | fixtures/logs | mitigate | Capture categories, 0600 stores, placeholder-only fixtures, check-secrets and quiet gate. |
|
||||
| T-08-DCR-FLOOD | Denial of Service | register route | mitigate | Recorded native errors plus focused rate/body/cap tests run by gate. |
|
||||
| T-08-SURFACE | Elevation | MCP/backend boundary | mitigate | Gate asserts correct RFC 9728 ownership and exact backend challenges. |
|
||||
| T-08-SC | Tampering | reused Node dependencies | mitigate | No install; use checked-in lockfile/node_modules and dependency preflight. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `cd ../fonoteka.go && go test ./parity -run 'TestOAuthFlows|TestParityCorpus|TestParityContract' -count=1`
|
||||
- `scripts/check-phase8.sh --pre-security` at the Wave 8 boundary; this mode proves the complete lifecycle but intentionally does not require the not-yet-created security review.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Nine OAuth routes and the full lifecycle replay pass against Go with no leaked fixture secret.
|
||||
- The real unchanged MCP discovers, authorizes, initializes, executes a tool, refreshes, and observes replay/revoke failure.
|
||||
- Resource-server and authorization-server header ownership is proven exactly, not conflated.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/08-oauth2-1-authorization-server/08-09-SUMMARY.md` when done.
|
||||
</output>
|
||||
228
.planning/phases/08-oauth2-1-authorization-server/08-10-PLAN.md
Normal file
228
.planning/phases/08-oauth2-1-authorization-server/08-10-PLAN.md
Normal file
@@ -0,0 +1,228 @@
|
||||
---
|
||||
phase: 08-oauth2-1-authorization-server
|
||||
plan: 10
|
||||
type: execute
|
||||
wave: 9
|
||||
depends_on: [08-09]
|
||||
files_modified:
|
||||
- wristband/phase08_coverage_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/phase08_coverage_test.go
|
||||
- ../fonoteka.go/parity/oauth_audit_test.go
|
||||
- scripts/check-phase8.sh
|
||||
- .planning/phases/08-oauth2-1-authorization-server/08-PHP-TEST-MAP.md
|
||||
- .planning/phases/08-oauth2-1-authorization-server/08-SECURITY-REVIEW.md
|
||||
- .planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
|
||||
autonomous: false
|
||||
requirements: [AUTH-05, AUTH-06, AUTH-07]
|
||||
must_haves:
|
||||
truths:
|
||||
- "D-18: Every PHP OAuth functional/security test method maps to a named passing Go test or subtest, and both repositories pass vet/test/race."
|
||||
- "D-04: Every T-08 threat maps to a failing-when-broken test with zero open high-severity findings."
|
||||
- "D-14: The final phase gate refuses missing tests, UI/route parity, secret scans, unchanged-client evidence, or an unverified security review."
|
||||
artifacts:
|
||||
- path: ".planning/phases/08-oauth2-1-authorization-server/08-PHP-TEST-MAP.md"
|
||||
provides: "Auditable one-to-one map of all 103 PHP methods to Go evidence"
|
||||
- path: ".planning/phases/08-oauth2-1-authorization-server/08-SECURITY-REVIEW.md"
|
||||
provides: "ASVS L1 threat disposition and executed evidence"
|
||||
- path: ".planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md"
|
||||
provides: "Nyquist-complete task/status and gate sign-off"
|
||||
key_links:
|
||||
- from: "08-SECURITY-REVIEW.md"
|
||||
to: "named Go tests"
|
||||
via: "file:TestName evidence for every mitigated threat"
|
||||
pattern: "T-08-"
|
||||
- from: "scripts/check-phase8.sh"
|
||||
to: "08-SECURITY-REVIEW.md"
|
||||
via: "fail-closed verified/zero-open audit check"
|
||||
pattern: "08-SECURITY-REVIEW"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Close Phase 8 with complete unit/security coverage, an independent security-review agent pass, and one fail-closed verification gate.
|
||||
|
||||
Purpose: Demonstrate that the exact OAuth implementation is not merely functional but resistant to every identified high-severity replay, redirect, timing, scope, ownership, leakage, flooding, and surface threat.
|
||||
Output: Coverage tests, 103-method audit map, verified security review, signed validation strategy, and final gate.
|
||||
</objective>
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** Płytarium operator, **I want to** rely on independently reviewed OAuth behavior and complete regression evidence, **so that** unchanged connectors can be enabled without accepting an unproven high-severity security risk.
|
||||
|
||||
<execution_context>
|
||||
@/home/jin/.codex/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jin/.codex/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-RESEARCH.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-09-SUMMARY.md
|
||||
@.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-SECURITY-REVIEW.md
|
||||
|
||||
<interfaces>
|
||||
Security-review evidence contract:
|
||||
- One register row per `T-08-*` threat with category, component, disposition, mitigation, and exact `file:TestName` evidence.
|
||||
- Frontmatter reports total/closed/open and status; completion requires `status: verified`, `threats_open: 0`, and no unmitigated HIGH.
|
||||
|
||||
PHP test inventory contract:
|
||||
- 103 methods: authorize 11, client-command 8, metadata 2, migration 7, register 10, token 10, consent/scope 7, refresh rotation 10, revocation 8, surface isolation 30.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Close the 103-method PHP audit and Phase 8 coverage gaps</name>
|
||||
<files>wristband/phase08_coverage_test.go, ../fonoteka.go/plugins/golem15/fonoteka/phase08_coverage_test.go, ../fonoteka.go/parity/oauth_audit_test.go, .planning/phases/08-oauth2-1-authorization-server/08-PHP-TEST-MAP.md</files>
|
||||
<read_first>
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
|
||||
wristband/registration_test.go
|
||||
wristband/authorize_test.go
|
||||
wristband/token_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/oauth_registration_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/oauth_connect_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/oauth_lifecycle_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/oauth_tools_test.go
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthAuthorizeTest.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthClientCommandTest.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthMetadataTest.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthMigrationTest.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthRegisterTest.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthTokenTest.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/security/OAuthConsentScopeCeilingTest.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/security/OAuthRefreshRotationTest.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/security/OAuthRevocationTest.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/security/TokenSurfaceIsolationTest.php
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Every PHP method is listed once with its source class, behavior, and a named Go test/subtest that actually runs.
|
||||
- Coverage tests exercise error branches, encoding failures, nil/misconfigured dependencies, clock/entropy errors, parser edges, and route/config drift not already covered.
|
||||
- Audit fails if a mapped Go test is renamed/missing or if any PHP method is unmapped/duplicated.
|
||||
</behavior>
|
||||
<action>D-18: enumerate all 103 PHP methods into `08-PHP-TEST-MAP.md`, map each to existing Phase 8 tests, and add focused coverage tests only where no named evidence exists. Add an executable audit that parses the inventory/map and Go test list so counts alone cannot hide missing or duplicate mappings. Close framework handler/store branches, app boot/config/route/controller/command branches, parity projections, UI harness invocation, and every exact response/header path. Do not replace behavior assertions with coverage-only calls or map one broad test to methods whose distinct assertions are absent.</action>
|
||||
<verify>
|
||||
<automated>go test ./wristband -count=1 && cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... ./parity -run 'Test(OAuth|Phase08|PHPTestMap|TokenSurface|MeToken)' -count=1</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- The map contains exactly 103 unique PHP method rows distributed 11/8/2/7/10/10/7/10/8/30 by source suite.
|
||||
- The executable audit confirms every mapped Go `TestName[/subtest]` exists and executes; missing or duplicate rows make it fail.
|
||||
- Both repositories pass full `go vet ./...`, `go test ./...`, and `go test -race ./...`, including nested plugin modules.
|
||||
- Coverage additions retain exact byte/header/concurrency assertions for security branches.
|
||||
</acceptance_criteria>
|
||||
<done>All PHP OAuth behavior has one-to-one named Go evidence and the phase's code paths are covered by meaningful regression tests.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Run the mandated security-review agent and close every high-severity finding</name>
|
||||
<files>.planning/phases/08-oauth2-1-authorization-server/08-SECURITY-REVIEW.md, .planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md, scripts/check-phase8.sh</files>
|
||||
<read_first>
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-RESEARCH.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-PHP-TEST-MAP.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
|
||||
.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-SECURITY-REVIEW.md
|
||||
scripts/check-phase8.sh
|
||||
wristband/server.go
|
||||
wristband/authorize.go
|
||||
wristband/token.go
|
||||
wristband/register.go
|
||||
wristband/crypto.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/connected_app_controller.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/routes.go
|
||||
</read_first>
|
||||
<action>D-04: invoke the `gsd-security-auditor` against all Phase 8 production/test changes and the locked threat map, requiring OWASP ASVS L1 review of every named T-08 threat and supply-chain status. Write `08-SECURITY-REVIEW.md` in the Phase 6 format with executed `file:TestName` evidence. If any HIGH exists, stop sign-off and create a targeted Phase 8 gap plan that explicitly declares the owning production and regression-test files; do not modify undeclared production files from this audit task. Execute that gap plan, rerun focused evidence, and re-run the auditor until no HIGH remains before resuming this task. Update VALIDATION task IDs/statuses and Nyquist flags only after evidence is green. Add the fail-closed review check to `check-phase8.sh`, but reserve the complete gate for Task 3.</action>
|
||||
<verify>
|
||||
<automated>scripts/check-phase8.sh --security-review-only</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `08-SECURITY-REVIEW.md` has `status: verified`, `threats_open: 0`, and one evidence-backed disposition for every named T-08 threat plus T-08-SC.
|
||||
- Every HIGH finding is mitigated by a named failing-when-broken test; no HIGH is accepted, deferred, or omitted.
|
||||
- Static evidence finds `crypto/subtle.ConstantTimeCompare` for client secret and PKCE, row locks for code/refresh, committed replay kill, 64 KiB register cap, raw/JWT/personal route isolation, and no sensitive-value logging.
|
||||
- `08-VALIDATION.md` maps final plan/task IDs, all required test/gate files exist, all statuses are green, and both Nyquist flags are true.
|
||||
- `scripts/check-phase8.sh` exits nonzero if the review is missing, unverified, has a nonzero open count, or lacks any required T-08 row.
|
||||
</acceptance_criteria>
|
||||
<done>An independent security agent has reviewed the implemented phase, all high-severity findings are closed with executable evidence, and the final gate enforces the review.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking-human">
|
||||
<name>Task 3: Approve the OAuth security and unchanged-client evidence</name>
|
||||
<files>.planning/phases/08-oauth2-1-authorization-server/08-SECURITY-REVIEW.md, .planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md</files>
|
||||
<read_first>
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-SECURITY-REVIEW.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-PHP-TEST-MAP.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-09-SUMMARY.md
|
||||
</read_first>
|
||||
<action>Present the completed automated evidence after the security-review agent has produced zero open high-severity findings. Do not ask the user to rerun automation; show the exact gate result, threat totals, 103-method audit result, nine-route parity result, real-MCP lifecycle result, and unchanged Nuxt/MCP worktree checks. Block completion if any displayed result is missing or non-green.</action>
|
||||
<verify>
|
||||
<automated>scripts/check-phase8.sh</automated>
|
||||
</verify>
|
||||
<what-built>Direct standard-library OAuth authorization server with DCR, S256 PKCE, JWT consent, authorization-code and rotating-refresh grants, connected-app revocation, operator client command, MCP bootstrap endpoint, PHP parity, and unchanged real-MCP proof.</what-built>
|
||||
<how-to-verify>
|
||||
1. Review `08-SECURITY-REVIEW.md`; expect `status: verified`, zero open threats, and named test evidence for every T-08 row.
|
||||
2. Review the recorded gate transcript; expect both repositories' vet/test/race, 103/103 PHP method map, nine OAuth route replays, secret scan, and real MCP lifecycle to be green.
|
||||
3. Confirm the Nuxt and fonoteka-mcp repositories have no Phase 8 source diff.
|
||||
</how-to-verify>
|
||||
<acceptance_criteria>
|
||||
- Human approval occurs only after zero open HIGH findings and a passing `scripts/check-phase8.sh` result are shown.
|
||||
- The evidence explicitly includes exact Basic `WWW-Authenticate` at backend invalid-client, unchanged no-challenge token 401, and MCP-owned rich Bearer/resource-metadata behavior.
|
||||
- Rejection includes the failing threat/test/gate identifier so remediation is deterministic.
|
||||
</acceptance_criteria>
|
||||
<resume-signal>Type "approved" to close Phase 8, or provide the failed threat/test/gate identifier.</resume-signal>
|
||||
<done>The user has accepted the complete automated OAuth compatibility and security evidence.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Phase implementation → independent auditor | Claims must be supported by executable evidence, not implementation intent. |
|
||||
| Test inventory → completion status | Missing/renamed tests or unmapped PHP methods must fail closed. |
|
||||
| Security report → phase gate | Stale, missing, or open findings must prevent sign-off. |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-08-PKCE | Spoofing/Elevation | authorize/exchange | mitigate | Independent audit plus missing/plain/wrong/syntax/constant-time tests. |
|
||||
| T-08-CODE-REPLAY | Spoofing | code transaction | mitigate | Sequential/concurrent single-winner tests and row-lock source evidence. |
|
||||
| T-08-REFRESH-REPLAY | Spoofing/Elevation | refresh transaction | mitigate | Branch-concurrency and post-error persisted lineage-kill evidence. |
|
||||
| T-08-OPEN-REDIRECT | Spoofing/Disclosure | redirect construction | mitigate | Exact allow-list/validation-order and no-Location tests. |
|
||||
| T-08-SECRET-TIMING | Information Disclosure | crypto/client auth | mitigate | `subtle.ConstantTimeCompare` source gate and invalid-secret behavior tests. |
|
||||
| T-08-SCOPE-CEILING | Elevation | authorize/consent/refresh | mitigate | End-to-end requested/submitted/ceiling/mintable and server-derived tenant proofs. |
|
||||
| T-08-CROSS-USER | Elevation | consent/connected apps | mitigate | Foreign ownership tests with indistinguishable 404s. |
|
||||
| T-08-REQUEST-LEAK | Information Disclosure | logs/fixtures/output | mitigate | Log capture, source scan, fixture secret scan, positive output allow-lists. |
|
||||
| T-08-DCR-FLOOD | Denial of Service | register | mitigate | 64 KiB cap, limiter, atomic client cap, sweep, concurrency evidence. |
|
||||
| T-08-SURFACE | Elevation | route/MCP boundary | mitigate | Assembled route table and real client header-ownership gate. |
|
||||
| T-08-SC | Tampering | package supply chain | mitigate | No added package; module-diff and package-audit checks. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `go vet ./... && go test ./... && go test -race ./...`
|
||||
- `cd ../fonoteka.go && go vet ./... && go test ./... && go test -race ./...`
|
||||
- `scripts/check-phase8.sh`
|
||||
- Human approval after independent security review reports zero open HIGH findings.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- All 103 PHP methods map uniquely to named passing Go tests/subtests.
|
||||
- All T-08 threats have explicit dispositions and executable evidence; zero high-severity findings remain open.
|
||||
- Nyquist validation, parity, secret scan, and unchanged real-MCP lifecycle are green in the final gate.
|
||||
- The blocking human security checkpoint is approved.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/08-oauth2-1-authorization-server/08-10-SUMMARY.md` when done.
|
||||
</output>
|
||||
@@ -39,12 +39,12 @@ Out of scope: social login (`/oauth/{provider}`, `oauth-identities` routes, Phas
|
||||
- **D-15:** No live vendor connect (Claude, ChatGPT, Grok) is part of acceptance; the automated gates cover the same DCR-plus-PKCE chain those apps use. The first real vendor connect happens at cutover.
|
||||
|
||||
### Parity corpus and lifecycle
|
||||
- **D-16 (corpus):** A second PHP flow, `mcp-lifecycle`, is recorded against the isolated PHP instance with the Phase 2 `tide` tooling (capture rules, private 0600 vars store, `pkce.vars`, no live secrets in git): DCR → authorize → consent → token → refresh → replay of the spent refresh token (`invalid_grant`, lineage dead) → connected-apps list showing the app → revoke → refresh after revoke fails → a deny path; plus a confidential client issued by `fonoteka:oauth-client` with a scope ceiling using `client_secret_basic`, showing ceiling truncation and the `invalid_scope` redirect. The four RFC route entries and five JWT-group entries in `parity/manifest.yaml` flip pending → ported through the usual gate; pending never counts as passing.
|
||||
- **D-17 (sweep):** Wristband adds an expiry sweep that PHP lacks, run where PHP runs its client sweep (`/register`) and also on `/token`, deleting only rows past `expires_at`: pending requests, codes (used or not) and refresh rows. Revoked or rotated rows that have not expired stay, so replay detection and connected-apps semantics match PHP. No timer, no goroutine; Phase 11 may move it into a River job. It is unobservable in recorded replays because nothing expires within a run; the schema-diff and db-capture harness must not be affected.
|
||||
- **D-18 (tests):** All PHP OAuth tests are ported (functional: OAuthAuthorizeTest, OAuthClientCommandTest, OAuthMetadataTest, OAuthMigrationTest, OAuthRegisterTest, OAuthTokenTest; security: OAuthConsentScopeCeilingTest, OAuthRefreshRotationTest, OAuthRevocationTest, TokenSurfaceIsolationTest). Framework behaviour tests run on wristband with the in-memory store; app tests run on real Postgres through the existing `classes` TestMain harness, and route-surface isolation tests inspect the surf route table as Phase 6 did. Each PHP test method maps to a named Go test so coverage can be audited.
|
||||
- **D-19 (command):** `fonoteka:oauth-client` is ported in `fonoteka.go` as a bonfire command scaffolded the Phase 4 way with the same signature (`name`, `--redirect-uri=*`, `--scope=*`, `--auth-method`, `--client-id`, `--list`) and the same output lines (`client_id=`, `client_secret=` printed once, the non-recoverable warning, `--list` never printing a secret). It is thin over wristband's client issuing helper and the app `ClientStore`.
|
||||
- **D-20 (MCP prerequisite):** Phase 8 ports the minimal exact `GET /api/v1/fonoteka/me` personal-token endpoint required by `fonoteka-mcp/src/http.ts` before it constructs the MCP server. The endpoint authenticates through the existing `inv_token` surface and implements only the response contract needed by the unchanged MCP client. This prerequisite is in scope solely to preserve D-14's real MCP tool-call gate; broader user/profile API work remains deferred.
|
||||
- **D-21 (DCR body bound):** `POST /oauth/mcp/register` accepts at most 64 KiB of JSON request body. An oversized document returns the endpoint's normal `invalid_client_metadata` response rather than a house envelope or generic HTML error. The bound is enforced before unbounded JSON decoding and is covered by the `T-08-DCR-FLOOD` failing-when-broken test.
|
||||
- **D-16:** (corpus) A second PHP flow, `mcp-lifecycle`, is recorded against the isolated PHP instance with the Phase 2 `tide` tooling (capture rules, private 0600 vars store, `pkce.vars`, no live secrets in git): DCR → authorize → consent → token → refresh → replay of the spent refresh token (`invalid_grant`, lineage dead) → connected-apps list showing the app → revoke → refresh after revoke fails → a deny path; plus a confidential client issued by `fonoteka:oauth-client` with a scope ceiling using `client_secret_basic`, showing ceiling truncation and the `invalid_scope` redirect. The four RFC route entries and five JWT-group entries in `parity/manifest.yaml` flip pending → ported through the usual gate; pending never counts as passing.
|
||||
- **D-17:** (sweep) Wristband adds an expiry sweep that PHP lacks, run where PHP runs its client sweep (`/register`) and also on `/token`, deleting only rows past `expires_at`: pending requests, codes (used or not) and refresh rows. Revoked or rotated rows that have not expired stay, so replay detection and connected-apps semantics match PHP. No timer, no goroutine; Phase 11 may move it into a River job. It is unobservable in recorded replays because nothing expires within a run; the schema-diff and db-capture harness must not be affected.
|
||||
- **D-18:** (tests) All PHP OAuth tests are ported (functional: OAuthAuthorizeTest, OAuthClientCommandTest, OAuthMetadataTest, OAuthMigrationTest, OAuthRegisterTest, OAuthTokenTest; security: OAuthConsentScopeCeilingTest, OAuthRefreshRotationTest, OAuthRevocationTest, TokenSurfaceIsolationTest). Framework behaviour tests run on wristband with the in-memory store; app tests run on real Postgres through the existing `classes` TestMain harness, and route-surface isolation tests inspect the surf route table as Phase 6 did. Each PHP test method maps to a named Go test so coverage can be audited.
|
||||
- **D-19:** (command) `fonoteka:oauth-client` is ported in `fonoteka.go` as a bonfire command scaffolded the Phase 4 way with the same signature (`name`, `--redirect-uri=*`, `--scope=*`, `--auth-method`, `--client-id`, `--list`) and the same output lines (`client_id=`, `client_secret=` printed once, the non-recoverable warning, `--list` never printing a secret). It is thin over wristband's client issuing helper and the app `ClientStore`.
|
||||
- **D-20:** (MCP prerequisite) Phase 8 ports the minimal exact `GET /api/v1/fonoteka/me` personal-token endpoint required by `fonoteka-mcp/src/http.ts` before it constructs the MCP server. The endpoint authenticates through the existing `inv_token` surface and implements only the response contract needed by the unchanged MCP client. This prerequisite is in scope solely to preserve D-14's real MCP tool-call gate; broader user/profile API work remains deferred.
|
||||
- **D-21:** (DCR body bound) `POST /oauth/mcp/register` accepts at most 64 KiB of JSON request body. An oversized document returns the endpoint's normal `invalid_client_metadata` response rather than a house envelope or generic HTML error. The bound is enforced before unbounded JSON decoding and is covered by the `T-08-DCR-FLOOD` failing-when-broken test.
|
||||
|
||||
### Claude's Discretion
|
||||
- Interface names and signatures in wristband, the transaction seam, in-memory store design, and where shared helpers (base64url, sha256 hex, constant-time compare) live.
|
||||
|
||||
@@ -4,56 +4,56 @@ All required GOAL, REQ, RESEARCH, CONTEXT, VALIDATION, and UI-SPEC items are pla
|
||||
|
||||
| Source | ID | Feature / requirement | Plan | Status | Notes |
|
||||
|--------|----|-----------------------|------|--------|-------|
|
||||
| GOAL | — | PHP-compatible authorization server serves unchanged MCP/connector with exact header ownership | 01-06 | COVERED | Direct standard-library `wristband` per locked D-01; real-client proof in 05. |
|
||||
| REQ | AUTH-05 | Metadata, DCR, S256 authorize/consent, code/refresh grants, resource handling, exact discovery/challenges | 01-05 | COVERED | Backend Basic challenge and MCP RFC 9728 ownership are tested separately. |
|
||||
| REQ | AUTH-06 | Form/query/JSON source rules, CSRF-free raw routes, rate limits, unwrapped responses, cache headers | 01-03, 05-06 | COVERED | Route-table, byte, header, parity, and final audit coverage. |
|
||||
| REQ | AUTH-07 | Persistent OAuth models, connected-app list/revoke, unchanged MCP install/auth/tool flow | 01, 03-06 | COVERED | Includes schema correction, `/me`, lifecycle, and real MCP. |
|
||||
| RESEARCH | R-01 | Additive nullability/index migration and pointer models | 01 | COVERED | Safe rollback refusal is explicit. |
|
||||
| GOAL | — | PHP-compatible authorization server serves unchanged MCP/connector with exact header ownership | 01-10 | COVERED | Direct standard-library `wristband` per locked D-01; real-client proof in 09. |
|
||||
| REQ | AUTH-05 | Metadata, DCR, S256 authorize/consent, code/refresh grants, resource handling, exact discovery/challenges | 01, 03-07, 09-10 | COVERED | Backend Basic challenge and MCP RFC 9728 ownership are tested separately. |
|
||||
| REQ | AUTH-06 | Form/query/JSON source rules, CSRF-free raw routes, rate limits, unwrapped responses, cache headers | 01, 03-05, 09-10 | COVERED | Route-table, byte, header, parity, and final audit coverage. |
|
||||
| REQ | AUTH-07 | Persistent OAuth models, connected-app list/revoke, unchanged MCP install/auth/tool flow | 02-03, 05-10 | COVERED | Includes schema correction, `/me`, lifecycle, and real MCP. |
|
||||
| RESEARCH | R-01 | Additive nullability/index migration and pointer models | 02 | COVERED | Safe rollback refusal is explicit. |
|
||||
| RESEARCH | R-02 | App-agnostic transaction-scoped store bundle with GORM row locks in app tier | 01-03 | COVERED | Framework never imports GORM/fonoteka. |
|
||||
| RESEARCH | R-03 | Commit refresh replay lineage kill before returning `invalid_grant` | 03, 06 | COVERED | Persisted post-error evidence and concurrency tests. |
|
||||
| RESEARCH | R-04 | Ordered RFC3986 redirects and endpoint-specific parsers | 02, 05-06 | COVERED | Exact bytes/parity. |
|
||||
| RESEARCH | R-05 | No new package; standard-library crypto/HTTP and package-legitimacy audit not applicable | 01-06 | COVERED | T-08-SC included in every threat model. |
|
||||
| RESEARCH | R-06 | 103 PHP-method audit and complete validation architecture | 06 | COVERED | Exact distribution and executable missing-name gate. |
|
||||
| RESEARCH | R-07 | Real MCP `/me` prerequisite and 64 KiB DCR bound | 01, 04-06 | COVERED | Both resolved questions are locked as D-20/D-21. |
|
||||
| RESEARCH | R-03 | Commit refresh replay lineage kill before returning `invalid_grant` | 06, 10 | COVERED | Persisted post-error evidence and concurrency tests. |
|
||||
| RESEARCH | R-04 | Ordered RFC3986 redirects and endpoint-specific parsers | 04-05, 09-10 | COVERED | Exact bytes/parity. |
|
||||
| RESEARCH | R-05 | No new package; standard-library crypto/HTTP and package-legitimacy audit not applicable | 01-10 | COVERED | T-08-SC included in every threat model. |
|
||||
| RESEARCH | R-06 | 103 PHP-method audit and complete validation architecture | 10 | COVERED | Exact distribution and executable missing-name gate. |
|
||||
| RESEARCH | R-07 | Real MCP `/me` prerequisite and 64 KiB DCR bound | 01, 08-10 | COVERED | Both resolved questions are locked as D-20/D-21. |
|
||||
| CONTEXT | D-01 | Direct stdlib port; no zitadel/oidc; correct roadmap/requirement wording | 01, planning update | COVERED | No dependency install. |
|
||||
| CONTEXT | D-02 | Query/form/JSON parameter sources | 01-02, 05-06 | COVERED | JSON token rejection, ParseForm precedence, JSON-only register. |
|
||||
| CONTEXT | D-02 | Query/form/JSON parameter sources | 01, 04, 09-10 | COVERED | JSON token rejection, ParseForm precedence, JSON-only register. |
|
||||
| CONTEXT | D-03 | TTLs, caps, issuer/resource/consent configuration | 01-03 | COVERED | Exact PHP defaults in plan 01. |
|
||||
| CONTEXT | D-04 | Full T-08 security treatment and constant-time comparisons | 01-06 | COVERED | Independent security agent and blocking approval in 06. |
|
||||
| CONTEXT | D-04 | Full T-08 security treatment and constant-time comparisons | 01-10 | COVERED | Independent security agent and blocking approval in 10. |
|
||||
| CONTEXT | D-05 | `wristband` owns RFC surface/state machine | 01-03 | COVERED | Framework structure and import boundary explicit. |
|
||||
| CONTEXT | D-06 | PHP-minimal response shapes are defaults; no hooks | 01-03, 05 | COVERED | Exact response/header tests and parity. |
|
||||
| CONTEXT | D-07 | App stores, issuer, transaction boundary, row locks | 01-03 | COVERED | Real Postgres concurrency tests. |
|
||||
| CONTEXT | D-08 | App owns consent and connected apps | 02-03 | COVERED | Exact UI payloads and ownership. |
|
||||
| CONTEXT | D-09 | Raw routes and per-route token/register throttles | 01-02, 06 | COVERED | Route-table inspection. |
|
||||
| CONTEXT | D-10 | Retire reserved oauth guard; access stays `inv_token` | 01-04, 06 | COVERED | Negative guard/source tests. |
|
||||
| CONTEXT | D-11 | Preserve configured `inv_` prefix | 02, 04-05 | COVERED | Actual MCP install/HTTP consumption. |
|
||||
| CONTEXT | D-12 | Backend Basic challenge; MCP owns rich Bearer/resource metadata | 01-06 | COVERED | Unit, route, and real-process evidence. |
|
||||
| CONTEXT | D-13 | Replay projected MCP flows in Go tests | 05 | COVERED | Stable named-step projections fail on disappearance. |
|
||||
| CONTEXT | D-14 | Full real Node MCP lifecycle gate | 05-06 | COVERED | Includes discovery, DCR, PKCE, login/consent, `/me`, tool, refresh. |
|
||||
| CONTEXT | D-08 | App owns consent and connected apps | 05-06 | COVERED | Exact UI payloads and ownership. |
|
||||
| CONTEXT | D-09 | Raw routes and per-route token/register throttles | 03, 05, 10 | COVERED | Route-table inspection. |
|
||||
| CONTEXT | D-10 | Retire reserved oauth guard; access stays `inv_token` | 03-05, 08, 10 | COVERED | Negative guard/source tests. |
|
||||
| CONTEXT | D-11 | Preserve configured `inv_` prefix | 04, 08-09 | COVERED | Actual MCP install/HTTP consumption. |
|
||||
| CONTEXT | D-12 | Backend Basic challenge; MCP owns rich Bearer/resource metadata | 03-05, 08-10 | COVERED | Unit, route, and real-process evidence. |
|
||||
| CONTEXT | D-13 | Replay projected MCP flows in Go tests | 09 | COVERED | Stable named-step projections fail on disappearance. |
|
||||
| CONTEXT | D-14 | Full real Node MCP lifecycle gate | 09-10 | COVERED | Includes discovery, DCR, PKCE, login/consent, `/me`, tool, refresh. |
|
||||
| CONTEXT | D-15 | No live vendor connection in Phase 8 | — | EXCLUDED | Deferred to cutover by explicit decision. |
|
||||
| CONTEXT | D-16 | Record clean `mcp-lifecycle`; nine routes ported honestly | 05 | COVERED | Secret-scrubbed fixture and corpus audit. |
|
||||
| CONTEXT | D-17 | On-request expiry sweep, expired rows only | 01, 03 | COVERED | No timer/goroutine; replay evidence retained. |
|
||||
| CONTEXT | D-18 | Port every named PHP OAuth test | 01-06 | COVERED | Final one-to-one 103-method map. |
|
||||
| CONTEXT | D-19 | Exact app-side `fonoteka:oauth-client` | 04 | COVERED | Repeatable bonfire flags and one-time secret. |
|
||||
| CONTEXT | D-20 | Exact personal-token `/me` MCP prerequisite | 04-05 | COVERED | Existing guard/middleware and positive allow-list. |
|
||||
| CONTEXT | D-21 | Register body bounded at 64 KiB with native error | 01, 06 | COVERED | Bound precedes JSON decode. |
|
||||
| VALIDATION | W0-01 | Framework metadata/authorize/token/register/PKCE/refresh tests | 01-03, 06 | COVERED | Fast in-memory tests plus audit. |
|
||||
| VALIDATION | W0-02 | Real-Postgres migration/store locking/replay/sweep tests | 01-03, 06 | COVERED | Existing auth TestMain harness. |
|
||||
| VALIDATION | W0-03 | Raw routing/parser/rate/body/header isolation | 01-02, 06 | COVERED | Assembled route tests. |
|
||||
| VALIDATION | W0-04 | Consent/collection/connected-app ownership | 02-03, 06 | COVERED | Real-Postgres controllers. |
|
||||
| VALIDATION | W0-05 | Nine routes, lifecycle replay, 103-method map | 05-06 | COVERED | Corpus/fixture/map gates. |
|
||||
| VALIDATION | W0-06 | Personal-token `/me` | 04-06 | COVERED | MCP startup prerequisite. |
|
||||
| VALIDATION | W0-07 | Full unchanged MCP and security gate | 05-06 | COVERED | Final script plus review checkpoint. |
|
||||
| UI-SPEC | UI-01 | Nuxt remains unchanged | 02-06 | COVERED | Git status checks and backend-only files. |
|
||||
| UI-SPEC | UI-02 | Consent read/allow/deny states and exact payload/status/redirect semantics | 02, 05-06 | COVERED | Includes stale/foreign/used 404 and empty-scope 422. |
|
||||
| UI-SPEC | UI-03 | Connected-app empty/populated/error/list/revoke contracts | 03, 05-06 | COVERED | Positive allow-list, manual count, identical 404. |
|
||||
| UI-SPEC | UI-04 | Untrusted names/hosts, scope order, server-derived collection, no secrets | 02-03, 06 | COVERED | Sanitization, host-only, intersection, output audits. |
|
||||
| UI-SPEC | UI-05 | Existing accessibility/responsive/i18n behavior is preserved | 02-03, 05 | COVERED | No frontend changes; exact data/state selectors exercised. |
|
||||
| CONTEXT | D-16 | Record clean `mcp-lifecycle`; nine routes ported honestly | 09 | COVERED | Secret-scrubbed fixture and corpus audit. |
|
||||
| CONTEXT | D-17 | On-request expiry sweep, expired rows only | 01-02, 04, 06 | COVERED | No timer/goroutine; replay evidence retained. |
|
||||
| CONTEXT | D-18 | Port every named PHP OAuth test | 01-10 | COVERED | Final one-to-one 103-method map. |
|
||||
| CONTEXT | D-19 | Exact app-side `fonoteka:oauth-client` | 07 | COVERED | Repeatable bonfire flags and one-time secret. |
|
||||
| CONTEXT | D-20 | Exact personal-token `/me` MCP prerequisite | 08-09 | COVERED | Existing guard/middleware and positive allow-list. |
|
||||
| CONTEXT | D-21 | Register body bounded at 64 KiB with native error | 01, 10 | COVERED | Bound precedes JSON decode. |
|
||||
| VALIDATION | W0-01 | Framework metadata/authorize/token/register/PKCE/refresh tests | 01, 04, 06, 10 | COVERED | Fast in-memory tests plus audit. |
|
||||
| VALIDATION | W0-02 | Real-Postgres migration/store locking/replay/sweep tests | 02, 04, 06, 10 | COVERED | Existing auth TestMain harness. |
|
||||
| VALIDATION | W0-03 | Raw routing/parser/rate/body/header isolation | 03-05, 10 | COVERED | Assembled route tests. |
|
||||
| VALIDATION | W0-04 | Consent/collection/connected-app ownership | 05-06, 10 | COVERED | Real-Postgres controllers. |
|
||||
| VALIDATION | W0-05 | Nine routes, lifecycle replay, 103-method map | 09-10 | COVERED | Corpus/fixture/map gates. |
|
||||
| VALIDATION | W0-06 | Personal-token `/me` | 08-09 | COVERED | MCP startup prerequisite. |
|
||||
| VALIDATION | W0-07 | Full unchanged MCP and security gate | 09-10 | COVERED | Final script plus review checkpoint. |
|
||||
| UI-SPEC | UI-01 | Nuxt remains unchanged | 05-10 | COVERED | Read-only harness and scoped path-diff checks. |
|
||||
| UI-SPEC | UI-02 | Consent read/allow/deny states and exact payload/status/redirect semantics | 05, 09-10 | COVERED | Includes invalid-handle no-request, safe login return, stale/foreign/used 404, and empty-scope 422. |
|
||||
| UI-SPEC | UI-03 | Connected-app empty/populated/error/list/revoke contracts | 05-06, 09-10 | COVERED | Browser matrix plus positive allow-list, manual count, identical 404. |
|
||||
| UI-SPEC | UI-04 | Untrusted names/hosts, scope order, server-derived collection, no secrets | 05-06, 10 | COVERED | Sanitization, host-only, intersection, output audits. |
|
||||
| UI-SPEC | UI-05 | Existing accessibility/responsive/i18n behavior is preserved | 05, 09-10 | COVERED | Existing return/i18n scripts plus read-only Playwright keyboard/focus/44px/mobile matrix. |
|
||||
|
||||
## Deferred and Out-of-Scope Audit
|
||||
|
||||
- Summer-themed token prefix: excluded; `inv_` remains locked.
|
||||
- River expiry job: excluded; request-time sweep ships here and River remains Phase 11.
|
||||
- Generic framework client command: excluded; app command ships in Plan 04.
|
||||
- Generic framework client command: excluded; app command ships in Plan 07.
|
||||
- Live Claude/ChatGPT/Grok connection: excluded; scripted SDK plus unchanged MCP is the Phase 8 acceptance gate.
|
||||
- Social login and oauth-identities: excluded; no plan creates those routes.
|
||||
- Frontend redesign/new UI: excluded; Nuxt must remain unchanged.
|
||||
|
||||
@@ -39,13 +39,14 @@ created: 2026-09-23
|
||||
|
||||
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
||||
| 08-W0-01 | 08-01, 08-02, 08-03, 08-06 | 1-3, 6 | AUTH-05 | T-08-PKCE / T-08-CODE-REPLAY | Metadata, authorize, PKCE S256, code exchange, refresh, DCR, and ordered redirects have deterministic framework tests | unit | `go test ./wristband -run 'Test(Metadata|Authorize|Token|Register|PKCE|Refresh)' -count=1` | ❌ W0 | ⬜ pending |
|
||||
| 08-W0-02 | 08-01, 08-03, 08-06 | 1, 3, 6 | AUTH-05, AUTH-07 | T-08-CODE-REPLAY / T-08-REFRESH-REPLAY | Nullability, row locks, single-use codes, committed lineage kill, sweeps, and indexes work on real Postgres | integration | `cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/classes/... -run 'TestOAuth' -count=1` | ❌ W0 | ⬜ pending |
|
||||
| 08-W0-03 | 08-01, 08-02, 08-06 | 1, 2, 6 | AUTH-06 | T-08-DCR-FLOOD / T-08-SURFACE | Raw routing, parser rules, rate limits, 64 KiB DCR bound, exact bare bodies, and headers remain isolated from house middleware | route/integration | `cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestOAuth' -count=1` | ❌ W0 | ⬜ pending |
|
||||
| 08-W0-04 | 08-02, 08-03, 08-06 | 2, 3, 6 | AUTH-07 | T-08-SCOPE-CEILING / T-08-CROSS-USER | Consent, active-collection binding, connected-app ownership, list, and revoke semantics match PHP | Postgres integration | `cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/controllers/... -run 'TestOAuth' -count=1` | ❌ W0 | ⬜ pending |
|
||||
| 08-W0-05 | 08-05, 08-06 | 5, 6 | AUTH-05, AUTH-06, AUTH-07 | T-08-REQUEST-LEAK / T-08-SURFACE | Nine manifest routes plus `mcp-lifecycle` replay exactly and every one of 103 PHP OAuth/security methods maps to a named Go test | parity/corpus | `cd ../fonoteka.go && go test ./parity -run 'TestOAuthFlows|TestParityCorpus' -count=1` | ❌ W0 | ⬜ pending |
|
||||
| 08-W0-06 | 08-04, 08-05 | 4, 5 | AUTH-07 | T-08-SURFACE | Exact authenticated `/api/v1/fonoteka/me` lets the unchanged MCP process initialize without expanding the profile API surface | integration/e2e | `cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestMe|TestTokenSurface' -count=1` | ❌ W0 | ⬜ pending |
|
||||
| 08-W0-07 | 08-05, 08-06 | 5, 6 | AUTH-05, AUTH-07 | All T-08 threats | Real SDK discovery, DCR, PKCE, JWT consent, token, MCP tool call, refresh/replay, connected-app revoke, and post-revoke failure complete unchanged | e2e | `scripts/check-phase8.sh` | ❌ W0 | ⬜ pending |
|
||||
| 08-W0-01 | 08-01, 08-04, 08-06, 08-10 | 1, 4, 6, 9 | AUTH-05 | T-08-PKCE / T-08-CODE-REPLAY | Metadata, authorize, PKCE S256, code exchange, refresh, DCR, and ordered redirects have deterministic framework tests | unit | `go test ./wristband -run 'Test(Metadata|Authorize|Token|Register|PKCE|Refresh)' -count=1` | ❌ W0 | ⬜ pending |
|
||||
| 08-W0-02 | 08-02, 08-04, 08-06, 08-10 | 2, 4, 6, 9 | AUTH-05, AUTH-07 | T-08-CODE-REPLAY / T-08-REFRESH-REPLAY | Nullability, row locks, single-use codes, committed lineage kill, sweeps, and indexes work on real Postgres | integration | `cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/classes/... -run 'TestOAuth' -count=1` | ❌ W0 | ⬜ pending |
|
||||
| 08-W0-03 | 08-03, 08-04, 08-05, 08-10 | 3-5, 9 | AUTH-06 | T-08-DCR-FLOOD / T-08-SURFACE | Raw routing, parser rules, rate limits, 64 KiB DCR bound, exact bare bodies, and headers remain isolated from house middleware | route/integration | `cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestOAuth' -count=1` | ❌ W0 | ⬜ pending |
|
||||
| 08-W0-04 | 08-05, 08-06, 08-10 | 5-6, 9 | AUTH-07 | T-08-SCOPE-CEILING / T-08-CROSS-USER | Consent, active-collection binding, connected-app ownership, list, and revoke semantics match PHP | Postgres integration | `cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/controllers/... -run 'TestOAuth' -count=1` | ❌ W0 | ⬜ pending |
|
||||
| 08-W0-05 | 08-09, 08-10 | 8-9 | AUTH-05, AUTH-06, AUTH-07 | T-08-REQUEST-LEAK / T-08-SURFACE | Nine manifest routes plus `mcp-lifecycle` replay exactly and every one of 103 PHP OAuth/security methods maps to a named Go test | parity/corpus | `cd ../fonoteka.go && go test ./parity -run 'TestOAuthFlows|TestParityCorpus' -count=1` | ❌ W0 | ⬜ pending |
|
||||
| 08-W0-06 | 08-08, 08-09 | 7-8 | AUTH-07 | T-08-SURFACE | Exact authenticated `/api/v1/fonoteka/me` lets the unchanged MCP process initialize without expanding the profile API surface | integration/e2e | `cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestMe|TestTokenSurface' -count=1` | ❌ W0 | ⬜ pending |
|
||||
| 08-W0-07 | 08-09, 08-10 | 8-9 | AUTH-05, AUTH-07 | All T-08 threats | Real SDK discovery, DCR, PKCE, JWT consent, token, MCP tool call, refresh/replay, connected-app revoke, and post-revoke failure complete unchanged | e2e | `scripts/check-phase8.sh` | ❌ W0 | ⬜ pending |
|
||||
| 08-W0-08 | 08-05, 08-09, 08-10 | 5, 8-9 | AUTH-05, AUTH-07 | T-08-CROSS-USER / T-08-SURFACE | Invalid-handle no-request, safe login return, consent/connected-app state matrices, accessibility, mobile, and en/pl copy remain intact without Nuxt changes | browser/contract | `node scripts/check-phase8-ui.mjs --focused` plus existing Nuxt `verify:oauth-return-path` and `verify:oauth-i18n` | ❌ W0 | ⬜ pending |
|
||||
|
||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
||||
|
||||
@@ -60,6 +61,8 @@ created: 2026-09-23
|
||||
- [ ] `../fonoteka.go/plugins/golem15/fonoteka/controllers/api/*me*_test.go` — minimal `inv_token`-authenticated MCP bootstrap contract.
|
||||
- [ ] `../fonoteka.go/parity/oauth_flow_test.go` and `mcp-lifecycle` fixture — projected existing flows and clean lifecycle/replay coverage.
|
||||
- [ ] `scripts/check-phase8.sh` — two-repository vet/test/race, corpus, secret, security-review, and real-MCP gate.
|
||||
- [ ] `scripts/check-phase8-ui.mjs` — read-only unchanged-Nuxt state, accessibility, return-path, i18n, and responsive contract gate.
|
||||
- [ ] `scripts/check-phase8-red.sh` — compiling RED verifier that rejects syntax/setup/missing-test/unrelated failures.
|
||||
- [ ] `08-SECURITY-REVIEW.md` — map every `T-08-*` threat to a failing-when-broken test and close all high-severity threats.
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user