fix: logout revokes the session under serve: the jwt blacklist and GORM hooks are wired when the database is published
Boot runs before summer serve publishes the database, so the plugin's GORM hooks, the Postgres jwt_blacklist store and its sweep now go through lagoon.OnDatabase, and the jwt guard resolves the published store on every request. A logged-out token is refused on jwt.auth, /fetch and /refresh. The jwt guard reads the cookie names from classes.SessionCookieNames().
This commit is contained in:
81
serve_order_test.go
Normal file
81
serve_order_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"git.golem15.com/golem15/sm-user-plugin/classes"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/bouncer"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TestBootBeforeDatabase pins logout revocation when Boot runs before the
|
||||
// database is published, as under summer serve: nothing database-backed is
|
||||
// installed before lagoon.Publish, the Postgres blacklist store and the GORM
|
||||
// hooks are installed when it runs, and a token ended by POST /logout is
|
||||
// refused on jwt.auth through every carrier, at /fetch and at /refresh, with
|
||||
// its jwt_blacklist row.
|
||||
func TestBootBeforeDatabase(t *testing.T) {
|
||||
// The classes hook registrants are process-global and cannot be removed,
|
||||
// so the test hook is gated and goes inert once this test ends.
|
||||
var hooked atomic.Pointer[gorm.DB]
|
||||
var active atomic.Bool
|
||||
active.Store(true)
|
||||
t.Cleanup(func() { active.Store(false) })
|
||||
classes.RegisterHook(func(gdb *gorm.DB) error {
|
||||
if active.Load() {
|
||||
hooked.Store(gdb)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
e := newServeOrderEnv(t, func(app *backpack.App) {
|
||||
if _, ok := app.Lookup[*gorm.DB](); ok {
|
||||
t.Fatal("a *gorm.DB is published before lagoon.Publish")
|
||||
}
|
||||
if _, ok := app.Lookup[bouncer.BlacklistStore](); ok {
|
||||
t.Fatal("a bouncer.BlacklistStore is published before lagoon.Publish")
|
||||
}
|
||||
if hooked.Load() != nil {
|
||||
t.Fatal("the GORM hooks ran before lagoon.Publish")
|
||||
}
|
||||
})
|
||||
|
||||
store, ok := e.app.Lookup[bouncer.BlacklistStore]()
|
||||
if !ok {
|
||||
t.Fatal("no bouncer.BlacklistStore published after lagoon.Publish")
|
||||
}
|
||||
if _, ok := store.(*bouncer.PostgresBlacklist); !ok {
|
||||
t.Fatalf("published blacklist store is %T, want *bouncer.PostgresBlacklist", store)
|
||||
}
|
||||
if got := hooked.Load(); got != e.gdb {
|
||||
t.Fatalf("the GORM hooks ran with %p, want the published handle %p", got, e.gdb)
|
||||
}
|
||||
|
||||
token := e.sessionLogin(t, "serve-order")
|
||||
if code := e.guarded(t, token, ""); code != http.StatusOK {
|
||||
t.Fatalf("jwt.auth before logout (bearer) = %d", code)
|
||||
}
|
||||
for _, name := range classes.SessionCookieNames() {
|
||||
if code := e.guarded(t, token, name); code != http.StatusOK {
|
||||
t.Fatalf("jwt.auth before logout (cookie %q) = %d", name, code)
|
||||
}
|
||||
}
|
||||
|
||||
assertLoggedOut(t, e.call(t, http.MethodPost, userLogoutPath, token, nil))
|
||||
e.assertSessionEnded(t, token)
|
||||
|
||||
jti, _ := parseToken(t, token)["jti"].(string)
|
||||
if jti == "" {
|
||||
t.Fatal("session token has no jti")
|
||||
}
|
||||
var rows int64
|
||||
if err := e.gdb.Raw(`SELECT COUNT(*) FROM jwt_blacklist WHERE jti = ?`, jti).Scan(&rows).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rows != 1 {
|
||||
t.Fatalf("jwt_blacklist rows for %s = %d, want 1", jti, rows)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user