fix(08): revise oauth plans after checker feedback
This commit is contained in:
@@ -5,36 +5,36 @@ type: execute
|
||||
wave: 3
|
||||
depends_on: [08-02]
|
||||
files_modified:
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/config/config.yaml
|
||||
- ../fonoteka.go/config/app.yaml
|
||||
- wristband/authorize.go
|
||||
- wristband/authorize_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/plugin.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/routes.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/oauth_registration_test.go
|
||||
- ../fonoteka.go/plugins/golem15/fonoteka/oauth_authorize_test.go
|
||||
autonomous: true
|
||||
requirements: [AUTH-05, AUTH-06, AUTH-07]
|
||||
must_haves:
|
||||
truths:
|
||||
- "D-03: The assembled app exposes configured PHP-default TTLs, caps, issuer, resource, consent URL, and registration bound."
|
||||
- "D-09: Metadata and registration are raw routes and registration alone carries its named throttle."
|
||||
- "D-02: Authorize reads query only and validates the client and exact redirect before any redirect response."
|
||||
- "D-05: S256, scope/resource policy, ordered RFC3986 errors, and pending-request creation live in wristband."
|
||||
- "D-09: Authorize is connector-visible on the raw route surface without house/auth middleware."
|
||||
- "D-10: No oauth guard is registered; OAuth access remains on inv_token."
|
||||
- "D-12: Backend challenge ownership stays unchanged and RFC 9728 behavior remains in fonoteka-mcp."
|
||||
artifacts:
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/plugin.go"
|
||||
provides: "Configured store-backed wristband server construction"
|
||||
- path: "wristband/authorize.go"
|
||||
provides: "Ordered validation, S256/resource/scope policy, and pending request creation"
|
||||
- path: "../fonoteka.go/plugins/golem15/fonoteka/routes.go"
|
||||
provides: "Raw metadata and register route mounting"
|
||||
provides: "Assembled raw authorize route"
|
||||
key_links:
|
||||
- from: "plugin.go"
|
||||
to: "wristband.New"
|
||||
via: "configured Options and GORM backend"
|
||||
pattern: "wristband\\.New"
|
||||
to: "wristband.Server.Authorize"
|
||||
via: "configured server retained from persistent DCR slice"
|
||||
pattern: "oauth/mcp/authorize"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Mount the proven discovery/DCR engine on the real application with persistent state and exact raw-route isolation.
|
||||
Deliver an assembled connector-visible authorize-request slice that creates durable pending consent state with exact PKCE/redirect/scope/resource behavior.
|
||||
|
||||
Purpose: Deliver the first connector-visible vertical outcome without mixing schema work into protocol implementation.
|
||||
Output: OAuth config, boot wiring, raw routes, and assembled Postgres-backed tests.
|
||||
Purpose: Let a connector start authorization immediately after persistent DCR, leaving only consent and exchange for subsequent slices.
|
||||
Output: Wristband authorize handler, assembled raw route, pending-request persistence, and exact unit/integration tests.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@@ -47,39 +47,69 @@ Output: OAuth config, boot wiring, raw routes, and assembled Postgres-backed tes
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-UI-SPEC.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-PATTERNS.md
|
||||
@.planning/phases/08-oauth2-1-authorization-server/08-02-SUMMARY.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Specify assembled discovery and registration in RED</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/oauth_registration_test.go</files>
|
||||
<name>Task 1: Specify authorize validation and assembled pending-request behavior in RED</name>
|
||||
<files>wristband/authorize.go, wristband/authorize_test.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_authorize_test.go</files>
|
||||
<read_first>
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-CONTEXT.md
|
||||
.planning/phases/08-oauth2-1-authorization-server/08-PATTERNS.md
|
||||
wristband/server.go
|
||||
wristband/stores.go
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/OAuthAuthorizeController.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/OAuthClient.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/tests/functional/OAuthAuthorizeTest.php
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Assembled routes return exact metadata and persistent public/confidential DCR responses.
|
||||
- Route table rejects JWT, inv_token, inv.scope, body-limit, and house middleware; register has only its named throttle.
|
||||
- Failures use `PHASE8_RED:registration-app`, not compile/setup/missing-test failure.
|
||||
- Unknown/unusable client and unregistered redirect return exact local text/plain 400 with no Location.
|
||||
- Later failures redirect with ordered `error`, `error_description`, `iss`, optional `state` using RFC3986 bytes.
|
||||
- Valid S256 request stores one pending row and redirects to `/connect?request=<opaque>`; exact framework/app RED tests are the only failures.
|
||||
</behavior>
|
||||
<action>D-18: add an assembled-router real-Postgres test against existing boot seams. Assert bytes and headers before decoding, exact configured defaults, route isolation, and no oauth guard. Keep the test compiling against 08-01/08-02 contracts and mark only absent app wiring with `PHASE8_RED:registration-app`.</action>
|
||||
<action>D-02/D-04/D-05/D-18: define compiling authorize seams, deterministic unit cases, and an assembled real-Postgres case. Preserve exact validation order: usable client, exact redirect, `response_type=code`, `code_challenge_method=S256`, verifier syntax/challenge, scope ceiling, resource, pending creation. Build redirects from ordered pairs, never `url.Values.Encode`. Use exact `TestPhase8RedAuthorize`/`PHASE8_RED:authorize` and `TestPhase8RedAuthorizeApp`/`PHASE8_RED:authorize-app` JSON verifier invocations; reject every unexpected failing action/package/test and non-behavior failure.</action>
|
||||
<verify>
|
||||
<automated>scripts/check-phase8-red.sh registration-app bash -lc "cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestOAuth(Metadata|Register|RawRoute|Config)' -count=1"</automated>
|
||||
<automated>scripts/check-phase8-red.sh go PHASE8_RED:authorize git.golem15.com/golem15/summercms/wristband TestPhase8RedAuthorize -- go test -json ./wristband -run '^TestPhase8RedAuthorize$' -count=1 && scripts/check-phase8-red.sh go PHASE8_RED:authorize-app git.golem15.com/golem15/fonoteka/plugins/golem15/fonoteka TestPhase8RedAuthorizeApp -- bash -lc "cd ../fonoteka.go && go test -json ./plugins/golem15/fonoteka -run '^TestPhase8RedAuthorizeApp$' -count=1"</automated>
|
||||
</verify>
|
||||
<done>Assembled tests execute and fail solely because config/boot/routes are not wired.</done>
|
||||
<acceptance_criteria>
|
||||
- Both RED invocations select exactly one named test in one named package and reject compile/setup/panic/no-test/unrelated failures.
|
||||
- Tables assert exact status, Content-Type, body, Location absence/presence, parameter order, `%20` encoding, optional state placement, and no credential/request-handle logging.
|
||||
- The assembled RED test uses the real boot/router/Postgres seams and fails only because authorize is not mounted/implemented.
|
||||
</acceptance_criteria>
|
||||
<done>Executable RED evidence completely specifies the connector-visible authorize contract.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Configure, boot, and route persistent discovery and DCR</name>
|
||||
<files>../fonoteka.go/plugins/golem15/fonoteka/config/config.yaml, ../fonoteka.go/config/app.yaml, ../fonoteka.go/plugins/golem15/fonoteka/plugin.go, ../fonoteka.go/plugins/golem15/fonoteka/routes.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_registration_test.go</files>
|
||||
<name>Task 2: Implement and mount exact authorize request creation</name>
|
||||
<files>wristband/authorize.go, wristband/authorize_test.go, ../fonoteka.go/plugins/golem15/fonoteka/plugin.go, ../fonoteka.go/plugins/golem15/fonoteka/routes.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_authorize_test.go</files>
|
||||
<read_first>
|
||||
wristband/authorize_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/oauth_authorize_test.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/plugin.go
|
||||
../fonoteka.go/plugins/golem15/fonoteka/routes.go
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/OAuthAuthorizeController.php
|
||||
/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/classes/auth/OAuthCodeManager.php
|
||||
../fonoteka.go/parity/fixtures/mcp/mcp-oauth.yaml
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Defaults are pending/code 600s, access 3600s, refresh 30 days, DCR cap 200, stale age 24h, resource URL, and 65,536-byte register maximum.
|
||||
- Issuer trims the app URL once; metadata and registration use the actual GORM backend.
|
||||
- Valid requests persist a hash-only opaque pending handle with requested/ceiling scopes, exact redirect/resource/client binding, challenge, state, and 600s expiry.
|
||||
- Scope output preserves PHP order and never exceeds the registered ceiling; resource mismatch cannot create pending state.
|
||||
- Assembled authorize route is raw and metadata/DCR remain unchanged.
|
||||
</behavior>
|
||||
<action>D-03: add `plugins.golem15.fonoteka.oauth.*` defaults and use `app.url` as issuer. Construct the backend and wristband server in Plugin.Boot and retain it for later route/command factories. D-09: mount metadata and register inside the existing raw group; pass `throttle:fonoteka-oauth-register` only to register. D-10: register no oauth guard. D-12: preserve the exact backend personal-token 401 and do not add protected-resource metadata or rich Bearer challenges.</action>
|
||||
<action>D-02/D-03/D-04/D-05/D-06: implement query-only parsing, exact ordered validation/redirects, constant-time S256 verification seam, scope/resource policy, opaque pending creation, and 600-second expiry using the configured server/backend from 08-02. D-09: mount GET `/oauth/mcp/authorize` raw with no middleware. D-10/D-12: register no oauth guard and add no backend Bearer/resource metadata. Preserve metadata/register behavior byte-for-byte and prove invalid requests create no rows.</action>
|
||||
<verify>
|
||||
<automated>cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestOAuth(Metadata|Register|RawRoute|Config)' -count=1</automated>
|
||||
<automated>go test ./wristband -run '^Test(Authorize|OrderedRedirect|PKCE)' -count=1 && (cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestOAuthAuthorizeAssembled$' -count=1)</automated>
|
||||
</verify>
|
||||
<done>An unchanged connector can discover and dynamically register against the assembled app with persistent Postgres state and exact route boundaries.</done>
|
||||
<acceptance_criteria>
|
||||
- Unknown client/unregistered redirect have no Location; each later error has exact ordered RFC3986 Location bytes including issuer and optional state.
|
||||
- Missing/plain/malformed S256, excess/unknown scope, wrong resource, query/body ambiguity, and unusable client create zero pending rows.
|
||||
- A valid assembled request creates one durable hash-only pending row with 600s expiry and redirects to the configured `/connect?request=` URL; raw route inspection is clean.
|
||||
- 08-01 metadata and 08-02 DCR exact-byte tests remain green.
|
||||
</acceptance_criteria>
|
||||
<done>An unchanged connector can register and start a PKCE-bound authorization request through the assembled production router.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
@@ -89,26 +119,27 @@ Output: OAuth config, boot wiring, raw routes, and assembled Postgres-backed tes
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Internet → raw routes | Unauthenticated protocol traffic enters the assembled app. |
|
||||
| Config → public metadata | Deployment values become client trust anchors. |
|
||||
| Connector → raw authorize | Untrusted query data requests a durable consent transaction. |
|
||||
| Validated redirect → Location | Client-controlled redirect is trusted only after exact allow-list match. |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-08-DCR-FLOOD | Denial of Service | register route | mitigate | Named per-IP limiter plus framework body/cap controls. |
|
||||
| T-08-PKCE | Spoofing/Elevation | authorize | mitigate | Mandatory S256 syntax/policy and bound pending state. |
|
||||
| T-08-OPEN-REDIRECT | Spoofing/Disclosure | authorize | mitigate | Exact redirect validation before any Location. |
|
||||
| T-08-SCOPE-CEILING | Elevation | authorize | mitigate | Requested scopes intersect the registered ceiling before persistence. |
|
||||
| T-08-SURFACE | Elevation | route groups | mitigate | Assembled route-table test for exact middleware. |
|
||||
| T-08-SC | Tampering | dependencies | mitigate | No new package. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- Focused assembled discovery/DCR tests pass.
|
||||
- `go vet ./... && go test ./...` passes in both repositories at the wave boundary.
|
||||
- Focused wristband and assembled authorize tests pass in the task feedback budget.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Metadata and DCR are reachable through the real app with exact PHP-compatible responses.
|
||||
- Public/confidential clients persist and raw routes remain isolated.
|
||||
- A registered connector can create a durable PKCE-bound pending authorization request through the real app.
|
||||
- All local/redirect error bytes and raw-route boundaries match PHP exactly.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
||||
Reference in New Issue
Block a user