12 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 | 04 | auth |
|
|
|
|
|
|
|
|
~15min | 2026-09-23 |
Phase 08 Plan 04: Token Exchange Summary
wristband.Server.Token ports the exact PHP token-endpoint parser, client authentication, and atomic authorization-code exchange (lock, consume, mint inv_ access token, create refresh row) in one transaction, proven against both an in-memory backend and real Postgres including a synchronized concurrent-replay test with exactly one winner.
Performance
- Duration: ~15 min
- Started: 2026-09-23T20:16:00+02:00 (approx.)
- Completed: 2026-09-23T20:31:00+02:00 (approx.)
- Tasks: 2 completed (3 commits: RED/GREEN pair in summercms.go, GREEN companion in fonoteka.go)
- Files modified: 5 (3 in summercms.go's wristband package, 1 created + 1 modified in fonoteka.go)
Accomplishments
wristband.Server.Tokenis a byte-for-byte port ofOAuthTokenController::token/OAuthCodeManager::exchangeCode's validation order: JSON-body rejection before any form parsing (closing Pitfall 4 — a JSON-labeled request cannot smuggle a grant through the query string),net/http.Request.ParseForm's own body-over-query precedence (verified directly against the Go 1.27 stdlib source, not assumed), Basic-credentials-override-form-credentials client authentication, and exactinvalid_request/unsupported_grant_type/invalid_client/invalid_grantbodies- Client authentication dispatches public (
token_endpoint_auth_method: "none") vs. confidential clients exactly like PHP, comparing the confidential secret's sha256 hex throughcrypto/subtle.ConstantTimeCompare(T-08-SECRET-TIMING) and returningWWW-Authenticate: Basic realm="OAuth"only oninvalid_client exchangeAuthorizationCoderuns the entire lock/consume/mint/refresh-create sequence inside oneWithinTxcallback:ByCodeHashForUpdaterow-locks the code, every PHP binding (used/expired/client/redirect/resource/PKCE S256) is checked before any mutation,MarkUsedconsumes the code,Mintproduces theinv_access token (name truncated to 120 runes exactly like PHP'smb_substr), and a refresh-token row is created linked to it — all committed together (T-08-CODE-REPLAY)- A synchronized concurrent-replay test proves exactly one winner both at the framework level (in-memory backend, mutex-serialized
WithinTx) and against real Postgres (ByCodeHashForUpdate'sFOR UPDATErow lock): two goroutines racing the same code yield exactly one 200 and oneinvalid_grant, and exactly one persisted access token MintablePrefixbecame a var (D-11 groundwork toward a config-backed prefix) with its default byte-identical to the existing"inv_", proven unchanged byTestOAuthCodeExchangeMintsInvAccessAndRefreshTokensandTestOAuthIssuerMintsConfiguredPrefixAndStampsClientagainst real Postgres
Task Commits
Each task was committed atomically (TDD RED then GREEN):
- Task 1: code-exchange RED anchor —
5ca830b(test, summercms.go):Server.Token501 stub,TestPhase8RedCodeExchangefails the exact valid-S256-exchange success contract against it (PHASE8_RED:code-exchange), plusOptions.AccessTokenTTL/RefreshTokenTTL. Verified fail-closed viascripts/check-phase8-red.sh. - Task 2: implement and prove atomic code exchange —
4bd3b3d(feat, summercms.go): the realTokenhandler,authenticateClient,verifyPkce,exchangeAuthorizationCode, therotateRefreshTokenplaceholder, and the fullwristbandbehavior matrix (TestToken*);510c5b4(feat, fonoteka.go):MintablePrefixvar andoauth_token_issuer_test.go's real-PostgresTestOAuthCode*/TestOAuthIssuer*proofs.
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's GREEN splits across the two repositories it touches.
Files Created/Modified
wristband/token.go—Server.Token,authenticateClient,verifyPkce,exchangeAuthorizationCode,rotateRefreshToken(placeholder),writeTokenError,tokenSuccessBody/tokenErrorBody/tokenIssueResult,errInvalidClient/errInvalidGrantwristband/token_test.go—TestPhase8RedCodeExchange(RED anchor) plus the full unit matrix: JSON rejection, body-over-query precedence, Basic-over-form credentials, missing/unsupported grant type, unknown/revoked/wrong-secret/missing-secret client (with exactWWW-Authenticateassertion), public-client-ignores-secret, wrong verifier, client/redirect/resource binding mismatches, resource-omitted acceptance, expired code, missing required fields, sequential and concurrent replay (exactly one winner), offline_access scope appending, no-envelope/no-trailing-newline/exact-headers, refresh-grant-dispatch-accepted-but-not-implemented, backend-unavailable 500wristband/server.go—OptionsgainsAccessTokenTTL/RefreshTokenTTLwith PHP-parity defaults../fonoteka.go/plugins/golem15/fonoteka/classes/auth/api_token_manager.go—MintablePrefixchanged fromconsttovar(D-11)../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_token_issuer_test.go—TestOAuthCodeExchangeMintsInvAccessAndRefreshTokens,TestOAuthCodeExchangeWrongVerifierIsInvalidGrantAndMintsNothing,TestOAuthCodeExchangeConcurrentReplayHasExactlyOneWinner,TestOAuthIssuerMintsConfiguredPrefixAndStampsClient
Decisions Made
See frontmatter key-decisions. Most notable: this plan deliberately does not mount POST /oauth/mcp/token on the assembled fonoteka.go app (no routes.go/plugin.go changes) — the plan's own files_modified list and Task 2's verify command (classes/auth package tests only, not a route/controller/parity test) scope 08-04 to the wristband handler plus a direct real-Postgres store-adapter proof. Mounting the route happens once 08-05 wires consent, since only then does a real issued code (as opposed to a directly-seeded test fixture) exist to drive the endpoint end to end through the app.
Deviations from Plan
None — plan executed as written. The grant_type=refresh_token placeholder and the "no route mounting" scoping were both already implied by the plan's own action text (D-05 owns refresh rotation as future scope per the roadmap wave split) and file list respectively, not additions beyond it.
Issues Encountered
None. Full go vet/go test ./... in summercms.go is green; go vet/go test ./... in fonoteka.go's plugins/golem15/fonoteka module is green including the new real-Postgres tests. The root parity module's two pre-existing migration-count test failures (documented in 08-03-SUMMARY.md's Issues Encountered and deferred-items.md) are unchanged and out of this plan's scope (no migration/model files touched here).
User Setup Required
None — no external service configuration required.
Next Phase Readiness
AuthCodeRecord's post-consent shape (CodeHashset,RequestIDnil,UserIDset) is exactly what 08-05's consent flow must produce viaAuthCodeStore.MarkIssuedbefore a real/tokencall can succeed end to end through the assembled app.Options.AccessTokenTTL/RefreshTokenTTLare ready for 08-06's refresh rotation to reuse the same TTL fields rather than adding new ones.rotateRefreshToken's placeholder is the explicit seam 08-06 must replace with real lock/rotate/lineage-kill logic (T-08-REFRESH-REPLAY); it is not wired into any route yet, so there is no user-visible regression to fix, only a function body to complete.POST /oauth/mcp/tokenis still unmounted on the assembled app; 08-05 (or whichever plan first needs an end-to-end/tokencall through the real route) must add theroutes.go/plugin.gowiring 08-03 established for/authorize.- AUTH-05/AUTH-06 remain Pending in REQUIREMENTS.md, continuing 08-01/08-02/08-03's decision: this plan ships code exchange only; consent, refresh rotation, DCR client command, and the
/meprerequisite remain for later Phase 8 plans. - No blockers.