212 lines
14 KiB
Markdown
212 lines
14 KiB
Markdown
---
|
|
phase: 08-oauth2-1-authorization-server
|
|
plan: 04
|
|
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
|
|
autonomous: true
|
|
requirements: [AUTH-05, AUTH-07]
|
|
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."
|
|
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"
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
Deliver the operator and MCP bootstrap slice required for confidential-client coverage and unchanged real-MCP startup.
|
|
|
|
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.
|
|
</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
|
|
</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-PATTERNS.md
|
|
@.planning/phases/08-oauth2-1-authorization-server/08-UI-SPEC.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>
|
|
<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.
|
|
</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>
|
|
<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>
|
|
</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>
|
|
</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>
|
|
<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.
|
|
</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>
|
|
<verify>
|
|
<automated>go test ./bonfire -run 'Test.*Flag' -count=1 && cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run TestOAuthClientCommand -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>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| 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. |
|
|
|
|
## 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. |
|
|
</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.
|
|
</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.
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/08-oauth2-1-authorization-server/08-04-SUMMARY.md` when done.
|
|
</output>
|