feat(09-02): implement backend JWT lifecycle, throttle, and auth logs
- Refresh, logout, and me use a separate PostgreSQL jti blacklist and safe profile - Login stamps last_login only after a successful check and throttles repeated attempts
This commit is contained in:
@@ -3,6 +3,7 @@ package cabana
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
@@ -23,11 +24,17 @@ type Routes struct {
|
||||
}
|
||||
|
||||
type service struct {
|
||||
app *backpack.App
|
||||
reg *Registry
|
||||
secret string
|
||||
ttl time.Duration
|
||||
issuer string
|
||||
app *backpack.App
|
||||
reg *Registry
|
||||
secret string
|
||||
ttl time.Duration
|
||||
refreshTTL time.Duration
|
||||
grace time.Duration
|
||||
bcryptCost int
|
||||
loginMax int
|
||||
loginDecay int
|
||||
issuer string
|
||||
bl bouncer.BlacklistStore
|
||||
}
|
||||
|
||||
// Activate compiles admin controllers and, when any exist, requires
|
||||
@@ -58,10 +65,7 @@ func Activate(app *backpack.App, plugins []party.Plugin) (*Routes, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var bl bouncer.BlacklistStore
|
||||
if store, ok := app.Lookup[bouncer.BlacklistStore](); ok {
|
||||
bl = store
|
||||
}
|
||||
bl := adminBlacklist(app)
|
||||
guard := bouncer.NewBackendJWTGuard(secret, lazyBackendUsers{app: app}, bl, writeUnauthenticated)
|
||||
if _, err := guards.Middleware("backend"); err != nil {
|
||||
if err := guards.Register("summercms.cabana", "backend", guard); err != nil {
|
||||
@@ -72,12 +76,19 @@ func Activate(app *backpack.App, plugins []party.Plugin) (*Routes, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
loginMax, loginDecay := adminLoginWindow(app)
|
||||
svc := &service{
|
||||
app: app,
|
||||
reg: reg,
|
||||
secret: secret,
|
||||
ttl: adminTTL(app),
|
||||
issuer: adminIssuer(app),
|
||||
app: app,
|
||||
reg: reg,
|
||||
secret: secret,
|
||||
ttl: adminTTL(app),
|
||||
refreshTTL: adminRefreshTTL(app),
|
||||
grace: adminGrace(app),
|
||||
bcryptCost: adminBcryptCost(app),
|
||||
loginMax: loginMax,
|
||||
loginDecay: loginDecay,
|
||||
issuer: adminIssuer(app),
|
||||
bl: bl,
|
||||
}
|
||||
return &Routes{Middleware: mw, Mount: svc.mount}, nil
|
||||
}
|
||||
@@ -102,10 +113,14 @@ func (p lazyBackendUsers) FindByID(ctx context.Context, id uint) (*bouncer.Princ
|
||||
}
|
||||
|
||||
func (s *service) mount(r pact.Router) {
|
||||
throttle := fmt.Sprintf("throttle:%d,%d", s.loginMax, s.loginDecay)
|
||||
r.GroupRaw("/_admin/api/v1/auth", nil, func(g pact.Router) {
|
||||
g.Post("/login", s.login)
|
||||
g.Post("/login", s.login, throttle)
|
||||
g.Post("/refresh", s.refresh)
|
||||
})
|
||||
r.GroupRaw("/_admin/api/v1", []string{"backend"}, func(g pact.Router) {
|
||||
g.Post("/auth/logout", s.logout)
|
||||
g.Get("/auth/me", s.me)
|
||||
g.Get("/{vendor}/{plugin}/{controller}/schema/list", s.listSchema)
|
||||
constrainController(g)
|
||||
g.Get("/{vendor}/{plugin}/{controller}", s.list)
|
||||
@@ -182,6 +197,7 @@ func (s *service) protect(w http.ResponseWriter, r *http.Request, fn func(*Compi
|
||||
return
|
||||
}
|
||||
if !Allows(principal, requiredOf(cc.Controller)) {
|
||||
s.logAuth(r, "denied", principal.ID)
|
||||
WriteError(w, http.StatusForbidden, "forbidden", msgForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user