docs(07-01): complete framework auth primitives plan

This commit is contained in:
Jakub Zych
2026-09-22 13:43:15 +02:00
parent 8fcaff77cf
commit ae9e11f65d
3 changed files with 142 additions and 14 deletions

View File

@@ -277,7 +277,7 @@ Plans:
Plans: Plans:
**Wave 1** **Wave 1**
- [ ] 07-01-PLAN.md — bouncer JWT lifecycle, password hashing, I18N-02 locale-from-principal, lagoon.Validate extensions - [x] 07-01-PLAN.md — bouncer JWT lifecycle, password hashing, I18N-02 locale-from-principal, lagoon.Validate extensions
**Wave 2** *(blocked on 07-01)* **Wave 2** *(blocked on 07-01)*
@@ -448,7 +448,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 →
| 4. CLI scaffolding, i18n and mail | 4/4 | Complete | 2026-09-18 | | 4. CLI scaffolding, i18n and mail | 4/4 | Complete | 2026-09-18 |
| 5. Data layer full fidelity | 6/6 | Complete | 2026-09-18 | | 5. Data layer full fidelity | 6/6 | Complete | 2026-09-18 |
| 6. HTTP routing, auth groups and rate limiting | 14/14 | Complete | 2026-09-21 | | 6. HTTP routing, auth groups and rate limiting | 14/14 | Complete | 2026-09-21 |
| 7. User plugin and authentication | 0/TBD | Not started | - | | 7. User plugin and authentication | 1/6 | In Progress| |
| 8. OAuth2.1 authorization server | 0/TBD | Not started | - | | 8. OAuth2.1 authorization server | 0/TBD | Not started | - |
| 9. Backend admin authentication and schema pipeline | 0/TBD | Not started | - | | 9. Backend admin authentication and schema pipeline | 0/TBD | Not started | - |
| 10. Admin Vue SPA | 0/TBD | Not started | - | | 10. Admin Vue SPA | 0/TBD | Not started | - |

View File

@@ -3,14 +3,14 @@ gsd_state_version: 1.0
milestone: v1.0 milestone: v1.0
milestone_name: milestone milestone_name: milestone
status: executing status: executing
stopped_at: Phase 7 context gathered stopped_at: Completed 07-01-PLAN.md
last_updated: "2026-09-22T10:39:04.511Z" last_updated: "2026-09-22T11:43:03.651Z"
last_activity: 2026-09-22 -- Phase 7 planning complete last_activity: 2026-09-22
progress: progress:
total_phases: 15 total_phases: 15
completed_phases: 6 completed_phases: 6
total_plans: 43 total_plans: 43
completed_plans: 37 completed_plans: 38
percent: 40 percent: 40
--- ---
@@ -21,16 +21,16 @@ progress:
See: .planning/PROJECT.md (updated 2026-09-16) See: .planning/PROJECT.md (updated 2026-09-16)
**Core value:** An existing WinterCMS-shaped app can be ported plugin by plugin to a single Go binary without its frontend noticing: the PHP version's API contract is the acceptance test. **Core value:** An existing WinterCMS-shaped app can be ported plugin by plugin to a single Go binary without its frontend noticing: the PHP version's API contract is the acceptance test.
**Current focus:** Phase 7 — user plugin and authentication **Current focus:** Phase 07 — user-plugin-and-authentication
## Current Position ## Current Position
Phase: 7 Phase: 07 (user-plugin-and-authentication) — EXECUTING
Plan: Not started Plan: 2 of 6
Status: Ready to execute Status: Ready to execute
Last activity: 2026-09-22 -- Phase 7 planning complete Last activity: 2026-09-22
Progress: [██████████] 100% Progress: [█████████░] 88%
## Performance Metrics ## Performance Metrics
@@ -81,6 +81,7 @@ Progress: [██████████] 100%
| Phase 06 P09 | 4 min | 1 tasks | 2 files | | Phase 06 P09 | 4 min | 1 tasks | 2 files |
| Phase 06 P10 | 3h 15m | 1 tasks | 2 files | | Phase 06 P10 | 3h 15m | 1 tasks | 2 files |
| Phase 06 P11 | 12h 30m | 1 tasks | 1 files | | Phase 06 P11 | 12h 30m | 1 tasks | 1 files |
| Phase 07 P01 | 12 min | 3 tasks | 20 files |
## Accumulated Context ## Accumulated Context
@@ -191,6 +192,7 @@ Recent decisions affecting current work:
- [Phase 06]: Assert exact denial bytes before JSON shape checks — Whitespace normalization would hide response-contract regressions. - [Phase 06]: Assert exact denial bytes before JSON shape checks — Whitespace normalization would hide response-contract regressions.
- [Phase 06]: Retain all four earlier accepted risks unchanged; T-06-23 through T-06-27 are mitigated, not accepted or deferred. — Both repositories' authoritative race and vet gates passed, and each new threat has concrete source and named regression evidence. - [Phase 06]: Retain all four earlier accepted risks unchanged; T-06-23 through T-06-27 are mitigated, not accepted or deferred. — Both repositories' authoritative race and vet gates passed, and each new threat has concrete source and named regression evidence.
- [Phase 06]: Anonymous inline limiter identity is documented only as inline:domainless|<ClientIP>, excluding policy text and request or forwarded Host inputs. — The production resolver and three executed regressions prove Host rotation and inline-parameter changes cannot create fresh anonymous budgets while authenticated principals keep isolated u:<id> keys. - [Phase 06]: Anonymous inline limiter identity is documented only as inline:domainless|<ClientIP>, excluding policy text and request or forwarded Host inputs. — The production resolver and three executed regressions prove Host rotation and inline-parameter changes cannot create fresh anonymous budgets while authenticated principals keep isolated u:<id> keys.
- [Phase 07]: Blacklist storage expiry follows PHP jwt-auth (later of exp and iat+refreshTTL, plus one minute). — Using the raw access exp would drop a logged-out token that is still inside the refresh window.
### Pending Todos ### Pending Todos
@@ -212,6 +214,6 @@ Items acknowledged and carried forward from previous milestone close:
## Session Continuity ## Session Continuity
Last session: 2026-09-21T22:24:22.108Z Last session: 2026-09-22T11:42:50.114Z
Stopped at: Phase 7 context gathered Stopped at: Completed 07-01-PLAN.md
Resume file: .planning/phases/07-user-plugin-and-authentication/07-CONTEXT.md Resume file: None

View File

@@ -0,0 +1,126 @@
---
phase: 07-user-plugin-and-authentication
plan: 01
subsystem: auth
tags: [jwt, bcrypt, blacklist, locale, validation]
requires:
- phase: 06-http-routing-auth-groups-and-rate-limiting
provides: Bearer JWT guard, Principal, lagoon.Validate, surf middleware registration
provides:
- 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
affects: [07-02, 07-03, 07-04]
tech-stack:
added: [golang.org/x/crypto v0.57.0]
patterns: [HS256 mint with hardcoded prv hash, refresh without exp validation, grace-windowed jti blacklist]
key-files:
created:
- bouncer/mint.go
- bouncer/refresh.go
- bouncer/blacklist.go
- bouncer/password.go
- surf/locale_from_principal.go
modified:
- bouncer/jwt.go
- bouncer/context.go
- surf/router.go
- lagoon/validate.go
- go.mod
key-decisions:
- "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"
patterns-established:
- "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"
requirements-completed: [AUTH-01, I18N-02]
duration: 12min
completed: 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.