Lifecycle hooks
Both useJWESession and useJWSSession accept a hooks object in their config — attach callbacks to key moments in the session lifecycle for logging, revocation tracking, key rotation, or side-effects.
useJWESession(event, {
key: secret,
hooks: {
onRead({ session, event, config }) {
/* ... */
},
onUpdate({ session, oldSession, event, config }) {
/* ... */
},
onClear({ oldSession, event, config }) {
/* ... */
},
onExpire({ session, event, error, config }) {
/* ... */
},
onError({ session, event, error, config }) {
/* ... */
},
onUnsealKeyLookup({ header, event, config }) {
/* JWE: return a key */
},
// JWS uses `onVerifyKeyLookup` instead
},
});
All hooks are async-compatible.
When each hook fires
| Hook | Fires when | Mutually exclusive with |
|---|---|---|
onRead | A valid session token was decoded and loaded. | onExpire, onError |
onUpdate | After a successful session.update() (token signed/encrypted). | — |
onClear | After an explicit session.clear() call. | onExpire |
onExpire | A token's exp is in the past (clock-driven expiry). | onRead, onClear |
onError | Token verification/decryption fails for a non-expiry reason. | onRead |
onExpiredoes not call clearSession. It invalidates the cookie inline and fires its hook. onClear is reserved for explicit termination (user logout, forced revocation) — these two events are semantically distinct and should trigger different downstream side effects.onRead — every valid request
Fires once per request when a valid session token is loaded. Use for request-scoped logging, last-activity tracking, etc.
hooks: {
async onRead({ session, event }) {
await metrics.increment("session.read", {
userId: session.data.userId,
});
},
},
Only fires when the session was successfully decoded — failures go through onError or onExpire instead.
onUpdate — session created or rotated
Fires after session.update() succeeds. Receives both the new session and a snapshot of the old one:
hooks: {
async onUpdate({ session, oldSession, event }) {
// First-time session creation: oldSession.id is undefined
if (!oldSession.id) {
await audit.log("session.created", { userId: session.data.userId });
} else {
await audit.log("session.rotated", {
oldJti: oldSession.id,
newJti: session.id,
});
}
},
},
Useful for audit trails, detecting first-time logins, and syncing session state to external stores.
onClear — explicit termination
Fires after session.clear(). Receives the old session snapshot (may be undefined if no session existed):
hooks: {
async onClear({ oldSession, event }) {
if (oldSession?.id) {
await revocationList.add(oldSession.id);
}
},
},
This is the logout path — users intentionally ending their session. onExpire is a different event (clock-driven, not user-driven) and doesn't fire here.
onExpire — natural expiry
Fires when a token's exp claim is in the past. Receives a snapshot of the expired token:
hooks: {
async onExpire({ session, event, error, config }) {
// session.id is the jti from the expired token (if the token was decodable)
// session.expiresAt is the old exp × 1000 ms
// error is a JWTError with code like "ERR_JWT_EXPIRED"
if (session.id) {
await revocationList.add(session.id);
}
},
},
onExpire is where the refresh-token pattern lives — check a separate refresh session and mint a new access token:
import { getJWESession, updateJWSSession } from "unjwt/adapters/h3v2";
hooks: {
async onExpire({ event, config }) {
const refresh = await getJWESession(event, refreshConfig);
if (refresh.data.sub) {
await updateJWSSession(event, config, {
sub: refresh.data.sub,
scope: refresh.data.scope,
});
}
},
},
session.id in onExpire is populated from the expired token's jti when the token was cryptographically valid but past its exp. This lets you correlate expired-token events with issued-session records (e.g. to decrement an active-session counter).
onError — verification failures
Fires when a token exists in the cookie/header but fails verification for a reason other than expiry — bad signature, unknown kid, disallowed algorithm, malformed structure:
hooks: {
async onError({ session, event, error }) {
logger.warn("session.verify.failed", {
code: error.code,
ip: event.request.headers.get("x-forwarded-for"),
});
},
},
Useful for tamper detection and fraud signals. The session argument is an empty manager (no data, id: undefined) because verification couldn't produce a valid session.
Key-lookup hooks
These are different — they don't fire on lifecycle events. They override the key used for verification / unsealing on a per-request basis.
onVerifyKeyLookup (JWS)
hooks: {
onVerifyKeyLookup({ header, event, config }) {
// Return a JWSVerifyJWK (HMAC oct or public asymmetric with a signing alg) or a JWKSet
return keyStore.forKid(header.kid);
},
},
Overrides the config.key just for verification — signing still uses config.key. Classic use: rotate signing keys while continuing to verify tokens signed with retired keys.
onUnsealKeyLookup (JWE)
hooks: {
async onUnsealKeyLookup({ header, event, config }) {
// Return a symmetric or private JWK
return await keyStore.forKid(header.kid!) ?? fallbackKey;
},
},
Same idea for encrypted sessions — decryption uses the lookup result, encryption uses config.key.
onUnsealKeyLookup returns the old key when the token's kid matches. Once all old-key-wrapped tokens have expired, retire the old key.Hook signatures
interface SessionHooksJWS<T, MaxAge, TEvent> {
onRead?(args: {
session: SessionJWS<T, MaxAge> & { id: string; token: string };
event: TEvent;
config: SessionConfigJWS<T, MaxAge, TEvent>;
}): void | Promise<void>;
onUpdate?(args: {
session: SessionJWS<T, MaxAge> & { id: string; token: string };
oldSession: SessionJWS<T, MaxAge>;
event: TEvent;
config: SessionConfigJWS<T, MaxAge, TEvent>;
}): void | Promise<void>;
onClear?(args: {
oldSession: SessionJWS<T, MaxAge> | undefined;
event: TEvent;
config: SessionConfigJWS<T, MaxAge, TEvent>;
}): void | Promise<void>;
onExpire?(args: {
session: {
id: string | undefined;
createdAt: number | undefined;
expiresAt: number | undefined;
token: string;
};
event: TEvent;
error: Error;
config: SessionConfigJWS<T, MaxAge, TEvent>;
}): void | Promise<void>;
onError?(args: {
session: SessionJWS<T, MaxAge>;
event: TEvent;
error: any;
config: SessionConfigJWS<T, MaxAge, TEvent>;
}): void | Promise<void>;
onVerifyKeyLookup?(args: {
header: JWKLookupFunctionHeader;
event: TEvent;
config: SessionConfigJWS<T, MaxAge, TEvent>;
}): JWKSet | JWSVerifyJWK | Promise<JWKSet | JWSVerifyJWK>;
}
SessionHooksJWE has the same shape; swap JWS types for JWE types, and onVerifyKeyLookup for onUnsealKeyLookup (which returns a JWEDecryptJWK).