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
Task 1: Approve golang.org/x/crypto — human checkpoint, approved. Promotion landed in the Task 3 commit.
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.