Files
summercms/.planning/phases/07-user-plugin-and-authentication/07-01-SUMMARY.md

5.5 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
07-user-plugin-and-authentication 01 auth
jwt
bcrypt
blacklist
locale
validation
phase provides
06-http-routing-auth-groups-and-rate-limiting Bearer JWT guard, Principal, lagoon.Validate, surf middleware registration
bouncer.Mint, Refresh, BlacklistStore, VerifyClaims
bcrypt HashPassword/CheckPassword/NeedsRehash
Principal.PreferredLocale and TokensValidAfter
surf locale.from-principal middleware
lagoon email, confirmed, different, and mimes rules
07-02
07-03
07-04
added patterns
golang.org/x/crypto v0.57.0
HS256 mint with hardcoded prv hash
refresh without exp validation
grace-windowed jti blacklist
created modified
bouncer/mint.go
bouncer/refresh.go
bouncer/blacklist.go
bouncer/password.go
surf/locale_from_principal.go
bouncer/jwt.go
bouncer/context.go
surf/router.go
lagoon/validate.go
go.mod
Blacklist storage expiry follows PHP jwt-auth: later of exp and iat+refreshTTL, plus one minute
A blacklisted jti reuses the existing bad-signature 401 text
Refresh rebuilds the access TTL from the old token's exp-iat because the signature has no separate ttl argument
golang.org/x/crypto was promoted with go get @latest (v0.57.0) after the human checkpoint
Pattern: Mint stamps iss from the calling endpoint URL and prv from the hardcoded User class hash
Pattern: only Refresh uses jwt.WithoutClaimsValidation; Verify and the guard still require exp
AUTH-01
I18N-02
12min 2026-09-22

Phase 7 Plan 01: Framework auth primitives Summary

JWT mint, sliding refresh, and a grace-windowed jti blacklist, plus bcrypt, a post-auth locale override, and email/confirmed/different/mimes validation.

Performance

  • Duration: 12 min
  • Started: 2026-09-22T11:28:00Z
  • Completed: 2026-09-22T11:39:34Z
  • Tasks: 3
  • Files modified: 20

Accomplishments

  • bouncer.Mint / Refresh / BlacklistStore / VerifyClaims are in place for the user plugin's login, refresh, and logout handlers.
  • Principal now carries PreferredLocale and TokensValidAfter, and surf registers locale.from-principal.
  • lagoon.Validate accepts email, confirmed, different:field, and mimes:list. golang.org/x/crypto is a direct dependency, and a real PHP $2y$ hash verifies.

Task Commits

  1. Task 1: Approve golang.org/x/crypto — human checkpoint, approved. Promotion landed in the Task 3 commit.
  2. Task 2: JWT lifecycle primitives — 251f3cc (test), cad445a (feat)
  3. Task 3: Password hashing, locale override, validation — bccd7f8 (test), 8fcaff7 (feat)

Files Created/Modified

  • bouncer/mint.go — HS256 mint with the hardcoded prv hash
  • bouncer/refresh.go — sliding refresh that skips exp and blacklists the old jti
  • bouncer/blacklist.go — memory and Postgres stores with a grace window
  • bouncer/password.go — bcrypt hash, check, and rehash
  • bouncer/jwt.go — cookie fallback, blacklist check, TokensValidAfter cutoff, VerifyClaims
  • bouncer/context.go — PreferredLocale and TokensValidAfter
  • surf/locale_from_principal.go — post-auth locale override
  • surf/router.go — registers locale.from-principal
  • lagoon/validate.go — email, confirmed, different, mimes
  • go.mod — direct golang.org/x/crypto v0.57.0

Decisions Made

Blacklist rows live until the later of the old exp and iat+refreshTTL, plus one minute, matching PHP Blacklist::getMinutesUntilExpired. A blacklisted token returns the existing "Token Signature could not be verified." body. Refresh copies the previous access lifetime (exp-iat) onto the new token.

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] Blacklist storage expiry was the raw access exp

  • Found during: Task 2 (JWT lifecycle primitives)
  • Issue: The plan set expiresAt to the old token's exp. For a token that is already expired but still inside refreshTTL, that timestamp is in the past, so lazy expiry and Sweep would drop the row and a logged-out token could be refreshed again.
  • Fix: Storage expiry is the later of exp and iat+refreshTTL, plus one minute. validUntil is still now+grace.
  • Files modified: bouncer/refresh.go
  • Verification: TestRefreshBlacklistsOldJTI (expired access token, grace 0, still blacklisted; grace window still open otherwise)
  • Committed in: cad445a

Total deviations: 1 auto-fixed (Rule 1) Impact on plan: Correctness fix so logout and refresh revocation survive the refresh window. No new API surface.

Issues Encountered

None

User Setup Required

None - no external service configuration required.

Next Phase Readiness

Ready for 07-02. The user plugin can import Mint, Refresh, NewPostgresBlacklist, HashPassword, and the new Principal fields. AUTH-01 and I18N-02 are not fully delivered yet: the session routes, locale endpoints, and must-change-password exemption are still 07-02 through 07-04.

Self-Check: PASSED

  • bouncer/mint.go, bouncer/refresh.go, bouncer/blacklist.go, bouncer/password.go, and surf/locale_from_principal.go exist.
  • git log --oneline --grep=07-01 shows the test and feat commits above.
  • go vet ./... and go test ./... -short passed. go test ./bouncer/... ./surf/... ./lagoon/... -race -short passed.