@gatwech/auth 1.0.0-beta.20 → 1.0.0-beta.21
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/dist/adapters/drizzle-adapter/index.mjs +11 -2
- package/dist/adapters/index.d.mts +2 -2
- package/dist/adapters/kysely-adapter/index.mjs +1 -1
- package/dist/adapters/memory-adapter/index.d.mts +1 -1
- package/dist/adapters/memory-adapter/index.mjs +2 -2
- package/dist/api/index.d.mts +2 -2
- package/dist/api/index.mjs +1 -1
- package/dist/{audit-log-DKsUWR8O.mjs → audit-log-DgqHJ-uv.mjs} +132 -63
- package/dist/{auth-vfCyIIeV.mjs → auth-BoEA9bN7.mjs} +14 -1
- package/dist/client/index.d.mts +2 -2
- package/dist/client/plugins/index.d.mts +47 -47
- package/dist/client/plugins/index.mjs +2 -2
- package/dist/client/react/index.d.mts +2 -2
- package/dist/{client-F67q-Lbi.d.mts → client-B00gkznv.d.mts} +1 -1
- package/dist/{client-4QEb0rUr.mjs → client-CMsC6-rl.mjs} +1 -1
- package/dist/{client-C_qmyez2.d.mts → client-Da7tq8m6.d.mts} +1 -1
- package/dist/cookies/index.d.mts +3 -3
- package/dist/db/index.d.mts +2 -2
- package/dist/db/index.mjs +1 -1
- package/dist/{db-7vHnL1XA.mjs → db-CV59HpEF.mjs} +2 -2
- package/dist/{index-BX7rl07Z.d.mts → index-BECNG13R.d.mts} +13 -13
- package/dist/{index-D57DuhZR.d.mts → index-BKy-8t2n.d.mts} +179 -126
- package/dist/{index-Df0KhQRf-BEa5tdwJ.d.mts → index-BL_t8cRp-C4QYBsac.d.mts} +56 -17
- package/dist/{index-D6ku0rUC.d.mts → index-BWYXZVsV.d.mts} +5 -5
- package/dist/{index-DwPAddfE.d.mts → index-Bhc37y9F.d.mts} +3 -3
- package/dist/{index-BNhJglHC.d.mts → index-BjCQ3jmE.d.mts} +119 -119
- package/dist/{index-CzpdkTdv.d.mts → index-Bxive_YT.d.mts} +9 -9
- package/dist/{index-DGeoJ4gu.d.mts → index-C-IDh5yy.d.mts} +2 -2
- package/dist/{index-Dnd8S4S4.d.mts → index-CM1rFdwV.d.mts} +9 -9
- package/dist/{index-CNd5jqLl.d.mts → index-CMG4Pnen.d.mts} +4 -4
- package/dist/{index-4I1YhYhk.d.mts → index-CTqynuxm.d.mts} +4 -4
- package/dist/{index-CP8x1TFq.d.mts → index-CbBQdS-u.d.mts} +6 -6
- package/dist/{index-DyXi7S2M.d.mts → index-CpH-1sse.d.mts} +10 -10
- package/dist/{index-CC920PPp.d.mts → index-D1OZ2e4m.d.mts} +14 -14
- package/dist/{index-CofuaNMl.d.mts → index-Dt2I0G4X.d.mts} +7 -7
- package/dist/{index-XfE0WjTN.d.mts → index-IX1to9TB.d.mts} +34 -34
- package/dist/{index-KhbNmMmG.d.mts → index-TrOnY3pM.d.mts} +170 -170
- package/dist/{index-CsxtcAO0.d.mts → index-VUyESU4p.d.mts} +1 -1
- package/dist/{index-CBiGsvDd.d.mts → index-WV7VzW2H.d.mts} +8 -8
- package/dist/{index-Bml-PCqP.d.mts → index-YeXSxzhX.d.mts} +19 -19
- package/dist/{index-BsSKcKgO.d.mts → index-cPt-00xm.d.mts} +6 -6
- package/dist/{index-sb-UFLuo.d.mts → index-iRoaTsOj.d.mts} +2 -2
- package/dist/{index-DFSRYWFI.d.mts → index-jEkJ2Zfk.d.mts} +6 -6
- package/dist/{index-DTkcY7xH.d.mts → index-oNlp4SvD.d.mts} +10 -10
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +3 -3
- package/dist/integrations/next-js.d.mts +4 -4
- package/dist/integrations/node.d.mts +2 -2
- package/dist/integrations/react-start.d.mts +4 -4
- package/dist/{kysely-adapter-Bkm1liPW.mjs → kysely-adapter-BidlNTiV.mjs} +4 -2
- package/dist/oauth2/index.d.mts +2 -2
- package/dist/oauth2/index.mjs +1 -1
- package/dist/{organization-CkTKs7bx.mjs → organization-D4QMpChF.mjs} +1 -1
- package/dist/plugins/admin/index.d.mts +3 -3
- package/dist/plugins/admin/index.mjs +1 -1
- package/dist/plugins/anonymous/index.d.mts +3 -3
- package/dist/plugins/anonymous/index.mjs +1 -1
- package/dist/plugins/audit-log/client.d.mts +24 -24
- package/dist/plugins/audit-log/index.d.mts +23 -23
- package/dist/plugins/audit-log/index.mjs +2 -2
- package/dist/plugins/bearer/index.d.mts +1 -1
- package/dist/plugins/captcha/index.d.mts +1 -1
- package/dist/plugins/custom-session/index.d.mts +3 -3
- package/dist/plugins/custom-session/index.mjs +1 -1
- package/dist/plugins/device-authorization/index.d.mts +1 -1
- package/dist/plugins/device-authorization/index.mjs +1 -1
- package/dist/plugins/email-otp/index.d.mts +2 -2
- package/dist/plugins/email-otp/index.mjs +1 -1
- package/dist/plugins/generic-oauth/index.d.mts +3 -3
- package/dist/plugins/generic-oauth/index.mjs +1 -1
- package/dist/plugins/haveibeenpwned/index.d.mts +1 -1
- package/dist/plugins/haveibeenpwned/index.mjs +1 -1
- package/dist/plugins/index.d.mts +23 -23
- package/dist/plugins/index.mjs +5 -5
- package/dist/plugins/jwt/index.d.mts +3 -3
- package/dist/plugins/jwt/index.mjs +1 -1
- package/dist/plugins/magic-link/index.d.mts +1 -1
- package/dist/plugins/magic-link/index.mjs +1 -1
- package/dist/plugins/multi-session/index.d.mts +2 -2
- package/dist/plugins/multi-session/index.mjs +1 -1
- package/dist/plugins/oauth-proxy/index.d.mts +1 -1
- package/dist/plugins/oauth-proxy/index.mjs +1 -1
- package/dist/plugins/one-tap/index.d.mts +1 -1
- package/dist/plugins/one-tap/index.mjs +1 -1
- package/dist/plugins/one-time-token/index.d.mts +3 -3
- package/dist/plugins/one-time-token/index.mjs +5 -5
- package/dist/plugins/organization/index.d.mts +3 -3
- package/dist/plugins/organization/index.mjs +2 -2
- package/dist/plugins/passkey/index.d.mts +3 -3
- package/dist/plugins/passkey/index.mjs +1 -1
- package/dist/plugins/phone-number/index.d.mts +3 -3
- package/dist/plugins/phone-number/index.mjs +1 -1
- package/dist/plugins/sentinel/client.d.mts +24 -24
- package/dist/plugins/sentinel/client.mjs +2 -2
- package/dist/plugins/sentinel/index.d.mts +23 -23
- package/dist/plugins/sentinel/index.mjs +3 -3
- package/dist/plugins/two-factor/index.d.mts +3 -3
- package/dist/plugins/two-factor/index.mjs +1 -1
- package/dist/plugins/username/index.d.mts +1 -1
- package/dist/plugins/username/index.mjs +1 -1
- package/dist/{pow-OK01yPG3.mjs → pow-DvkUopsK.mjs} +17 -1
- package/dist/{sentinel-DiPqydKG.mjs → sentinel-B4Wo79HK.mjs} +295 -92
- package/dist/src/adapters/memory-adapter/memory-adapter.d.ts +1 -1
- package/dist/src/adapters/prisma-adapter/test/auth.d.ts +1 -1
- package/dist/src/adapters/tests/auth-flow.d.ts +3 -3
- package/dist/src/adapters/tests/normal.d.ts +10 -2
- package/dist/src/adapters/tests/number-id.d.ts +4 -4
- package/dist/src/adapters/tests/performance.d.ts +3 -3
- package/dist/src/adapters/tests/transactions.d.ts +3 -3
- package/dist/src/api/index.d.ts +8 -8
- package/dist/src/plugins/admin/admin.d.ts +2 -2
- package/dist/src/plugins/api-key/index.d.ts +2 -2
- package/dist/src/plugins/audit-log/index.d.ts +5 -14
- package/dist/src/plugins/audit-log/remote.d.ts +4 -4
- package/dist/src/plugins/audit-log/types.d.ts +22 -0
- package/dist/src/plugins/email-otp/index.d.ts +1 -1
- package/dist/src/plugins/multi-session/index.d.ts +8 -8
- package/dist/src/plugins/organization/routes/crud-access-control.d.ts +8 -8
- package/dist/src/plugins/organization/routes/crud-invites.d.ts +46 -46
- package/dist/src/plugins/organization/routes/crud-members.d.ts +40 -40
- package/dist/src/plugins/organization/routes/crud-org.d.ts +36 -36
- package/dist/src/plugins/organization/routes/crud-team.d.ts +50 -50
- package/dist/src/plugins/sentinel/index.d.ts +10 -1
- package/dist/src/plugins/sentinel/intel.d.ts +8 -1
- package/dist/src/plugins/sentinel/memory-cache.d.ts +12 -0
- package/dist/src/plugins/sentinel/schema.d.ts +4 -0
- package/dist/src/plugins/sentinel/store.d.ts +7 -0
- package/dist/src/plugins/sentinel/types.d.ts +29 -1
- package/dist/src/test-utils/test-instance.d.ts +25 -25
- package/dist/test-utils/index.d.mts +1045 -1045
- package/dist/test-utils/index.mjs +7 -7
- package/dist/types/index.d.mts +2 -2
- package/package.json +3 -3
|
@@ -4,7 +4,7 @@ import { n as deleteSessionCookie } from "./cookies-BuvlgcA-.mjs";
|
|
|
4
4
|
import { n as mergeSchema } from "./date-CMr04reD.mjs";
|
|
5
5
|
import { a as CREDENTIAL_SIGN_IN_PATHS, c as SIGN_IN_PATHS, l as VERIFICATION_PATHS, s as SECURITY_EVENT_TYPES, t as getAuditRecorder } from "./registry-CqG_3Qnx.mjs";
|
|
6
6
|
import { t as getPasswordBreachCount } from "./haveibeenpwned-CzNda7Tn.mjs";
|
|
7
|
-
import { c as verifySolution, i as createNonce, n as POW_REASON_HEADER, o as parsePowHeader, r as POW_SOLUTION_HEADER, t as POW_CHALLENGE_HEADER } from "./pow-
|
|
7
|
+
import { c as verifySolution, i as createNonce, n as POW_REASON_HEADER, o as parsePowHeader, r as POW_SOLUTION_HEADER, t as POW_CHALLENGE_HEADER } from "./pow-DvkUopsK.mjs";
|
|
8
8
|
import { env } from "@gatwech/core/env";
|
|
9
9
|
import { PayvoAuthError } from "@gatwech/core/error";
|
|
10
10
|
import { defineErrorCodes } from "@gatwech/core/utils";
|
|
@@ -15,15 +15,20 @@ import { betterFetch } from "@better-fetch/fetch";
|
|
|
15
15
|
//#region src/plugins/sentinel/actions.ts
|
|
16
16
|
const CHALLENGE_TTL_MS = 6e4;
|
|
17
17
|
/**
|
|
18
|
+
* Challenges an IP address can be issued within one challenge lifetime.
|
|
19
|
+
* Each one is stored, so past this the request is blocked instead.
|
|
20
|
+
*/
|
|
21
|
+
const MAX_CHALLENGES_PER_IP = 30;
|
|
22
|
+
/**
|
|
18
23
|
* Applies a security action: records the event and decides whether the
|
|
19
24
|
* request continues, gets a proof-of-work challenge or is blocked
|
|
20
25
|
*/
|
|
21
26
|
async function executeAction(params) {
|
|
22
27
|
const { auth, event, action, reason } = params;
|
|
23
28
|
const recorder = getAuditRecorder(auth);
|
|
24
|
-
const record = async (eventType, status) => {
|
|
29
|
+
const record = async (eventType, status, metadata = params.metadata) => {
|
|
25
30
|
if (!recorder) {
|
|
26
|
-
auth.logger.warn(`[sentinel] ${reason}: ${status}`,
|
|
31
|
+
auth.logger.warn(`[sentinel] ${reason}: ${status}`, metadata);
|
|
27
32
|
return;
|
|
28
33
|
}
|
|
29
34
|
await recorder(event, {
|
|
@@ -36,7 +41,7 @@ async function executeAction(params) {
|
|
|
36
41
|
eventData: {
|
|
37
42
|
reason,
|
|
38
43
|
action,
|
|
39
|
-
...
|
|
44
|
+
...metadata
|
|
40
45
|
}
|
|
41
46
|
});
|
|
42
47
|
};
|
|
@@ -49,12 +54,32 @@ async function executeAction(params) {
|
|
|
49
54
|
await record(SECURITY_EVENT_TYPES.ALLOWED_AFTER_CHALLENGE, "success");
|
|
50
55
|
return { outcome: "allow" };
|
|
51
56
|
}
|
|
57
|
+
if (params.ipAddress) {
|
|
58
|
+
const challengesIssued = await params.store.increment(`pow-issued:${params.ipAddress}`, CHALLENGE_TTL_MS / 1e3).catch((e) => {
|
|
59
|
+
auth.logger.error("[sentinel] Failed to count challenges", e);
|
|
60
|
+
return 0;
|
|
61
|
+
});
|
|
62
|
+
if (challengesIssued > MAX_CHALLENGES_PER_IP) {
|
|
63
|
+
const metadata = {
|
|
64
|
+
...params.metadata,
|
|
65
|
+
challengesIssued,
|
|
66
|
+
maxChallenges: MAX_CHALLENGES_PER_IP
|
|
67
|
+
};
|
|
68
|
+
await record(params.eventType, "blocked", metadata);
|
|
69
|
+
await record(SECURITY_EVENT_TYPES.BLOCKED, "blocked", metadata);
|
|
70
|
+
return {
|
|
71
|
+
outcome: "block",
|
|
72
|
+
reason
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
52
76
|
const nonce = createNonce();
|
|
53
77
|
try {
|
|
54
78
|
await params.store.saveChallenge({
|
|
55
79
|
nonce,
|
|
56
80
|
difficulty: params.difficulty,
|
|
57
81
|
reason,
|
|
82
|
+
path: event.path ?? null,
|
|
58
83
|
ipAddress: params.ipAddress,
|
|
59
84
|
visitorId: params.visitorId,
|
|
60
85
|
expiresAt: new Date(Date.now() + CHALLENGE_TTL_MS)
|
|
@@ -203,15 +228,15 @@ const isMissingRecord = (e) => {
|
|
|
203
228
|
};
|
|
204
229
|
/** Whether a domain has an A or AAAA record, null when a lookup failed */
|
|
205
230
|
async function hasAddressRecords(dns, domain) {
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
208
|
-
return
|
|
231
|
+
const addressLookups = await Promise.allSettled([dns.resolve4(domain), dns.resolve6(domain)]);
|
|
232
|
+
if (addressLookups.some((lookup) => lookup.status === "fulfilled" && lookup.value.length > 0)) return true;
|
|
233
|
+
return addressLookups.some((lookup) => lookup.status === "rejected" && !isMissingRecord(lookup.reason)) ? null : false;
|
|
209
234
|
}
|
|
210
|
-
async function
|
|
235
|
+
async function canReceiveMail(dns, domain) {
|
|
211
236
|
try {
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
214
|
-
if (
|
|
237
|
+
const mxRecords = await dns.resolveMx(domain);
|
|
238
|
+
if (mxRecords.length === 1 && (mxRecords[0].exchange === "" || mxRecords[0].exchange === ".")) return false;
|
|
239
|
+
if (mxRecords.length > 0) return true;
|
|
215
240
|
} catch (e) {
|
|
216
241
|
const code = e.code;
|
|
217
242
|
if (code === "ENOTFOUND" || code === "ENOTIMP") return false;
|
|
@@ -239,7 +264,7 @@ async function hasMxRecords(domain, options = {}) {
|
|
|
239
264
|
timer = setTimeout(() => resolve(null), options.timeoutMs ?? MAIL_LOOKUP_TIMEOUT_MS);
|
|
240
265
|
});
|
|
241
266
|
try {
|
|
242
|
-
return await Promise.race([
|
|
267
|
+
return await Promise.race([canReceiveMail(dns, domain), timeout]);
|
|
243
268
|
} finally {
|
|
244
269
|
clearTimeout(timer);
|
|
245
270
|
}
|
|
@@ -278,6 +303,41 @@ function travelSpeedKmh(from, to) {
|
|
|
278
303
|
return distance / (Math.max(Math.abs(to.at.getTime() - from.at.getTime()), 6e4) / 36e5);
|
|
279
304
|
}
|
|
280
305
|
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region src/plugins/sentinel/memory-cache.ts
|
|
308
|
+
/**
|
|
309
|
+
* A small in-memory cache that forgets entries after a TTL and drops the
|
|
310
|
+
* least recently used one when it's full. Used when there's no secondary
|
|
311
|
+
* storage to share verdicts through.
|
|
312
|
+
*/
|
|
313
|
+
function createMemoryCache(options) {
|
|
314
|
+
const entries = /* @__PURE__ */ new Map();
|
|
315
|
+
return {
|
|
316
|
+
get(key) {
|
|
317
|
+
const entry = entries.get(key);
|
|
318
|
+
if (!entry) return void 0;
|
|
319
|
+
if (entry.expiresAt <= Date.now()) {
|
|
320
|
+
entries.delete(key);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
entries.delete(key);
|
|
324
|
+
entries.set(key, entry);
|
|
325
|
+
return entry.value;
|
|
326
|
+
},
|
|
327
|
+
set(key, value, ttlMs = options.ttlMs) {
|
|
328
|
+
entries.delete(key);
|
|
329
|
+
if (entries.size >= options.maxSize) {
|
|
330
|
+
const oldest = entries.keys().next().value;
|
|
331
|
+
if (oldest !== void 0) entries.delete(oldest);
|
|
332
|
+
}
|
|
333
|
+
entries.set(key, {
|
|
334
|
+
value,
|
|
335
|
+
expiresAt: Date.now() + ttlMs
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
281
341
|
//#endregion
|
|
282
342
|
//#region src/plugins/sentinel/intel.ts
|
|
283
343
|
const BOT_USER_AGENT = /bot|crawl|spider|slurp|headless|phantomjs|selenium|puppeteer|playwright|curl|wget|httpie|python-requests|python-urllib|aiohttp|go-http-client|okhttp|java\/|libwww|scrapy|postman|insomnia/i;
|
|
@@ -296,9 +356,18 @@ const UNKNOWN = {
|
|
|
296
356
|
bot: null,
|
|
297
357
|
geo: null
|
|
298
358
|
};
|
|
359
|
+
/** Verdicts kept in memory when there's no secondary storage */
|
|
360
|
+
const MEMORY_CACHE_SIZE = 5e3;
|
|
361
|
+
/** How long a verified visitor id is remembered */
|
|
362
|
+
const VISITOR_CACHE_SECONDS = 300;
|
|
363
|
+
/** How long an in-memory request verdict lasts when the backend sets none */
|
|
364
|
+
const DEFAULT_INTEL_CACHE_SECONDS = 60;
|
|
365
|
+
/** The longest an in-memory request verdict lasts */
|
|
366
|
+
const MAX_INTEL_CACHE_SECONDS = 300;
|
|
299
367
|
/**
|
|
300
368
|
* Asks the hosted backend about a request's IP address and client. Verdicts
|
|
301
|
-
* are cached
|
|
369
|
+
* are cached for as long as the backend allows, in secondary storage or, when
|
|
370
|
+
* there is none, in memory.
|
|
302
371
|
* Returns null verdicts when the backend isn't configured or can't answer.
|
|
303
372
|
*/
|
|
304
373
|
function createIntelClient(options) {
|
|
@@ -312,15 +381,26 @@ function createIntelClient(options) {
|
|
|
312
381
|
baseDelay: options.kvOptions?.retry?.baseDelay ?? 400,
|
|
313
382
|
maxDelay: options.kvOptions?.retry?.maxDelay ?? 600
|
|
314
383
|
};
|
|
384
|
+
const visitorVerdicts = createMemoryCache({
|
|
385
|
+
maxSize: MEMORY_CACHE_SIZE,
|
|
386
|
+
ttlMs: VISITOR_CACHE_SECONDS * 1e3
|
|
387
|
+
});
|
|
388
|
+
const requestVerdicts = createMemoryCache({
|
|
389
|
+
maxSize: MEMORY_CACHE_SIZE,
|
|
390
|
+
ttlMs: DEFAULT_INTEL_CACHE_SECONDS * 1e3
|
|
391
|
+
});
|
|
315
392
|
return {
|
|
316
393
|
configured,
|
|
317
394
|
verifiesVisitors: Boolean(kvUrl && apiKey),
|
|
318
395
|
misconfigured: Boolean((apiUrl || kvUrl) && !apiKey) || Boolean(apiKey && !apiUrl && !kvUrl),
|
|
319
|
-
async verifyVisitor(auth, request) {
|
|
396
|
+
async verifyVisitor(auth, request, limit) {
|
|
320
397
|
if (!kvUrl || !apiKey) return request.visitorId;
|
|
321
398
|
const cacheKey = `sentinel:visitor:${request.visitorId}:${request.ip ?? ""}`;
|
|
322
399
|
const cached = await auth.secondaryStorage?.get(cacheKey);
|
|
323
400
|
if (typeof cached === "string") return cached || null;
|
|
401
|
+
const remembered = visitorVerdicts.get(cacheKey);
|
|
402
|
+
if (remembered !== void 0 && !auth.secondaryStorage) return remembered;
|
|
403
|
+
if (limit && !await limit.allowed()) return null;
|
|
324
404
|
for (let attempt = 0; attempt <= kv.attempts; attempt++) {
|
|
325
405
|
if (attempt > 0) {
|
|
326
406
|
const wait = Math.min(kv.baseDelay * 2 ** (attempt - 1), kv.maxDelay);
|
|
@@ -340,7 +420,9 @@ function createIntelClient(options) {
|
|
|
340
420
|
}));
|
|
341
421
|
if (data && !error) {
|
|
342
422
|
const visitorId = data.valid ? data.visitorId ?? request.visitorId : null;
|
|
343
|
-
|
|
423
|
+
if (!visitorId) await limit?.rejected();
|
|
424
|
+
if (auth.secondaryStorage) await auth.secondaryStorage.set(cacheKey, visitorId ?? "", VISITOR_CACHE_SECONDS);
|
|
425
|
+
else visitorVerdicts.set(cacheKey, visitorId);
|
|
344
426
|
return visitorId;
|
|
345
427
|
}
|
|
346
428
|
}
|
|
@@ -352,6 +434,8 @@ function createIntelClient(options) {
|
|
|
352
434
|
const cacheKey = `sentinel:intel:${ip}:${visitorId ?? ""}`;
|
|
353
435
|
const cached = safeJSONParse(await auth.secondaryStorage?.get(cacheKey));
|
|
354
436
|
if (cached) return cached;
|
|
437
|
+
const remembered = auth.secondaryStorage ? void 0 : requestVerdicts.get(cacheKey);
|
|
438
|
+
if (remembered) return remembered;
|
|
355
439
|
const { data, error } = await betterFetch(`${apiUrl}/v1/intel/request`, {
|
|
356
440
|
method: "POST",
|
|
357
441
|
headers: {
|
|
@@ -378,7 +462,12 @@ function createIntelClient(options) {
|
|
|
378
462
|
bot: data.bot?.verdict === "bot" ? true : data.bot?.verdict === "human" ? false : null,
|
|
379
463
|
geo: data.geo ?? null
|
|
380
464
|
};
|
|
381
|
-
if (
|
|
465
|
+
if (auth.secondaryStorage) {
|
|
466
|
+
if (data.ttlSeconds) await auth.secondaryStorage.set(cacheKey, JSON.stringify(intel), data.ttlSeconds);
|
|
467
|
+
} else {
|
|
468
|
+
const ttlSeconds = Math.min(data.ttlSeconds ?? DEFAULT_INTEL_CACHE_SECONDS, MAX_INTEL_CACHE_SECONDS);
|
|
469
|
+
requestVerdicts.set(cacheKey, intel, ttlSeconds * 1e3);
|
|
470
|
+
}
|
|
382
471
|
return intel;
|
|
383
472
|
}
|
|
384
473
|
};
|
|
@@ -466,6 +555,10 @@ const schema = {
|
|
|
466
555
|
type: "string",
|
|
467
556
|
required: true
|
|
468
557
|
},
|
|
558
|
+
path: {
|
|
559
|
+
type: "string",
|
|
560
|
+
required: false
|
|
561
|
+
},
|
|
469
562
|
ipAddress: {
|
|
470
563
|
type: "string",
|
|
471
564
|
required: false
|
|
@@ -488,6 +581,35 @@ const schema = {
|
|
|
488
581
|
//#endregion
|
|
489
582
|
//#region src/plugins/sentinel/store.ts
|
|
490
583
|
const PREFIX = "sentinel:";
|
|
584
|
+
/** How long after expiring a row is deleted */
|
|
585
|
+
const EXPIRED_ROW_GRACE_MS = 6e4;
|
|
586
|
+
/** Tries to increment a counter in the database before giving up */
|
|
587
|
+
const MAX_COUNTER_ATTEMPTS = 20;
|
|
588
|
+
/** When each auth instance last deleted expired rows, keyed by its adapter */
|
|
589
|
+
const lastChecked = /* @__PURE__ */ new WeakMap();
|
|
590
|
+
/**
|
|
591
|
+
* Deletes expired counters and challenges from the database, at most every
|
|
592
|
+
* 10 seconds per auth instance. Secondary storage expires them itself.
|
|
593
|
+
*/
|
|
594
|
+
async function deleteAllExpiredRows(auth, byPassLastCheckTime = false) {
|
|
595
|
+
if (auth.secondaryStorage) return;
|
|
596
|
+
const checkedAt = lastChecked.get(auth.adapter);
|
|
597
|
+
if (checkedAt && !byPassLastCheckTime) {
|
|
598
|
+
if (Date.now() - checkedAt.getTime() < 1e4) return;
|
|
599
|
+
}
|
|
600
|
+
lastChecked.set(auth.adapter, /* @__PURE__ */ new Date());
|
|
601
|
+
const expiredBefore = new Date(Date.now() - EXPIRED_ROW_GRACE_MS);
|
|
602
|
+
await Promise.all(["sentinelCounter", "powChallenge"].map((model) => auth.adapter.deleteMany({
|
|
603
|
+
model,
|
|
604
|
+
where: [{
|
|
605
|
+
field: "expiresAt",
|
|
606
|
+
operator: "lt",
|
|
607
|
+
value: expiredBefore
|
|
608
|
+
}]
|
|
609
|
+
}))).catch((error) => {
|
|
610
|
+
auth.logger.error("[sentinel] Failed to delete expired rows:", error);
|
|
611
|
+
});
|
|
612
|
+
}
|
|
491
613
|
/**
|
|
492
614
|
* Counters and challenges, kept in secondary storage when it's configured
|
|
493
615
|
* and in the database otherwise
|
|
@@ -522,45 +644,45 @@ function createSentinelStore(auth) {
|
|
|
522
644
|
}), Math.max(Math.ceil((expiresAt - now) / 1e3), 1));
|
|
523
645
|
return count;
|
|
524
646
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
647
|
+
deleteAllExpiredRows(auth);
|
|
648
|
+
for (let attempt = 0; attempt < MAX_COUNTER_ATTEMPTS; attempt++) {
|
|
649
|
+
const row = await findCounter(key);
|
|
650
|
+
if (!row) {
|
|
651
|
+
if (await auth.adapter.create({
|
|
652
|
+
model: "sentinelCounter",
|
|
653
|
+
data: {
|
|
654
|
+
key,
|
|
655
|
+
count: 1,
|
|
656
|
+
expiresAt: new Date(Date.now() + ttlSeconds * 1e3)
|
|
657
|
+
}
|
|
658
|
+
}).then(() => true, () => false)) return 1;
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
const expiresAt = new Date(row.expiresAt);
|
|
662
|
+
const expired = expiresAt.getTime() <= Date.now();
|
|
663
|
+
if (await auth.adapter.updateMany({
|
|
540
664
|
model: "sentinelCounter",
|
|
541
|
-
where: [
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
665
|
+
where: [
|
|
666
|
+
{
|
|
667
|
+
field: "id",
|
|
668
|
+
value: row.id
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
field: "count",
|
|
672
|
+
value: row.count
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
field: "expiresAt",
|
|
676
|
+
value: expiresAt
|
|
677
|
+
}
|
|
678
|
+
],
|
|
545
679
|
update: {
|
|
546
|
-
count: 1,
|
|
547
|
-
expiresAt: new Date(Date.now() + ttlSeconds * 1e3)
|
|
680
|
+
count: expired ? 1 : row.count + 1,
|
|
681
|
+
...expired || options.sliding ? { expiresAt: new Date(Date.now() + ttlSeconds * 1e3) } : {}
|
|
548
682
|
}
|
|
549
|
-
});
|
|
550
|
-
return 1;
|
|
683
|
+
}) > 0) return expired ? 1 : row.count + 1;
|
|
551
684
|
}
|
|
552
|
-
|
|
553
|
-
model: "sentinelCounter",
|
|
554
|
-
where: [{
|
|
555
|
-
field: "id",
|
|
556
|
-
value: row.id
|
|
557
|
-
}],
|
|
558
|
-
update: {
|
|
559
|
-
count: row.count + 1,
|
|
560
|
-
...options.sliding ? { expiresAt: new Date(Date.now() + ttlSeconds * 1e3) } : {}
|
|
561
|
-
}
|
|
562
|
-
});
|
|
563
|
-
return row.count + 1;
|
|
685
|
+
throw new Error(`Failed to increment counter ${key}`);
|
|
564
686
|
},
|
|
565
687
|
async get(name) {
|
|
566
688
|
const key = PREFIX + name;
|
|
@@ -595,6 +717,7 @@ function createSentinelStore(auth) {
|
|
|
595
717
|
await storage.set(`${PREFIX}pow:${challenge.nonce}`, JSON.stringify(challenge), ttlSeconds);
|
|
596
718
|
return;
|
|
597
719
|
}
|
|
720
|
+
deleteAllExpiredRows(auth);
|
|
598
721
|
await auth.adapter.create({
|
|
599
722
|
model: "powChallenge",
|
|
600
723
|
data: {
|
|
@@ -678,11 +801,26 @@ const NEW_PASSWORD_FIELDS = {
|
|
|
678
801
|
* towards credential stuffing
|
|
679
802
|
*/
|
|
680
803
|
const isAttemptPath = (path) => CREDENTIAL_SIGN_IN_PATHS.includes(path) || VERIFICATION_PATHS.includes(path);
|
|
804
|
+
/** Endpoints a bot is blocked on: sign-in, sign-up and password reset */
|
|
805
|
+
const BOT_CHECK_PATHS = [
|
|
806
|
+
...CREDENTIAL_SIGN_IN_PATHS,
|
|
807
|
+
...VERIFICATION_PATHS,
|
|
808
|
+
...SIGN_UP_PATHS,
|
|
809
|
+
...PASSWORD_RESET_PATHS
|
|
810
|
+
];
|
|
681
811
|
/** Account creation that isn't done by the visitor signing up */
|
|
682
812
|
const isManagedAccountCreation = (path) => !path || path.startsWith("/admin/") || path === "/sign-in/anonymous";
|
|
683
813
|
/** Social sign-ups, whose email the provider has already verified */
|
|
684
814
|
const OAUTH_CALLBACK_PATHS = ["/callback/:id", "/oauth2/callback/:providerId"];
|
|
685
815
|
const DAY_SECONDS = 1440 * 60;
|
|
816
|
+
/** Window the identification service limits per IP address count over */
|
|
817
|
+
const VISITOR_LIMIT_WINDOW_SECONDS = 60;
|
|
818
|
+
/** Calls to the identification service per IP address in the window */
|
|
819
|
+
const MAX_VERIFICATIONS_PER_IP = 20;
|
|
820
|
+
/** Visitor ids that weren't genuine per IP address in the window */
|
|
821
|
+
const MAX_INVALID_VISITORS_PER_IP = 5;
|
|
822
|
+
/** Sign-ins kept per user for impossible travel, stale and new devices */
|
|
823
|
+
const SIGN_IN_HISTORY_LIMIT = 50;
|
|
686
824
|
/**
|
|
687
825
|
* Sign-ins checked with a password, which can be repeated with a challenge
|
|
688
826
|
* solution
|
|
@@ -732,7 +870,10 @@ const sentinel = (options) => {
|
|
|
732
870
|
thresholds: security.freeTrialAbuse.thresholds,
|
|
733
871
|
maxAccountsPerVisitor: security.freeTrialAbuse.maxAccountsPerVisitor ?? 3,
|
|
734
872
|
retentionSeconds: Math.min(Math.max(security.freeTrialAbuse.retentionSeconds ?? 90 * DAY_SECONDS, DAY_SECONDS), 365 * DAY_SECONDS),
|
|
735
|
-
action: security.freeTrialAbuse.action ?? "block"
|
|
873
|
+
action: security.freeTrialAbuse.action ?? "block",
|
|
874
|
+
maxAccountsPerIp: security.freeTrialAbuse.maxAccountsPerIp ?? 10,
|
|
875
|
+
ipWindowSeconds: Math.min(Math.max(security.freeTrialAbuse.ipWindowSeconds ?? DAY_SECONDS, 1), 365 * DAY_SECONDS),
|
|
876
|
+
requireVerifiedVisitor: security.freeTrialAbuse.requireVerifiedVisitor ?? false
|
|
736
877
|
} : null;
|
|
737
878
|
const botBlocking = resolveToggle(security.botBlocking, "challenge");
|
|
738
879
|
const suspiciousIpBlocking = resolveToggle(security.suspiciousIpBlocking, "block");
|
|
@@ -766,16 +907,32 @@ const sentinel = (options) => {
|
|
|
766
907
|
const unknownDeviceNotification = security.unknownDeviceNotification ?? false;
|
|
767
908
|
const tracksSignIns = Boolean(impossibleTravel || staleUsers || unknownDeviceNotification);
|
|
768
909
|
const checksAccountCreation = Boolean(freeTrialAbuse || velocity || emailValidation);
|
|
769
|
-
/**
|
|
770
|
-
const
|
|
771
|
-
/**
|
|
772
|
-
const
|
|
910
|
+
/** Whether each request's solution was accepted, so it's checked once */
|
|
911
|
+
const acceptedSolutions = /* @__PURE__ */ new WeakMap();
|
|
912
|
+
/** Each request's verified visitor, so it's checked once */
|
|
913
|
+
const verifiedVisitors = /* @__PURE__ */ new WeakMap();
|
|
773
914
|
function clientIp(request, auth) {
|
|
774
915
|
if (!request) return null;
|
|
775
916
|
if (opts.providers?.getClientIp) return opts.providers.getClientIp(request);
|
|
776
917
|
return getIp(request, auth.options);
|
|
777
918
|
}
|
|
778
919
|
/**
|
|
920
|
+
* Limits the calls to the identification service for one IP address: after
|
|
921
|
+
* too many calls, or too many ids that weren't genuine, its ids go
|
|
922
|
+
* unchecked and aren't trusted
|
|
923
|
+
*/
|
|
924
|
+
function visitorVerificationLimit(store, ip) {
|
|
925
|
+
return {
|
|
926
|
+
async allowed() {
|
|
927
|
+
if (await store.get(`visitor-invalid:${ip}`) >= MAX_INVALID_VISITORS_PER_IP) return false;
|
|
928
|
+
return await store.increment(`visitor-verify:${ip}`, VISITOR_LIMIT_WINDOW_SECONDS) <= MAX_VERIFICATIONS_PER_IP;
|
|
929
|
+
},
|
|
930
|
+
async rejected() {
|
|
931
|
+
await store.increment(`visitor-invalid:${ip}`, VISITOR_LIMIT_WINDOW_SECONDS);
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
779
936
|
* The visitor id sent by the client, checked with the identification
|
|
780
937
|
* service when one is configured
|
|
781
938
|
*/
|
|
@@ -785,22 +942,23 @@ const sentinel = (options) => {
|
|
|
785
942
|
id: null,
|
|
786
943
|
verified: false
|
|
787
944
|
});
|
|
788
|
-
const
|
|
789
|
-
if (
|
|
945
|
+
const cachedVisitor = verifiedVisitors.get(headers);
|
|
946
|
+
if (cachedVisitor) return cachedVisitor;
|
|
790
947
|
const unchecked = {
|
|
791
948
|
id: opts.failOpen ? sent : null,
|
|
792
949
|
verified: false
|
|
793
950
|
};
|
|
794
|
-
const
|
|
951
|
+
const store = createSentinelStore(auth);
|
|
952
|
+
const visitor = intel.verifyVisitor(auth, {
|
|
795
953
|
visitorId: sent,
|
|
796
954
|
ip,
|
|
797
955
|
userAgent: headers.get("user-agent")
|
|
798
|
-
}).then((verified) => verified === void 0 ? unchecked : {
|
|
956
|
+
}, ip ? visitorVerificationLimit(store, ip) : void 0).then((verified) => verified === void 0 ? unchecked : {
|
|
799
957
|
id: verified,
|
|
800
958
|
verified: intel.verifiesVisitors
|
|
801
959
|
}).catch(() => unchecked);
|
|
802
|
-
|
|
803
|
-
return
|
|
960
|
+
verifiedVisitors.set(headers, visitor);
|
|
961
|
+
return visitor;
|
|
804
962
|
}
|
|
805
963
|
function relativePath(request, auth) {
|
|
806
964
|
const pathname = new URL(request.url).pathname;
|
|
@@ -809,27 +967,28 @@ const sentinel = (options) => {
|
|
|
809
967
|
}
|
|
810
968
|
/**
|
|
811
969
|
* Whether the request carries a valid, unused solution to a challenge
|
|
812
|
-
* issued to the same IP address and visitor
|
|
970
|
+
* issued to the same IP address and visitor, for the same endpoint
|
|
813
971
|
*/
|
|
814
|
-
function challengePassed(key, details, store, auth) {
|
|
815
|
-
const
|
|
816
|
-
if (
|
|
817
|
-
const
|
|
818
|
-
const
|
|
819
|
-
if (!
|
|
972
|
+
function challengePassed(key, details, store, auth, path) {
|
|
973
|
+
const cachedAcceptance = acceptedSolutions.get(key);
|
|
974
|
+
if (cachedAcceptance) return cachedAcceptance;
|
|
975
|
+
const accepted = (async () => {
|
|
976
|
+
const solution = parsePowHeader(details.headers?.get(POW_SOLUTION_HEADER));
|
|
977
|
+
if (!solution) return false;
|
|
820
978
|
try {
|
|
821
|
-
const challenge = await store.consumeChallenge(
|
|
979
|
+
const challenge = await store.consumeChallenge(solution.nonce);
|
|
822
980
|
if (!challenge) return false;
|
|
823
981
|
if (challenge.ipAddress && challenge.ipAddress !== details.ip) return false;
|
|
824
982
|
if (challenge.visitorId && challenge.visitorId !== details.visitorId) return false;
|
|
825
|
-
|
|
983
|
+
if (challenge.path && challenge.path !== path) return false;
|
|
984
|
+
return await verifySolution(solution.nonce, solution.value, challenge.difficulty);
|
|
826
985
|
} catch (e) {
|
|
827
986
|
auth.logger.error("[sentinel] Failed to verify challenge", e);
|
|
828
987
|
return false;
|
|
829
988
|
}
|
|
830
989
|
})();
|
|
831
|
-
|
|
832
|
-
return
|
|
990
|
+
acceptedSolutions.set(key, accepted);
|
|
991
|
+
return accepted;
|
|
833
992
|
}
|
|
834
993
|
/** Runs a check, applying `failOpen` when it can't complete */
|
|
835
994
|
async function guard(auth, check) {
|
|
@@ -876,12 +1035,12 @@ const sentinel = (options) => {
|
|
|
876
1035
|
visitorId: details.visitorId,
|
|
877
1036
|
identifier: details.identifier,
|
|
878
1037
|
userId,
|
|
879
|
-
challengePassed: action === "challenge" ? await challengePassed(challengeKey(ctx, details), details, store, ctx.context) : false,
|
|
1038
|
+
challengePassed: action === "challenge" ? await challengePassed(challengeKey(ctx, details), details, store, ctx.context, ctx.path) : false,
|
|
880
1039
|
difficulty,
|
|
881
1040
|
metadata
|
|
882
1041
|
}));
|
|
883
1042
|
}
|
|
884
|
-
/** The key a request's
|
|
1043
|
+
/** The key a request's accepted solution is cached under */
|
|
885
1044
|
function challengeKey(ctx, details) {
|
|
886
1045
|
return ctx.request ?? details.headers ?? ctx;
|
|
887
1046
|
}
|
|
@@ -939,6 +1098,31 @@ const sentinel = (options) => {
|
|
|
939
1098
|
if (opts.providers?.resolveGeo) return opts.providers.resolveGeo(details.ip);
|
|
940
1099
|
return (await intel.lookup(auth, details)).geo;
|
|
941
1100
|
}
|
|
1101
|
+
/** Deletes a user's sign-ins beyond the most recent ones Sentinel keeps */
|
|
1102
|
+
async function deleteOlderSignIns(auth, userId) {
|
|
1103
|
+
const olderSignIns = await auth.adapter.findMany({
|
|
1104
|
+
model: "sentinelSignIn",
|
|
1105
|
+
where: [{
|
|
1106
|
+
field: "userId",
|
|
1107
|
+
value: userId
|
|
1108
|
+
}],
|
|
1109
|
+
sortBy: {
|
|
1110
|
+
field: "createdAt",
|
|
1111
|
+
direction: "desc"
|
|
1112
|
+
},
|
|
1113
|
+
offset: SIGN_IN_HISTORY_LIMIT,
|
|
1114
|
+
limit: 100
|
|
1115
|
+
});
|
|
1116
|
+
if (!olderSignIns.length) return;
|
|
1117
|
+
await auth.adapter.deleteMany({
|
|
1118
|
+
model: "sentinelSignIn",
|
|
1119
|
+
where: [{
|
|
1120
|
+
field: "id",
|
|
1121
|
+
operator: "in",
|
|
1122
|
+
value: olderSignIns.map((signIn) => signIn.id)
|
|
1123
|
+
}]
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
942
1126
|
async function lastSignIn(auth, userId) {
|
|
943
1127
|
const [row] = await auth.adapter.findMany({
|
|
944
1128
|
model: "sentinelSignIn",
|
|
@@ -1062,6 +1246,20 @@ const sentinel = (options) => {
|
|
|
1062
1246
|
});
|
|
1063
1247
|
}));
|
|
1064
1248
|
}
|
|
1249
|
+
if (freeTrialAbuse) {
|
|
1250
|
+
if (details.ip) {
|
|
1251
|
+
const ip = details.ip;
|
|
1252
|
+
throwForDecision(await guard(ctx.context, async () => {
|
|
1253
|
+
const created = await store.get(`trial:ip:${ip}`);
|
|
1254
|
+
if (created < freeTrialAbuse.maxAccountsPerIp) return { outcome: "allow" };
|
|
1255
|
+
return act(event, store, details, freeTrialAbuse.action, SECURITY_EVENT_TYPES.FREE_TRIAL_ABUSE, "free_trial_abuse", {
|
|
1256
|
+
accounts: created,
|
|
1257
|
+
signal: "ip"
|
|
1258
|
+
});
|
|
1259
|
+
}));
|
|
1260
|
+
}
|
|
1261
|
+
if (freeTrialAbuse.requireVerifiedVisitor && !details.visitorVerified) throwForDecision(await act(event, store, details, "challenge", SECURITY_EVENT_TYPES.FREE_TRIAL_ABUSE, "visitor_not_verified", {}));
|
|
1262
|
+
}
|
|
1065
1263
|
if (freeTrialAbuse && details.visitorId) {
|
|
1066
1264
|
const visitorId = details.visitorId;
|
|
1067
1265
|
throwForDecision(await guard(ctx.context, async () => {
|
|
@@ -1099,9 +1297,10 @@ const sentinel = (options) => {
|
|
|
1099
1297
|
const headers = event.headers ?? event.request?.headers;
|
|
1100
1298
|
const ip = clientIp(event.request ?? headers, context.context);
|
|
1101
1299
|
const { id: visitorId } = await resolveVisitor(context.context, headers, ip);
|
|
1102
|
-
if (!visitorId) return;
|
|
1103
1300
|
try {
|
|
1104
|
-
|
|
1301
|
+
const store = createSentinelStore(context.context);
|
|
1302
|
+
if (visitorId) await store.increment(`trial:${visitorId}`, freeTrialAbuse.retentionSeconds, { sliding: true });
|
|
1303
|
+
if (ip) await store.increment(`trial:ip:${ip}`, freeTrialAbuse.ipWindowSeconds);
|
|
1105
1304
|
} catch (e) {
|
|
1106
1305
|
context.context.logger.error("[sentinel] Failed to track sign-up", e);
|
|
1107
1306
|
}
|
|
@@ -1118,34 +1317,37 @@ const sentinel = (options) => {
|
|
|
1118
1317
|
},
|
|
1119
1318
|
async onRequest(request, ctx) {
|
|
1120
1319
|
if (!botBlocking && !suspiciousIpBlocking && !geoBlocking) return;
|
|
1320
|
+
const path = relativePath(request, ctx);
|
|
1321
|
+
const checkBot = Boolean(botBlocking) && BOT_CHECK_PATHS.includes(path);
|
|
1322
|
+
if (!checkBot && !suspiciousIpBlocking && !geoBlocking) return;
|
|
1121
1323
|
const store = createSentinelStore(ctx);
|
|
1122
1324
|
const event = {
|
|
1123
1325
|
context: ctx,
|
|
1124
1326
|
request,
|
|
1125
1327
|
headers: request.headers,
|
|
1126
|
-
path
|
|
1328
|
+
path
|
|
1127
1329
|
};
|
|
1128
1330
|
const response = responseForDecision(await guard(ctx, async () => {
|
|
1129
1331
|
const details = await requestDetails(event);
|
|
1130
1332
|
const { ip } = details;
|
|
1131
|
-
const
|
|
1333
|
+
const backendVerdicts = await intel.lookup(ctx, details);
|
|
1132
1334
|
if (suspiciousIpBlocking && ip) {
|
|
1133
|
-
if (opts.providers?.isSuspiciousIp ? await opts.providers.isSuspiciousIp(ip) :
|
|
1134
|
-
const
|
|
1135
|
-
if (
|
|
1335
|
+
if (opts.providers?.isSuspiciousIp ? await opts.providers.isSuspiciousIp(ip) : backendVerdicts.suspiciousIp) {
|
|
1336
|
+
const suspiciousIpDecision = await act(event, store, details, suspiciousIpBlocking.action, SECURITY_EVENT_TYPES.SUSPICIOUS_IP, "suspicious_ip", { ip });
|
|
1337
|
+
if (suspiciousIpDecision.outcome !== "allow") return suspiciousIpDecision;
|
|
1136
1338
|
}
|
|
1137
1339
|
}
|
|
1138
1340
|
if (geoBlocking && ip) {
|
|
1139
|
-
const country = (opts.providers?.resolveGeo ? await opts.providers.resolveGeo(ip) :
|
|
1341
|
+
const country = (opts.providers?.resolveGeo ? await opts.providers.resolveGeo(ip) : backendVerdicts.geo)?.country?.toUpperCase() ?? null;
|
|
1140
1342
|
if (country ? geoBlocking.allowList ? !geoBlocking.allowList.includes(country) : Boolean(geoBlocking.denyList?.includes(country)) : Boolean(geoBlocking.allowList) && !opts.failOpen) {
|
|
1141
|
-
const
|
|
1142
|
-
if (
|
|
1343
|
+
const geoDecision = await act(event, store, details, geoBlocking.action, SECURITY_EVENT_TYPES.GEO_BLOCKED, "geo_blocking", { country });
|
|
1344
|
+
if (geoDecision.outcome !== "allow") return geoDecision;
|
|
1143
1345
|
}
|
|
1144
1346
|
}
|
|
1145
|
-
if (botBlocking) {
|
|
1146
|
-
if (opts.providers?.isBot ? await opts.providers.isBot(request) :
|
|
1147
|
-
const
|
|
1148
|
-
if (
|
|
1347
|
+
if (botBlocking && checkBot) {
|
|
1348
|
+
if (opts.providers?.isBot ? await opts.providers.isBot(request) : backendVerdicts.bot ?? looksLikeBot(request)) {
|
|
1349
|
+
const botDecision = await act(event, store, details, botBlocking.action, SECURITY_EVENT_TYPES.BOT_BLOCKED, "bot", { userAgent: request.headers.get("user-agent") });
|
|
1350
|
+
if (botDecision.outcome !== "allow") return botDecision;
|
|
1149
1351
|
}
|
|
1150
1352
|
}
|
|
1151
1353
|
return { outcome: "allow" };
|
|
@@ -1191,7 +1393,7 @@ const sentinel = (options) => {
|
|
|
1191
1393
|
}
|
|
1192
1394
|
if (credentialStuffing && isAttemptPath(ctx.path)) throwForDecision(await guard(ctx.context, async () => {
|
|
1193
1395
|
const keys = attemptKeys(details);
|
|
1194
|
-
const solved = Boolean(details.headers?.get(POW_SOLUTION_HEADER)) && await challengePassed(challengeKey(ctx, details), details, store, ctx.context);
|
|
1396
|
+
const solved = Boolean(details.headers?.get(POW_SOLUTION_HEADER)) && await challengePassed(challengeKey(ctx, details), details, store, ctx.context, ctx.path);
|
|
1195
1397
|
const blocked = (await Promise.all(counterNames(keys).map((key) => store.get(`${key}:blocked`)))).some((value) => value > 0);
|
|
1196
1398
|
const failures = await countAttempt(store, keys, { readOnly: solved || blocked });
|
|
1197
1399
|
const { thresholds } = credentialStuffing;
|
|
@@ -1220,12 +1422,12 @@ const sentinel = (options) => {
|
|
|
1220
1422
|
if (limit) {
|
|
1221
1423
|
const count = await store.increment(limit.name, windowSeconds);
|
|
1222
1424
|
if (count > limit.max) {
|
|
1223
|
-
const
|
|
1425
|
+
const limitDecision = await act(ctx, store, details, velocity.action, SECURITY_EVENT_TYPES.VELOCITY_EXCEEDED, "velocity", {
|
|
1224
1426
|
count,
|
|
1225
1427
|
limit: limit.max,
|
|
1226
1428
|
windowSeconds
|
|
1227
1429
|
});
|
|
1228
|
-
if (
|
|
1430
|
+
if (limitDecision.outcome !== "allow") return limitDecision;
|
|
1229
1431
|
}
|
|
1230
1432
|
}
|
|
1231
1433
|
if (ids.length && isTracked) {
|
|
@@ -1402,6 +1604,7 @@ const sentinel = (options) => {
|
|
|
1402
1604
|
createdAt: /* @__PURE__ */ new Date()
|
|
1403
1605
|
}
|
|
1404
1606
|
});
|
|
1607
|
+
await deleteOlderSignIns(auth, user.id);
|
|
1405
1608
|
} catch (e) {
|
|
1406
1609
|
if (e instanceof APIError) throw e;
|
|
1407
1610
|
auth.logger.error("[sentinel] Failed to track sign-in", e);
|
|
@@ -6,4 +6,4 @@ export interface MemoryDB {
|
|
|
6
6
|
export interface MemoryAdapterConfig {
|
|
7
7
|
debugLogs?: DBAdapterDebugLogOption | undefined;
|
|
8
8
|
}
|
|
9
|
-
export declare const memoryAdapter: (db: MemoryDB, config?: MemoryAdapterConfig | undefined) => (options: PayvoAuthOptions) => import("packages/core/dist/index-
|
|
9
|
+
export declare const memoryAdapter: (db: MemoryDB, config?: MemoryAdapterConfig | undefined) => (options: PayvoAuthOptions) => import("packages/core/dist/index-BL_t8cRp.mjs").nt<PayvoAuthOptions>;
|