Files
summercms/.planning/phases/08-oauth2-1-authorization-server/08-09-PLAN.md
2026-09-23 17:13:47 +02:00

215 lines
14 KiB
Markdown

---
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 &amp;&amp; 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 &amp;&amp; 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>