---
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.