13 KiB
phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
| phase | plan | subsystem | tags | requires | provides | affects | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | duration | completed | |||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 08-oauth2-1-authorization-server | 03 | auth |
|
|
|
|
|
|
|
|
~20min | 2026-09-23 |
Phase 08 Plan 03: Authorize Summary
wristband.Server.Authorize ports the exact PHP authorize-request validation order and open-redirect defense, persists an opaque PKCE-bound pending row through the transaction-scoped store bundle, and is now connector-visible raw on the assembled Go app.
Performance
- Duration: ~20 min
- Started: 2026-09-23T19:55:00Z (approx.)
- Completed: 2026-09-23T20:08:45+02:00
- Tasks: 2 completed (4 commits: RED/GREEN pairs across both repos)
- Files modified: 6 (3 in summercms.go's wristband package, 3 in fonoteka.go)
Accomplishments
wristband.Server.Authorizeis a byte-for-byte port ofOAuthAuthorizeController::authorize's validation order: usable client, exact redirect-URI membership,response_type=code,code_challenge_method=S256, challenge length (43-128), scope parsing with a["read"]default, client scope-ceiling truncation (preservingoffline_accessand never rejecting except on an empty data-scope intersection), and the RFC 8707 resource check — only after all of these does it persist a pending row and redirect- An unknown/unusable client or an unregistered
redirect_urireturns a localtext/plain; charset=UTF-8400 with noLocationheader (T-08-OPEN-REDIRECT); every later validation failure redirects to the now-trustedredirect_uriwith an orderederror,error_description,iss, optionalstatequery built by a dedicated RFC 3986 encoder (rfc3986Escape/buildOrderedQuery/appendOrderedQuery) that never usesnet/url.Values.Encode(which sorts keys and encodes spaces as+) - A valid S256 request persists exactly one durable, hash-only-free pending
AuthCodeRecord(nilCodeHash, nilUserID, 600s expiry fromOptions.PendingRequestTTL) and redirects to<issuer>/connect?request=<opaque>withCache-Control: no-storeand nocode/state/client_secretleaked onto the app's own redirect GET /oauth/mcp/authorizeis now mounted on the assembled fonoteka.go raw route group with zero middleware (D-09);golem15.fonoteka.oauth.resourceand.pending_request_ttl_secondsare wired from config intowristband.OptionsinPlugin.Boot- 08-01 metadata and 08-02 DCR exact-byte tests, plus the full
plugins/golem15/fonotekamodule test suite (including-race), remain green
Task Commits
Each task was committed atomically (TDD RED then GREEN, split per repo since both repos changed):
- Task 1: authorize RED anchor —
787e612(test, summercms.go):Server.Authorize501 stub,TestPhase8RedAuthorizefails the exact S256 success contract against it;57049f8(test, fonoteka.go):TestPhase8RedAuthorizeAppfails 404 against the unmounted route. Both verified fail-closed viascripts/check-phase8-red.sh. - Task 2: implement and mount authorize —
90752be(feat, summercms.go): the realAuthorizehandler, the RFC 3986 ordered-query encoder,Options.Resource/Options.PendingRequestTTL, and the full framework-level test matrix (TestAuthorize*,TestPKCE*,TestOrderedRedirect*);804da83(feat, fonoteka.go): mounts the raw route, wires config intoOptions, and addsTestOAuthAuthorizeAssembled,TestOAuthAuthorizeInvalidRequestsCreateNoPendingRows,TestOAuthAuthorizeRawRouteSurface.
Plan metadata: committed as part of this summary/state-update commit.
Note: both tasks carry tdd="true"; RED/GREEN pairs land as separate commits, and Task 2 splits its GREEN across the two repositories it touches.
Files Created/Modified
wristband/authorize.go—Server.Authorize,writeAuthorizeLocalError,authorizeErrorRedirect,parseAuthorizeScopes,stringSliceContains,rfc3986Escape/isRFC3986Unreserved,buildOrderedQuery,appendOrderedQuerywristband/authorize_test.go—TestPhase8RedAuthorize(RED anchor) plus the full unit matrix: unknown/revoked client, unregistered/trailing-slash redirect, response-type/PKCE-method/PKCE-length failures, valid success + pending-row assertions,iss-on-every-error, scope default/invalid/ceiling truncation/ceiling-rejection (4 cases mirroring the PHP test file), resource mismatch/omission, RFC3986 encoding fixtures, backend-unavailable 500wristband/server.go—OptionsgainsResourceandPendingRequestTTLwith PHP-parity defaults../fonoteka.go/plugins/golem15/fonoteka/plugin.go— wiresgolem15.fonoteka.oauth.resource/.pending_request_ttl_secondsintowristband.Options../fonoteka.go/plugins/golem15/fonoteka/routes.go— mountsGET /oauth/mcp/authorizeon the raw group with no middleware../fonoteka.go/plugins/golem15/fonoteka/oauth_authorize_test.go—TestPhase8RedAuthorizeApp(RED anchor),TestOAuthAuthorizeAssembled,TestOAuthAuthorizeInvalidRequestsCreateNoPendingRows,TestOAuthAuthorizeRawRouteSurface
Decisions Made
See frontmatter key-decisions. Most notable: Options was extended with Resource/PendingRequestTTL even though wristband/server.go was not in the plan's <files> list for either task — this follows the exact precedent 08-02 set when it extended the same struct for DCR's cap/sweep/body-limit options, and is necessary to make the resource check and 600s TTL configurable rather than hardcoded (Rule 2 — auto-add missing critical functionality implied by the task's own action text: "600-second expiry using the configured server/backend from 08-02").
Deviations from Plan
1. [Rule 2 - Missing functionality] Extended wristband.Options with Resource and PendingRequestTTL
- Found during: Task 2 (implementing the resource check and pending-request expiry)
- Issue: The plan's action text requires the resource check and 600-second expiry to be "configured" through the server, but neither value existed on
Optionsyet (only DCR-related fields were added in 08-02) - Fix: Added
Options.Resource(default"https://mcp.plytarium.com/mcp") andOptions.PendingRequestTTL(default600 * time.Second") toDefaultOptions(), and wired both fromgolem15.fonoteka.oauth.resource/.pending_request_ttl_secondsconfig inplugin.go - Files modified:
wristband/server.go,../fonoteka.go/plugins/golem15/fonoteka/plugin.go - Verification:
TestAuthorizeResourceMismatchRedirectsInvalidTarget,TestAuthorizeResourceOmittedIsAccepted, and the pending-rowExpiresAtassertion inTestAuthorizeValidRequestRedirectsToConnectWithOpaqueHandleOnlyall pass - Committed in:
90752be(Task 2 GREEN commit, summercms.go)
Total deviations: 1 auto-fixed (1 missing functionality) Impact on plan: No scope change; a struct-field addition following an established precedent, required by the task's own stated action.
Issues Encountered
Full-suite verification (go vet/go test ./... in both repos, per CLAUDE.md) surfaced two pre-existing, out-of-scope failures unrelated to this plan's changed files — logged to deferred-items.md rather than fixed here (scope-boundary rule):
fonoteka.go's root-moduleparitypackage:TestRemainingMigrationsUpDownandTestRollbackIsolatesFonotekaFullSchemafail because 08-02's202609230019_oauth_schema_correctionmigration is now the last registered migration, which these Phase-5-era tests' hardcoded "last migration" assertions don't know about. Neither test file nor any migration/model file is in this plan'sfiles_modified.summercms.go'sfetchguardpackage:TestFetchTooLargeIsStreamingis intermittently flaky under the fullgo test ./...run but passes reliably in isolation;fetchguardis untouched by this plan.
The module this plan actually modifies (fonoteka.go/plugins/golem15/fonoteka) is fully green, including go vet and go test -race ./....
User Setup Required
None — no external service configuration required.
Next Phase Readiness
- The RFC 3986 ordered-query encoder (
buildOrderedQuery/appendOrderedQuery) is ready for 08-04's token-endpoint redirects and 08-05's consent/deny redirects to reuse directly. - The pending
AuthCodeRecordcreated byAuthorize(hash-only-free,RequestIDset,CodeHash/UserIDnil) is exactly the shape 08-05's consent flow needs to read viaAuthCodeStore.ByRequestIDand turn into an issued code viaMarkIssued. Options.Resource/Options.PendingRequestTTLestablish the pattern for 08-04 to addOptions.CodeTTL/AccessTokenTTL/RefreshTokenTTLthe same way.- AUTH-05/AUTH-06/AUTH-07 remain Pending in REQUIREMENTS.md, continuing 08-01/08-02's decision: this plan ships authorize only; consent, token exchange, and refresh remain for 08-04/08-05.
- No blockers.
Self-Check: PASSED
- FOUND: wristband/authorize.go, wristband/authorize_test.go, wristband/server.go, .planning/phases/08-oauth2-1-authorization-server/08-03-SUMMARY.md
- FOUND: ../fonoteka.go/plugins/golem15/fonoteka/plugin.go, routes.go, oauth_authorize_test.go
- FOUND commits (summercms.go):
787e612,90752be - FOUND commits (fonoteka.go): 57049f8, 804da83