@nifrajs/auth 5.4.0 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @nifrajs/auth
2
2
 
3
+ ## 5.5.0
4
+
5
+ ### Security and migration
6
+
7
+ - 8debc4c: Session managers refuse eventually consistent stores, including `KVSessionStore`. Cached session records and missing revocation markers could restore a logged-out session and let it keep renewing. Existing KV-backed session applications must migrate to a strongly consistent shared `SessionStore`; the legacy KV adapter remains exported for reading and migration. Custom adapters may declare `consistency: "strong"`; omitting it asserts the same required contract.
8
+
9
+ ### Minor Changes
10
+
11
+ - 662afb0: A store-backed session that `regenerate()` rotates while a logout of it runs can no longer come out of that race as a live session. A rotated ID is now the session's original ID, `~`, and a new random part, and logout with any generation's cookie revokes every generation, so the rotating request either sends no cookie or sends one that loads as an empty session. `SessionStore` gains an optional `rotate(previousId, nextId, record, marker)` that moves an existing, unrevoked record to its new ID in one atomic step and resolves whether it did. `MemorySessionStore` implements it, and drops every generation's record when one is revoked, so evicting a revocation marker cannot bring them back. A store without `rotate` keeps working: rotation writes an `<id>.rotated` marker beside the `<id>.revoked` one, and the store must keep both until their `expiresAt`. A load reads the rotation marker, and for a rotated ID the original ID's revocation marker, in parallel with the record.
12
+ - f6a71bc: `SessionStore` gains an optional `replace(id, record)` method that overwrites `id` only while it still holds a record and resolves whether it wrote. When a store implements it, a loaded session renews through `replace` instead of `set`, so a logout that lands between the commit's checks and its write wins, even once the logout's revocation marker is gone. Implement it as one atomic step that also sets the new expiry, such as Redis `SET key value XX PXAT <record.expiresAt>` (a bare `XX` drops the key's TTL) or a SQL `UPDATE ... WHERE id = ?` that reports the rows it changed. `MemorySessionStore` implements it. A store without it works as before and must keep each revocation marker until its `expiresAt`, as `KVSessionStore` does: Workers KV has no conditional write.
13
+
14
+ ### Patch Changes
15
+
16
+ - b747034: A store-backed session renewal on a store without `replace()` can no longer undo a logout. When the record it renews expires while the write is in flight, a logout in that gap finds nothing to revoke; the renewal now deletes what it wrote and sends no cookie instead of restoring the session.
17
+ - 32c701e: In store mode, `destroy(c)` called with a cookie that a `regenerate()` rotation has already replaced ends the whole session family only within 60 seconds of that rotation, while the rotating response may still be on its way to the browser. After that it drops only the replaced ID, so a pre-login cookie kept elsewhere can no longer end the session that replaced it. A cookie naming the live session, and `destroy(c, session)` with a loaded session, end the family as before.
18
+ - e14bc02: A store-backed session that a request loaded before a logout or rotation can no longer be written back by that request's later `commit()`. A loaded session now commits only while its record still exists and no revocation marker names it, so the guarantee no longer depends on the logout's marker outliving the request: other revocations pushing that marker out of a bounded `MemorySessionStore` cannot bring the session back, and neither can a commit that reads between a logout's marker write and its delete. The commit reads the record and the marker in parallel, so it waits on no extra store round trip. A `MemorySessionStore`, which can evict markers, also makes the renewal itself conditional: it writes only while the record still exists, in one step with the check, so a logout that lands after the commit's reads still wins.
19
+
3
20
  ## 5.4.0
4
21
 
5
22
  No changes in this release.
package/README.md CHANGED
@@ -13,6 +13,38 @@ Part of the **[nifra](https://nifra.dev)** full-stack TypeScript framework - one
13
13
  bun add @nifrajs/auth
14
14
  ```
15
15
 
16
+ ## Store revocation and rotation
17
+
18
+ Store mode requires strongly consistent reads: every instance must observe completed writes and
19
+ deletes, including revocation markers. `createSessions` refuses adapters declaring
20
+ `consistency: "eventual"`. `KVSessionStore` declares this and is no longer accepted: Workers KV can
21
+ serve a cached session and a cached missing marker after logout, allowing renewal to restore it.
22
+ Its export remains available for reading and migrating legacy records.
23
+
24
+ Use a strongly consistent shared `SessionStore`, for example a transactional adapter or a Durable
25
+ Object, in production. `MemorySessionStore` remains suitable for development or an explicitly allowed
26
+ single-instance deployment. Custom adapters may declare `consistency: "strong"`; omitting the
27
+ declaration preserves compatibility but asserts the same strong-consistency contract. Wrapping or
28
+ relabeling Workers KV does not make its reads strongly consistent. No eventual-consistency opt-in is
29
+ provided, including when rolling renewal is disabled.
30
+
31
+ In store mode, `destroy` revokes the entire session family, including later IDs issued by
32
+ `regenerate`. A cookie that a rotation has already replaced does this only for 60 seconds after
33
+ the rotation, while the rotating response may still be on its way to the browser. After that,
34
+ logging out with it drops only that superseded ID, so a kept pre-login cookie cannot end the
35
+ session that replaced it. Rotated IDs remain opaque signed cookie values. Store adapters must retain
36
+ `<id>.rotated` and `<family>.revoked` marker records until their `expiresAt`, including after the
37
+ original session record is deleted. Do not interpret markers as sessions.
38
+ For store adapters, the family is the session's original ID: a rotated ID has the form
39
+ `<family>~<replacement>`, and repeated rotations keep that same family prefix.
40
+
41
+ Adapters may implement `SessionStore.rotate(previousId, nextId, record, marker)` atomically: refuse
42
+ when the previous record is missing, rotated, or its family is revoked; otherwise write the rotation
43
+ marker, delete the old record, and store the replacement in one transaction. Return whether it won.
44
+ The built-in memory store implements this operation. Adapters with only `get`/`set`/`delete` keep
45
+ logout markers separate from rotation markers and check family revocation after writing; concurrent
46
+ rotations can produce multiple replacement IDs, all revoked by one logout of the family.
47
+
16
48
  ## Docs
17
49
 
18
50
  - Reference: <https://nifra.dev/docs>
package/dist/session.d.ts CHANGED
@@ -46,7 +46,8 @@ export interface SessionOptions {
46
46
  * secret, drop the old one after `maxAge` has elapsed. A bare secret swap (no list) invalidates
47
47
  * all existing sessions. */
48
48
  readonly secret: string | readonly string[];
49
- /** Provide a store for **store mode**; omit for **cookie mode** (stateless). */
49
+ /** Strongly consistent backend for **store mode**; omit for **cookie mode** (stateless).
50
+ * Declared eventually consistent stores (including KVSessionStore) are refused. */
50
51
  readonly store?: SessionStore;
51
52
  /** Cookie name. Default `nifra_session`. */
52
53
  readonly cookieName?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
package/dist/session.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * anonymous session, never an error.
11
11
  */
12
12
  import { hasDuplicateCookie, parseCookies, signValue, unsignValue, } from "@nifrajs/core/cookies";
13
- import { REVOKED_SUFFIX } from "./store.js";
13
+ import { isSessionMarker, REVOKED_SUFFIX, ROTATED_SUFFIX, rotateExisting, sessionFamily, } from "./store.js";
14
14
  const DAY_SECONDS = 86_400;
15
15
  // 256-bit floor, matching @nifrajs/core's signed-cookie/webhook guard. Sessions sign via core's
16
16
  // signValue (which enforces the same bar); this early check gives an auth-specific message before
@@ -29,6 +29,10 @@ const randomId = () => encodeBase64Url(crypto.getRandomValues(new Uint8Array(32)
29
29
  // commits after it rewrites the record, and a blind `set` cannot be made conditional on every store,
30
30
  // but it cannot overwrite a key it never writes. `.` is outside the base64url id alphabet.
31
31
  const revokedKey = (id) => `${id}${REVOKED_SUFFIX}`;
32
+ // A superseded cookie is still the user's own while the rotating response is on its way to the
33
+ // browser. Past that it is a copy someone else kept (a planted pre-login cookie), and it must not end
34
+ // the session that replaced it.
35
+ const ROTATION_GRACE_MS = 60_000;
32
36
  /** Validate a cookie-mode payload at the trust boundary - malformed JSON / shape → `null` (no session). */
33
37
  const parsePayload = (json) => {
34
38
  let value;
@@ -49,6 +53,12 @@ const parsePayload = (json) => {
49
53
  };
50
54
  export function createSessions(options) {
51
55
  const { secret, store } = options;
56
+ // A cached marker miss can authenticate or recreate a session after logout, even with fixed expiry.
57
+ if (store?.consistency !== undefined && store.consistency !== "strong") {
58
+ throw new Error("[nifra/auth] sessions require a strongly consistent store for logout and rotation; " +
59
+ "eventually consistent stores such as KVSessionStore are not supported. " +
60
+ "Use a strongly consistent shared SessionStore instead.");
61
+ }
52
62
  const secretList = typeof secret === "string" ? [secret] : secret;
53
63
  if (secretList.length === 0) {
54
64
  throw new Error("[nifra/auth] session secret list cannot be empty");
@@ -131,15 +141,18 @@ export function createSessions(options) {
131
141
  return make({}); // tampered / wrong secret → fail closed (anonymous)
132
142
  if (store !== undefined) {
133
143
  // Only reachable with a value the app signed itself (legacy mode): never read a marker as a session.
134
- if (unsigned.endsWith(REVOKED_SUFFIX))
144
+ if (isSessionMarker(unsigned))
135
145
  return make({});
136
- const [record, revoked] = await Promise.all([
146
+ const family = sessionFamily(unsigned);
147
+ const [record, revoked, rotated, familyRevoked] = await Promise.all([
137
148
  store.get(unsigned),
138
149
  store.get(revokedKey(unsigned)),
150
+ store.get(`${unsigned}${ROTATED_SUFFIX}`),
151
+ family === unsigned ? undefined : store.get(revokedKey(family)),
139
152
  ]);
140
153
  if (record === undefined)
141
154
  return make({});
142
- if (revoked !== undefined) {
155
+ if (revoked !== undefined || rotated !== undefined || familyRevoked !== undefined) {
143
156
  await store.delete(unsigned); // a commit that raced the logout wrote it back
144
157
  return make({});
145
158
  }
@@ -174,7 +187,7 @@ export function createSessions(options) {
174
187
  const revoke = async (id) => {
175
188
  if (store === undefined)
176
189
  return;
177
- await store.set(revokedKey(id), { data: {}, expiresAt: now() + maxAge * 1000 });
190
+ await store.set(revokedKey(sessionFamily(id)), { data: {}, expiresAt: now() + maxAge * 1000 });
178
191
  await store.delete(id);
179
192
  };
180
193
  const commit = async (c, session) => {
@@ -190,30 +203,76 @@ export function createSessions(options) {
190
203
  const data = Object.fromEntries(state.data);
191
204
  if (store !== undefined) {
192
205
  if (state.id !== undefined) {
193
- const revoked = await store.get(revokedKey(state.id));
194
- // Check again after the read: a delayed store operation can outlive both session and marker.
195
- if (revoked !== undefined || (previousExpiry !== undefined && previousExpiry <= now()))
206
+ // A loaded session commits only while its record exists and no logout has marked it. The
207
+ // record covers a marker already evicted; the marker covers a logout between its two writes.
208
+ const family = sessionFamily(state.id);
209
+ const [current, revoked, rotated, familyRevoked] = await Promise.all([
210
+ store.get(state.id),
211
+ store.get(revokedKey(state.id)),
212
+ store.get(`${state.id}${ROTATED_SUFFIX}`),
213
+ family === state.id ? undefined : store.get(revokedKey(family)),
214
+ ]);
215
+ // Check again after the reads: a delayed store operation can outlive both session and marker.
216
+ if (current === undefined ||
217
+ revoked !== undefined ||
218
+ rotated !== undefined ||
219
+ familyRevoked !== undefined ||
220
+ (previousExpiry !== undefined && previousExpiry <= now()))
196
221
  return;
197
222
  }
198
- if (state.regenerate) {
199
- if (state.id !== undefined)
200
- await revoke(state.id); // drop the pre-rotation session
201
- if (previousExpiry !== undefined && previousExpiry <= now())
223
+ const record = { data, expiresAt };
224
+ const previousId = state.regenerate ? state.id : undefined;
225
+ const loaded = state.id !== undefined && previousId === undefined;
226
+ if (previousId !== undefined) {
227
+ const nextId = `${sessionFamily(previousId)}~${randomId()}`;
228
+ const rotated = await rotateExisting(store, previousId, nextId, record, {
229
+ data: { rotatedAt: committedAt },
230
+ expiresAt: committedAt + maxAge * 1000,
231
+ });
232
+ if (!rotated)
233
+ return;
234
+ if (previousExpiry !== undefined && previousExpiry <= now()) {
235
+ await revoke(nextId);
236
+ return;
237
+ }
238
+ state.id = nextId;
239
+ }
240
+ else {
241
+ state.id ??= randomId();
242
+ // A store with `replace` makes the renewal conditional, so a logout landing after the checks
243
+ // above wins outright; any other store relies on keeping the marker until it expires.
244
+ if (!loaded || store.replace === undefined) {
245
+ await store.set(state.id, record);
246
+ // The renewed record may have expired while the write was in flight, and a logout in that
247
+ // gap found nothing to mark, so the write would revive what it ended.
248
+ if (loaded && previousExpiry !== undefined && previousExpiry <= now()) {
249
+ await store.delete(state.id);
250
+ return;
251
+ }
252
+ }
253
+ else if (!(await store.replace(state.id, record)))
202
254
  return;
203
- state.id = undefined;
204
- state.regenerate = false;
205
255
  }
206
- const loaded = state.id !== undefined;
207
- state.id ??= randomId();
208
- await store.set(state.id, { data, expiresAt });
256
+ state.regenerate = false;
209
257
  // Checked after the write: a revocation that lands before it is seen here, one that lands after
210
258
  // it deletes the record itself. Either way the cookie is left alone - the browser may hold a
211
259
  // newer session by now, and this response must not replace or clear it.
212
- if (loaded && (await store.get(revokedKey(state.id))) !== undefined) {
260
+ if (loaded &&
261
+ (await Promise.all([
262
+ store.get(revokedKey(state.id)),
263
+ store.get(`${state.id}${ROTATED_SUFFIX}`),
264
+ store.get(revokedKey(sessionFamily(state.id))),
265
+ ])).some((marker) => marker !== undefined)) {
213
266
  await store.delete(state.id);
214
267
  return;
215
268
  }
216
269
  const signed = await sign(state.id);
270
+ if (previousId !== undefined &&
271
+ ((await store.get(state.id)) === undefined ||
272
+ (await store.get(revokedKey(sessionFamily(state.id)))) !== undefined)) {
273
+ await store.delete(state.id);
274
+ return;
275
+ }
217
276
  state.expiresAt = expiresAt;
218
277
  writeCookie(c, signed, expiresAt);
219
278
  return;
@@ -223,22 +282,40 @@ export function createSessions(options) {
223
282
  state.expiresAt = expiresAt;
224
283
  writeCookie(c, signed, expiresAt);
225
284
  };
285
+ // Whether logging out with this id may end its whole family: the live generation always may, a
286
+ // rotated-away one only within the grace window, and an id with neither record nor marker never.
287
+ const speaksForFamily = async (activeStore, id) => {
288
+ const [record, rotated] = await Promise.all([
289
+ activeStore.get(id),
290
+ activeStore.get(`${id}${ROTATED_SUFFIX}`),
291
+ ]);
292
+ if (rotated === undefined)
293
+ return record !== undefined;
294
+ const stamped = rotated.data.rotatedAt;
295
+ // Markers written before the stamp existed carry the rotation time only through their expiry.
296
+ const rotatedAt = typeof stamped === "number" ? stamped : rotated.expiresAt - maxAge * 1000;
297
+ return now() - rotatedAt < ROTATION_GRACE_MS;
298
+ };
226
299
  const destroy = async (c, session) => {
227
300
  const state = session === undefined ? undefined : states.get(session);
228
301
  if (store !== undefined) {
229
- let id;
230
302
  if (session !== undefined) {
231
- id = state?.id;
303
+ const id = state?.id;
304
+ if (id !== undefined && !isSessionMarker(id))
305
+ await revoke(id);
232
306
  }
233
307
  else {
234
308
  // Revoke the record addressed by the signed request cookie even when the caller did not
235
309
  // first load a Session object. The signature check prevents deleting attacker-chosen IDs.
236
310
  const raw = sessionCookie(c);
237
- if (raw !== undefined)
238
- id = (await unsign(raw)) ?? undefined;
311
+ const id = raw === undefined ? null : await unsign(raw);
312
+ if (id !== null && !isSessionMarker(id)) {
313
+ if (await speaksForFamily(store, id))
314
+ await revoke(id);
315
+ else
316
+ await store.delete(id);
317
+ }
239
318
  }
240
- if (id !== undefined && !id.endsWith(REVOKED_SUFFIX))
241
- await revoke(id);
242
319
  }
243
320
  // A later commit of this object must neither write the ended data back (cookie mode) nor vanish
244
321
  // against the revocation marker (store mode).
@@ -1 +1 @@
1
- {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
package/dist/store.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Session stores (the **store mode** backend) - a pluggable `SessionStore` plus an in-memory
3
- * implementation (dev / single-instance, prod-guarded) and a Cloudflare Workers KV implementation
4
- * (the durable, shared production path). The store is dumb persistence: the session *manager* owns the
3
+ * implementation (dev / single-instance, prod-guarded). The legacy Cloudflare Workers KV adapter is
4
+ * retained for migration, but is not safe for session revocation. The session *manager* owns the
5
5
  * clock and authoritatively enforces expiry, so the store stays clock-free and trivially testable.
6
6
  */
7
7
  /** A persisted session - its data plus an absolute expiry (ms epoch). */
@@ -11,23 +11,49 @@ export interface SessionRecord {
11
11
  readonly expiresAt: number;
12
12
  }
13
13
  /**
14
- * Pluggable session backend (store mode). Async so a network store (KV/Redis) fits the same shape.
14
+ * Pluggable session backend (store mode). Reads must observe completed writes and deletes, including
15
+ * revocation markers, across all instances. Eventual consistency cannot provide logout guarantees.
15
16
  * **Production needs a shared/durable store** so sessions hold across instances - {@link MemorySessionStore}
16
17
  * prod-guards against the per-instance footgun. Logout and rotation also write a revocation marker, an
17
- * empty record under `<id>.revoked`, so a store must accept that key like any other, and keep it until
18
- * its `expiresAt`: a marker dropped earlier lets a request that loaded the session before the logout
19
- * write it back.
18
+ * empty record under `<id>.revoked`, and rotation one under `<id>.rotated`, so a store must accept those
19
+ * keys like any other and keep each until its `expiresAt`. A dropped marker lets a request that loaded
20
+ * the session before the logout write it back, unless the store implements `replace`; it also lets a
21
+ * rotated generation outlive a logout made with an earlier generation's cookie.
20
22
  */
21
23
  export interface SessionStore {
24
+ /** Read consistency. Omission asserts strong consistency for existing custom adapters. */
25
+ readonly consistency?: "strong" | "eventual";
22
26
  /** The record for `id`, or `undefined` on a miss. */
23
27
  get(id: string): Promise<SessionRecord | undefined>;
24
28
  /** Store (or overwrite) the record for `id`. */
25
29
  set(id: string, record: SessionRecord): Promise<void>;
26
30
  /** Drop `id` (logout / regeneration). A no-op if absent. */
27
31
  delete(id: string): Promise<void>;
32
+ /**
33
+ * Overwrite `id` only while it still holds a record, resolving whether it wrote. The check and the
34
+ * write must be one atomic step that also sets the new expiry, such as Redis
35
+ * `SET key value XX PXAT <record.expiresAt>` (a bare `XX` drops the key's TTL) or a SQL
36
+ * `UPDATE ... WHERE id = ?` that reports the rows it changed. Optional: when present, a loaded session renews through it instead
37
+ * of `set`, so a logout that lands between the renewal's checks and its write wins even after the
38
+ * logout's revocation marker is gone.
39
+ */
40
+ replace?(id: string, record: SessionRecord): Promise<boolean>;
41
+ /**
42
+ * Move the record at `previousId` to `nextId` only while it exists, is unrevoked and was not rotated
43
+ * already, resolving whether it moved. One atomic step writes `marker` under `<previousId>.rotated`,
44
+ * deletes `previousId` and stores `record` at `nextId`; it refuses when `<previousId>.revoked` or the
45
+ * revocation marker of the original ID (the part before `~`) exists. Optional: without it, rotation
46
+ * makes those writes one at a time and checks the revocation afterwards.
47
+ */
48
+ rotate?(previousId: string, nextId: string, record: SessionRecord, marker: SessionRecord): Promise<boolean>;
28
49
  }
29
50
  /** The suffix of a revocation marker's key: `<id>.revoked`. */
30
51
  export declare const REVOKED_SUFFIX = ".revoked";
52
+ export declare const ROTATED_SUFFIX = ".rotated";
53
+ export declare const sessionFamily: (id: string) => string;
54
+ export declare const isSessionMarker: (id: string) => boolean;
55
+ /** A generic store retains logout markers separately: rotating must never overwrite a racing logout. */
56
+ export declare function rotateExisting(store: SessionStore, previousId: string, nextId: string, record: SessionRecord, marker: SessionRecord): Promise<boolean>;
31
57
  export interface MemorySessionStoreOptions {
32
58
  /** Allow the in-memory store in production. Off by default - per-instance sessions don't hold across
33
59
  * instances and are lost on restart. */
@@ -40,9 +66,13 @@ export interface MemorySessionStoreOptions {
40
66
  * In-process session store. Refuses to run in production unless explicitly allowed (mirrors the ISR
41
67
  * `MemoryCacheStore` + the rate-limit `MemoryStore` - a per-instance store is unsafe across instances).
42
68
  * Bounded: oldest-inserted entries evict past `max`, sessions and revocation markers each on their own.
69
+ * Because a marker can be evicted, it implements `replace`, so a request that loaded the session before
70
+ * a logout cannot restore it once the marker is gone.
43
71
  */
44
72
  export declare class MemorySessionStore implements SessionStore {
73
+ readonly consistency = "strong";
45
74
  private readonly sessions;
75
+ private readonly families;
46
76
  private readonly markers;
47
77
  private readonly max;
48
78
  constructor(options?: MemorySessionStoreOptions);
@@ -50,6 +80,10 @@ export declare class MemorySessionStore implements SessionStore {
50
80
  get(id: string): Promise<SessionRecord | undefined>;
51
81
  set(id: string, record: SessionRecord): Promise<void>;
52
82
  delete(id: string): Promise<void>;
83
+ replace(id: string, record: SessionRecord): Promise<boolean>;
84
+ rotate(previousId: string, nextId: string, record: SessionRecord, marker: SessionRecord): Promise<boolean>;
85
+ private write;
86
+ private removeSession;
53
87
  }
54
88
  /**
55
89
  * Minimal structural shape of a Cloudflare Workers **KV namespace** binding - just the three methods
@@ -65,13 +99,16 @@ export interface KVNamespaceLike {
65
99
  delete(key: string): Promise<void>;
66
100
  }
67
101
  /**
68
- * A {@link SessionStore} backed by a **Cloudflare Workers KV** namespace (or any {@link KVNamespaceLike})
69
- * - the durable, shared production store: sessions hold across worker instances and survive restarts.
102
+ * Legacy Cloudflare Workers KV persistence, retained for reading and migrating existing records.
103
+ * `createSessions` rejects this adapter: cached records and missing revocation markers can resurrect
104
+ * a logged-out session. Use a strongly consistent {@link SessionStore} for authenticated sessions.
70
105
  * Records serialize to JSON; the entry's KV `expiration` is set from the record's `expiresAt` so KV
71
106
  * auto-evicts the session (a GC backstop - the manager still authoritatively checks expiry on read).
72
107
  * Every read is validated, so a corrupt/skewed entry reads as no session.
108
+ * @deprecated Workers KV is eventually consistent and cannot enforce session revocation.
73
109
  */
74
110
  export declare class KVSessionStore implements SessionStore {
111
+ readonly consistency = "eventual";
75
112
  private readonly kv;
76
113
  private readonly now;
77
114
  constructor(kv: KVNamespaceLike, options?: {
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,yEAAyE;AACzE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACtC,uFAAuF;IACvF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,qDAAqD;IACrD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAAA;IACnD,gDAAgD;IAChD,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrD,4DAA4D;IAC5D,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAClC;AAED,+DAA+D;AAC/D,eAAO,MAAM,cAAc,aAAa,CAAA;AAExC,MAAM,WAAW,yBAAyB;IACxC;4CACwC;IACxC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;IACpC;+EAC2E;IAC3E,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;;;GAIG;AACH,qBAAa,kBAAmB,YAAW,YAAY;IACrD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;IAG5D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;IAC3D,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;gBAEhB,OAAO,GAAE,yBAA8B;IAenD,OAAO,CAAC,MAAM;IAId,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC;IAInD,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAYrD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAIlC;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACxC,8FAA8F;IAC9F,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1F,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACnC;AAgBD;;;;;;GAMG;AACH,qBAAa,cAAe,YAAW,YAAY;IACjD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;IACpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;gBAEtB,EAAE,EAAE,eAAe,EAAE,OAAO,GAAE;QAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;KAAO;IAKxE,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC;IAYnD,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAWrD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGxC"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
package/dist/store.js CHANGED
@@ -1,18 +1,40 @@
1
1
  /**
2
2
  * Session stores (the **store mode** backend) - a pluggable `SessionStore` plus an in-memory
3
- * implementation (dev / single-instance, prod-guarded) and a Cloudflare Workers KV implementation
4
- * (the durable, shared production path). The store is dumb persistence: the session *manager* owns the
3
+ * implementation (dev / single-instance, prod-guarded). The legacy Cloudflare Workers KV adapter is
4
+ * retained for migration, but is not safe for session revocation. The session *manager* owns the
5
5
  * clock and authoritatively enforces expiry, so the store stays clock-free and trivially testable.
6
6
  */
7
7
  /** The suffix of a revocation marker's key: `<id>.revoked`. */
8
8
  export const REVOKED_SUFFIX = ".revoked";
9
+ export const ROTATED_SUFFIX = ".rotated";
10
+ // A rotated cookie carries its original ID, so one logout revokes every generation. That ID
11
+ // may be a legacy cookie's value of any length; a rotation only ever appends one random part.
12
+ export const sessionFamily = (id) => /^([^~]+)~[A-Za-z0-9_-]{43}$/.exec(id)?.[1] ?? id;
13
+ export const isSessionMarker = (id) => id.endsWith(REVOKED_SUFFIX) || id.endsWith(ROTATED_SUFFIX);
14
+ /** A generic store retains logout markers separately: rotating must never overwrite a racing logout. */
15
+ export async function rotateExisting(store, previousId, nextId, record, marker) {
16
+ if (store.rotate !== undefined)
17
+ return store.rotate(previousId, nextId, record, marker);
18
+ await store.set(`${previousId}${ROTATED_SUFFIX}`, marker);
19
+ await store.delete(previousId);
20
+ await store.set(nextId, record);
21
+ if ((await store.get(`${sessionFamily(previousId)}${REVOKED_SUFFIX}`)) !== undefined) {
22
+ await store.delete(nextId);
23
+ return false;
24
+ }
25
+ return true;
26
+ }
9
27
  /**
10
28
  * In-process session store. Refuses to run in production unless explicitly allowed (mirrors the ISR
11
29
  * `MemoryCacheStore` + the rate-limit `MemoryStore` - a per-instance store is unsafe across instances).
12
30
  * Bounded: oldest-inserted entries evict past `max`, sessions and revocation markers each on their own.
31
+ * Because a marker can be evicted, it implements `replace`, so a request that loaded the session before
32
+ * a logout cannot restore it once the marker is gone.
13
33
  */
14
34
  export class MemorySessionStore {
35
+ consistency = "strong";
15
36
  sessions = new Map();
37
+ families = new Map();
16
38
  // Apart from the sessions: anonymous sessions written after a logout must not push its marker out.
17
39
  // Markers share one lifetime, so the oldest-inserted is the first to expire.
18
40
  markers = new Map();
@@ -21,7 +43,7 @@ export class MemorySessionStore {
21
43
  const isProd = typeof process !== "undefined" && process.env?.NODE_ENV === "production";
22
44
  if (options.allowInProduction !== true && isProd) {
23
45
  throw new Error("[nifra/auth] MemorySessionStore is per-instance and unsafe in production (sessions don't hold " +
24
- "across instances and are lost on restart). Use a shared store (KVSessionStore, Redis), or " +
46
+ "across instances and are lost on restart). Use a strongly consistent shared store, or " +
25
47
  "pass { allowInProduction: true } for a single-instance deploy.");
26
48
  }
27
49
  this.max = options.max ?? 10_000;
@@ -30,26 +52,78 @@ export class MemorySessionStore {
30
52
  }
31
53
  }
32
54
  mapFor(id) {
33
- return id.endsWith(REVOKED_SUFFIX) ? this.markers : this.sessions;
55
+ return isSessionMarker(id) ? this.markers : this.sessions;
34
56
  }
35
57
  get(id) {
36
58
  return Promise.resolve(this.mapFor(id).get(id));
37
59
  }
38
60
  set(id, record) {
61
+ this.write(this.mapFor(id), id, record);
62
+ if (id.endsWith(REVOKED_SUFFIX)) {
63
+ const family = id.slice(0, -REVOKED_SUFFIX.length);
64
+ // A bounded marker can later be evicted. Remove every generation now so conditional renewals
65
+ // and rotations cannot restore it after that eviction.
66
+ const members = this.families.get(family);
67
+ if (members !== undefined)
68
+ for (const key of members)
69
+ this.removeSession(key);
70
+ }
71
+ return Promise.resolve();
72
+ }
73
+ delete(id) {
74
+ if (isSessionMarker(id))
75
+ this.markers.delete(id);
76
+ else
77
+ this.removeSession(id);
78
+ return Promise.resolve();
79
+ }
80
+ replace(id, record) {
39
81
  const entries = this.mapFor(id);
82
+ if (!entries.has(id))
83
+ return Promise.resolve(false);
84
+ this.write(entries, id, record);
85
+ return Promise.resolve(true);
86
+ }
87
+ rotate(previousId, nextId, record, marker) {
88
+ if (!this.sessions.has(previousId) ||
89
+ this.markers.has(`${previousId}${REVOKED_SUFFIX}`) ||
90
+ this.markers.has(`${sessionFamily(previousId)}${REVOKED_SUFFIX}`) ||
91
+ this.markers.has(`${previousId}${ROTATED_SUFFIX}`))
92
+ return Promise.resolve(false);
93
+ this.write(this.markers, `${previousId}${ROTATED_SUFFIX}`, marker);
94
+ this.removeSession(previousId);
95
+ this.write(this.sessions, nextId, record);
96
+ return Promise.resolve(true);
97
+ }
98
+ write(entries, id, record) {
40
99
  entries.delete(id); // re-insert at the tail so Map order tracks recency for the bounded evict
41
100
  entries.set(id, record);
101
+ if (entries === this.sessions) {
102
+ const family = sessionFamily(id);
103
+ let members = this.families.get(family);
104
+ if (members === undefined) {
105
+ members = new Set();
106
+ this.families.set(family, members);
107
+ }
108
+ members.add(id);
109
+ }
42
110
  while (entries.size > this.max) {
43
111
  const oldest = entries.keys().next().value;
44
112
  if (oldest === undefined)
45
113
  break;
46
- entries.delete(oldest);
114
+ if (entries === this.sessions)
115
+ this.removeSession(oldest);
116
+ else
117
+ entries.delete(oldest);
47
118
  }
48
- return Promise.resolve();
49
119
  }
50
- delete(id) {
51
- this.mapFor(id).delete(id);
52
- return Promise.resolve();
120
+ removeSession(id) {
121
+ this.sessions.delete(id);
122
+ const family = sessionFamily(id);
123
+ const members = this.families.get(family);
124
+ members?.delete(id);
125
+ if (members?.size === 0)
126
+ this.families.delete(family);
53
127
  }
54
128
  }
55
129
  /** Structural validation of a KV-read value before it's trusted as a {@link SessionRecord}. The store
@@ -65,13 +139,16 @@ const isSessionRecord = (value) => {
65
139
  v.data !== null);
66
140
  };
67
141
  /**
68
- * A {@link SessionStore} backed by a **Cloudflare Workers KV** namespace (or any {@link KVNamespaceLike})
69
- * - the durable, shared production store: sessions hold across worker instances and survive restarts.
142
+ * Legacy Cloudflare Workers KV persistence, retained for reading and migrating existing records.
143
+ * `createSessions` rejects this adapter: cached records and missing revocation markers can resurrect
144
+ * a logged-out session. Use a strongly consistent {@link SessionStore} for authenticated sessions.
70
145
  * Records serialize to JSON; the entry's KV `expiration` is set from the record's `expiresAt` so KV
71
146
  * auto-evicts the session (a GC backstop - the manager still authoritatively checks expiry on read).
72
147
  * Every read is validated, so a corrupt/skewed entry reads as no session.
148
+ * @deprecated Workers KV is eventually consistent and cannot enforce session revocation.
73
149
  */
74
150
  export class KVSessionStore {
151
+ consistency = "eventual";
75
152
  kv;
76
153
  now;
77
154
  constructor(kv, options = {}) {
package/dist/store.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/auth",
3
- "version": "5.4.0",
3
+ "version": "5.5.0",
4
4
  "description": "Session primitives for nifra - signed-cookie + server-store sessions, CSRF, and route guards. Bring your own identity (Better Auth / Lucia / OAuth); nifra owns the session.",
5
5
  "keywords": [
6
6
  "bun",
@@ -41,7 +41,7 @@
41
41
  "access": "public"
42
42
  },
43
43
  "peerDependencies": {
44
- "@nifrajs/core": "^5.4.0"
44
+ "@nifrajs/core": "^5.5.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@nifrajs/core": "workspace:*"
package/src/session.ts CHANGED
@@ -16,7 +16,15 @@ import {
16
16
  signValue,
17
17
  unsignValue,
18
18
  } from "@nifrajs/core/cookies"
19
- import { REVOKED_SUFFIX, type SessionRecord, type SessionStore } from "./store.ts"
19
+ import {
20
+ isSessionMarker,
21
+ REVOKED_SUFFIX,
22
+ ROTATED_SUFFIX,
23
+ rotateExisting,
24
+ type SessionRecord,
25
+ type SessionStore,
26
+ sessionFamily,
27
+ } from "./store.ts"
20
28
 
21
29
  /** The cookie + response surface the manager needs - a structural subset of nifra's `Context`, so any
22
30
  * `c` satisfies it and it's testable with a stub. */
@@ -53,7 +61,8 @@ export interface SessionOptions {
53
61
  * secret, drop the old one after `maxAge` has elapsed. A bare secret swap (no list) invalidates
54
62
  * all existing sessions. */
55
63
  readonly secret: string | readonly string[]
56
- /** Provide a store for **store mode**; omit for **cookie mode** (stateless). */
64
+ /** Strongly consistent backend for **store mode**; omit for **cookie mode** (stateless).
65
+ * Declared eventually consistent stores (including KVSessionStore) are refused. */
57
66
  readonly store?: SessionStore
58
67
  /** Cookie name. Default `nifra_session`. */
59
68
  readonly cookieName?: string
@@ -116,6 +125,11 @@ const randomId = (): string => encodeBase64Url(crypto.getRandomValues(new Uint8A
116
125
  // but it cannot overwrite a key it never writes. `.` is outside the base64url id alphabet.
117
126
  const revokedKey = (id: string): string => `${id}${REVOKED_SUFFIX}`
118
127
 
128
+ // A superseded cookie is still the user's own while the rotating response is on its way to the
129
+ // browser. Past that it is a copy someone else kept (a planted pre-login cookie), and it must not end
130
+ // the session that replaced it.
131
+ const ROTATION_GRACE_MS = 60_000
132
+
119
133
  interface Payload {
120
134
  data: Record<string, unknown>
121
135
  expiresAt: number
@@ -140,6 +154,14 @@ export function createSessions<Data extends Record<string, unknown> = Record<str
140
154
  options: SessionOptions,
141
155
  ): SessionManager<Data> {
142
156
  const { secret, store } = options
157
+ // A cached marker miss can authenticate or recreate a session after logout, even with fixed expiry.
158
+ if (store?.consistency !== undefined && store.consistency !== "strong") {
159
+ throw new Error(
160
+ "[nifra/auth] sessions require a strongly consistent store for logout and rotation; " +
161
+ "eventually consistent stores such as KVSessionStore are not supported. " +
162
+ "Use a strongly consistent shared SessionStore instead.",
163
+ )
164
+ }
143
165
  const secretList = typeof secret === "string" ? [secret] : secret
144
166
  if (secretList.length === 0) {
145
167
  throw new Error("[nifra/auth] session secret list cannot be empty")
@@ -237,13 +259,16 @@ export function createSessions<Data extends Record<string, unknown> = Record<str
237
259
 
238
260
  if (store !== undefined) {
239
261
  // Only reachable with a value the app signed itself (legacy mode): never read a marker as a session.
240
- if (unsigned.endsWith(REVOKED_SUFFIX)) return make({})
241
- const [record, revoked] = await Promise.all([
262
+ if (isSessionMarker(unsigned)) return make({})
263
+ const family = sessionFamily(unsigned)
264
+ const [record, revoked, rotated, familyRevoked] = await Promise.all([
242
265
  store.get(unsigned),
243
266
  store.get(revokedKey(unsigned)),
267
+ store.get(`${unsigned}${ROTATED_SUFFIX}`),
268
+ family === unsigned ? undefined : store.get(revokedKey(family)),
244
269
  ])
245
270
  if (record === undefined) return make({})
246
- if (revoked !== undefined) {
271
+ if (revoked !== undefined || rotated !== undefined || familyRevoked !== undefined) {
247
272
  await store.delete(unsigned) // a commit that raced the logout wrote it back
248
273
  return make({})
249
274
  }
@@ -284,7 +309,7 @@ export function createSessions<Data extends Record<string, unknown> = Record<str
284
309
  // Marker first: a racing commit's write either precedes the delete below or follows the marker.
285
310
  const revoke = async (id: string): Promise<void> => {
286
311
  if (store === undefined) return
287
- await store.set(revokedKey(id), { data: {}, expiresAt: now() + maxAge * 1000 })
312
+ await store.set(revokedKey(sessionFamily(id)), { data: {}, expiresAt: now() + maxAge * 1000 })
288
313
  await store.delete(id)
289
314
  }
290
315
 
@@ -302,28 +327,80 @@ export function createSessions<Data extends Record<string, unknown> = Record<str
302
327
 
303
328
  if (store !== undefined) {
304
329
  if (state.id !== undefined) {
305
- const revoked = await store.get(revokedKey(state.id))
306
- // Check again after the read: a delayed store operation can outlive both session and marker.
307
- if (revoked !== undefined || (previousExpiry !== undefined && previousExpiry <= now()))
330
+ // A loaded session commits only while its record exists and no logout has marked it. The
331
+ // record covers a marker already evicted; the marker covers a logout between its two writes.
332
+ const family = sessionFamily(state.id)
333
+ const [current, revoked, rotated, familyRevoked] = await Promise.all([
334
+ store.get(state.id),
335
+ store.get(revokedKey(state.id)),
336
+ store.get(`${state.id}${ROTATED_SUFFIX}`),
337
+ family === state.id ? undefined : store.get(revokedKey(family)),
338
+ ])
339
+ // Check again after the reads: a delayed store operation can outlive both session and marker.
340
+ if (
341
+ current === undefined ||
342
+ revoked !== undefined ||
343
+ rotated !== undefined ||
344
+ familyRevoked !== undefined ||
345
+ (previousExpiry !== undefined && previousExpiry <= now())
346
+ )
308
347
  return
309
348
  }
310
- if (state.regenerate) {
311
- if (state.id !== undefined) await revoke(state.id) // drop the pre-rotation session
312
- if (previousExpiry !== undefined && previousExpiry <= now()) return
313
- state.id = undefined
314
- state.regenerate = false
349
+ const record = { data, expiresAt } satisfies SessionRecord
350
+ const previousId = state.regenerate ? state.id : undefined
351
+ const loaded = state.id !== undefined && previousId === undefined
352
+ if (previousId !== undefined) {
353
+ const nextId = `${sessionFamily(previousId)}~${randomId()}`
354
+ const rotated = await rotateExisting(store, previousId, nextId, record, {
355
+ data: { rotatedAt: committedAt },
356
+ expiresAt: committedAt + maxAge * 1000,
357
+ })
358
+ if (!rotated) return
359
+ if (previousExpiry !== undefined && previousExpiry <= now()) {
360
+ await revoke(nextId)
361
+ return
362
+ }
363
+ state.id = nextId
364
+ } else {
365
+ state.id ??= randomId()
366
+ // A store with `replace` makes the renewal conditional, so a logout landing after the checks
367
+ // above wins outright; any other store relies on keeping the marker until it expires.
368
+ if (!loaded || store.replace === undefined) {
369
+ await store.set(state.id, record)
370
+ // The renewed record may have expired while the write was in flight, and a logout in that
371
+ // gap found nothing to mark, so the write would revive what it ended.
372
+ if (loaded && previousExpiry !== undefined && previousExpiry <= now()) {
373
+ await store.delete(state.id)
374
+ return
375
+ }
376
+ } else if (!(await store.replace(state.id, record))) return
315
377
  }
316
- const loaded = state.id !== undefined
317
- state.id ??= randomId()
318
- await store.set(state.id, { data, expiresAt } satisfies SessionRecord)
378
+ state.regenerate = false
319
379
  // Checked after the write: a revocation that lands before it is seen here, one that lands after
320
380
  // it deletes the record itself. Either way the cookie is left alone - the browser may hold a
321
381
  // newer session by now, and this response must not replace or clear it.
322
- if (loaded && (await store.get(revokedKey(state.id))) !== undefined) {
382
+ if (
383
+ loaded &&
384
+ (
385
+ await Promise.all([
386
+ store.get(revokedKey(state.id)),
387
+ store.get(`${state.id}${ROTATED_SUFFIX}`),
388
+ store.get(revokedKey(sessionFamily(state.id))),
389
+ ])
390
+ ).some((marker) => marker !== undefined)
391
+ ) {
323
392
  await store.delete(state.id)
324
393
  return
325
394
  }
326
395
  const signed = await sign(state.id)
396
+ if (
397
+ previousId !== undefined &&
398
+ ((await store.get(state.id)) === undefined ||
399
+ (await store.get(revokedKey(sessionFamily(state.id)))) !== undefined)
400
+ ) {
401
+ await store.delete(state.id)
402
+ return
403
+ }
327
404
  state.expiresAt = expiresAt
328
405
  writeCookie(c, signed, expiresAt)
329
406
  return
@@ -334,19 +411,36 @@ export function createSessions<Data extends Record<string, unknown> = Record<str
334
411
  writeCookie(c, signed, expiresAt)
335
412
  }
336
413
 
414
+ // Whether logging out with this id may end its whole family: the live generation always may, a
415
+ // rotated-away one only within the grace window, and an id with neither record nor marker never.
416
+ const speaksForFamily = async (activeStore: SessionStore, id: string): Promise<boolean> => {
417
+ const [record, rotated] = await Promise.all([
418
+ activeStore.get(id),
419
+ activeStore.get(`${id}${ROTATED_SUFFIX}`),
420
+ ])
421
+ if (rotated === undefined) return record !== undefined
422
+ const stamped = rotated.data.rotatedAt
423
+ // Markers written before the stamp existed carry the rotation time only through their expiry.
424
+ const rotatedAt = typeof stamped === "number" ? stamped : rotated.expiresAt - maxAge * 1000
425
+ return now() - rotatedAt < ROTATION_GRACE_MS
426
+ }
427
+
337
428
  const destroy = async (c: SessionContext, session?: Session<Data>): Promise<void> => {
338
429
  const state = session === undefined ? undefined : states.get(session)
339
430
  if (store !== undefined) {
340
- let id: string | undefined
341
431
  if (session !== undefined) {
342
- id = state?.id
432
+ const id = state?.id
433
+ if (id !== undefined && !isSessionMarker(id)) await revoke(id)
343
434
  } else {
344
435
  // Revoke the record addressed by the signed request cookie even when the caller did not
345
436
  // first load a Session object. The signature check prevents deleting attacker-chosen IDs.
346
437
  const raw = sessionCookie(c)
347
- if (raw !== undefined) id = (await unsign(raw)) ?? undefined
438
+ const id = raw === undefined ? null : await unsign(raw)
439
+ if (id !== null && !isSessionMarker(id)) {
440
+ if (await speaksForFamily(store, id)) await revoke(id)
441
+ else await store.delete(id)
442
+ }
348
443
  }
349
- if (id !== undefined && !id.endsWith(REVOKED_SUFFIX)) await revoke(id)
350
444
  }
351
445
  // A later commit of this object must neither write the ended data back (cookie mode) nor vanish
352
446
  // against the revocation marker (store mode).
package/src/store.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Session stores (the **store mode** backend) - a pluggable `SessionStore` plus an in-memory
3
- * implementation (dev / single-instance, prod-guarded) and a Cloudflare Workers KV implementation
4
- * (the durable, shared production path). The store is dumb persistence: the session *manager* owns the
3
+ * implementation (dev / single-instance, prod-guarded). The legacy Cloudflare Workers KV adapter is
4
+ * retained for migration, but is not safe for session revocation. The session *manager* owns the
5
5
  * clock and authoritatively enforces expiry, so the store stays clock-free and trivially testable.
6
6
  */
7
7
 
@@ -13,24 +13,78 @@ export interface SessionRecord {
13
13
  }
14
14
 
15
15
  /**
16
- * Pluggable session backend (store mode). Async so a network store (KV/Redis) fits the same shape.
16
+ * Pluggable session backend (store mode). Reads must observe completed writes and deletes, including
17
+ * revocation markers, across all instances. Eventual consistency cannot provide logout guarantees.
17
18
  * **Production needs a shared/durable store** so sessions hold across instances - {@link MemorySessionStore}
18
19
  * prod-guards against the per-instance footgun. Logout and rotation also write a revocation marker, an
19
- * empty record under `<id>.revoked`, so a store must accept that key like any other, and keep it until
20
- * its `expiresAt`: a marker dropped earlier lets a request that loaded the session before the logout
21
- * write it back.
20
+ * empty record under `<id>.revoked`, and rotation one under `<id>.rotated`, so a store must accept those
21
+ * keys like any other and keep each until its `expiresAt`. A dropped marker lets a request that loaded
22
+ * the session before the logout write it back, unless the store implements `replace`; it also lets a
23
+ * rotated generation outlive a logout made with an earlier generation's cookie.
22
24
  */
23
25
  export interface SessionStore {
26
+ /** Read consistency. Omission asserts strong consistency for existing custom adapters. */
27
+ readonly consistency?: "strong" | "eventual"
24
28
  /** The record for `id`, or `undefined` on a miss. */
25
29
  get(id: string): Promise<SessionRecord | undefined>
26
30
  /** Store (or overwrite) the record for `id`. */
27
31
  set(id: string, record: SessionRecord): Promise<void>
28
32
  /** Drop `id` (logout / regeneration). A no-op if absent. */
29
33
  delete(id: string): Promise<void>
34
+ /**
35
+ * Overwrite `id` only while it still holds a record, resolving whether it wrote. The check and the
36
+ * write must be one atomic step that also sets the new expiry, such as Redis
37
+ * `SET key value XX PXAT <record.expiresAt>` (a bare `XX` drops the key's TTL) or a SQL
38
+ * `UPDATE ... WHERE id = ?` that reports the rows it changed. Optional: when present, a loaded session renews through it instead
39
+ * of `set`, so a logout that lands between the renewal's checks and its write wins even after the
40
+ * logout's revocation marker is gone.
41
+ */
42
+ replace?(id: string, record: SessionRecord): Promise<boolean>
43
+ /**
44
+ * Move the record at `previousId` to `nextId` only while it exists, is unrevoked and was not rotated
45
+ * already, resolving whether it moved. One atomic step writes `marker` under `<previousId>.rotated`,
46
+ * deletes `previousId` and stores `record` at `nextId`; it refuses when `<previousId>.revoked` or the
47
+ * revocation marker of the original ID (the part before `~`) exists. Optional: without it, rotation
48
+ * makes those writes one at a time and checks the revocation afterwards.
49
+ */
50
+ rotate?(
51
+ previousId: string,
52
+ nextId: string,
53
+ record: SessionRecord,
54
+ marker: SessionRecord,
55
+ ): Promise<boolean>
30
56
  }
31
57
 
32
58
  /** The suffix of a revocation marker's key: `<id>.revoked`. */
33
59
  export const REVOKED_SUFFIX = ".revoked"
60
+ export const ROTATED_SUFFIX = ".rotated"
61
+
62
+ // A rotated cookie carries its original ID, so one logout revokes every generation. That ID
63
+ // may be a legacy cookie's value of any length; a rotation only ever appends one random part.
64
+ export const sessionFamily = (id: string): string =>
65
+ /^([^~]+)~[A-Za-z0-9_-]{43}$/.exec(id)?.[1] ?? id
66
+
67
+ export const isSessionMarker = (id: string): boolean =>
68
+ id.endsWith(REVOKED_SUFFIX) || id.endsWith(ROTATED_SUFFIX)
69
+
70
+ /** A generic store retains logout markers separately: rotating must never overwrite a racing logout. */
71
+ export async function rotateExisting(
72
+ store: SessionStore,
73
+ previousId: string,
74
+ nextId: string,
75
+ record: SessionRecord,
76
+ marker: SessionRecord,
77
+ ): Promise<boolean> {
78
+ if (store.rotate !== undefined) return store.rotate(previousId, nextId, record, marker)
79
+ await store.set(`${previousId}${ROTATED_SUFFIX}`, marker)
80
+ await store.delete(previousId)
81
+ await store.set(nextId, record)
82
+ if ((await store.get(`${sessionFamily(previousId)}${REVOKED_SUFFIX}`)) !== undefined) {
83
+ await store.delete(nextId)
84
+ return false
85
+ }
86
+ return true
87
+ }
34
88
 
35
89
  export interface MemorySessionStoreOptions {
36
90
  /** Allow the in-memory store in production. Off by default - per-instance sessions don't hold across
@@ -45,9 +99,13 @@ export interface MemorySessionStoreOptions {
45
99
  * In-process session store. Refuses to run in production unless explicitly allowed (mirrors the ISR
46
100
  * `MemoryCacheStore` + the rate-limit `MemoryStore` - a per-instance store is unsafe across instances).
47
101
  * Bounded: oldest-inserted entries evict past `max`, sessions and revocation markers each on their own.
102
+ * Because a marker can be evicted, it implements `replace`, so a request that loaded the session before
103
+ * a logout cannot restore it once the marker is gone.
48
104
  */
49
105
  export class MemorySessionStore implements SessionStore {
106
+ readonly consistency = "strong"
50
107
  private readonly sessions = new Map<string, SessionRecord>()
108
+ private readonly families = new Map<string, Set<string>>()
51
109
  // Apart from the sessions: anonymous sessions written after a logout must not push its marker out.
52
110
  // Markers share one lifetime, so the oldest-inserted is the first to expire.
53
111
  private readonly markers = new Map<string, SessionRecord>()
@@ -58,7 +116,7 @@ export class MemorySessionStore implements SessionStore {
58
116
  if (options.allowInProduction !== true && isProd) {
59
117
  throw new Error(
60
118
  "[nifra/auth] MemorySessionStore is per-instance and unsafe in production (sessions don't hold " +
61
- "across instances and are lost on restart). Use a shared store (KVSessionStore, Redis), or " +
119
+ "across instances and are lost on restart). Use a strongly consistent shared store, or " +
62
120
  "pass { allowInProduction: true } for a single-instance deploy.",
63
121
  )
64
122
  }
@@ -69,7 +127,7 @@ export class MemorySessionStore implements SessionStore {
69
127
  }
70
128
 
71
129
  private mapFor(id: string): Map<string, SessionRecord> {
72
- return id.endsWith(REVOKED_SUFFIX) ? this.markers : this.sessions
130
+ return isSessionMarker(id) ? this.markers : this.sessions
73
131
  }
74
132
 
75
133
  get(id: string): Promise<SessionRecord | undefined> {
@@ -77,20 +135,75 @@ export class MemorySessionStore implements SessionStore {
77
135
  }
78
136
 
79
137
  set(id: string, record: SessionRecord): Promise<void> {
138
+ this.write(this.mapFor(id), id, record)
139
+ if (id.endsWith(REVOKED_SUFFIX)) {
140
+ const family = id.slice(0, -REVOKED_SUFFIX.length)
141
+ // A bounded marker can later be evicted. Remove every generation now so conditional renewals
142
+ // and rotations cannot restore it after that eviction.
143
+ const members = this.families.get(family)
144
+ if (members !== undefined) for (const key of members) this.removeSession(key)
145
+ }
146
+ return Promise.resolve()
147
+ }
148
+
149
+ delete(id: string): Promise<void> {
150
+ if (isSessionMarker(id)) this.markers.delete(id)
151
+ else this.removeSession(id)
152
+ return Promise.resolve()
153
+ }
154
+
155
+ replace(id: string, record: SessionRecord): Promise<boolean> {
80
156
  const entries = this.mapFor(id)
157
+ if (!entries.has(id)) return Promise.resolve(false)
158
+ this.write(entries, id, record)
159
+ return Promise.resolve(true)
160
+ }
161
+
162
+ rotate(
163
+ previousId: string,
164
+ nextId: string,
165
+ record: SessionRecord,
166
+ marker: SessionRecord,
167
+ ): Promise<boolean> {
168
+ if (
169
+ !this.sessions.has(previousId) ||
170
+ this.markers.has(`${previousId}${REVOKED_SUFFIX}`) ||
171
+ this.markers.has(`${sessionFamily(previousId)}${REVOKED_SUFFIX}`) ||
172
+ this.markers.has(`${previousId}${ROTATED_SUFFIX}`)
173
+ )
174
+ return Promise.resolve(false)
175
+ this.write(this.markers, `${previousId}${ROTATED_SUFFIX}`, marker)
176
+ this.removeSession(previousId)
177
+ this.write(this.sessions, nextId, record)
178
+ return Promise.resolve(true)
179
+ }
180
+
181
+ private write(entries: Map<string, SessionRecord>, id: string, record: SessionRecord): void {
81
182
  entries.delete(id) // re-insert at the tail so Map order tracks recency for the bounded evict
82
183
  entries.set(id, record)
184
+ if (entries === this.sessions) {
185
+ const family = sessionFamily(id)
186
+ let members = this.families.get(family)
187
+ if (members === undefined) {
188
+ members = new Set()
189
+ this.families.set(family, members)
190
+ }
191
+ members.add(id)
192
+ }
83
193
  while (entries.size > this.max) {
84
194
  const oldest = entries.keys().next().value
85
195
  if (oldest === undefined) break
86
- entries.delete(oldest)
196
+ if (entries === this.sessions) this.removeSession(oldest)
197
+ else entries.delete(oldest)
87
198
  }
88
- return Promise.resolve()
89
199
  }
90
200
 
91
- delete(id: string): Promise<void> {
92
- this.mapFor(id).delete(id)
93
- return Promise.resolve()
201
+ private removeSession(id: string): void {
202
+ this.sessions.delete(id)
203
+ const family = sessionFamily(id)
204
+ const members = this.families.get(family)
205
+ members?.delete(id)
206
+ if (members?.size === 0) this.families.delete(family)
94
207
  }
95
208
  }
96
209
 
@@ -121,13 +234,16 @@ const isSessionRecord = (value: unknown): value is SessionRecord => {
121
234
  }
122
235
 
123
236
  /**
124
- * A {@link SessionStore} backed by a **Cloudflare Workers KV** namespace (or any {@link KVNamespaceLike})
125
- * - the durable, shared production store: sessions hold across worker instances and survive restarts.
237
+ * Legacy Cloudflare Workers KV persistence, retained for reading and migrating existing records.
238
+ * `createSessions` rejects this adapter: cached records and missing revocation markers can resurrect
239
+ * a logged-out session. Use a strongly consistent {@link SessionStore} for authenticated sessions.
126
240
  * Records serialize to JSON; the entry's KV `expiration` is set from the record's `expiresAt` so KV
127
241
  * auto-evicts the session (a GC backstop - the manager still authoritatively checks expiry on read).
128
242
  * Every read is validated, so a corrupt/skewed entry reads as no session.
243
+ * @deprecated Workers KV is eventually consistent and cannot enforce session revocation.
129
244
  */
130
245
  export class KVSessionStore implements SessionStore {
246
+ readonly consistency = "eventual"
131
247
  private readonly kv: KVNamespaceLike
132
248
  private readonly now: () => number
133
249