@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 +17 -0
- package/README.md +32 -0
- package/dist/session.d.ts +2 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +101 -24
- package/dist/session.js.map +1 -1
- package/dist/store.d.ts +45 -8
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +88 -11
- package/dist/store.js.map +1 -1
- package/package.json +2 -2
- package/src/session.ts +116 -22
- package/src/store.ts +131 -15
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
|
-
/**
|
|
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;
|
package/dist/session.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EACL,KAAK,aAAa,EAKnB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,
|
|
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
|
|
144
|
+
if (isSessionMarker(unsigned))
|
|
135
145
|
return make({});
|
|
136
|
-
const
|
|
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
|
-
|
|
194
|
-
//
|
|
195
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
|
|
238
|
-
|
|
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).
|
package/dist/session.js.map
CHANGED
|
@@ -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)
|
|
4
|
-
*
|
|
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).
|
|
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
|
|
18
|
-
* its `expiresAt
|
|
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
|
-
*
|
|
69
|
-
*
|
|
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?: {
|
package/dist/store.d.ts.map
CHANGED
|
@@ -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
|
|
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)
|
|
4
|
-
*
|
|
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
|
|
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
|
|
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.
|
|
114
|
+
if (entries === this.sessions)
|
|
115
|
+
this.removeSession(oldest);
|
|
116
|
+
else
|
|
117
|
+
entries.delete(oldest);
|
|
47
118
|
}
|
|
48
|
-
return Promise.resolve();
|
|
49
119
|
}
|
|
50
|
-
|
|
51
|
-
this.
|
|
52
|
-
|
|
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
|
-
*
|
|
69
|
-
*
|
|
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":"AAAA;;;;;GAKG;
|
|
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.
|
|
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.
|
|
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 {
|
|
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
|
-
/**
|
|
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
|
|
241
|
-
const
|
|
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
|
-
|
|
306
|
-
//
|
|
307
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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)
|
|
4
|
-
*
|
|
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).
|
|
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
|
|
20
|
-
* its `expiresAt
|
|
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
|
|
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
|
|
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.
|
|
196
|
+
if (entries === this.sessions) this.removeSession(oldest)
|
|
197
|
+
else entries.delete(oldest)
|
|
87
198
|
}
|
|
88
|
-
return Promise.resolve()
|
|
89
199
|
}
|
|
90
200
|
|
|
91
|
-
|
|
92
|
-
this.
|
|
93
|
-
|
|
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
|
-
*
|
|
125
|
-
*
|
|
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
|
|