@fonderie/auth 1.2.0 → 1.3.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/dist/index.cjs +53 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +39 -24
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.cjs +39 -0
- package/dist/middlewares/index.cjs.map +1 -1
- package/dist/middlewares/index.d.cts +1 -1
- package/dist/middlewares/index.d.ts +1 -1
- package/dist/middlewares/index.js +39 -0
- package/dist/middlewares/index.js.map +1 -1
- package/dist/{session-C54ksYE8.d.cts → session-DbtCnbiG.d.cts} +1 -0
- package/dist/{session-C54ksYE8.d.ts → session-DbtCnbiG.d.ts} +1 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -254,6 +254,7 @@ var EVENT_KEYS = {
|
|
|
254
254
|
};
|
|
255
255
|
|
|
256
256
|
// src/services/jwt.ts
|
|
257
|
+
var import_node_crypto = require("crypto");
|
|
257
258
|
var import_jsonwebtoken = __toESM(require("jsonwebtoken"), 1);
|
|
258
259
|
function issueMfaPendingToken(userId, config, loginMethod) {
|
|
259
260
|
return import_jsonwebtoken.default.sign(
|
|
@@ -270,19 +271,21 @@ function issueMfaPendingToken(userId, config, loginMethod) {
|
|
|
270
271
|
}
|
|
271
272
|
function issueTokenPair(userId, config, options) {
|
|
272
273
|
const duration = config.sessionDuration ?? "7d";
|
|
274
|
+
const accessDuration = config.accessTokenDuration ?? "24h";
|
|
273
275
|
const loginMethod = options.loginMethod;
|
|
274
276
|
const phoneVerified = options.phoneVerified ?? false;
|
|
277
|
+
const sid = (0, import_node_crypto.randomUUID)();
|
|
275
278
|
const accessToken = import_jsonwebtoken.default.sign(
|
|
276
|
-
{ sub: userId, type: "access", loginMethod, phoneVerified },
|
|
279
|
+
{ sub: userId, type: "access", loginMethod, phoneVerified, sid },
|
|
277
280
|
config.jwtSecret,
|
|
278
|
-
{ expiresIn:
|
|
281
|
+
{ expiresIn: accessDuration }
|
|
279
282
|
);
|
|
280
283
|
const refreshToken = import_jsonwebtoken.default.sign(
|
|
281
|
-
{ sub: userId, type: "refresh", loginMethod, phoneVerified },
|
|
284
|
+
{ sub: userId, type: "refresh", loginMethod, phoneVerified, sid },
|
|
282
285
|
config.jwtSecret,
|
|
283
286
|
{ expiresIn: duration }
|
|
284
287
|
);
|
|
285
|
-
return { accessToken, refreshToken };
|
|
288
|
+
return { accessToken, refreshToken, sid };
|
|
286
289
|
}
|
|
287
290
|
function refreshTokenExpiry(token) {
|
|
288
291
|
const decoded = import_jsonwebtoken.default.decode(token);
|
|
@@ -297,7 +300,7 @@ function verifyToken(token, config) {
|
|
|
297
300
|
}
|
|
298
301
|
|
|
299
302
|
// src/services/mfa.ts
|
|
300
|
-
var
|
|
303
|
+
var import_node_crypto2 = require("crypto");
|
|
301
304
|
var STEP = 30;
|
|
302
305
|
var DRIFT = 1;
|
|
303
306
|
var DIGITS = 6;
|
|
@@ -309,7 +312,7 @@ function hotp(secret, counter) {
|
|
|
309
312
|
c >>= 8;
|
|
310
313
|
}
|
|
311
314
|
const key = Buffer.from(base32Decode(secret));
|
|
312
|
-
const hmac = (0,
|
|
315
|
+
const hmac = (0, import_node_crypto2.createHmac)("sha1", key).update(buf).digest();
|
|
313
316
|
const offset = (hmac[19] ?? 0) & 15;
|
|
314
317
|
const code = (((hmac[offset] ?? 0) & 127) << 24 | ((hmac[offset + 1] ?? 0) & 255) << 16 | ((hmac[offset + 2] ?? 0) & 255) << 8 | (hmac[offset + 3] ?? 0) & 255) % Math.pow(10, DIGITS);
|
|
315
318
|
return code.toString().padStart(DIGITS, "0");
|
|
@@ -356,7 +359,7 @@ function base32Encode(buf) {
|
|
|
356
359
|
return output;
|
|
357
360
|
}
|
|
358
361
|
function generateTotpSecret() {
|
|
359
|
-
return base32Encode((0,
|
|
362
|
+
return base32Encode((0, import_node_crypto2.randomBytes)(20));
|
|
360
363
|
}
|
|
361
364
|
function generateTotpUri(email2, secret, issuer) {
|
|
362
365
|
const params = new URLSearchParams({
|
|
@@ -378,7 +381,7 @@ function verifyTotpToken(token, secret) {
|
|
|
378
381
|
return false;
|
|
379
382
|
}
|
|
380
383
|
function generateBackupCodes(count = 8) {
|
|
381
|
-
return Array.from({ length: count }, () => (0,
|
|
384
|
+
return Array.from({ length: count }, () => (0, import_node_crypto2.randomBytes)(4).toString("hex").toUpperCase());
|
|
382
385
|
}
|
|
383
386
|
|
|
384
387
|
// src/controllers/mfa.controller.ts
|
|
@@ -638,12 +641,12 @@ var SessionModel = class {
|
|
|
638
641
|
this.store = store;
|
|
639
642
|
}
|
|
640
643
|
store;
|
|
641
|
-
async create(userId, token, expiresAt) {
|
|
644
|
+
async create(userId, token, expiresAt, sid) {
|
|
642
645
|
await this.store.query(
|
|
643
|
-
`INSERT INTO fonderie_sessions (user_id, token, expires_at)
|
|
644
|
-
VALUES ($1, $2, $3)
|
|
646
|
+
`INSERT INTO fonderie_sessions (user_id, token, expires_at, sid)
|
|
647
|
+
VALUES ($1, $2, $3, $4)
|
|
645
648
|
ON CONFLICT (token) DO NOTHING`,
|
|
646
|
-
[userId, token, expiresAt]
|
|
649
|
+
[userId, token, expiresAt, sid ?? null]
|
|
647
650
|
);
|
|
648
651
|
}
|
|
649
652
|
async delete(token) {
|
|
@@ -656,6 +659,14 @@ var SessionModel = class {
|
|
|
656
659
|
);
|
|
657
660
|
return rows.length > 0;
|
|
658
661
|
}
|
|
662
|
+
// Liveness check for session-bound access tokens (by the sid claim).
|
|
663
|
+
async aliveBySid(sid) {
|
|
664
|
+
const rows = await this.store.query(
|
|
665
|
+
`SELECT id FROM fonderie_sessions WHERE sid = $1 AND expires_at > now()`,
|
|
666
|
+
[sid]
|
|
667
|
+
);
|
|
668
|
+
return rows.length > 0;
|
|
669
|
+
}
|
|
659
670
|
};
|
|
660
671
|
|
|
661
672
|
// src/models/backup-code.model.ts
|
|
@@ -786,10 +797,10 @@ function mfaController(store, config, issuer, bus) {
|
|
|
786
797
|
await users.enableMfa(ctx.user.id);
|
|
787
798
|
}
|
|
788
799
|
}
|
|
789
|
-
const { accessToken, refreshToken } = issueTokenPair(ctx.user.id, config, {
|
|
800
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(ctx.user.id, config, {
|
|
790
801
|
loginMethod: ctx.user.loginMethod
|
|
791
802
|
});
|
|
792
|
-
await sessions.create(ctx.user.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
803
|
+
await sessions.create(ctx.user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
793
804
|
const fullUser = await users.findById(ctx.user.id);
|
|
794
805
|
if (!fullUser) {
|
|
795
806
|
return (0, import_core3.setApiResponse)(import_core3.HTTP.SERVER_ERROR, "SERVER_ERROR", "User not found after MFA verify");
|
|
@@ -866,7 +877,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
866
877
|
}
|
|
867
878
|
|
|
868
879
|
// src/controllers/auth.controller.ts
|
|
869
|
-
var
|
|
880
|
+
var import_node_crypto3 = require("crypto");
|
|
870
881
|
var import_events2 = require("@fonderie/events");
|
|
871
882
|
var import_core4 = require("@fonderie/core");
|
|
872
883
|
|
|
@@ -1090,7 +1101,7 @@ function authController(store, config, bus) {
|
|
|
1090
1101
|
if (!row) {
|
|
1091
1102
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1092
1103
|
}
|
|
1093
|
-
const pin = (0,
|
|
1104
|
+
const pin = (0, import_node_crypto3.randomInt)(1e5, 1e6).toString();
|
|
1094
1105
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
1095
1106
|
await emailVerif.create(row.id, pin, expiresAt);
|
|
1096
1107
|
const user = await users.findById(row.id);
|
|
@@ -1121,10 +1132,10 @@ function authController(store, config, bus) {
|
|
|
1121
1132
|
reqOpts
|
|
1122
1133
|
).catch(() => {
|
|
1123
1134
|
});
|
|
1124
|
-
const { accessToken, refreshToken } = issueTokenPair(user.id, config, {
|
|
1135
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
1125
1136
|
loginMethod: "email"
|
|
1126
1137
|
});
|
|
1127
|
-
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
1138
|
+
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1128
1139
|
const resolvedRegister = { ...config, ...config.resolve?.(ctx) };
|
|
1129
1140
|
const requiresVerification = !!resolvedRegister.requireVerification && !user.emailVerifiedAt;
|
|
1130
1141
|
return Response.json(
|
|
@@ -1155,7 +1166,7 @@ function authController(store, config, bus) {
|
|
|
1155
1166
|
firstName ?? null,
|
|
1156
1167
|
lastName ?? null
|
|
1157
1168
|
);
|
|
1158
|
-
const otp = (0,
|
|
1169
|
+
const otp = (0, import_node_crypto3.randomInt)(1e5, 1e6).toString();
|
|
1159
1170
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
1160
1171
|
await phoneVerif.upsert(id, normalizePhone(phone2), otp, expiresAt);
|
|
1161
1172
|
const user = await users.findById(id);
|
|
@@ -1186,10 +1197,10 @@ function authController(store, config, bus) {
|
|
|
1186
1197
|
reqOpts2
|
|
1187
1198
|
).catch(() => {
|
|
1188
1199
|
});
|
|
1189
|
-
const { accessToken, refreshToken } = issueTokenPair(user.id, config, {
|
|
1200
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
1190
1201
|
loginMethod: "phone"
|
|
1191
1202
|
});
|
|
1192
|
-
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
1203
|
+
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1193
1204
|
return Response.json(
|
|
1194
1205
|
{
|
|
1195
1206
|
reason: "USER_PHONE_REGISTERED",
|
|
@@ -1242,10 +1253,10 @@ function authController(store, config, bus) {
|
|
|
1242
1253
|
mfaToken
|
|
1243
1254
|
});
|
|
1244
1255
|
}
|
|
1245
|
-
const { accessToken, refreshToken } = issueTokenPair(user.id, config, {
|
|
1256
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
1246
1257
|
loginMethod: "email"
|
|
1247
1258
|
});
|
|
1248
|
-
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
1259
|
+
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1249
1260
|
const resolvedLogin = { ...config, ...config.resolve?.(ctx) };
|
|
1250
1261
|
const requiresVerification = !!resolvedLogin.requireVerification && !user.emailVerifiedAt;
|
|
1251
1262
|
return Response.json(
|
|
@@ -1279,7 +1290,7 @@ function authController(store, config, bus) {
|
|
|
1279
1290
|
"Account suspended. Please contact support."
|
|
1280
1291
|
);
|
|
1281
1292
|
}
|
|
1282
|
-
const otp = (0,
|
|
1293
|
+
const otp = (0, import_node_crypto3.randomInt)(1e5, 1e6).toString();
|
|
1283
1294
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
1284
1295
|
await phoneVerif.upsert(user.id, normalizePhone(phone2), otp, expiresAt);
|
|
1285
1296
|
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
@@ -1288,10 +1299,10 @@ function authController(store, config, bus) {
|
|
|
1288
1299
|
recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
|
|
1289
1300
|
}).catch(() => {
|
|
1290
1301
|
});
|
|
1291
|
-
const { accessToken, refreshToken } = issueTokenPair(user.id, config, {
|
|
1302
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
1292
1303
|
loginMethod: "phone"
|
|
1293
1304
|
});
|
|
1294
|
-
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
1305
|
+
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1295
1306
|
return Response.json(
|
|
1296
1307
|
{
|
|
1297
1308
|
reason: "USER_PHONE_OTP_SENT",
|
|
@@ -1352,11 +1363,11 @@ function authController(store, config, bus) {
|
|
|
1352
1363
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
1353
1364
|
}
|
|
1354
1365
|
await sessions.delete(token);
|
|
1355
|
-
const { accessToken, refreshToken } = issueTokenPair(user.id, config, {
|
|
1366
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
1356
1367
|
loginMethod: payload.loginMethod ?? "email",
|
|
1357
1368
|
phoneVerified: payload.phoneVerified ?? false
|
|
1358
1369
|
});
|
|
1359
|
-
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
1370
|
+
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1360
1371
|
return Response.json(
|
|
1361
1372
|
{
|
|
1362
1373
|
reason: "TOKENS_REFRESHED",
|
|
@@ -1399,7 +1410,7 @@ function authController(store, config, bus) {
|
|
|
1399
1410
|
"Password reset email sent (if account exists)."
|
|
1400
1411
|
);
|
|
1401
1412
|
}
|
|
1402
|
-
const pin = (0,
|
|
1413
|
+
const pin = (0, import_node_crypto3.randomInt)(1e5, 1e6).toString();
|
|
1403
1414
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60);
|
|
1404
1415
|
await passwordReset.create(user.id, pin, expiresAt);
|
|
1405
1416
|
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
@@ -1487,11 +1498,11 @@ function authController(store, config, bus) {
|
|
|
1487
1498
|
);
|
|
1488
1499
|
}
|
|
1489
1500
|
await phoneVerif.deleteByUser(ctx.user.id);
|
|
1490
|
-
const { accessToken, refreshToken } = issueTokenPair(ctx.user.id, config, {
|
|
1501
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(ctx.user.id, config, {
|
|
1491
1502
|
loginMethod: "phone",
|
|
1492
1503
|
phoneVerified: true
|
|
1493
1504
|
});
|
|
1494
|
-
await sessions.create(ctx.user.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
1505
|
+
await sessions.create(ctx.user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1495
1506
|
const verifiedUser = await users.findById(ctx.user.id);
|
|
1496
1507
|
if (!verifiedUser) {
|
|
1497
1508
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
@@ -1567,7 +1578,7 @@ function authController(store, config, bus) {
|
|
|
1567
1578
|
}
|
|
1568
1579
|
);
|
|
1569
1580
|
}
|
|
1570
|
-
const otp = (0,
|
|
1581
|
+
const otp = (0, import_node_crypto3.randomInt)(1e5, 1e6).toString();
|
|
1571
1582
|
const expiresAt2 = new Date(Date.now() + OTP_TTL_MS);
|
|
1572
1583
|
await phoneVerif.upsert(ctx.user.id, phone2, otp, expiresAt2);
|
|
1573
1584
|
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
@@ -1606,7 +1617,7 @@ function authController(store, config, bus) {
|
|
|
1606
1617
|
}
|
|
1607
1618
|
);
|
|
1608
1619
|
}
|
|
1609
|
-
const pin = (0,
|
|
1620
|
+
const pin = (0, import_node_crypto3.randomInt)(1e5, 1e6).toString();
|
|
1610
1621
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
1611
1622
|
await emailVerif.replace(ctx.user.id, pin, expiresAt);
|
|
1612
1623
|
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
@@ -1623,7 +1634,7 @@ function authController(store, config, bus) {
|
|
|
1623
1634
|
}
|
|
1624
1635
|
|
|
1625
1636
|
// src/controllers/user.controller.ts
|
|
1626
|
-
var
|
|
1637
|
+
var import_node_crypto4 = require("crypto");
|
|
1627
1638
|
var import_core5 = require("@fonderie/core");
|
|
1628
1639
|
var import_events3 = require("@fonderie/events");
|
|
1629
1640
|
function normalizePhone2(phone2) {
|
|
@@ -1716,7 +1727,7 @@ function userController(store, config, bus) {
|
|
|
1716
1727
|
if (existing) {
|
|
1717
1728
|
return (0, import_core5.setApiResponse)(import_core5.HTTP.CONFLICT, "EMAIL_IN_USE", "Email already in use");
|
|
1718
1729
|
}
|
|
1719
|
-
const pin = (0,
|
|
1730
|
+
const pin = (0, import_node_crypto4.randomInt)(1e5, 1e6).toString();
|
|
1720
1731
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
1721
1732
|
await emailVerif.replace(ctx.user.id, pin, expiresAt);
|
|
1722
1733
|
await users.updateEmail(ctx.user.id, normalised);
|
|
@@ -1758,7 +1769,7 @@ function userController(store, config, bus) {
|
|
|
1758
1769
|
if (existing) {
|
|
1759
1770
|
return (0, import_core5.setApiResponse)(import_core5.HTTP.CONFLICT, "PHONE_IN_USE", "Phone number already in use");
|
|
1760
1771
|
}
|
|
1761
|
-
const otp = (0,
|
|
1772
|
+
const otp = (0, import_node_crypto4.randomInt)(1e5, 1e6).toString();
|
|
1762
1773
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
|
|
1763
1774
|
await phoneVerif.upsert(ctx.user.id, normalised, otp, expiresAt);
|
|
1764
1775
|
await users.updatePhone(ctx.user.id, normalised);
|
|
@@ -1909,10 +1920,10 @@ function oauthController(store, config) {
|
|
|
1909
1920
|
if (!fullUser) {
|
|
1910
1921
|
return (0, import_core6.setApiResponse)(import_core6.HTTP.SERVER_ERROR, "SERVER_ERROR", "OAuth login failed");
|
|
1911
1922
|
}
|
|
1912
|
-
const { accessToken, refreshToken } = issueTokenPair(upserted.id, config, {
|
|
1923
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, config, {
|
|
1913
1924
|
loginMethod: "google"
|
|
1914
1925
|
});
|
|
1915
|
-
await sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
1926
|
+
await sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1916
1927
|
return Response.json(
|
|
1917
1928
|
{
|
|
1918
1929
|
reason: "GOOGLE_AUTH_SUCCESS",
|
|
@@ -1994,6 +2005,7 @@ function buildAuthRoutes(store, config, bus) {
|
|
|
1994
2005
|
// src/middlewares/session.ts
|
|
1995
2006
|
function withSession(store, config) {
|
|
1996
2007
|
const users = new UserModel(store);
|
|
2008
|
+
const sessions = new SessionModel(store);
|
|
1997
2009
|
return async (ctx, next) => {
|
|
1998
2010
|
const token = extractToken(ctx.request);
|
|
1999
2011
|
if (!token) {
|
|
@@ -2003,6 +2015,9 @@ function withSession(store, config) {
|
|
|
2003
2015
|
if (!payload || payload.type !== "access") {
|
|
2004
2016
|
return next();
|
|
2005
2017
|
}
|
|
2018
|
+
if (payload.sid && !await sessions.aliveBySid(payload.sid)) {
|
|
2019
|
+
return next();
|
|
2020
|
+
}
|
|
2006
2021
|
const user = await users.findById(payload.sub);
|
|
2007
2022
|
if (!user || user.suspended || user.deletedAt) {
|
|
2008
2023
|
return next();
|