Skip to content

Sample: Session Revocation

I want JWTs to be revocable server-side — logout, password change, or admin force-logout should invalidate outstanding tokens before they expire.

The production wiring shape: an app builds createSessionCallbacks() against a live DB handle, then hands the creator, revoker, and checker to buildServer({ auth: ... }). From there the framework handles the rest — login persists a sid, the middleware checks it on every request, logout flips the DB row, and a forged JWT without a matching session is rejected.

  1. createSessionCallbacks({ db }) returns sessionCreator, sessionRevoker, and sessionChecker backed by read_user_sessions.
  2. buildServer({ auth: { sessionCreator, sessionRevoker, sessionChecker, ... } }) links the JWT jti claim to that table on every request.
  3. After logout (or mass-revoke on password change), the same JWT returns 401 even though the signature is still valid.
const db = createDbConnection(config.databaseUrl);
const callbacks = createSessionCallbacks({ db });
const server = buildServer({
registry: buildRegistry([
createSessionsFeature({
autoRevokeOnPasswordChange: callbacks.sessionMassRevoker,
}),
// ... other features
]),
context: { db, /* ... */ },
jwtSecret: process.env.JWT_SECRET,
auth: {
membershipQuery: "tenant:query:memberships",
loginHandler: "auth-email-password:write:login",
sessionCreator: callbacks.sessionCreator,
sessionRevoker: callbacks.sessionRevoker,
sessionChecker: callbacks.sessionChecker,
},
});
  1. Sessions are a feature, not built-in. Without createSessionsFeature() and wired callbacks, the app issues plain stateless JWTs — valid until expiry, no revocation path.
  2. Storage is pluggable. This sample uses the default DB-backed impl; swap in Redis or Memcached by implementing the same SessionCreator / SessionRevoker / SessionChecker signatures.
  3. jti is the link. When a checker is wired, the middleware never trusts the JWT alone — it confirms the session row is still live.
  • End-to-end: login → authenticated query → logout → same token rejected with 401
  • Wiring contract: copies the buildServer({ auth: ... }) block from the sample comment verbatim

The feature entry point — embedded straight from the source file, so the code here is exactly what runs. Multi-file samples keep their remaining files next to it on GitHub (link below):

// Session Revocation Sample
//
// Shows the production-wiring shape: an app builds `createSessionCallbacks()`
// against a live DB handle, then hands the creator/revoker/checker to
// `buildServer({ auth: ... })`. From there the framework handles the rest —
// login persists a sid, the middleware checks it on every request, logout
// flips the DB row, a forged JWT without a matching session is rejected.
//
// What to copy into your own app:
//
// const db = createDbConnection(config.databaseUrl);
// const callbacks = createSessionCallbacks({ db });
// const server = buildServer({
// registry: buildRegistry([
// // Pass the mass-revoker into the sessions feature to wire
// // "password-change signs you out everywhere" — the feature registers
// // a cross-feature entity-hook on the user entity that runs it.
// createSessionsFeature({
// autoRevokeOnPasswordChange: callbacks.sessionMassRevoker,
// }),
// // ... other features
// ]),
// context: { db, ... },
// jwtSecret: config.jwtSecret,
// auth: {
// membershipQuery: "tenant:query:memberships",
// loginHandler: "auth-email-password:write:login",
// sessionCreator: callbacks.sessionCreator,
// sessionRevoker: callbacks.sessionRevoker,
// sessionChecker: callbacks.sessionChecker,
// },
// });
//
// Design rules the sample demonstrates:
//
// 1. Sessions are a feature, not built-in. An app that doesn't register
// `createSessionsFeature()` (or doesn't wire callbacks) issues plain
// stateless JWTs — valid until expiry, no revocation path.
// 2. Session storage is abstracted behind the three callback signatures.
// This sample uses the default DB-backed impl; you could swap in a
// Redis- or Memcached-backed version by wiring your own callbacks that
// match `SessionCreator`/`SessionRevoker`/`SessionChecker`.
// 3. The `jti` claim on the JWT is the link between stateless token and
// stateful server. The middleware never trusts the JWT alone when a
// checker is wired — it confirms the sid is still live.
export {
createSessionCallbacks,
createSessionsFeature,
} from "@cosmicdrift/kumiko-bundled-features/sessions";

📄 On GitHub: samples/recipes/session-revocation/src/feature.ts