@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 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: "24h" }
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 import_node_crypto = require("crypto");
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, import_node_crypto.createHmac)("sha1", key).update(buf).digest();
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, import_node_crypto.randomBytes)(20));
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, import_node_crypto.randomBytes)(4).toString("hex").toUpperCase());
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 import_node_crypto2 = require("crypto");
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, import_node_crypto2.randomInt)(1e5, 1e6).toString();
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, import_node_crypto2.randomInt)(1e5, 1e6).toString();
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, import_node_crypto2.randomInt)(1e5, 1e6).toString();
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, import_node_crypto2.randomInt)(1e5, 1e6).toString();
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, import_node_crypto2.randomInt)(1e5, 1e6).toString();
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, import_node_crypto2.randomInt)(1e5, 1e6).toString();
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 import_node_crypto3 = require("crypto");
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, import_node_crypto3.randomInt)(1e5, 1e6).toString();
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, import_node_crypto3.randomInt)(1e5, 1e6).toString();
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();