@fonderie/auth 1.1.1 → 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 +113 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +102 -28
- 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 +2 -1
- package/dist/middlewares/index.d.ts +2 -1
- package/dist/middlewares/index.js +39 -0
- package/dist/middlewares/index.js.map +1 -1
- package/dist/{session-BGjjwz5_.d.cts → session-DbtCnbiG.d.cts} +12 -1
- package/dist/{session-BGjjwz5_.d.ts → session-DbtCnbiG.d.ts} +12 -1
- package/package.json +5 -3
package/dist/index.cjs
CHANGED
|
@@ -57,6 +57,8 @@ __export(index_exports, {
|
|
|
57
57
|
AUTH_CONFIG_KEYS: () => AUTH_CONFIG_KEYS,
|
|
58
58
|
AuthModule: () => AuthModule,
|
|
59
59
|
MESSAGE_KEYS: () => MESSAGE_KEYS,
|
|
60
|
+
buildAuthAccountLimiter: () => buildAuthAccountLimiter,
|
|
61
|
+
buildAuthIpLimiter: () => buildAuthIpLimiter,
|
|
60
62
|
normalizeEmail: () => normalizeEmail,
|
|
61
63
|
normalizeEmailSafe: () => normalizeEmailSafe,
|
|
62
64
|
requireAuth: () => import_middlewares3.requireAuth,
|
|
@@ -86,6 +88,55 @@ var requireEmailLogin = async (ctx, next) => {
|
|
|
86
88
|
// src/middlewares/validate.ts
|
|
87
89
|
var import_middlewares = require("@fonderie/core/middlewares");
|
|
88
90
|
|
|
91
|
+
// src/services/rate-limit.ts
|
|
92
|
+
var import_rate_limit = require("@fonderie/rate-limit");
|
|
93
|
+
var min = (n) => n * 60;
|
|
94
|
+
var DEFAULTS = {
|
|
95
|
+
// login: 10/15min per IP AND 5/15min per account (credential stuffing
|
|
96
|
+
// rotates IPs, so the per-account bucket is the one that bites).
|
|
97
|
+
login: {
|
|
98
|
+
ip: { capacity: 10, refillPerSec: 10 / min(15) },
|
|
99
|
+
account: { capacity: 5, refillPerSec: 5 / min(15) },
|
|
100
|
+
accountField: "email"
|
|
101
|
+
},
|
|
102
|
+
// register: 5/hour per IP — signup abuse is IP-shaped.
|
|
103
|
+
register: { ip: { capacity: 5, refillPerSec: 5 / min(60) } },
|
|
104
|
+
// forgot: 5/hour per IP AND 3/hour per account (email-bombing protection).
|
|
105
|
+
forgot: {
|
|
106
|
+
ip: { capacity: 5, refillPerSec: 5 / min(60) },
|
|
107
|
+
account: { capacity: 3, refillPerSec: 3 / min(60) },
|
|
108
|
+
accountField: "email"
|
|
109
|
+
},
|
|
110
|
+
// mfaVerify: 10/15min per IP — TOTP brute-force.
|
|
111
|
+
mfaVerify: { ip: { capacity: 10, refillPerSec: 10 / min(15) } }
|
|
112
|
+
};
|
|
113
|
+
function resolve(route, store, config) {
|
|
114
|
+
if (config === false) return null;
|
|
115
|
+
const override = config?.rules?.[route];
|
|
116
|
+
if (override === false) return null;
|
|
117
|
+
const backing = config?.store ?? new import_rate_limit.StoreAdapterStore(store);
|
|
118
|
+
const def = DEFAULTS[route];
|
|
119
|
+
return {
|
|
120
|
+
store: backing,
|
|
121
|
+
ipRule: override ?? def.ip,
|
|
122
|
+
...def.account && def.accountField ? { account: { rule: def.account, field: def.accountField } } : {}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function buildAuthIpLimiter(route, store, config) {
|
|
126
|
+
const r = resolve(route, store, config);
|
|
127
|
+
if (!r) return null;
|
|
128
|
+
return (0, import_rate_limit.rateLimit)({ store: r.store, rule: r.ipRule, key: (0, import_rate_limit.byIp)(`auth:${route}`) });
|
|
129
|
+
}
|
|
130
|
+
function buildAuthAccountLimiter(route, store, config) {
|
|
131
|
+
const r = resolve(route, store, config);
|
|
132
|
+
if (!r || !r.account) return null;
|
|
133
|
+
return (0, import_rate_limit.rateLimit)({
|
|
134
|
+
store: r.store,
|
|
135
|
+
rule: r.account.rule,
|
|
136
|
+
key: (0, import_rate_limit.byBodyField)(`auth:${route}`, r.account.field)
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
89
140
|
// src/schemas.ts
|
|
90
141
|
var schemas_exports = {};
|
|
91
142
|
__export(schemas_exports, {
|
|
@@ -203,6 +254,7 @@ var EVENT_KEYS = {
|
|
|
203
254
|
};
|
|
204
255
|
|
|
205
256
|
// src/services/jwt.ts
|
|
257
|
+
var import_node_crypto = require("crypto");
|
|
206
258
|
var import_jsonwebtoken = __toESM(require("jsonwebtoken"), 1);
|
|
207
259
|
function issueMfaPendingToken(userId, config, loginMethod) {
|
|
208
260
|
return import_jsonwebtoken.default.sign(
|
|
@@ -219,19 +271,21 @@ function issueMfaPendingToken(userId, config, loginMethod) {
|
|
|
219
271
|
}
|
|
220
272
|
function issueTokenPair(userId, config, options) {
|
|
221
273
|
const duration = config.sessionDuration ?? "7d";
|
|
274
|
+
const accessDuration = config.accessTokenDuration ?? "24h";
|
|
222
275
|
const loginMethod = options.loginMethod;
|
|
223
276
|
const phoneVerified = options.phoneVerified ?? false;
|
|
277
|
+
const sid = (0, import_node_crypto.randomUUID)();
|
|
224
278
|
const accessToken = import_jsonwebtoken.default.sign(
|
|
225
|
-
{ sub: userId, type: "access", loginMethod, phoneVerified },
|
|
279
|
+
{ sub: userId, type: "access", loginMethod, phoneVerified, sid },
|
|
226
280
|
config.jwtSecret,
|
|
227
|
-
{ expiresIn:
|
|
281
|
+
{ expiresIn: accessDuration }
|
|
228
282
|
);
|
|
229
283
|
const refreshToken = import_jsonwebtoken.default.sign(
|
|
230
|
-
{ sub: userId, type: "refresh", loginMethod, phoneVerified },
|
|
284
|
+
{ sub: userId, type: "refresh", loginMethod, phoneVerified, sid },
|
|
231
285
|
config.jwtSecret,
|
|
232
286
|
{ expiresIn: duration }
|
|
233
287
|
);
|
|
234
|
-
return { accessToken, refreshToken };
|
|
288
|
+
return { accessToken, refreshToken, sid };
|
|
235
289
|
}
|
|
236
290
|
function refreshTokenExpiry(token) {
|
|
237
291
|
const decoded = import_jsonwebtoken.default.decode(token);
|
|
@@ -246,7 +300,7 @@ function verifyToken(token, config) {
|
|
|
246
300
|
}
|
|
247
301
|
|
|
248
302
|
// src/services/mfa.ts
|
|
249
|
-
var
|
|
303
|
+
var import_node_crypto2 = require("crypto");
|
|
250
304
|
var STEP = 30;
|
|
251
305
|
var DRIFT = 1;
|
|
252
306
|
var DIGITS = 6;
|
|
@@ -258,7 +312,7 @@ function hotp(secret, counter) {
|
|
|
258
312
|
c >>= 8;
|
|
259
313
|
}
|
|
260
314
|
const key = Buffer.from(base32Decode(secret));
|
|
261
|
-
const hmac = (0,
|
|
315
|
+
const hmac = (0, import_node_crypto2.createHmac)("sha1", key).update(buf).digest();
|
|
262
316
|
const offset = (hmac[19] ?? 0) & 15;
|
|
263
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);
|
|
264
318
|
return code.toString().padStart(DIGITS, "0");
|
|
@@ -305,7 +359,7 @@ function base32Encode(buf) {
|
|
|
305
359
|
return output;
|
|
306
360
|
}
|
|
307
361
|
function generateTotpSecret() {
|
|
308
|
-
return base32Encode((0,
|
|
362
|
+
return base32Encode((0, import_node_crypto2.randomBytes)(20));
|
|
309
363
|
}
|
|
310
364
|
function generateTotpUri(email2, secret, issuer) {
|
|
311
365
|
const params = new URLSearchParams({
|
|
@@ -327,7 +381,7 @@ function verifyTotpToken(token, secret) {
|
|
|
327
381
|
return false;
|
|
328
382
|
}
|
|
329
383
|
function generateBackupCodes(count = 8) {
|
|
330
|
-
return Array.from({ length: count }, () => (0,
|
|
384
|
+
return Array.from({ length: count }, () => (0, import_node_crypto2.randomBytes)(4).toString("hex").toUpperCase());
|
|
331
385
|
}
|
|
332
386
|
|
|
333
387
|
// src/controllers/mfa.controller.ts
|
|
@@ -587,12 +641,12 @@ var SessionModel = class {
|
|
|
587
641
|
this.store = store;
|
|
588
642
|
}
|
|
589
643
|
store;
|
|
590
|
-
async create(userId, token, expiresAt) {
|
|
644
|
+
async create(userId, token, expiresAt, sid) {
|
|
591
645
|
await this.store.query(
|
|
592
|
-
`INSERT INTO fonderie_sessions (user_id, token, expires_at)
|
|
593
|
-
VALUES ($1, $2, $3)
|
|
646
|
+
`INSERT INTO fonderie_sessions (user_id, token, expires_at, sid)
|
|
647
|
+
VALUES ($1, $2, $3, $4)
|
|
594
648
|
ON CONFLICT (token) DO NOTHING`,
|
|
595
|
-
[userId, token, expiresAt]
|
|
649
|
+
[userId, token, expiresAt, sid ?? null]
|
|
596
650
|
);
|
|
597
651
|
}
|
|
598
652
|
async delete(token) {
|
|
@@ -605,6 +659,14 @@ var SessionModel = class {
|
|
|
605
659
|
);
|
|
606
660
|
return rows.length > 0;
|
|
607
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
|
+
}
|
|
608
670
|
};
|
|
609
671
|
|
|
610
672
|
// src/models/backup-code.model.ts
|
|
@@ -735,10 +797,10 @@ function mfaController(store, config, issuer, bus) {
|
|
|
735
797
|
await users.enableMfa(ctx.user.id);
|
|
736
798
|
}
|
|
737
799
|
}
|
|
738
|
-
const { accessToken, refreshToken } = issueTokenPair(ctx.user.id, config, {
|
|
800
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(ctx.user.id, config, {
|
|
739
801
|
loginMethod: ctx.user.loginMethod
|
|
740
802
|
});
|
|
741
|
-
await sessions.create(ctx.user.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
803
|
+
await sessions.create(ctx.user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
742
804
|
const fullUser = await users.findById(ctx.user.id);
|
|
743
805
|
if (!fullUser) {
|
|
744
806
|
return (0, import_core3.setApiResponse)(import_core3.HTTP.SERVER_ERROR, "SERVER_ERROR", "User not found after MFA verify");
|
|
@@ -815,7 +877,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
815
877
|
}
|
|
816
878
|
|
|
817
879
|
// src/controllers/auth.controller.ts
|
|
818
|
-
var
|
|
880
|
+
var import_node_crypto3 = require("crypto");
|
|
819
881
|
var import_events2 = require("@fonderie/events");
|
|
820
882
|
var import_core4 = require("@fonderie/core");
|
|
821
883
|
|
|
@@ -1039,7 +1101,7 @@ function authController(store, config, bus) {
|
|
|
1039
1101
|
if (!row) {
|
|
1040
1102
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.SERVER_ERROR, "SERVER_ERROR", "Registration failed");
|
|
1041
1103
|
}
|
|
1042
|
-
const pin = (0,
|
|
1104
|
+
const pin = (0, import_node_crypto3.randomInt)(1e5, 1e6).toString();
|
|
1043
1105
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
1044
1106
|
await emailVerif.create(row.id, pin, expiresAt);
|
|
1045
1107
|
const user = await users.findById(row.id);
|
|
@@ -1070,10 +1132,10 @@ function authController(store, config, bus) {
|
|
|
1070
1132
|
reqOpts
|
|
1071
1133
|
).catch(() => {
|
|
1072
1134
|
});
|
|
1073
|
-
const { accessToken, refreshToken } = issueTokenPair(user.id, config, {
|
|
1135
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
1074
1136
|
loginMethod: "email"
|
|
1075
1137
|
});
|
|
1076
|
-
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
1138
|
+
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1077
1139
|
const resolvedRegister = { ...config, ...config.resolve?.(ctx) };
|
|
1078
1140
|
const requiresVerification = !!resolvedRegister.requireVerification && !user.emailVerifiedAt;
|
|
1079
1141
|
return Response.json(
|
|
@@ -1104,7 +1166,7 @@ function authController(store, config, bus) {
|
|
|
1104
1166
|
firstName ?? null,
|
|
1105
1167
|
lastName ?? null
|
|
1106
1168
|
);
|
|
1107
|
-
const otp = (0,
|
|
1169
|
+
const otp = (0, import_node_crypto3.randomInt)(1e5, 1e6).toString();
|
|
1108
1170
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
1109
1171
|
await phoneVerif.upsert(id, normalizePhone(phone2), otp, expiresAt);
|
|
1110
1172
|
const user = await users.findById(id);
|
|
@@ -1135,10 +1197,10 @@ function authController(store, config, bus) {
|
|
|
1135
1197
|
reqOpts2
|
|
1136
1198
|
).catch(() => {
|
|
1137
1199
|
});
|
|
1138
|
-
const { accessToken, refreshToken } = issueTokenPair(user.id, config, {
|
|
1200
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
1139
1201
|
loginMethod: "phone"
|
|
1140
1202
|
});
|
|
1141
|
-
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
1203
|
+
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1142
1204
|
return Response.json(
|
|
1143
1205
|
{
|
|
1144
1206
|
reason: "USER_PHONE_REGISTERED",
|
|
@@ -1191,10 +1253,10 @@ function authController(store, config, bus) {
|
|
|
1191
1253
|
mfaToken
|
|
1192
1254
|
});
|
|
1193
1255
|
}
|
|
1194
|
-
const { accessToken, refreshToken } = issueTokenPair(user.id, config, {
|
|
1256
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
1195
1257
|
loginMethod: "email"
|
|
1196
1258
|
});
|
|
1197
|
-
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
1259
|
+
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1198
1260
|
const resolvedLogin = { ...config, ...config.resolve?.(ctx) };
|
|
1199
1261
|
const requiresVerification = !!resolvedLogin.requireVerification && !user.emailVerifiedAt;
|
|
1200
1262
|
return Response.json(
|
|
@@ -1228,7 +1290,7 @@ function authController(store, config, bus) {
|
|
|
1228
1290
|
"Account suspended. Please contact support."
|
|
1229
1291
|
);
|
|
1230
1292
|
}
|
|
1231
|
-
const otp = (0,
|
|
1293
|
+
const otp = (0, import_node_crypto3.randomInt)(1e5, 1e6).toString();
|
|
1232
1294
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
1233
1295
|
await phoneVerif.upsert(user.id, normalizePhone(phone2), otp, expiresAt);
|
|
1234
1296
|
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
@@ -1237,10 +1299,10 @@ function authController(store, config, bus) {
|
|
|
1237
1299
|
recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
|
|
1238
1300
|
}).catch(() => {
|
|
1239
1301
|
});
|
|
1240
|
-
const { accessToken, refreshToken } = issueTokenPair(user.id, config, {
|
|
1302
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
1241
1303
|
loginMethod: "phone"
|
|
1242
1304
|
});
|
|
1243
|
-
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
1305
|
+
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1244
1306
|
return Response.json(
|
|
1245
1307
|
{
|
|
1246
1308
|
reason: "USER_PHONE_OTP_SENT",
|
|
@@ -1301,11 +1363,11 @@ function authController(store, config, bus) {
|
|
|
1301
1363
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
1302
1364
|
}
|
|
1303
1365
|
await sessions.delete(token);
|
|
1304
|
-
const { accessToken, refreshToken } = issueTokenPair(user.id, config, {
|
|
1366
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
1305
1367
|
loginMethod: payload.loginMethod ?? "email",
|
|
1306
1368
|
phoneVerified: payload.phoneVerified ?? false
|
|
1307
1369
|
});
|
|
1308
|
-
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
1370
|
+
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1309
1371
|
return Response.json(
|
|
1310
1372
|
{
|
|
1311
1373
|
reason: "TOKENS_REFRESHED",
|
|
@@ -1348,7 +1410,7 @@ function authController(store, config, bus) {
|
|
|
1348
1410
|
"Password reset email sent (if account exists)."
|
|
1349
1411
|
);
|
|
1350
1412
|
}
|
|
1351
|
-
const pin = (0,
|
|
1413
|
+
const pin = (0, import_node_crypto3.randomInt)(1e5, 1e6).toString();
|
|
1352
1414
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60);
|
|
1353
1415
|
await passwordReset.create(user.id, pin, expiresAt);
|
|
1354
1416
|
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
@@ -1436,11 +1498,11 @@ function authController(store, config, bus) {
|
|
|
1436
1498
|
);
|
|
1437
1499
|
}
|
|
1438
1500
|
await phoneVerif.deleteByUser(ctx.user.id);
|
|
1439
|
-
const { accessToken, refreshToken } = issueTokenPair(ctx.user.id, config, {
|
|
1501
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(ctx.user.id, config, {
|
|
1440
1502
|
loginMethod: "phone",
|
|
1441
1503
|
phoneVerified: true
|
|
1442
1504
|
});
|
|
1443
|
-
await sessions.create(ctx.user.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
1505
|
+
await sessions.create(ctx.user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1444
1506
|
const verifiedUser = await users.findById(ctx.user.id);
|
|
1445
1507
|
if (!verifiedUser) {
|
|
1446
1508
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
@@ -1516,7 +1578,7 @@ function authController(store, config, bus) {
|
|
|
1516
1578
|
}
|
|
1517
1579
|
);
|
|
1518
1580
|
}
|
|
1519
|
-
const otp = (0,
|
|
1581
|
+
const otp = (0, import_node_crypto3.randomInt)(1e5, 1e6).toString();
|
|
1520
1582
|
const expiresAt2 = new Date(Date.now() + OTP_TTL_MS);
|
|
1521
1583
|
await phoneVerif.upsert(ctx.user.id, phone2, otp, expiresAt2);
|
|
1522
1584
|
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
@@ -1555,7 +1617,7 @@ function authController(store, config, bus) {
|
|
|
1555
1617
|
}
|
|
1556
1618
|
);
|
|
1557
1619
|
}
|
|
1558
|
-
const pin = (0,
|
|
1620
|
+
const pin = (0, import_node_crypto3.randomInt)(1e5, 1e6).toString();
|
|
1559
1621
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
1560
1622
|
await emailVerif.replace(ctx.user.id, pin, expiresAt);
|
|
1561
1623
|
bus?.emit(import_events2.NOTIFICATION_EVENT, {
|
|
@@ -1572,7 +1634,7 @@ function authController(store, config, bus) {
|
|
|
1572
1634
|
}
|
|
1573
1635
|
|
|
1574
1636
|
// src/controllers/user.controller.ts
|
|
1575
|
-
var
|
|
1637
|
+
var import_node_crypto4 = require("crypto");
|
|
1576
1638
|
var import_core5 = require("@fonderie/core");
|
|
1577
1639
|
var import_events3 = require("@fonderie/events");
|
|
1578
1640
|
function normalizePhone2(phone2) {
|
|
@@ -1665,7 +1727,7 @@ function userController(store, config, bus) {
|
|
|
1665
1727
|
if (existing) {
|
|
1666
1728
|
return (0, import_core5.setApiResponse)(import_core5.HTTP.CONFLICT, "EMAIL_IN_USE", "Email already in use");
|
|
1667
1729
|
}
|
|
1668
|
-
const pin = (0,
|
|
1730
|
+
const pin = (0, import_node_crypto4.randomInt)(1e5, 1e6).toString();
|
|
1669
1731
|
const expiresAt = new Date(Date.now() + 1e3 * 60 * 60 * 24);
|
|
1670
1732
|
await emailVerif.replace(ctx.user.id, pin, expiresAt);
|
|
1671
1733
|
await users.updateEmail(ctx.user.id, normalised);
|
|
@@ -1707,7 +1769,7 @@ function userController(store, config, bus) {
|
|
|
1707
1769
|
if (existing) {
|
|
1708
1770
|
return (0, import_core5.setApiResponse)(import_core5.HTTP.CONFLICT, "PHONE_IN_USE", "Phone number already in use");
|
|
1709
1771
|
}
|
|
1710
|
-
const otp = (0,
|
|
1772
|
+
const otp = (0, import_node_crypto4.randomInt)(1e5, 1e6).toString();
|
|
1711
1773
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1e3);
|
|
1712
1774
|
await phoneVerif.upsert(ctx.user.id, normalised, otp, expiresAt);
|
|
1713
1775
|
await users.updatePhone(ctx.user.id, normalised);
|
|
@@ -1858,10 +1920,10 @@ function oauthController(store, config) {
|
|
|
1858
1920
|
if (!fullUser) {
|
|
1859
1921
|
return (0, import_core6.setApiResponse)(import_core6.HTTP.SERVER_ERROR, "SERVER_ERROR", "OAuth login failed");
|
|
1860
1922
|
}
|
|
1861
|
-
const { accessToken, refreshToken } = issueTokenPair(upserted.id, config, {
|
|
1923
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, config, {
|
|
1862
1924
|
loginMethod: "google"
|
|
1863
1925
|
});
|
|
1864
|
-
await sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
1926
|
+
await sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1865
1927
|
return Response.json(
|
|
1866
1928
|
{
|
|
1867
1929
|
reason: "GOOGLE_AUTH_SUCCESS",
|
|
@@ -1889,14 +1951,17 @@ function buildAuthRoutes(store, config, bus) {
|
|
|
1889
1951
|
const oauth = oauthController(store, config);
|
|
1890
1952
|
const mfa = mfaController(store, config, config.appName ?? "Fonderie", bus);
|
|
1891
1953
|
const verifyGate = config.requireVerification ? import_middlewares2.requireVerified : (_ctx, next) => next();
|
|
1954
|
+
const passthrough = (_ctx, next) => next();
|
|
1955
|
+
const ipLimit = (route) => buildAuthIpLimiter(route, store, config.rateLimit) ?? passthrough;
|
|
1956
|
+
const acctLimit = (route) => buildAuthAccountLimiter(route, store, config.rateLimit) ?? passthrough;
|
|
1892
1957
|
const routes = [
|
|
1893
1958
|
// Registration & Login (Public)
|
|
1894
|
-
["POST", "/auth/register", (0, import_middlewares.validate)(registerSchema), auth.register],
|
|
1895
|
-
["POST", "/auth/login", (0, import_middlewares.validate)(loginSchema), auth.login],
|
|
1959
|
+
["POST", "/auth/register", ipLimit("register"), (0, import_middlewares.validate)(registerSchema), auth.register],
|
|
1960
|
+
["POST", "/auth/login", ipLimit("login"), (0, import_middlewares.validate)(loginSchema), acctLimit("login"), auth.login],
|
|
1896
1961
|
// Token Management (Public)
|
|
1897
1962
|
["POST", "/auth/refresh", (0, import_middlewares.validate)(refreshSchema), auth.refresh],
|
|
1898
1963
|
// Email — Password Recovery (Public)
|
|
1899
|
-
["POST", "/auth/email/forgot", (0, import_middlewares.validate)(forgotPasswordSchema), auth.forgotPassword],
|
|
1964
|
+
["POST", "/auth/email/forgot", ipLimit("forgot"), (0, import_middlewares.validate)(forgotPasswordSchema), acctLimit("forgot"), auth.forgotPassword],
|
|
1900
1965
|
["POST", "/auth/email/reset", (0, import_middlewares.validate)(resetPasswordSchema), auth.resetPassword],
|
|
1901
1966
|
// Verification (Protected — email or phone, determined by loginMethod)
|
|
1902
1967
|
["POST", "/auth/verify", import_middlewares2.requireAuth, (0, import_middlewares.validate)(verifySchema), auth.verify],
|
|
@@ -1916,7 +1981,7 @@ function buildAuthRoutes(store, config, bus) {
|
|
|
1916
1981
|
["POST", "/auth/mfa/setup", import_middlewares2.requireAuth, requireEmailLogin, import_middlewares2.requireVerified, mfa.setup],
|
|
1917
1982
|
// /auth/mfa/verify accepts both mfaPending tokens (TOTP/backup-code login)
|
|
1918
1983
|
// and full tokens (setup confirmation), so requireAnyAuth is used here.
|
|
1919
|
-
["POST", "/auth/mfa/verify", import_middlewares2.requireAnyAuth, requireEmailLogin, import_middlewares2.requireVerified, (0, import_middlewares.validate)(mfaTokenSchema), mfa.verify],
|
|
1984
|
+
["POST", "/auth/mfa/verify", ipLimit("mfaVerify"), import_middlewares2.requireAnyAuth, requireEmailLogin, import_middlewares2.requireVerified, (0, import_middlewares.validate)(mfaTokenSchema), mfa.verify],
|
|
1920
1985
|
["POST", "/auth/mfa/disable", import_middlewares2.requireAuth, requireEmailLogin, import_middlewares2.requireVerified, (0, import_middlewares.validate)(mfaTokenSchema), mfa.disable],
|
|
1921
1986
|
[
|
|
1922
1987
|
"POST",
|
|
@@ -1940,6 +2005,7 @@ function buildAuthRoutes(store, config, bus) {
|
|
|
1940
2005
|
// src/middlewares/session.ts
|
|
1941
2006
|
function withSession(store, config) {
|
|
1942
2007
|
const users = new UserModel(store);
|
|
2008
|
+
const sessions = new SessionModel(store);
|
|
1943
2009
|
return async (ctx, next) => {
|
|
1944
2010
|
const token = extractToken(ctx.request);
|
|
1945
2011
|
if (!token) {
|
|
@@ -1949,6 +2015,9 @@ function withSession(store, config) {
|
|
|
1949
2015
|
if (!payload || payload.type !== "access") {
|
|
1950
2016
|
return next();
|
|
1951
2017
|
}
|
|
2018
|
+
if (payload.sid && !await sessions.aliveBySid(payload.sid)) {
|
|
2019
|
+
return next();
|
|
2020
|
+
}
|
|
1952
2021
|
const user = await users.findById(payload.sub);
|
|
1953
2022
|
if (!user || user.suspended || user.deletedAt) {
|
|
1954
2023
|
return next();
|
|
@@ -2001,6 +2070,8 @@ var import_middlewares3 = require("@fonderie/core/middlewares");
|
|
|
2001
2070
|
AUTH_CONFIG_KEYS,
|
|
2002
2071
|
AuthModule,
|
|
2003
2072
|
MESSAGE_KEYS,
|
|
2073
|
+
buildAuthAccountLimiter,
|
|
2074
|
+
buildAuthIpLimiter,
|
|
2004
2075
|
normalizeEmail,
|
|
2005
2076
|
normalizeEmailSafe,
|
|
2006
2077
|
requireAuth,
|