Files
summercms/.planning/phases/08-oauth2-1-authorization-server/08-02-PLAN.md
2026-09-23 13:38:58 +02:00

17 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/authorize.go
wristband/token.go
wristband/authorize_test.go
wristband/token_test.go
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_token_issuer.go
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/api_token_manager.go
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller_test.go
../fonoteka.go/plugins/golem15/fonoteka/plugin.go
../fonoteka.go/plugins/golem15/fonoteka/routes.go
../fonoteka.go/plugins/golem15/fonoteka/oauth_connect_test.go
true
AUTH-05
AUTH-06
AUTH-07
truths artifacts key_links
A registered client can start an authorization request only with an exact redirect URI, S256 PKCE, valid resource, and ceiling-bounded scopes.
A JWT-authenticated user can inspect, allow, or deny the pending request using the unchanged Nuxt consent screen contract.
An allowed request yields a single-use authorization code that exchanges atomically for an ordinary `inv_` access token and refresh token.
path provides
wristband/authorize.go Authorize validation, ordered redirects, pending-state creation, scope/resource/PKCE policy
path provides
wristband/token.go Authorization-code token exchange and exact RFC 6749 responses
path provides
../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go JWT consent show/allow/deny payloads for unchanged Nuxt
path provides
../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_token_issuer.go Transaction-bound `inv_` access-token mint/revoke adapter
from to via pattern
wristband/token.go wristband.Backend.WithinTx lock, consume code, mint access token, and create refresh token atomically WithinTx
from to via pattern
oauth_consent_controller.go wristband.Server issue-code and deny operations rather than direct OAuth row mutation IssueCode|Deny
from to via pattern
oauth_token_issuer.go api_token_manager.go configured-prefix personal-token mint with oauth_client_id stamp MintPersonalToken
Deliver the interactive authorization-code vertical slice from a connector's PKCE authorize request through Nuxt consent to one atomic token exchange.

Purpose: Make the unchanged browser and connector complete the primary OAuth flow with exact redirect, body, header, scope, tenant, and token semantics. Output: Authorize/token framework handlers, consent controllers, transaction-bound token issuer, store transitions, app routes, and end-to-end tests.

Phase Goal

As a Płytarium user connecting a chat application, I want to review its requested permissions and approve or deny them, so that only an exact PKCE-bound grant can access my active collection.

<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-PATTERNS.md @.planning/phases/08-oauth2-1-authorization-server/08-UI-SPEC.md @.planning/phases/08-oauth2-1-authorization-server/08-01-SUMMARY.md From Plan 08-01: - `wristband.Server` owns configured RFC handlers and app-agnostic operations. - `wristband.Backend.WithinTx(ctx, func(Tx) error) error` provides one atomic store/token boundary.

Existing app interfaces:

  • auth.MintPersonalToken(userID uint, name string, scopes []string, expiresAt *time.Time, collectionIDs []uint) (string, *models.ApiToken, error).
  • classes.ResolveActiveCollection(ctx, db, userID) returns the trusted collection and persists fallback context.
  • bouncer.User(ctx) supplies the JWT principal; app controllers write exact JSON through wire.WriteJSON.

Locked consent payload:

  • GET returns data{client_name,redirect_host,scopes_requested,collection_name,expires_at}.
  • POST consent/deny returns data{redirect_to} with ordered RFC3986 query parameters.
Task 1: Specify the complete PKCE consent and code-exchange path wristband/authorize_test.go, wristband/token_test.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller_test.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_connect_test.go .planning/phases/08-oauth2-1-authorization-server/08-UI-SPEC.md .planning/phases/08-oauth2-1-authorization-server/08-VALIDATION.md wristband/server.go wristband/stores.go ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/token_api_controller.go /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/OAuthAuthorizeController.php /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/OAuthTokenController.php /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/controllers/api/OAuthConsentController.php /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/classes/auth/OAuthCodeManager.php - Unknown client and unregistered redirect return local plain-text 400 with no `Location`; later failures redirect only to the exact registered URI in PHP parameter order. - S256 is mandatory; challenge/verifier syntax and lengths are enforced; wrong/missing/plain PKCE never yields a token. - Scope ceiling truncates data scopes while keeping `offline_access` protocol-only; consent cannot add scopes or collection IDs. - Pending requests are JWT owner-bound and single-use; stale, consumed, or foreign handles share the exact 404. - Code exchange locks and consumes the code once, compares S256 in constant time, mints `inv_`, stamps `oauth_client_id`, and returns exact no-store/no-cache token bytes. Per D-02, D-04, D-08, D-12, and D-18, add RED unit and assembled real-Postgres tests for the full public-client happy path plus T-08-PKCE, T-08-CODE-REPLAY, T-08-OPEN-REDIRECT, T-08-SCOPE-CEILING, T-08-CROSS-USER, T-08-REQUEST-LEAK, and T-08-SURFACE. Assert exact query parameter order and RFC 3986 `%20`, body bytes, content types, `Cache-Control`, `Pragma`, and `WWW-Authenticate: Basic realm="OAuth"` on invalid confidential-client authentication. Prove JSON `/token` is `invalid_request`, form body values override query values, and Basic credentials override form credentials. test -f wristband/authorize_test.go && test -f wristband/token_test.go && cd ../fonoteka.go && test -f plugins/golem15/fonoteka/oauth_connect_test.go - Named tests cover authorize validation order, ordered redirects, consent show/allow/deny, PKCE syntax/mismatch, sequential and concurrent code replay, token parser asymmetry, Basic precedence, exact headers, and cross-user denial. - The assembled happy-path test begins at `GET /oauth/mcp/authorize`, uses JWT consent endpoints, and finishes at `POST /oauth/mcp/token` with an `inv_` access token. - Focused tests fail on absent implementation while compiling against the interfaces produced by 08-01. The RED suite proves the exact end-to-end connection contract and every high-severity boundary before implementation. Task 2: Implement authorize validation and atomic authorization-code exchange wristband/authorize.go, wristband/token.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_token_issuer.go, ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/api_token_manager.go wristband/authorize_test.go wristband/token_test.go wristband/server.go wristband/stores.go wristband/crypto.go ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/api_token_manager.go ../fonoteka.go/plugins/golem15/fonoteka/classes/auth/token_guard.go ../fonoteka.go/plugins/golem15/fonoteka/models/api_token.go ../fonoteka.go/plugins/golem15/fonoteka/models/oauth_auth_code.go - Authorize reads query only and validates client then redirect before any redirect-capable error. - Ordered redirect encoder retains existing query, appends with `&`, preserves field order, and encodes spaces as `%20`. - Code exchange executes code lock/consume, token mint/persist, oauth-client stamp, and refresh-row creation in one transaction. Implement D-02, D-03, D-04, D-05, D-06, D-07, D-11, and D-17. Add authorize validation in locked order: usable client, exact redirect, response type, S256 method/challenge, requested/ceiling scopes, optional exact resource, then pending creation. Build redirects from ordered pairs rather than `url.Values.Encode`. Add token parsing with JSON rejection then `ParseForm`; authenticate `none`, post, or Basic using fixed SHA-256 transforms and constant-time comparison. Exchange codes under a store row lock; compare S256 through `subtle.ConstantTimeCompare`, mark the code used, mint/persist an access token, set `oauth_client_id`, and create the refresh record atomically. Make the personal-token prefix config-backed at the app adapter while preserving `inv_` as Płytarium's configured value. Run the expiry sweep on token entry but delete only expired rows. go test ./wristband -run 'Test(Authorize|Token|PKCE|Code)' -count=1 && cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/classes/auth -run 'TestOAuth(Code|Issuer)' -count=1 - Framework tests prove local 400s have no `Location`, trusted errors have ordered `error,error_description,iss,state`, and successful consent redirects use `code,iss,state`. - Sequential and synchronized concurrent code exchanges produce exactly one usable token grant. - `rg -n 'subtle\.ConstantTimeCompare' wristband` finds both client-secret and PKCE comparisons; no direct equality compares presented secrets/verifiers. - Issued access tokens start with configured `inv_`, store only SHA-256 hash, carry exact scopes/collection IDs/expiry, and have `oauth_client_id` set. The framework can safely create pending authorization state and atomically exchange one PKCE-bound code for the existing personal-token model. Task 3: Wire JWT consent and the raw authorize/token routes ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller_test.go, ../fonoteka.go/plugins/golem15/fonoteka/plugin.go, ../fonoteka.go/plugins/golem15/fonoteka/routes.go, ../fonoteka.go/plugins/golem15/fonoteka/oauth_connect_test.go ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/oauth_consent_controller_test.go ../fonoteka.go/plugins/golem15/fonoteka/oauth_connect_test.go ../fonoteka.go/plugins/golem15/fonoteka/controllers/api/token_api_controller.go ../fonoteka.go/plugins/golem15/fonoteka/classes/active_collection.go ../fonoteka.go/plugins/golem15/fonoteka/plugin.go ../fonoteka.go/plugins/golem15/fonoteka/routes.go /media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/app/pages/connect.vue /media/nvme/dev/golem15/fonoteka/vue-fonoteka-app/app/stores/fonoteka.ts - Consent show returns sanitized client name, host-only redirect, ordered mintable scopes, server-resolved collection name, and ISO expiry. - Allow grants only submitted ∩ requested ∩ ceiling ∩ mintable scopes and server-derived collection ids; deny consumes the request and returns ordered `access_denied,iss,state`. - Raw authorize/token endpoints remain unenveloped and token alone receives `throttle:fonoteka-oauth-token`; JWT management routes never appear under personal-token auth. Per D-08, D-09, D-10, D-12 and the UI-SPEC, implement GET request, POST consent, and POST deny inside the established JWT+locale+must-change-password group. Use `bouncer.User`, `ResolveActiveCollection`, `lagoon.Validate`, Vue-safe plain strings, and wristband operations; never accept collection IDs or extra scopes from the request. Return the exact UI payload/status shapes and collapse absent, stale, consumed, and foreign handles to `{"error":"Request not found"}` 404. Mount authorize and token on the raw group; token gets only `throttle:fonoteka-oauth-token`. Preserve no house middleware, no `oauth` guard, and no backend RFC 9728 challenge. cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestOAuth(Authorize|Consent|Deny|CodeExchange|Surface)' -count=1 - The assembled PKCE happy-path test passes from authorize through JWT consent and token exchange against real Postgres. - Consent tests prove empty/no-longer-grantable scope intersection is exact 422 and foreign/stale/used handles are indistinguishable exact 404. - Route-table tests prove four raw RFC routes and three JWT consent routes occupy only their locked auth surfaces. - `git -C /media/nvme/dev/golem15/fonoteka/vue-fonoteka-app status --short` shows no Phase 8 changes. The unchanged Nuxt consent page and a registered PKCE client complete one secure authorization-code flow through the assembled Go app.

<threat_model>

Trust Boundaries

Boundary Description
Connector → authorize/token Untrusted query/form/Basic credentials attempt to create or redeem grants.
Browser JWT principal → consent API Authenticated but untrusted scope selection crosses tenant/ownership boundaries.
wristband transaction → personal-token store A one-time authorization code becomes durable access/refresh credentials.

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-08-PKCE Spoofing/Elevation authorize and code exchange mitigate Mandatory S256 syntax plus constant-time computed challenge comparison; missing/plain/wrong tests.
T-08-CODE-REPLAY Spoofing code exchange/store mitigate FOR UPDATE, one transaction, used stamp, sequential and concurrent single-winner tests.
T-08-OPEN-REDIRECT Spoofing/Disclosure authorize/consent redirects mitigate Validate usable client and exact redirect before constructing any Location; ordered trusted-URI tests.
T-08-SECRET-TIMING Information Disclosure token client auth and PKCE mitigate Constant-time fixed-transform comparisons only.
T-08-SCOPE-CEILING Elevation authorize and consent mitigate Requested ∩ ceiling before display; submitted ∩ pending ∩ mintable at consent; collection IDs server-derived.
T-08-CROSS-USER Elevation consent endpoints mitigate Principal-scoped pending lookup/consume and indistinguishable 404s.
T-08-REQUEST-LEAK Information Disclosure browser/logging mitigate Opaque short-lived handle, no value logging, host-only UI payload, no-referrer client behavior retained unchanged.
T-08-SURFACE Elevation routes mitigate Assembled route-table isolation across raw, JWT, and personal-token groups.
T-08-SC Tampering dependencies mitigate No new dependency; standard-library implementation enforced by import tests.
</threat_model>
- `go test ./wristband -run 'Test(Authorize|Token|PKCE|Code)' -count=1` - `cd ../fonoteka.go && go test ./plugins/golem15/fonoteka/... -run 'TestOAuth(Authorize|Consent|Deny|CodeExchange|Surface)' -count=1` - `go vet ./... && go test ./...` passes in each repository.

<success_criteria>

  • A registered client completes authorize → JWT consent → code exchange and receives an inv_ access token plus refresh token.
  • PKCE, exact redirect, scope ceiling, ownership, parser precedence, single use, and exact raw response headers are all failing-when-broken tests.
  • The existing consent UI receives every locked field and status without any Nuxt change. </success_criteria>
Create `.planning/phases/08-oauth2-1-authorization-server/08-02-SUMMARY.md` when done.