Commit Graph

14 Commits

Author SHA1 Message Date
Jakub Zych
f4259560c2 test(08-10): close wristband 103-method PHP audit gaps
- register loopback http:// acceptance, javascript: URI rejection, and
  error-body no-secret/no-stack-trace evidence
- token-exchange plain PKCE rejection even when verifier equals the
  stored (non-S256) challenge
2026-09-24 00:05:41 +02:00
Jakub Zych
6cc07a42e2 fix(08-09): match wristband OAuth byte contract to live-recorded PHP
Recording the full mcp-lifecycle fixture against real isolated PHP
(08-09-PLAN.md Task 2) uncovered three byte-level gaps between wristband's
assumed contract and actual production PHP behavior:

- Every explicit "Cache-Control: no-store" PHP sets is actually delivered
  as "no-store, private" (Laravel's session-cookie default merges "private"
  onto any explicit value); wristband's own default for unheadered JSON
  error responses is "no-cache, private" (matching the house convention
  already used elsewhere), not empty.
- PHP's redirect responses (authorize success and every error redirect)
  render Symfony's default HTML redirect body with Content-Type
  "text/html; charset=utf-8"; Go's bare 302 with no body never matched.
  wristband/redirect_html.go ports that exact byte template, including
  PHP's htmlspecialchars(ENT_QUOTES) escaping (Go's html.EscapeString uses
  different quote entities).

tide/normalize.go: isIDKey now also masks "_ids" plural array fields
(e.g. collection_ids), a latent parity-corpus gap no prior fixture had
exercised with a literal, non-empty, non-placeholder array value.
2026-09-23 23:12:02 +02:00
Jakub Zych
398353b135 feat(08-07): wire repeatable bonfire flags and export client-issuing helpers
- bonfire.wrap registers a Repeatable Flag as a Cobra StringSlice so
  Input.Flags returns every repeated --name=value occurrence in order;
  scalar/bare flags are unaffected (D-19)
- wristband.IssueClientCredentials/RejectRedirectURI export the exact
  random-id/secret/hash and redirect-URI validation RFC 7591
  registration already uses, so the fonoteka:oauth-client operator
  command shares one hash/validation path with DCR (T-08-SECRET-TIMING)
2026-09-23 21:56:00 +02:00
Jakub Zych
dab2b8f31a feat(08-06): implement refresh rotation, replay lineage-kill, and token sweep
rotateRefreshToken ports OAuthCodeManager::rotateRefresh: a fresh refresh
token rotates atomically (revoke old access token, mint successor, link
rotated_to_id) while a replayed (already-rotated) token instead revokes the
whole lineage and commits that kill before Token maps it to invalid_grant
outside the transaction (T-08-REFRESH-REPLAY). Token also runs the D-17
expiry sweep (DeleteExpiredCodes/DeleteExpiredRefreshTokens) before grant
processing. Server.Revoke is the new cascade-revoke seam a connected-app
controller uses instead of touching refresh rows directly.
2026-09-23 21:26:35 +02:00
Jakub Zych
b2c2cc0bb7 test(08-06): add failing refresh-lifecycle RED anchor and store interface
TestPhase8RedLifecycleFramework drives exchange -> rotate -> replay against
the real (in-memory-backed) Server.Token and fails while rotateRefreshToken
is 08-04's invalid_grant placeholder (PHASE8_RED:lifecycle-framework,
verified fail-closed via scripts/check-phase8-red.sh). Extends the
RefreshTokenStore/AuthCodeStore interfaces with the store seams Task 2's
implementation needs (ByAPITokenIDForUpdate, MarkRotated,
DeleteExpiredCodes, DeleteExpiredRefreshTokens) and updates the framework's
in-memory test double to satisfy them.
2026-09-23 21:26:05 +02:00
Jakub Zych
a1fa9c6f44 feat(08-05): add wristband consent issue/deny operations
Server.PendingRequest/IssueCode/DenyPending port PHP
OAuthConsentController::pendingFor/OAuthCodeManager::issueCode as
app-agnostic protocol operations (08-CONTEXT.md D-08): every missing,
foreign-owner, used, expired, or already-issued pending row collapses to
the identical ErrPendingNotFound (T-08-CROSS-USER/T-08-REQUEST-LEAK).
IssueCode trusts the caller's already-computed granted scopes/collection
ids and returns the ordered redirect_to URL built through the existing
RFC 3986 encoder.

AuthCodeStore.MarkIssued gains scopes/collectionIDs/expiresAt parameters
(PHP's issueCode overwrites all three, not just code_hash/user_id) and
ClientStore gains MarkConsented, both required for D-08's consented_at
stamping and server-derived grant persistence. Options gains CodeTTL
(600s PHP-parity default) following the established Options-extension
pattern.
2026-09-23 20:59:24 +02:00
Jakub Zych
4bd3b3db4f feat(08-04): implement atomic PKCE-bound code exchange in wristband
- Server.Token: JSON rejection before ParseForm, body-over-query precedence,
  Basic-over-form client auth, exact invalid_request/unsupported_grant_type/
  invalid_client/invalid_grant bodies, Cache-Control/Pragma on success only
- authenticateClient: public/confidential dispatch, constant-time secret
  compare (T-08-SECRET-TIMING)
- exchangeAuthorizationCode: single WithinTx lock/consume/mint/refresh-create
  covering code/client/redirect/resource/PKCE binding and single-use replay
  (T-08-CODE-REPLAY), sequential and concurrent proofs
- rotateRefreshToken: grant_type=refresh_token dispatches per PHP validity
  but is a deliberate invalid_grant placeholder; full rotation is 08-06
- full token_test.go behavior matrix appended alongside the RED anchor
2026-09-23 20:28:07 +02:00
Jakub Zych
5ca830beef test(08-04): add failing code-exchange RED test in wristband
- Server.Token 501 stub and TestPhase8RedCodeExchange (PHASE8_RED:code-exchange)
- Options gains AccessTokenTTL/RefreshTokenTTL with PHP-parity defaults
2026-09-23 20:25:16 +02:00
Jakub Zych
90752beb87 feat(08-03): implement exact authorize validation and pending creation in wristband
- Server.Authorize ports OAuthAuthorizeController::authorize's exact
  validation order: usable client, exact redirect, response_type=code,
  code_challenge_method=S256, challenge length, scope parsing/ceiling
  truncation, resource check, then opaque pending-request creation
- Unknown client/unregistered redirect are local text/plain 400s with no
  Location; every later failure is an ordered RFC3986 redirect with
  error/error_description/iss[/state], built via a dedicated encoder
  (never url.Values.Encode, which sorts keys and space-encodes as '+')
- Options gains Resource and PendingRequestTTL (both PHP-parity defaults)
  so authorize's resource check and 600s pending expiry are configurable
2026-09-23 20:08:37 +02:00
Jakub Zych
787e612ab3 test(08-03): add failing authorize RED test in wristband
- Server.Authorize stub returns 501
- TestPhase8RedAuthorize drives a full valid S256 request and asserts the
  exact 302 /connect success contract; fails with PHASE8_RED:authorize
  against the stub, verified fail-closed via check-phase8-red.sh
2026-09-23 20:01:18 +02:00
Jakub Zych
c0b1e3cfae feat(08-02): implement exact RFC 7591 registration in wristband
- Register validates redirect_uris/grant_types/response_types/auth-method
  in PHP's exact order, strips control characters and caps client_name at
  255 runes, and generates client_id/secret via crypto/rand base64url
- confidential clients return the raw secret once; only its sha256 hex
  persists (constant-time-comparable fixed transform)
- sweep-unconsented, the atomic cap check and the create all run inside one
  wristband.Backend.WithinTx transaction (T-08-DCR-FLOOD)
- 64 KiB body bound via http.MaxBytesReader collapses to the endpoint's
  native invalid_client_metadata body, matching D-21
2026-09-23 19:37:54 +02:00
Jakub Zych
c026b83f41 test(08-02): add failing RFC 7591 registration RED test in wristband
- TestPhase8RedRegistration asserts the exact public-client DCR success
  contract and fails while Server.Register is a 501 stub
- adds the Backend/Tx transaction-scoped store bundle (ClientStore,
  AuthCodeStore, RefreshTokenStore, AccessTokenIssuer) and wristband's own
  in-memory implementation for framework-level tests (D-07)
- adds crypto.go's fixed-transform helpers (random base64url, sha256 hex,
  constant-time compare, S256) and Options/Server seams for the DCR
  lifetimes, cap, sweep age and 64 KiB body bound (D-03/D-21)
2026-09-23 19:37:03 +02:00
Jakub Zych
c578bb58d7 feat(08-01): implement exact RFC 8414 metadata writer in wristband
- Server.Metadata now writes the unwrapped 11-field PHP-parity document
  through a local no-envelope, no-trailing-newline JSON writer with the
  PHP Cache-Control: no-cache, private header (D-06); response types,
  grant types and PKCE method stay fixed protocol constants
- TestPhase8RedMetadata now passes; TestMetadataExactBytes and
  TestMetadataUsesConfiguredOptions cover byte-exact output and the four
  configurable Options fields
2026-09-23 19:10:26 +02:00
Jakub Zych
24d35d85e8 test(08-01): add failing RFC 8414 metadata RED test and fail-closed verifier
- wristband.Server.Metadata is a compiling 501 stub; TestPhase8RedMetadata
  asserts the exact unwrapped PHP metadata document, headers and status and
  fails with the PHASE8_RED:metadata sentinel (D-06)
- scripts/check-phase8-red.sh implements the shared go/shell RED contract
  for the rest of Phase 8: exact selected test/package failure plus sentinel,
  rejecting unrelated fail actions, compile/setup failures, panics,
  malformed JSON, missing/duplicate sentinels and zero selection (D-04/D-18)
2026-09-23 19:09:14 +02:00