---
phase: 08-oauth2-1-authorization-server
plan: 05
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
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."
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)"
---
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.
@/home/jin/.codex/get-shit-done/workflows/execute-plan.md
@/home/jin/.codex/get-shit-done/templates/summary.md
@.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-04-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.
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.
test -f ../fonoteka.go/parity/oauth_flow_test.go && test -x scripts/check-phase8.sh
- `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.
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.
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
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.
scripts/check-phase8.sh
- 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.
## 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. |
- `cd ../fonoteka.go && go test ./parity -run 'TestOAuthFlows|TestParityCorpus|TestParityContract' -count=1`
- `scripts/check-phase8.sh`
- 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.