--- 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" --- 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. ## 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. @/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-PATTERNS.md @.planning/phases/08-oauth2-1-authorization-server/08-UI-SPEC.md @.planning/phases/08-oauth2-1-authorization-server/08-03-SUMMARY.md 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. Task 1: Specify operator provisioning and real MCP bootstrap contracts 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 .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 - 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. 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. 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} - 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. The operator and MCP bootstrap contracts are executable and fail only on the absent production behavior. Task 2: Add repeatable flags and the exact OAuth client command 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 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 - `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. 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. go test ./bonfire -run 'Test.*Flag' -count=1 && cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run TestOAuthClientCommand -count=1 - 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. Operators can safely provision confidential or ceiling-bounded clients with the exact PHP command contract. Task 3: Serve the MCP token bootstrap payload on the existing personal-token surface ../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 ../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 - `/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. 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. cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'Test(MeToken|TokenSurface|OAuthTools)' -count=1 - 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. The real MCP process can bootstrap from its OAuth-issued `inv_` token without any client change or backend header drift. ## 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. | - `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. - 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. Create `.planning/phases/08-oauth2-1-authorization-server/08-04-SUMMARY.md` when done.