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