securenow 8.3.0 → 8.4.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/NPM_README.md +5 -1
- package/package.json +1 -1
- package/sessions.d.ts +11 -1
- package/sessions.js +74 -11
package/NPM_README.md
CHANGED
|
@@ -263,9 +263,13 @@ npx securenow notifications read-all
|
|
|
263
263
|
|
|
264
264
|
```js
|
|
265
265
|
const securenow = require('securenow/sessions');
|
|
266
|
-
|
|
266
|
+
// One line: emits session.seen (feeds impossible-travel / concurrent-session
|
|
267
|
+
// detection) AND rejects revoked sessions. Auto-detects NextAuth/connect.sid/JWT.
|
|
268
|
+
app.use(securenow.guard());
|
|
267
269
|
// ...or check manually:
|
|
268
270
|
if (securenow.isRevoked({ sessionId, userId })) return res.status(401).send('re-auth');
|
|
271
|
+
// ...or emit-only (detection without enforcement):
|
|
272
|
+
app.use(securenow.capture());
|
|
269
273
|
```
|
|
270
274
|
|
|
271
275
|
Revoke from the CLI (the SDK enforces within ~seconds, outbound-only, fail-open):
|
package/package.json
CHANGED
package/sessions.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ export interface GuardOptions {
|
|
|
9
9
|
onRevoked?: (req: any, res: any, next: any, ctx: { sessionId: string | null; userId: string | null }) => void;
|
|
10
10
|
/** Background sync interval in ms (default 30000). */
|
|
11
11
|
syncIntervalMs?: number;
|
|
12
|
+
/** Emit session.seen events for detection (default true). Set false to enforce only. */
|
|
13
|
+
capture?: boolean;
|
|
14
|
+
/** Dedup window for session.seen emission, per (session, ip), in ms (default 300000). */
|
|
15
|
+
captureWindowMs?: number;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
/** Start background revocation sync (called automatically by guard()). */
|
|
@@ -20,8 +24,14 @@ export function syncOnce(): Promise<boolean>;
|
|
|
20
24
|
/** Is this session/user currently revoked? */
|
|
21
25
|
export function isRevoked(input: { sessionId?: string | null; userId?: string | null }): boolean;
|
|
22
26
|
|
|
23
|
-
/** Express-style middleware
|
|
27
|
+
/** Express-style middleware: emits session.seen (detection) AND rejects revoked sessions. Fail-open. */
|
|
24
28
|
export function guard(options?: GuardOptions): (req: any, res: any, next: any) => void;
|
|
25
29
|
|
|
30
|
+
/** Express-style middleware that ONLY emits session.seen (detection feed, no enforcement). */
|
|
31
|
+
export function capture(options?: GuardOptions): (req: any, res: any, next: any) => void;
|
|
32
|
+
|
|
33
|
+
/** Resolve the client IP from a request (X-Forwarded-For / CF / socket). */
|
|
34
|
+
export function resolveClientIp(req: any): string | null;
|
|
35
|
+
|
|
26
36
|
/** SHA-256 helper (used to hash raw session tokens consistently). */
|
|
27
37
|
export function sha256(value: string): string;
|
package/sessions.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
const crypto = require('crypto');
|
|
16
16
|
const appConfig = require('./app-config');
|
|
17
|
+
const events = require('./events');
|
|
17
18
|
|
|
18
19
|
let _entries = new Map(); // "type:value" -> expiryMs (0 = never)
|
|
19
20
|
let _etag = null;
|
|
@@ -187,6 +188,72 @@ function defaultExtract(req) {
|
|
|
187
188
|
return { sessionId, userId };
|
|
188
189
|
}
|
|
189
190
|
|
|
191
|
+
// --- Tier-0 auto-emit of session.seen (feeds session-theft detection) -------
|
|
192
|
+
// Emitting on EVERY request would be huge volume, so we dedup per
|
|
193
|
+
// (session, ip) within a window — enough to capture every new
|
|
194
|
+
// session/network combination, which is what concurrent-network /
|
|
195
|
+
// impossible-travel detection needs.
|
|
196
|
+
const _seen = new Map(); // "sid|ip" -> lastEmitMs
|
|
197
|
+
const _SEEN_MAX = 5000;
|
|
198
|
+
let _captureWindowMs = 5 * 60 * 1000;
|
|
199
|
+
|
|
200
|
+
function resolveClientIp(req) {
|
|
201
|
+
const h = (req && req.headers) || {};
|
|
202
|
+
const xff = h['x-forwarded-for'];
|
|
203
|
+
if (xff) return String(Array.isArray(xff) ? xff[0] : xff).split(',')[0].trim();
|
|
204
|
+
if (h['cf-connecting-ip']) return String(h['cf-connecting-ip']).trim();
|
|
205
|
+
if (h['x-real-ip']) return String(h['x-real-ip']).trim();
|
|
206
|
+
const sock = req && (req.socket || req.connection);
|
|
207
|
+
return (sock && sock.remoteAddress) || null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function emitSessionSeen(sessionId, userId, ip) {
|
|
211
|
+
if (!sessionId && !userId) return;
|
|
212
|
+
const key = `${sessionId || ''}|${ip || ''}`;
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
const last = _seen.get(key);
|
|
215
|
+
if (last && now - last < _captureWindowMs) return; // deduped
|
|
216
|
+
if (_seen.size > _SEEN_MAX) _seen.clear();
|
|
217
|
+
_seen.set(key, now);
|
|
218
|
+
try {
|
|
219
|
+
events.track('session.seen', { sessionId, userId, ip });
|
|
220
|
+
} catch {
|
|
221
|
+
/* never throw into the app */
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function extractIdentity(req, getSessionId, getUserId) {
|
|
226
|
+
let sessionId = null;
|
|
227
|
+
let userId = null;
|
|
228
|
+
if (getSessionId || getUserId) {
|
|
229
|
+
if (getSessionId) sessionId = getSessionId(req);
|
|
230
|
+
if (getUserId) userId = getUserId(req);
|
|
231
|
+
} else {
|
|
232
|
+
const d = defaultExtract(req);
|
|
233
|
+
sessionId = d.sessionId;
|
|
234
|
+
userId = d.userId;
|
|
235
|
+
}
|
|
236
|
+
return { sessionId, userId };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Middleware that ONLY emits session.seen (detection feed), no enforcement.
|
|
241
|
+
* Use this if you want SecureNow to see sessions but not block anything.
|
|
242
|
+
*/
|
|
243
|
+
function capture(options = {}) {
|
|
244
|
+
if (options.captureWindowMs) _captureWindowMs = options.captureWindowMs;
|
|
245
|
+
const { getSessionId, getUserId } = options;
|
|
246
|
+
return function securenowSessionCapture(req, res, next) {
|
|
247
|
+
try {
|
|
248
|
+
const { sessionId, userId } = extractIdentity(req, getSessionId, getUserId);
|
|
249
|
+
emitSessionSeen(sessionId, userId, resolveClientIp(req));
|
|
250
|
+
} catch {
|
|
251
|
+
/* fail-open */
|
|
252
|
+
}
|
|
253
|
+
return next();
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
190
257
|
function defaultOnRevoked(req, res) {
|
|
191
258
|
try {
|
|
192
259
|
const clears = SESSION_COOKIES.map((n) => `${n}=; Path=/; Max-Age=0; HttpOnly`);
|
|
@@ -211,20 +278,16 @@ function defaultOnRevoked(req, res) {
|
|
|
211
278
|
*/
|
|
212
279
|
function guard(options = {}) {
|
|
213
280
|
start(options);
|
|
281
|
+
if (options.captureWindowMs) _captureWindowMs = options.captureWindowMs;
|
|
214
282
|
const { getSessionId, getUserId } = options;
|
|
215
283
|
const onRevoked = options.onRevoked || defaultOnRevoked;
|
|
284
|
+
const doCapture = options.capture !== false; // emit session.seen by default
|
|
216
285
|
return function securenowSessionGuard(req, res, next) {
|
|
217
286
|
try {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
|
|
222
|
-
if (getUserId) userId = getUserId(req);
|
|
223
|
-
} else {
|
|
224
|
-
const d = defaultExtract(req);
|
|
225
|
-
sessionId = d.sessionId;
|
|
226
|
-
userId = d.userId;
|
|
227
|
-
}
|
|
287
|
+
const { sessionId, userId } = extractIdentity(req, getSessionId, getUserId);
|
|
288
|
+
// Detect: feed session-theft detection (deduped, fire-and-forget).
|
|
289
|
+
if (doCapture) emitSessionSeen(sessionId, userId, resolveClientIp(req));
|
|
290
|
+
// Respond: reject if this session/user has been revoked.
|
|
228
291
|
if (isRevoked({ sessionId, userId })) {
|
|
229
292
|
return onRevoked(req, res, next, { sessionId, userId });
|
|
230
293
|
}
|
|
@@ -235,4 +298,4 @@ function guard(options = {}) {
|
|
|
235
298
|
};
|
|
236
299
|
}
|
|
237
300
|
|
|
238
|
-
module.exports = { start, syncOnce, isRevoked, guard, sha256 };
|
|
301
|
+
module.exports = { start, syncOnce, isRevoked, guard, capture, resolveClientIp, sha256 };
|