@getstrata/core 2.2.33 → 2.2.34
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 +6 -0
- package/dist/core/auth/accountState.d.ts +9 -0
- package/dist/core/auth/accountStateMigration.d.ts +7 -0
- package/dist/core/auth/authContext.d.ts +3 -0
- package/dist/core/auth/guard.d.ts +1 -0
- package/dist/core/auth/jwt.d.ts +1 -0
- package/dist/core/auth/oneTimeToken.d.ts +2 -0
- package/dist/core/auth/passwordConfirmCookie.d.ts +2 -2
- package/dist/core/auth/passwordLogin.d.ts +2 -2
- package/dist/core/auth/pendingMfaCookie.d.ts +9 -0
- package/dist/core/auth/sessionCookie.d.ts +3 -0
- package/dist/core/contracts/authUserDirectory.d.ts +5 -0
- package/dist/entries/auth/accountState.js +18 -0
- package/dist/entries/auth/accountStateMigration.js +923 -0
- package/dist/entries/auth/basicAuthGuard.js +19 -1
- package/dist/entries/auth/jwtGuard.js +71 -23
- package/dist/entries/auth/oneTimeToken.js +16 -12
- package/dist/entries/auth/passwordConfirmCookie.js +63 -22
- package/dist/entries/auth/passwordLogin.js +15 -0
- package/dist/entries/auth/pendingMfaCookie.js +100 -0
- package/dist/entries/auth/sessionCookie.js +49 -21
- package/dist/entries/auth/sessionGuard.js +66 -22
- package/dist/entries/http/cookies.js +5 -1
- package/dist/entries/http/csrfMiddleware.js +5 -1
- package/dist/entries/http/csrfToken.js +5 -1
- package/dist/entries/http/requirePasswordConfirmMiddleware.js +1 -1
- package/dist/index.js +72 -38
- package/package.json +17 -2
|
@@ -1082,6 +1082,19 @@ function readBasicCredentials(request) {
|
|
|
1082
1082
|
// ../../src/core/auth/basicAuthGuard.ts
|
|
1083
1083
|
init_databaseTenantContext();
|
|
1084
1084
|
|
|
1085
|
+
// ../../src/core/auth/accountState.ts
|
|
1086
|
+
function isAccountActive(record) {
|
|
1087
|
+
return record.is_active === undefined || record.is_active === true;
|
|
1088
|
+
}
|
|
1089
|
+
function isAuthUserActive(user) {
|
|
1090
|
+
return user.isActive === undefined || user.isActive === true;
|
|
1091
|
+
}
|
|
1092
|
+
function matchesAuthVersion(issued, current) {
|
|
1093
|
+
if (current === undefined)
|
|
1094
|
+
return true;
|
|
1095
|
+
return Number.isSafeInteger(current) && current >= 0 && typeof issued === "number" && Number.isSafeInteger(issued) && issued >= 0 && issued === current;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1085
1098
|
// ../../src/core/auth/password.ts
|
|
1086
1099
|
async function hashPassword(password) {
|
|
1087
1100
|
return await Bun.password.hash(password, {
|
|
@@ -1574,6 +1587,8 @@ function parseRecoveryHashes(raw) {
|
|
|
1574
1587
|
}
|
|
1575
1588
|
}
|
|
1576
1589
|
function completePasswordLogin(user, options = {}) {
|
|
1590
|
+
if (!isAccountActive(user))
|
|
1591
|
+
return { ok: false, error: "account_inactive" };
|
|
1577
1592
|
if (!user.mfa_enabled) {
|
|
1578
1593
|
return { ok: true };
|
|
1579
1594
|
}
|
|
@@ -1637,6 +1652,8 @@ class BasicAuthGuard {
|
|
|
1637
1652
|
return null;
|
|
1638
1653
|
}
|
|
1639
1654
|
const record = typeof directory.findByEmail === "function" ? await directory.findByEmail(credentials.username) : null;
|
|
1655
|
+
if (record && !isAccountActive(record))
|
|
1656
|
+
return null;
|
|
1640
1657
|
if (record?.password) {
|
|
1641
1658
|
if (!await verifyPassword(credentials.password, record.password)) {
|
|
1642
1659
|
return null;
|
|
@@ -1654,12 +1671,13 @@ class BasicAuthGuard {
|
|
|
1654
1671
|
return {
|
|
1655
1672
|
id: record.id,
|
|
1656
1673
|
role: record.role,
|
|
1674
|
+
...record.auth_version !== undefined ? { authVersion: record.auth_version } : {},
|
|
1657
1675
|
emailVerifiedAt: record.email_verified_at ?? null
|
|
1658
1676
|
};
|
|
1659
1677
|
}
|
|
1660
1678
|
if (typeof directory.verifyCredentials === "function") {
|
|
1661
1679
|
const user = await directory.verifyCredentials(credentials.username, credentials.password);
|
|
1662
|
-
if (!user) {
|
|
1680
|
+
if (!user || !isAuthUserActive(user)) {
|
|
1663
1681
|
return null;
|
|
1664
1682
|
}
|
|
1665
1683
|
let record = typeof directory.findByEmail === "function" ? await directory.findByEmail(credentials.username) : null;
|
|
@@ -167,6 +167,19 @@ function readBasicCredentials(request) {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
// ../../src/core/auth/accountState.ts
|
|
171
|
+
function isAccountActive(record) {
|
|
172
|
+
return record.is_active === undefined || record.is_active === true;
|
|
173
|
+
}
|
|
174
|
+
function isAuthUserActive(user) {
|
|
175
|
+
return user.isActive === undefined || user.isActive === true;
|
|
176
|
+
}
|
|
177
|
+
function matchesAuthVersion(issued, current) {
|
|
178
|
+
if (current === undefined)
|
|
179
|
+
return true;
|
|
180
|
+
return Number.isSafeInteger(current) && current >= 0 && typeof issued === "number" && Number.isSafeInteger(issued) && issued >= 0 && issued === current;
|
|
181
|
+
}
|
|
182
|
+
|
|
170
183
|
// ../../src/core/auth/jwt.ts
|
|
171
184
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
172
185
|
|
|
@@ -333,6 +346,40 @@ function verifyJwt(token, secret) {
|
|
|
333
346
|
|
|
334
347
|
// ../../src/core/auth/sessionCookie.ts
|
|
335
348
|
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
349
|
+
|
|
350
|
+
// ../../src/core/http/cookies.ts
|
|
351
|
+
function readRequestCookie(request, name) {
|
|
352
|
+
const cookies = request.cookies;
|
|
353
|
+
if (cookies && typeof cookies.get === "function") {
|
|
354
|
+
const value = cookies.get(name);
|
|
355
|
+
if (value) {
|
|
356
|
+
return value;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const header = request.headers.get("cookie");
|
|
360
|
+
if (!header) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
for (const part of header.split(";")) {
|
|
364
|
+
const idx = part.indexOf("=");
|
|
365
|
+
if (idx === -1)
|
|
366
|
+
continue;
|
|
367
|
+
const cookieName = part.slice(0, idx).trim();
|
|
368
|
+
if (cookieName !== name)
|
|
369
|
+
continue;
|
|
370
|
+
try {
|
|
371
|
+
return decodeURIComponent(part.slice(idx + 1).trim());
|
|
372
|
+
} catch {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
function readBunRequestCookie(request, name) {
|
|
379
|
+
return request.cookies.get(name) ?? readRequestCookie(request, name);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ../../src/core/auth/sessionCookie.ts
|
|
336
383
|
var SESSION_COOKIE = appCookieName("session");
|
|
337
384
|
var SESSION_TTL_SECONDS = 60 * 60 * 24 * 7;
|
|
338
385
|
var SESSION_REMEMBER_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
@@ -352,25 +399,12 @@ function sessionRememberTtlSeconds() {
|
|
|
352
399
|
function resolveSessionSecret() {
|
|
353
400
|
return requireConfiguredSecret(["SESSION_SECRET"], "session-secret");
|
|
354
401
|
}
|
|
355
|
-
function signSession(userId, issuedAt, ttlSeconds) {
|
|
356
|
-
const payload = ttlSeconds === undefined ? `${userId}.${issuedAt}` : `${userId}.${issuedAt}.${ttlSeconds}`;
|
|
402
|
+
function signSession(userId, issuedAt, ttlSeconds, authVersion) {
|
|
403
|
+
const payload = authVersion !== undefined ? `v2.${userId}.${issuedAt}.${ttlSeconds}.${authVersion}` : ttlSeconds === undefined ? `${userId}.${issuedAt}` : `${userId}.${issuedAt}.${ttlSeconds}`;
|
|
357
404
|
const signature = createHmac2("sha256", resolveSessionSecret()).update(payload).digest("hex");
|
|
358
405
|
return `${payload}.${signature}`;
|
|
359
406
|
}
|
|
360
|
-
function
|
|
361
|
-
const cookieHeader = request.headers.get("cookie");
|
|
362
|
-
if (!cookieHeader) {
|
|
363
|
-
return null;
|
|
364
|
-
}
|
|
365
|
-
for (const part of cookieHeader.split(";")) {
|
|
366
|
-
const [name, ...rest] = part.trim().split("=");
|
|
367
|
-
if (name === cookieName) {
|
|
368
|
-
return decodeURIComponent(rest.join("="));
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
return null;
|
|
372
|
-
}
|
|
373
|
-
function readSignedSession(userIdRaw, issuedAtRaw, cookieSignature, ttlSeconds, remember) {
|
|
407
|
+
function readSignedSession(userIdRaw, issuedAtRaw, cookieSignature, ttlSeconds, remember, authVersion) {
|
|
374
408
|
const userId = Number.parseInt(userIdRaw, 10);
|
|
375
409
|
const issuedAt = Number.parseInt(issuedAtRaw, 10);
|
|
376
410
|
if (!Number.isInteger(userId) || userId <= 0 || !Number.isFinite(issuedAt)) {
|
|
@@ -382,7 +416,7 @@ function readSignedSession(userIdRaw, issuedAtRaw, cookieSignature, ttlSeconds,
|
|
|
382
416
|
if (Date.now() - issuedAt > ttlSeconds * 1000) {
|
|
383
417
|
return null;
|
|
384
418
|
}
|
|
385
|
-
const expectedSignature = (remember ? signSession(userId, issuedAt, ttlSeconds) : signSession(userId, issuedAt)).split(".").pop();
|
|
419
|
+
const expectedSignature = (authVersion !== undefined ? signSession(userId, issuedAt, ttlSeconds, authVersion) : remember ? signSession(userId, issuedAt, ttlSeconds) : signSession(userId, issuedAt)).split(".").pop();
|
|
386
420
|
if (!expectedSignature || !cookieSignature) {
|
|
387
421
|
return null;
|
|
388
422
|
}
|
|
@@ -394,14 +428,20 @@ function readSignedSession(userIdRaw, issuedAtRaw, cookieSignature, ttlSeconds,
|
|
|
394
428
|
if (!timingSafeEqual2(expectedBuffer, actualBuffer)) {
|
|
395
429
|
return null;
|
|
396
430
|
}
|
|
397
|
-
return { userId, issuedAt };
|
|
431
|
+
return { userId, issuedAt, ...authVersion !== undefined ? { authVersion } : {} };
|
|
398
432
|
}
|
|
399
433
|
function readSession(request) {
|
|
400
|
-
const cookieValue =
|
|
434
|
+
const cookieValue = readRequestCookie(request, sessionCookieName());
|
|
401
435
|
if (!cookieValue) {
|
|
402
436
|
return null;
|
|
403
437
|
}
|
|
404
438
|
const parts = cookieValue.split(".");
|
|
439
|
+
if (parts.length === 6 && parts[0] === "v2") {
|
|
440
|
+
const fields = parts.slice(1, 5);
|
|
441
|
+
if (fields.some((value) => !/^(0|[1-9][0-9]*)$/.test(value) || !Number.isSafeInteger(Number(value))))
|
|
442
|
+
return null;
|
|
443
|
+
return readSignedSession(String(parts[1]), String(parts[2]), parts[5], Number(parts[3]), true, Number(parts[4]));
|
|
444
|
+
}
|
|
405
445
|
if (parts.length === 4) {
|
|
406
446
|
const [userIdRaw, issuedAtRaw, ttlRaw, cookieSignature] = parts;
|
|
407
447
|
return readSignedSession(String(userIdRaw), String(issuedAtRaw), cookieSignature, Number.parseInt(String(ttlRaw), 10), true);
|
|
@@ -426,15 +466,18 @@ function isSessionInvalidated(issuedAt, validAfter) {
|
|
|
426
466
|
return issuedAt < timestamp;
|
|
427
467
|
}
|
|
428
468
|
function createSessionCookieDetails(userId, options = {}) {
|
|
469
|
+
if (options.authVersion !== undefined && (!Number.isSafeInteger(options.authVersion) || options.authVersion < 0))
|
|
470
|
+
throw new RangeError("authVersion must be a non-negative safe integer.");
|
|
429
471
|
const issuedAt = Date.now();
|
|
430
472
|
const ttlSeconds = options.remember ? sessionRememberTtlSeconds() : sessionTtlSeconds();
|
|
431
|
-
const value = options.remember ? signSession(userId, issuedAt, ttlSeconds) : signSession(userId, issuedAt);
|
|
473
|
+
const value = options.authVersion !== undefined ? signSession(userId, issuedAt, ttlSeconds, options.authVersion) : options.remember ? signSession(userId, issuedAt, ttlSeconds) : signSession(userId, issuedAt);
|
|
432
474
|
const secure = isProductionEnv() ? "; Secure" : "";
|
|
433
475
|
return {
|
|
434
476
|
header: `${sessionCookieName()}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${ttlSeconds}${secure}`,
|
|
435
477
|
userId,
|
|
436
478
|
issuedAt,
|
|
437
|
-
ttlSeconds
|
|
479
|
+
ttlSeconds,
|
|
480
|
+
...options.authVersion !== undefined ? { authVersion: options.authVersion } : {}
|
|
438
481
|
};
|
|
439
482
|
}
|
|
440
483
|
function createSessionCookie(userId, options = {}) {
|
|
@@ -496,10 +539,15 @@ class JwtGuard {
|
|
|
496
539
|
} catch {
|
|
497
540
|
return null;
|
|
498
541
|
}
|
|
499
|
-
if (
|
|
542
|
+
if (!isAccountActive(record) || !matchesAuthVersion(payload.authVersion, record.auth_version))
|
|
543
|
+
return null;
|
|
544
|
+
if (record.auth_version === undefined && isSessionInvalidated(payload.iat * 1000, record.session_valid_after)) {
|
|
500
545
|
return null;
|
|
501
546
|
}
|
|
502
|
-
return
|
|
547
|
+
return {
|
|
548
|
+
...authUserFromJwt(payload),
|
|
549
|
+
...record.auth_version !== undefined ? { authVersion: record.auth_version } : {}
|
|
550
|
+
};
|
|
503
551
|
}
|
|
504
552
|
}
|
|
505
553
|
export {
|
|
@@ -341,6 +341,8 @@ function hashOneTimeToken(plain) {
|
|
|
341
341
|
return hashApiToken(plain.trim());
|
|
342
342
|
}
|
|
343
343
|
function isMissingRelation(error) {
|
|
344
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ER_NO_SUCH_TABLE")
|
|
345
|
+
return true;
|
|
344
346
|
const message = error instanceof Error ? error.message : String(error);
|
|
345
347
|
return /no such table|does not exist|unknown table|er_no_such_table/i.test(message);
|
|
346
348
|
}
|
|
@@ -374,23 +376,25 @@ async function consumeOneTimeToken(sql, purpose, plain, now = new Date) {
|
|
|
374
376
|
}
|
|
375
377
|
async function revokeUserSessions(sql, userId, options = {}) {
|
|
376
378
|
const dialect = currentSqlDialect();
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
throw error;
|
|
383
|
-
}
|
|
379
|
+
const versionUpdate = options.incrementAuthVersion ? ", auth_version = auth_version + 1" : "";
|
|
380
|
+
await sql.unsafe(`UPDATE users SET session_valid_after = ${dialect.placeholder(1)}${versionUpdate} WHERE id = ${dialect.placeholder(2)}`, [new Date().toISOString(), userId]);
|
|
381
|
+
await deleteOptionalCredentials(sql, "sessions", userId);
|
|
382
|
+
if (options.revokeApiTokens !== false) {
|
|
383
|
+
await deleteOptionalCredentials(sql, "api_tokens", userId);
|
|
384
384
|
}
|
|
385
|
-
|
|
386
|
-
|
|
385
|
+
}
|
|
386
|
+
async function deleteOptionalCredentials(sql, table, userId) {
|
|
387
|
+
const dialect = currentSqlDialect();
|
|
388
|
+
if (dialect.driver === "pgsql") {
|
|
389
|
+
const [row] = await sql.unsafe(`SELECT to_regclass(${dialect.placeholder(1)}) AS relation`, [table]);
|
|
390
|
+
if (row?.relation == null)
|
|
391
|
+
return;
|
|
387
392
|
}
|
|
388
393
|
try {
|
|
389
|
-
await sql.unsafe(`DELETE FROM
|
|
394
|
+
await sql.unsafe(`DELETE FROM ${table} WHERE user_id = ${dialect.placeholder(1)}`, [userId]);
|
|
390
395
|
} catch (error) {
|
|
391
|
-
if (!isMissingRelation(error))
|
|
396
|
+
if (dialect.driver === "pgsql" || !isMissingRelation(error))
|
|
392
397
|
throw error;
|
|
393
|
-
}
|
|
394
398
|
}
|
|
395
399
|
}
|
|
396
400
|
export {
|
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
// ../../src/core/auth/passwordConfirmCookie.ts
|
|
3
3
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
4
4
|
|
|
5
|
+
// ../../src/core/http/cookies.ts
|
|
6
|
+
function readRequestCookie(request, name) {
|
|
7
|
+
const cookies = request.cookies;
|
|
8
|
+
if (cookies && typeof cookies.get === "function") {
|
|
9
|
+
const value = cookies.get(name);
|
|
10
|
+
if (value) {
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const header = request.headers.get("cookie");
|
|
15
|
+
if (!header) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
for (const part of header.split(";")) {
|
|
19
|
+
const idx = part.indexOf("=");
|
|
20
|
+
if (idx === -1)
|
|
21
|
+
continue;
|
|
22
|
+
const cookieName = part.slice(0, idx).trim();
|
|
23
|
+
if (cookieName !== name)
|
|
24
|
+
continue;
|
|
25
|
+
try {
|
|
26
|
+
return decodeURIComponent(part.slice(idx + 1).trim());
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function readBunRequestCookie(request, name) {
|
|
34
|
+
return request.cookies.get(name) ?? readRequestCookie(request, name);
|
|
35
|
+
}
|
|
36
|
+
|
|
5
37
|
// ../../src/core/runtime/appEnv.ts
|
|
6
38
|
var NON_PRODUCTION_APP_ENVS = new Set(["local", "development", "dev", "test", "testing", "ci"]);
|
|
7
39
|
function normalizeEnvValue(value) {
|
|
@@ -88,6 +120,19 @@ function sdkClientClassName() {
|
|
|
88
120
|
return fromName ? `${fromName}Client` : "AppClient";
|
|
89
121
|
}
|
|
90
122
|
|
|
123
|
+
// ../../src/core/auth/accountState.ts
|
|
124
|
+
function isAccountActive(record) {
|
|
125
|
+
return record.is_active === undefined || record.is_active === true;
|
|
126
|
+
}
|
|
127
|
+
function isAuthUserActive(user) {
|
|
128
|
+
return user.isActive === undefined || user.isActive === true;
|
|
129
|
+
}
|
|
130
|
+
function matchesAuthVersion(issued, current) {
|
|
131
|
+
if (current === undefined)
|
|
132
|
+
return true;
|
|
133
|
+
return Number.isSafeInteger(current) && current >= 0 && typeof issued === "number" && Number.isSafeInteger(issued) && issued >= 0 && issued === current;
|
|
134
|
+
}
|
|
135
|
+
|
|
91
136
|
// ../../src/core/auth/passwordConfirmCookie.ts
|
|
92
137
|
var PASSWORD_CONFIRM_COOKIE = appCookieName("password_confirmed");
|
|
93
138
|
var DEFAULT_PASSWORD_CONFIRM_TTL_SECONDS = 3 * 60 * 60;
|
|
@@ -101,33 +146,27 @@ function passwordConfirmTtlSeconds() {
|
|
|
101
146
|
function resolvePasswordConfirmSecret() {
|
|
102
147
|
return requireConfiguredSecret(["SESSION_SECRET", "OAUTH_STATE_SECRET"], "session-secret");
|
|
103
148
|
}
|
|
104
|
-
function signPasswordConfirm(userId, confirmedAt) {
|
|
105
|
-
const payload = `${userId}.${confirmedAt}`;
|
|
149
|
+
function signPasswordConfirm(userId, confirmedAt, authVersion) {
|
|
150
|
+
const payload = authVersion === undefined ? `${userId}.${confirmedAt}` : `v2.${userId}.${confirmedAt}.${authVersion}`;
|
|
106
151
|
const signature = createHmac("sha256", resolvePasswordConfirmSecret()).update(payload).digest("hex");
|
|
107
152
|
return `${payload}.${signature}`;
|
|
108
153
|
}
|
|
109
|
-
function
|
|
110
|
-
const
|
|
111
|
-
if (!cookieHeader) {
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
for (const part of cookieHeader.split(";")) {
|
|
115
|
-
const [name, ...rest] = part.trim().split("=");
|
|
116
|
-
if (name === cookieName) {
|
|
117
|
-
return decodeURIComponent(rest.join("="));
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
function hasFreshPasswordConfirmation(request, userId) {
|
|
123
|
-
const cookieValue = readCookieValue(request, passwordConfirmCookieName());
|
|
154
|
+
function hasFreshPasswordConfirmation(request, userId, authVersion) {
|
|
155
|
+
const cookieValue = readRequestCookie(request, passwordConfirmCookieName());
|
|
124
156
|
if (!cookieValue) {
|
|
125
157
|
return false;
|
|
126
158
|
}
|
|
127
159
|
const parts = cookieValue.split(".");
|
|
128
|
-
|
|
129
|
-
|
|
160
|
+
let proofVersion;
|
|
161
|
+
if (parts.length === 5 && parts[0] === "v2") {
|
|
162
|
+
if (parts.slice(1, 4).some((value) => !/^(0|[1-9][0-9]*)$/.test(value) || !Number.isSafeInteger(Number(value))))
|
|
163
|
+
return false;
|
|
164
|
+
proofVersion = Number(parts[3]);
|
|
165
|
+
parts.splice(3, 1);
|
|
166
|
+
parts.shift();
|
|
130
167
|
}
|
|
168
|
+
if (parts.length !== 3 || !matchesAuthVersion(proofVersion, authVersion))
|
|
169
|
+
return false;
|
|
131
170
|
const [userIdRaw, confirmedAtRaw, cookieSignature] = parts;
|
|
132
171
|
const cookieUserId = Number.parseInt(String(userIdRaw), 10);
|
|
133
172
|
const confirmedAt = Number.parseInt(String(confirmedAtRaw), 10);
|
|
@@ -137,7 +176,7 @@ function hasFreshPasswordConfirmation(request, userId) {
|
|
|
137
176
|
if (Date.now() - confirmedAt > passwordConfirmTtlSeconds() * 1000) {
|
|
138
177
|
return false;
|
|
139
178
|
}
|
|
140
|
-
const expectedSignature = signPasswordConfirm(cookieUserId, confirmedAt).split(".").pop();
|
|
179
|
+
const expectedSignature = signPasswordConfirm(cookieUserId, confirmedAt, proofVersion).split(".").pop();
|
|
141
180
|
if (!expectedSignature || !cookieSignature) {
|
|
142
181
|
return false;
|
|
143
182
|
}
|
|
@@ -148,9 +187,11 @@ function hasFreshPasswordConfirmation(request, userId) {
|
|
|
148
187
|
}
|
|
149
188
|
return timingSafeEqual(expectedBuffer, actualBuffer);
|
|
150
189
|
}
|
|
151
|
-
function createPasswordConfirmCookie(userId) {
|
|
190
|
+
function createPasswordConfirmCookie(userId, authVersion) {
|
|
191
|
+
if (authVersion !== undefined && (!Number.isSafeInteger(authVersion) || authVersion < 0))
|
|
192
|
+
throw new Error("Invalid authentication version.");
|
|
152
193
|
const confirmedAt = Date.now();
|
|
153
|
-
const value = signPasswordConfirm(userId, confirmedAt);
|
|
194
|
+
const value = signPasswordConfirm(userId, confirmedAt, authVersion);
|
|
154
195
|
const secure = isProductionEnv() ? "; Secure" : "";
|
|
155
196
|
return `${passwordConfirmCookieName()}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${passwordConfirmTtlSeconds()}${secure}`;
|
|
156
197
|
}
|
|
@@ -712,6 +712,19 @@ function verifyTotp(secret, token, window = 1) {
|
|
|
712
712
|
return matched;
|
|
713
713
|
}
|
|
714
714
|
|
|
715
|
+
// ../../src/core/auth/accountState.ts
|
|
716
|
+
function isAccountActive(record) {
|
|
717
|
+
return record.is_active === undefined || record.is_active === true;
|
|
718
|
+
}
|
|
719
|
+
function isAuthUserActive(user) {
|
|
720
|
+
return user.isActive === undefined || user.isActive === true;
|
|
721
|
+
}
|
|
722
|
+
function matchesAuthVersion(issued, current) {
|
|
723
|
+
if (current === undefined)
|
|
724
|
+
return true;
|
|
725
|
+
return Number.isSafeInteger(current) && current >= 0 && typeof issued === "number" && Number.isSafeInteger(issued) && issued >= 0 && issued === current;
|
|
726
|
+
}
|
|
727
|
+
|
|
715
728
|
// ../../src/core/auth/passwordLogin.ts
|
|
716
729
|
function parseRecoveryHashes(raw) {
|
|
717
730
|
if (!raw?.trim()) {
|
|
@@ -725,6 +738,8 @@ function parseRecoveryHashes(raw) {
|
|
|
725
738
|
}
|
|
726
739
|
}
|
|
727
740
|
function completePasswordLogin(user, options = {}) {
|
|
741
|
+
if (!isAccountActive(user))
|
|
742
|
+
return { ok: false, error: "account_inactive" };
|
|
728
743
|
if (!user.mfa_enabled) {
|
|
729
744
|
return { ok: true };
|
|
730
745
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/core/auth/pendingMfaCookie.ts
|
|
3
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
4
|
+
|
|
5
|
+
// ../../src/core/http/cookies.ts
|
|
6
|
+
function readRequestCookie(request, name) {
|
|
7
|
+
const cookies = request.cookies;
|
|
8
|
+
if (cookies && typeof cookies.get === "function") {
|
|
9
|
+
const value = cookies.get(name);
|
|
10
|
+
if (value) {
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const header = request.headers.get("cookie");
|
|
15
|
+
if (!header) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
for (const part of header.split(";")) {
|
|
19
|
+
const idx = part.indexOf("=");
|
|
20
|
+
if (idx === -1)
|
|
21
|
+
continue;
|
|
22
|
+
const cookieName = part.slice(0, idx).trim();
|
|
23
|
+
if (cookieName !== name)
|
|
24
|
+
continue;
|
|
25
|
+
try {
|
|
26
|
+
return decodeURIComponent(part.slice(idx + 1).trim());
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function readBunRequestCookie(request, name) {
|
|
34
|
+
return request.cookies.get(name) ?? readRequestCookie(request, name);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ../../src/core/runtime/appEnv.ts
|
|
38
|
+
var NON_PRODUCTION_APP_ENVS = new Set(["local", "development", "dev", "test", "testing", "ci"]);
|
|
39
|
+
function normalizeEnvValue(value) {
|
|
40
|
+
return (value ?? "").trim().toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
function isProductionEnv(env = process.env) {
|
|
43
|
+
const appEnv = normalizeEnvValue(env.APP_ENV);
|
|
44
|
+
const nodeEnv = normalizeEnvValue(env.NODE_ENV);
|
|
45
|
+
if (appEnv === "production" || nodeEnv === "production") {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
if (appEnv === "") {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return !NON_PRODUCTION_APP_ENVS.has(appEnv);
|
|
52
|
+
}
|
|
53
|
+
function envFlagEnabled(value) {
|
|
54
|
+
return value === "true";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ../../src/core/auth/pendingMfaCookie.ts
|
|
58
|
+
var COOKIE = "strata_mfa_pending";
|
|
59
|
+
var TTL = 600;
|
|
60
|
+
function signature(payload, secret) {
|
|
61
|
+
if (!secret.trim())
|
|
62
|
+
throw new Error("A pending MFA secret is required.");
|
|
63
|
+
return createHmac("sha256", secret).update(`pending-mfa:${payload}`).digest("hex");
|
|
64
|
+
}
|
|
65
|
+
function header(value, ttl) {
|
|
66
|
+
return `${COOKIE}=${value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${ttl}${isProductionEnv() ? "; Secure" : ""}`;
|
|
67
|
+
}
|
|
68
|
+
function createPendingMfaCookie(userId, authVersion, secret) {
|
|
69
|
+
if (!Number.isSafeInteger(userId) || userId <= 0 || !Number.isSafeInteger(authVersion) || authVersion < 0)
|
|
70
|
+
throw new Error("Invalid pending MFA account identity or version.");
|
|
71
|
+
const payload = `v1.${userId}.${authVersion}.${Date.now()}`;
|
|
72
|
+
return header(`${payload}.${signature(payload, secret)}`, TTL);
|
|
73
|
+
}
|
|
74
|
+
function readPendingMfaCookie(request, secret) {
|
|
75
|
+
const value = readRequestCookie(request, COOKIE);
|
|
76
|
+
if (!value)
|
|
77
|
+
return null;
|
|
78
|
+
const parts = value.split(".");
|
|
79
|
+
if (parts.length !== 5 || parts[0] !== "v1")
|
|
80
|
+
return null;
|
|
81
|
+
const fields = parts.slice(1, 4);
|
|
82
|
+
if (fields.some((part) => !/^(0|[1-9][0-9]*)$/.test(part) || !Number.isSafeInteger(Number(part))))
|
|
83
|
+
return null;
|
|
84
|
+
const userId = Number(parts[1]), authVersion = Number(parts[2]), issuedAt = Number(parts[3]);
|
|
85
|
+
if (userId <= 0 || issuedAt > Date.now() || Date.now() - issuedAt > TTL * 1000)
|
|
86
|
+
return null;
|
|
87
|
+
const expected = Buffer.from(signature(parts.slice(0, 4).join("."), secret));
|
|
88
|
+
const actual = Buffer.from(parts[4] ?? "");
|
|
89
|
+
if (expected.length !== actual.length || !timingSafeEqual(expected, actual))
|
|
90
|
+
return null;
|
|
91
|
+
return { userId, authVersion };
|
|
92
|
+
}
|
|
93
|
+
function clearPendingMfaCookie() {
|
|
94
|
+
return header("", 0);
|
|
95
|
+
}
|
|
96
|
+
export {
|
|
97
|
+
clearPendingMfaCookie,
|
|
98
|
+
createPendingMfaCookie,
|
|
99
|
+
readPendingMfaCookie
|
|
100
|
+
};
|
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
// ../../src/core/auth/sessionCookie.ts
|
|
3
3
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
4
4
|
|
|
5
|
+
// ../../src/core/http/cookies.ts
|
|
6
|
+
function readRequestCookie(request, name) {
|
|
7
|
+
const cookies = request.cookies;
|
|
8
|
+
if (cookies && typeof cookies.get === "function") {
|
|
9
|
+
const value = cookies.get(name);
|
|
10
|
+
if (value) {
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const header = request.headers.get("cookie");
|
|
15
|
+
if (!header) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
for (const part of header.split(";")) {
|
|
19
|
+
const idx = part.indexOf("=");
|
|
20
|
+
if (idx === -1)
|
|
21
|
+
continue;
|
|
22
|
+
const cookieName = part.slice(0, idx).trim();
|
|
23
|
+
if (cookieName !== name)
|
|
24
|
+
continue;
|
|
25
|
+
try {
|
|
26
|
+
return decodeURIComponent(part.slice(idx + 1).trim());
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
function readBunRequestCookie(request, name) {
|
|
34
|
+
return request.cookies.get(name) ?? readRequestCookie(request, name);
|
|
35
|
+
}
|
|
36
|
+
|
|
5
37
|
// ../../src/core/runtime/appEnv.ts
|
|
6
38
|
var NON_PRODUCTION_APP_ENVS = new Set(["local", "development", "dev", "test", "testing", "ci"]);
|
|
7
39
|
function normalizeEnvValue(value) {
|
|
@@ -108,25 +140,12 @@ function sessionRememberTtlSeconds() {
|
|
|
108
140
|
function resolveSessionSecret() {
|
|
109
141
|
return requireConfiguredSecret(["SESSION_SECRET"], "session-secret");
|
|
110
142
|
}
|
|
111
|
-
function signSession(userId, issuedAt, ttlSeconds) {
|
|
112
|
-
const payload = ttlSeconds === undefined ? `${userId}.${issuedAt}` : `${userId}.${issuedAt}.${ttlSeconds}`;
|
|
143
|
+
function signSession(userId, issuedAt, ttlSeconds, authVersion) {
|
|
144
|
+
const payload = authVersion !== undefined ? `v2.${userId}.${issuedAt}.${ttlSeconds}.${authVersion}` : ttlSeconds === undefined ? `${userId}.${issuedAt}` : `${userId}.${issuedAt}.${ttlSeconds}`;
|
|
113
145
|
const signature = createHmac("sha256", resolveSessionSecret()).update(payload).digest("hex");
|
|
114
146
|
return `${payload}.${signature}`;
|
|
115
147
|
}
|
|
116
|
-
function
|
|
117
|
-
const cookieHeader = request.headers.get("cookie");
|
|
118
|
-
if (!cookieHeader) {
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
for (const part of cookieHeader.split(";")) {
|
|
122
|
-
const [name, ...rest] = part.trim().split("=");
|
|
123
|
-
if (name === cookieName) {
|
|
124
|
-
return decodeURIComponent(rest.join("="));
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
function readSignedSession(userIdRaw, issuedAtRaw, cookieSignature, ttlSeconds, remember) {
|
|
148
|
+
function readSignedSession(userIdRaw, issuedAtRaw, cookieSignature, ttlSeconds, remember, authVersion) {
|
|
130
149
|
const userId = Number.parseInt(userIdRaw, 10);
|
|
131
150
|
const issuedAt = Number.parseInt(issuedAtRaw, 10);
|
|
132
151
|
if (!Number.isInteger(userId) || userId <= 0 || !Number.isFinite(issuedAt)) {
|
|
@@ -138,7 +157,7 @@ function readSignedSession(userIdRaw, issuedAtRaw, cookieSignature, ttlSeconds,
|
|
|
138
157
|
if (Date.now() - issuedAt > ttlSeconds * 1000) {
|
|
139
158
|
return null;
|
|
140
159
|
}
|
|
141
|
-
const expectedSignature = (remember ? signSession(userId, issuedAt, ttlSeconds) : signSession(userId, issuedAt)).split(".").pop();
|
|
160
|
+
const expectedSignature = (authVersion !== undefined ? signSession(userId, issuedAt, ttlSeconds, authVersion) : remember ? signSession(userId, issuedAt, ttlSeconds) : signSession(userId, issuedAt)).split(".").pop();
|
|
142
161
|
if (!expectedSignature || !cookieSignature) {
|
|
143
162
|
return null;
|
|
144
163
|
}
|
|
@@ -150,14 +169,20 @@ function readSignedSession(userIdRaw, issuedAtRaw, cookieSignature, ttlSeconds,
|
|
|
150
169
|
if (!timingSafeEqual(expectedBuffer, actualBuffer)) {
|
|
151
170
|
return null;
|
|
152
171
|
}
|
|
153
|
-
return { userId, issuedAt };
|
|
172
|
+
return { userId, issuedAt, ...authVersion !== undefined ? { authVersion } : {} };
|
|
154
173
|
}
|
|
155
174
|
function readSession(request) {
|
|
156
|
-
const cookieValue =
|
|
175
|
+
const cookieValue = readRequestCookie(request, sessionCookieName());
|
|
157
176
|
if (!cookieValue) {
|
|
158
177
|
return null;
|
|
159
178
|
}
|
|
160
179
|
const parts = cookieValue.split(".");
|
|
180
|
+
if (parts.length === 6 && parts[0] === "v2") {
|
|
181
|
+
const fields = parts.slice(1, 5);
|
|
182
|
+
if (fields.some((value) => !/^(0|[1-9][0-9]*)$/.test(value) || !Number.isSafeInteger(Number(value))))
|
|
183
|
+
return null;
|
|
184
|
+
return readSignedSession(String(parts[1]), String(parts[2]), parts[5], Number(parts[3]), true, Number(parts[4]));
|
|
185
|
+
}
|
|
161
186
|
if (parts.length === 4) {
|
|
162
187
|
const [userIdRaw, issuedAtRaw, ttlRaw, cookieSignature] = parts;
|
|
163
188
|
return readSignedSession(String(userIdRaw), String(issuedAtRaw), cookieSignature, Number.parseInt(String(ttlRaw), 10), true);
|
|
@@ -182,15 +207,18 @@ function isSessionInvalidated(issuedAt, validAfter) {
|
|
|
182
207
|
return issuedAt < timestamp;
|
|
183
208
|
}
|
|
184
209
|
function createSessionCookieDetails(userId, options = {}) {
|
|
210
|
+
if (options.authVersion !== undefined && (!Number.isSafeInteger(options.authVersion) || options.authVersion < 0))
|
|
211
|
+
throw new RangeError("authVersion must be a non-negative safe integer.");
|
|
185
212
|
const issuedAt = Date.now();
|
|
186
213
|
const ttlSeconds = options.remember ? sessionRememberTtlSeconds() : sessionTtlSeconds();
|
|
187
|
-
const value = options.remember ? signSession(userId, issuedAt, ttlSeconds) : signSession(userId, issuedAt);
|
|
214
|
+
const value = options.authVersion !== undefined ? signSession(userId, issuedAt, ttlSeconds, options.authVersion) : options.remember ? signSession(userId, issuedAt, ttlSeconds) : signSession(userId, issuedAt);
|
|
188
215
|
const secure = isProductionEnv() ? "; Secure" : "";
|
|
189
216
|
return {
|
|
190
217
|
header: `${sessionCookieName()}=${encodeURIComponent(value)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${ttlSeconds}${secure}`,
|
|
191
218
|
userId,
|
|
192
219
|
issuedAt,
|
|
193
|
-
ttlSeconds
|
|
220
|
+
ttlSeconds,
|
|
221
|
+
...options.authVersion !== undefined ? { authVersion: options.authVersion } : {}
|
|
194
222
|
};
|
|
195
223
|
}
|
|
196
224
|
function createSessionCookie(userId, options = {}) {
|