Files
summercms/.planning/phases/08-oauth2-1-authorization-server/08-04-PLAN.md
2026-09-23 13:38:58 +02:00

14 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
08-oauth2-1-authorization-server 04 execute 4
08-03
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
true
AUTH-05
AUTH-07
truths artifacts key_links
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.
path provides
../fonoteka.go/plugins/golem15/fonoteka/console/oauth_client.go Exact `fonoteka:oauth-client` operator command
path provides
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/me_token_controller.go Personal-token MCP bootstrap endpoint
path provides
bonfire/command.go Typed repeatable string-slice flag contract
from to via pattern
oauth_client.go wristband client issuance helper same validation/hash/one-time-secret path as DCR wristband
from to via pattern
me_token_controller.go bouncer.Credential reuse matched `*models.ApiToken` without re-parsing bearer input 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.

<execution_context> @/home/jin/.codex/get-shit-done/workflows/execute-plan.md @/home/jin/.codex/get-shit-done/templates/summary.md </execution_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 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.

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

<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>
Create `.planning/phases/08-oauth2-1-authorization-server/08-04-SUMMARY.md` when done.