---
phase: 08-oauth2-1-authorization-server
plan: 03
type: execute
wave: 3
depends_on: [08-02]
files_modified:
- 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
autonomous: true
requirements: [AUTH-05, AUTH-06, AUTH-07]
must_haves:
truths:
- "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."
artifacts:
- path: "wristband/authorize.go"
provides: "Ordered validation, S256/resource/scope policy, and pending request creation"
- path: "../fonoteka.go/plugins/golem15/fonoteka/routes.go"
provides: "Assembled raw authorize route"
key_links:
- from: "plugin.go"
to: "wristband.Server.Authorize"
via: "configured server retained from persistent DCR slice"
pattern: "oauth/mcp/authorize"
---
Deliver an assembled connector-visible authorize-request slice that creates durable pending consent state with exact PKCE/redirect/scope/resource behavior.
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.
@/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-02-SUMMARY.md
Task 1: Specify authorize validation and assembled pending-request behavior in RED
wristband/authorize.go, wristband/authorize_test.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_authorize_test.go
.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
- 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.
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.
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"
- 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.
Executable RED evidence completely specifies the connector-visible authorize contract.
Task 2: Implement and mount exact authorize request creation
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
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
- 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.
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.
go test ./wristband -run '^Test(Authorize|OrderedRedirect|PKCE)' -count=1 && (cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestOAuthAuthorizeAssembled$' -count=1)
- 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.
An unchanged connector can register and start a PKCE-bound authorization request through the assembled production router.
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| 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-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. |
- Focused wristband and assembled authorize tests pass in the task feedback budget.
- 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.