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.
path
provides
../fonoteka.go/parity/oauth_flow_test.go
Projected existing flows and full lifecycle replay
replay through the assembled Go app and real Postgres
newConfiguredTarget
from
to
via
pattern
scripts/check-phase8.sh
/media/nvme/dev/golem15/fonoteka/fonoteka-mcp
three configured URLs and scripted MCP SDK lifecycle
FONOTEKA_(API_URL|MCP_PUBLIC_URL|MCP_AUTH_SERVER)
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.
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.
@.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
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.
Task 1: Specify recorded and real-client OAuth acceptance before changing fixtures
../fonoteka.go/parity/oauth_flow_test.go, scripts/check-phase8.sh
.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
- 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.
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.
scripts/check-phase8-red.sh parity-gate bash -lc "cd ../fonoteka.go && go test ./parity -run 'TestOAuthFlows' -count=1"
- `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.
The acceptance harness demands the exact recorded and real-client lifecycle before any route can be claimed ported.
Task 2: Record, scrub, replay, and promote the complete OAuth lifecycle
../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
../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
- 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.
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.
cd ../fonoteka.go && go test ./parity -run 'TestOAuthFlows|TestParityCorpus|TestParityContract' -count=1
- `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.
The Go app passes the PHP-recorded OAuth route and lifecycle contracts without committing live secrets.
Task 3: Complete the real unchanged-MCP phase gate
scripts/check-phase8.sh
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
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.
scripts/check-phase8.sh --core-smoke
- 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.
The actual connector stack, including RFC 9728 resource-server behavior, runs unchanged through the complete Go authorization lifecycle.
<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.
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>
- `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.
<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>
Create `.planning/phases/08-oauth2-1-authorization-server/08-09-SUMMARY.md` when done.