Files
summercms/.planning/phases/08-oauth2-1-authorization-server/08-02-PLAN.md

15 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 02 execute 2
08-01
wristband/stores.go
wristband/crypto.go
wristband/register.go
wristband/registration_test.go
../fonoteka.go/plugins/golem15/fonoteka/models/oauth_client.go
../fonoteka.go/plugins/golem15/fonoteka/models/oauth_auth_code.go
../fonoteka.go/plugins/golem15/fonoteka/updates/12_oauth_schema_correction.go
../fonoteka.go/plugins/golem15/fonoteka/updates/oauth_schema_correction_test.go
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go
../fonoteka.go/plugins/golem15/fonoteka/config/config.yaml
../fonoteka.go/plugins/golem15/fonoteka/plugin.go
../fonoteka.go/plugins/golem15/fonoteka/routes.go
../fonoteka.go/plugins/golem15/fonoteka/oauth_registration_test.go
true
AUTH-05
AUTH-06
AUTH-07
truths artifacts key_links
D-03: Configuration defaults pending requests and authorization codes to 600s, access tokens to 3600s, refresh tokens to 30 days, the DCR client cap to 200, and the unconsented-client sweep to 24h; derives issuer from app.url with its trailing slash trimmed, defaults the RFC 8707 resource to https://mcp.plytarium.com/mcp, and builds consent URLs as app.url + /connect?request=<opaque>.
D-07: Public clients and multiple pending authorization requests persist through one transaction-scoped GORM adapter.
D-17: Expiry sweeps delete only expired lifecycle rows and retain unexpired replay evidence.
D-02/D-21: A connector can dynamically register through the assembled JSON-only 64 KiB-bounded route and receive an exact persistent response.
path provides
../fonoteka.go/plugins/golem15/fonoteka/updates/12_oauth_schema_correction.go Additive nullability and index correction with safe rollback refusal
path provides
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go GORM transaction-scoped wristband backend
path provides
../fonoteka.go/plugins/golem15/fonoteka/routes.go Connector-visible persistent RFC 7591 registration route
from to via pattern
oauth_store.go wristband.Backend WithinTx callback whose methods all use callback *gorm.DB WithinTx
Deliver a connector-visible persistent RFC 7591 registration slice, including the schema and transaction semantics it requires.

Purpose: Let a real connector register in Wave 2 while proving nullability, indexes, bounds, constant-time secret handling, locking, cap serialization, and sweep semantics. Output: Corrected models/migration, wristband DCR, GORM backend, configured raw route, and exact assembled tests.

<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-RESEARCH.md @.planning/phases/08-oauth2-1-authorization-server/08-01-SUMMARY.md Task 1: Correct the OAuth lifecycle schema with executable migration evidence ../fonoteka.go/plugins/golem15/fonoteka/models/oauth_client.go, ../fonoteka.go/plugins/golem15/fonoteka/models/oauth_auth_code.go, ../fonoteka.go/plugins/golem15/fonoteka/updates/12_oauth_schema_correction.go, ../fonoteka.go/plugins/golem15/fonoteka/updates/oauth_schema_correction_test.go .planning/phases/08-oauth2-1-authorization-server/08-PATTERNS.md ../fonoteka.go/plugins/golem15/fonoteka/models/oauth_client.go ../fonoteka.go/plugins/golem15/fonoteka/models/oauth_auth_code.go ../fonoteka.go/plugins/golem15/fonoteka/updates/11_secrets_slice.go ../fonoteka.go/plugins/golem15/fonoteka/updates/registry.go /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/updates/v1.1.7/create_oauth_tables.php /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/updates/v1.1.9/add_scope_ceiling_to_oauth_clients.php /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthMigrationTest.php - Public client secret and pending request id/code hash/user id are pointer-backed and nullable in real Postgres. - PHP-equivalent named operational indexes exist and safe rollback refuses when null lifecycle data exists. - `TestPhase8RedOAuthSchema` is the only selected failing test/action during RED. D-07 and D-18: first add a compiling real-Postgres `TestPhase8RedOAuthSchema`, validate it with `check-phase8-red.sh go`, then change the four model fields to pointers and add a new gormigrate correction rather than editing applied history. Drop four NOT NULL constraints, create the exact named indexes idempotently, and make rollback refuse without coercing/deleting when null lifecycle rows exist. Use the existing migration/Postgres harness and PHP schema/tests as the contract. (cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/updates -run '^TestOAuthSchemaCorrection$' -count=1) - Before implementation, `scripts/check-phase8-red.sh go PHASE8_RED:persistence-schema git.golem15.com/golem15/fonoteka/plugins/golem15/fonoteka/updates TestPhase8RedOAuthSchema -- bash -lc "cd ../fonoteka.go && go test -json ./plugins/golem15/fonoteka/updates -run '^TestPhase8RedOAuthSchema$' -count=1"` accepts only the exact behavior RED. - After implementation, real-Postgres assertions prove all four nullable columns and every PHP-equivalent named index. - Down succeeds when safe and refuses with rows unchanged when any required field is null; applied migration history remains byte-unchanged. The corrected additive schema can represent public clients and every pending/code transition without destructive rollback. Task 2: Implement bounded DCR and the transaction-scoped persistent backend wristband/stores.go, wristband/crypto.go, wristband/register.go, wristband/registration_test.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store_test.go .planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md .planning/phases/08-oauth2-1-authorization-server/08-PATTERNS.md ../fonoteka.go/plugins/golem15/fonoteka/classes/active_collection.go ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/postgres_test.go /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/OAuthRegisterController.php /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/OAuthClient.php /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthRegisterTest.php - JSON-only DCR enforces URI/grant/response/auth-method/name rules, 65,536-byte maximum, cap 200, and 24h stale-unconsented sweep. - Raw client secrets are returned once, SHA-256 hashes alone persist, and verification uses `crypto/subtle.ConstantTimeCompare` over fixed transforms. - Sweep/cap/create share one transaction; concurrent cap-1 registration yields one success and one native error. D-01/D-02/D-04/D-05/D-06/D-07/D-17/D-21: add app-agnostic Backend/Tx records, deterministic clock/entropy seams, fixed-transform crypto, a local exact response writer, and the RFC 7591 handler. Apply `http.MaxBytesReader` before decode and return the native `invalid_client_metadata` body for overflow/malformed/non-JSON. Strip control characters, cap names at 120, return raw secrets once, and persist hashes only. Implement the app GORM adapter using only the callback `*gorm.DB`; serialize stale sweep/cap/create in one transaction and expose later row-lock lifecycle methods without importing GORM into wristband. Before implementation, run `scripts/check-phase8-red.sh go PHASE8_RED:registration git.golem15.com/golem15/summercms/wristband TestPhase8RedRegistration -- go test -json ./wristband -run '^TestPhase8RedRegistration$' -count=1` and `scripts/check-phase8-red.sh go PHASE8_RED:registration-store git.golem15.com/golem15/fonoteka/plugins/golem15/fonoteka/classes/auth TestPhase8RedRegistrationStore -- bash -lc "cd ../fonoteka.go && go test -json ./plugins/golem15/fonoteka/classes/auth -run '^TestPhase8RedRegistrationStore$' -count=1"`; each invocation must observe its exact sentinel once, the anchored selected test and named package only, and zero unexpected fail actions/package/test events, build/setup/syntax failures, panics, malformed JSON events, or zero-test selection. Then make focused unit/Postgres tests green. go test ./wristband -run '^Test(Register|Registration)' -count=1 && (cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/classes/auth -run '^TestOAuth(RegistrationStore|RegistrationCap)$' -count=1) - RED uses `go test -json` with exact package/test/sentinel for `TestPhase8RedRegistration` and `TestPhase8RedRegistrationStore`; no unrelated failure can satisfy either invocation. - Exact tests cover public/confidential responses, wrong content type, malformed/oversized 65,537-byte input, five-URI/count/length bounds, unsupported grant/response/auth method, control-character name cleaning, and no newline/envelope. - A synchronized real-Postgres cap-1 test yields exactly one created row; stale unconsented rows are swept while consented/fresh rows remain, and all mutations use the callback transaction. - Persisted/logged/output audits find no raw client secret; fixed-transform comparison contains `crypto/subtle.ConstantTimeCompare`. Wristband and Postgres provide exact bounded, concurrency-safe, secret-safe registration behavior. Task 3: Configure and mount persistent DCR on the assembled raw surface ../fonoteka.go/plugins/golem15/fonoteka/config/config.yaml, ../fonoteka.go/plugins/golem15/fonoteka/plugin.go, ../fonoteka.go/plugins/golem15/fonoteka/routes.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_registration_test.go .planning/phases/08-oauth2-1-authorization-server/08-UI-SPEC.md ../fonoteka.go/plugins/golem15/fonoteka/plugin.go ../fonoteka.go/plugins/golem15/fonoteka/routes.go ../fonoteka.go/plugins/golem15/fonoteka/config/config.yaml ../fonoteka.go/config/app.yaml /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/routes.php /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/config/fonoteka.php - Assembled POST `/oauth/mcp/register` persists public/confidential clients and returns exact PHP bytes/headers. - Only register carries `throttle:fonoteka-oauth-register`; metadata remains raw with no middleware. - Config defaults are pending/code 600s, access 3600s, refresh 30 days, DCR cap 200, stale age 24h, resource URL, and register max 65,536. D-03: add `plugins.golem15.fonoteka.oauth.*` defaults and construct the store-backed server in Plugin.Boot while preserving 08-01 metadata. Read the existing `../fonoteka.go/config/app.yaml` only as the `app.url` source; do not modify it. D-09: mount register in the raw group with only its named throttle. D-10/D-12: register no oauth guard and add no rich Bearer/resource-server surface. Add an assembled real-Postgres `TestPhase8RedRegistrationApp`, then before implementation run `scripts/check-phase8-red.sh go PHASE8_RED:registration-app git.golem15.com/golem15/fonoteka/plugins/golem15/fonoteka TestPhase8RedRegistrationApp -- bash -lc "cd ../fonoteka.go && go test -json ./plugins/golem15/fonoteka -run '^TestPhase8RedRegistrationApp$' -count=1"`; it must observe the exact sentinel once, the anchored selected test and named package only, and zero unexpected fail actions/package/test events, build/setup/syntax failures, panics, malformed JSON events, or zero-test selection. Then assert exact bytes/headers, durable reload, middleware isolation, config values, and unchanged metadata. (cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestOAuth(RegisterAssembled|MetadataAssembled|RawRegistrationSurface)$' -count=1) - RED command names exact app package, `TestPhase8RedRegistrationApp`, and `PHASE8_RED:registration-app` under `go test -json`; compile/setup/no-test or another failing test is rejected. - A public and confidential registration each return status 201 and exact fields/order/headers; reload through a fresh transaction finds hash-only rows with null/non-null secret hash as appropriate. - Oversized and wrong-content-type requests retain endpoint-native errors through the assembled router; register has only its named limiter and metadata remains byte-identical to 08-01. An unchanged connector can dynamically register against the assembled app and its client persists correctly in Postgres.

<threat_model>

Trust Boundaries

Boundary Description
wristband records → GORM App-agnostic state crosses into persistent rows and locks.

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-08-DCR-FLOOD Denial of Service client store mitigate Transactionally serialized cap/sweep/create with contention test.
T-08-CODE-REPLAY Spoofing auth-code store mitigate App-tier row-lock methods and single transaction handle.
T-08-REFRESH-REPLAY Spoofing/Elevation refresh store mitigate Preserve replay evidence until expiry and expose locked traversal.
T-08-SC Tampering dependencies mitigate Existing GORM/Postgres only; no install.
</threat_model>
- Focused migration/store/DCR tests pass; no task command runs full repositories, race, parity, UI, or real MCP. - `rg -n 'clause.Locking' ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go` finds app-tier locks only.

<success_criteria>

  • Schema and store can represent every client/pending/code/refresh lifecycle state.
  • DCR cap and single-use operations have real-Postgres concurrency evidence. </success_criteria>
Create `.planning/phases/08-oauth2-1-authorization-server/08-02-SUMMARY.md` when done.