fix(08): revise plans based on checker feedback
This commit is contained in:
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>
|
||||
Reference in New Issue
Block a user