fix(cabana): enforce tokens_valid_after and is_activated on admin refresh

Fixes review finding CR-01 (quick 260927-q23): POST {prefix}/api/v1/auth/refresh
minted a new token without loading the admin, so a session kept alive by the
SPA's refresh-on-401 survived admin:reset-password, deactivation and deletion.
This broke Phase 9 truth T-09-04.

- bouncer: extract the JWT guard's subject lookup into subjectPrincipal and
  issuedBeforeCutoff (same order and messages), add ErrSubjectRejected
- bouncer: add RefreshAudienceFor, which runs the guard's subject checks
  after the token-only checks and before minting; Refresh and
  RefreshAudience are unchanged (nil hook)
- cabana: share one lazyBackendUsers provider between the backend guard and
  refresh; a cookie refresh refused for its subject expires summer_admin
- test: TestAdminRefreshRevocation (Postgres, real admin:reset-password)
This commit is contained in:
Jakub Zych
2026-09-27 18:58:15 +02:00
parent 815cb903c6
commit be4a923f36
6 changed files with 246 additions and 19 deletions

View File

@@ -41,6 +41,7 @@ type service struct {
loginDecay int
issuer string
bl bouncer.BlacklistStore
users bouncer.UserProvider // the backend guard's provider, reused by refresh
prefix string
spa http.Handler
// insecureCookie drops Secure from the admin cookie (backend.cookie_secure
@@ -109,7 +110,8 @@ func Activate(app *backpack.App, plugins []party.Plugin) (*Routes, error) {
}
}
bl := adminBlacklist(app)
guard := bouncer.NewBackendJWTGuard(secret, lazyBackendUsers{app: app, reg: reg}, bl, writeUnauthenticated, AdminCookieName)
users := lazyBackendUsers{app: app, reg: reg}
guard := bouncer.NewBackendJWTGuard(secret, users, bl, writeUnauthenticated, AdminCookieName)
if _, err := guards.Middleware("backend"); err != nil {
if err := guards.Register("summercms.cabana", "backend", guard); err != nil {
return nil, err
@@ -132,6 +134,7 @@ func Activate(app *backpack.App, plugins []party.Plugin) (*Routes, error) {
loginDecay: loginDecay,
issuer: adminIssuer(app, prefix),
bl: bl,
users: users,
prefix: prefix,
insecureCookie: !secureCookie,