@fonderie/auth 1.1.0 → 1.2.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 +95 -52
- 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 +98 -52
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.d.cts +2 -1
- package/dist/middlewares/index.d.ts +2 -1
- package/dist/{session-CLD1WPJs.d.cts → session-C54ksYE8.d.cts} +12 -1
- package/dist/{session-CLD1WPJs.d.ts → session-C54ksYE8.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, {
|
|
@@ -151,6 +202,26 @@ var changePasswordSchema = import_zod.z.object({
|
|
|
151
202
|
});
|
|
152
203
|
var mfaTokenSchema = import_zod.z.object({ token: import_zod.z.string().trim().min(6).max(64) });
|
|
153
204
|
|
|
205
|
+
// src/services/cookies.ts
|
|
206
|
+
function secureAttr(config) {
|
|
207
|
+
const secure = config.secureCookies ?? process.env["NODE_ENV"] === "production";
|
|
208
|
+
return secure ? "; Secure" : "";
|
|
209
|
+
}
|
|
210
|
+
function tokenPairCookies(accessToken, refreshToken, config) {
|
|
211
|
+
const secure = secureAttr(config);
|
|
212
|
+
return [
|
|
213
|
+
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/${secure}`,
|
|
214
|
+
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh${secure}`
|
|
215
|
+
].join(", ");
|
|
216
|
+
}
|
|
217
|
+
function clearedTokenCookies(config) {
|
|
218
|
+
const secure = secureAttr(config);
|
|
219
|
+
return [
|
|
220
|
+
`access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0${secure}`,
|
|
221
|
+
`refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0${secure}`
|
|
222
|
+
].join(", ");
|
|
223
|
+
}
|
|
224
|
+
|
|
154
225
|
// src/controllers/mfa.controller.ts
|
|
155
226
|
var import_qrcode = __toESM(require("qrcode"), 1);
|
|
156
227
|
var import_core3 = require("@fonderie/core");
|
|
@@ -735,10 +806,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
735
806
|
{
|
|
736
807
|
status: 200,
|
|
737
808
|
headers: {
|
|
738
|
-
"Set-Cookie":
|
|
739
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
740
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
741
|
-
].join(", ")
|
|
809
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
742
810
|
}
|
|
743
811
|
}
|
|
744
812
|
);
|
|
@@ -1072,10 +1140,7 @@ function authController(store, config, bus) {
|
|
|
1072
1140
|
{
|
|
1073
1141
|
status: 201,
|
|
1074
1142
|
headers: {
|
|
1075
|
-
"Set-Cookie":
|
|
1076
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1077
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1078
|
-
].join(", ")
|
|
1143
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1079
1144
|
}
|
|
1080
1145
|
}
|
|
1081
1146
|
);
|
|
@@ -1137,10 +1202,7 @@ function authController(store, config, bus) {
|
|
|
1137
1202
|
{
|
|
1138
1203
|
status: 202,
|
|
1139
1204
|
headers: {
|
|
1140
|
-
"Set-Cookie":
|
|
1141
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1142
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1143
|
-
].join(", ")
|
|
1205
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1144
1206
|
}
|
|
1145
1207
|
}
|
|
1146
1208
|
);
|
|
@@ -1199,10 +1261,7 @@ function authController(store, config, bus) {
|
|
|
1199
1261
|
{
|
|
1200
1262
|
status: 200,
|
|
1201
1263
|
headers: {
|
|
1202
|
-
"Set-Cookie":
|
|
1203
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1204
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1205
|
-
].join(", ")
|
|
1264
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1206
1265
|
}
|
|
1207
1266
|
}
|
|
1208
1267
|
);
|
|
@@ -1245,10 +1304,7 @@ function authController(store, config, bus) {
|
|
|
1245
1304
|
{
|
|
1246
1305
|
status: 202,
|
|
1247
1306
|
headers: {
|
|
1248
|
-
"Set-Cookie":
|
|
1249
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1250
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1251
|
-
].join(", ")
|
|
1307
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1252
1308
|
}
|
|
1253
1309
|
}
|
|
1254
1310
|
);
|
|
@@ -1269,10 +1325,7 @@ function authController(store, config, bus) {
|
|
|
1269
1325
|
{
|
|
1270
1326
|
status: 200,
|
|
1271
1327
|
headers: {
|
|
1272
|
-
"Set-Cookie":
|
|
1273
|
-
"access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0",
|
|
1274
|
-
"refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0"
|
|
1275
|
-
].join(", ")
|
|
1328
|
+
"Set-Cookie": clearedTokenCookies(config)
|
|
1276
1329
|
}
|
|
1277
1330
|
}
|
|
1278
1331
|
);
|
|
@@ -1313,10 +1366,7 @@ function authController(store, config, bus) {
|
|
|
1313
1366
|
{
|
|
1314
1367
|
status: 200,
|
|
1315
1368
|
headers: {
|
|
1316
|
-
"Set-Cookie":
|
|
1317
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1318
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1319
|
-
].join(", ")
|
|
1369
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1320
1370
|
}
|
|
1321
1371
|
}
|
|
1322
1372
|
);
|
|
@@ -1344,12 +1394,9 @@ function authController(store, config, bus) {
|
|
|
1344
1394
|
const remaining = checkCooldown(await passwordReset.findLastSentAt(user.id), cooldown);
|
|
1345
1395
|
if (remaining > 0) {
|
|
1346
1396
|
return (0, import_core4.setApiResponse)(
|
|
1347
|
-
import_core4.HTTP.
|
|
1348
|
-
"
|
|
1349
|
-
"
|
|
1350
|
-
{
|
|
1351
|
-
retryAfter: Math.ceil(remaining / 1e3)
|
|
1352
|
-
}
|
|
1397
|
+
import_core4.HTTP.OK,
|
|
1398
|
+
"PASSWORD_RESET_EMAIL_SENT",
|
|
1399
|
+
"Password reset email sent (if account exists)."
|
|
1353
1400
|
);
|
|
1354
1401
|
}
|
|
1355
1402
|
const pin = (0, import_node_crypto2.randomInt)(1e5, 1e6).toString();
|
|
@@ -1468,10 +1515,7 @@ function authController(store, config, bus) {
|
|
|
1468
1515
|
{
|
|
1469
1516
|
status: 200,
|
|
1470
1517
|
headers: {
|
|
1471
|
-
"Set-Cookie":
|
|
1472
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1473
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1474
|
-
].join(", ")
|
|
1518
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1475
1519
|
}
|
|
1476
1520
|
}
|
|
1477
1521
|
);
|
|
@@ -1588,7 +1632,7 @@ function normalizePhone2(phone2) {
|
|
|
1588
1632
|
function isValidPhone2(phone2) {
|
|
1589
1633
|
return typeof phone2 === "string" && /^\+?[1-9]\d{6,14}$/.test(normalizePhone2(phone2));
|
|
1590
1634
|
}
|
|
1591
|
-
function userController(store, bus) {
|
|
1635
|
+
function userController(store, config, bus) {
|
|
1592
1636
|
const users = new UserModel(store);
|
|
1593
1637
|
const emailVerif = new EmailVerificationModel(store);
|
|
1594
1638
|
const phoneVerif = new PhoneVerificationModel(store);
|
|
@@ -1777,10 +1821,7 @@ function userController(store, bus) {
|
|
|
1777
1821
|
{
|
|
1778
1822
|
status: 200,
|
|
1779
1823
|
headers: {
|
|
1780
|
-
"Set-Cookie":
|
|
1781
|
-
"access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0",
|
|
1782
|
-
"refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0"
|
|
1783
|
-
].join(", ")
|
|
1824
|
+
"Set-Cookie": clearedTokenCookies(config)
|
|
1784
1825
|
}
|
|
1785
1826
|
}
|
|
1786
1827
|
);
|
|
@@ -1884,10 +1925,7 @@ function oauthController(store, config) {
|
|
|
1884
1925
|
{
|
|
1885
1926
|
status: 200,
|
|
1886
1927
|
headers: {
|
|
1887
|
-
"Set-Cookie":
|
|
1888
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1889
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1890
|
-
].join(", ")
|
|
1928
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1891
1929
|
}
|
|
1892
1930
|
}
|
|
1893
1931
|
);
|
|
@@ -1897,19 +1935,22 @@ function oauthController(store, config) {
|
|
|
1897
1935
|
|
|
1898
1936
|
// src/routes.ts
|
|
1899
1937
|
function buildAuthRoutes(store, config, bus) {
|
|
1900
|
-
const user = userController(store, bus);
|
|
1938
|
+
const user = userController(store, config, bus);
|
|
1901
1939
|
const auth = authController(store, config, bus);
|
|
1902
1940
|
const oauth = oauthController(store, config);
|
|
1903
1941
|
const mfa = mfaController(store, config, config.appName ?? "Fonderie", bus);
|
|
1904
1942
|
const verifyGate = config.requireVerification ? import_middlewares2.requireVerified : (_ctx, next) => next();
|
|
1943
|
+
const passthrough = (_ctx, next) => next();
|
|
1944
|
+
const ipLimit = (route) => buildAuthIpLimiter(route, store, config.rateLimit) ?? passthrough;
|
|
1945
|
+
const acctLimit = (route) => buildAuthAccountLimiter(route, store, config.rateLimit) ?? passthrough;
|
|
1905
1946
|
const routes = [
|
|
1906
1947
|
// Registration & Login (Public)
|
|
1907
|
-
["POST", "/auth/register", (0, import_middlewares.validate)(registerSchema), auth.register],
|
|
1908
|
-
["POST", "/auth/login", (0, import_middlewares.validate)(loginSchema), auth.login],
|
|
1948
|
+
["POST", "/auth/register", ipLimit("register"), (0, import_middlewares.validate)(registerSchema), auth.register],
|
|
1949
|
+
["POST", "/auth/login", ipLimit("login"), (0, import_middlewares.validate)(loginSchema), acctLimit("login"), auth.login],
|
|
1909
1950
|
// Token Management (Public)
|
|
1910
1951
|
["POST", "/auth/refresh", (0, import_middlewares.validate)(refreshSchema), auth.refresh],
|
|
1911
1952
|
// Email — Password Recovery (Public)
|
|
1912
|
-
["POST", "/auth/email/forgot", (0, import_middlewares.validate)(forgotPasswordSchema), auth.forgotPassword],
|
|
1953
|
+
["POST", "/auth/email/forgot", ipLimit("forgot"), (0, import_middlewares.validate)(forgotPasswordSchema), acctLimit("forgot"), auth.forgotPassword],
|
|
1913
1954
|
["POST", "/auth/email/reset", (0, import_middlewares.validate)(resetPasswordSchema), auth.resetPassword],
|
|
1914
1955
|
// Verification (Protected — email or phone, determined by loginMethod)
|
|
1915
1956
|
["POST", "/auth/verify", import_middlewares2.requireAuth, (0, import_middlewares.validate)(verifySchema), auth.verify],
|
|
@@ -1929,7 +1970,7 @@ function buildAuthRoutes(store, config, bus) {
|
|
|
1929
1970
|
["POST", "/auth/mfa/setup", import_middlewares2.requireAuth, requireEmailLogin, import_middlewares2.requireVerified, mfa.setup],
|
|
1930
1971
|
// /auth/mfa/verify accepts both mfaPending tokens (TOTP/backup-code login)
|
|
1931
1972
|
// and full tokens (setup confirmation), so requireAnyAuth is used here.
|
|
1932
|
-
["POST", "/auth/mfa/verify", import_middlewares2.requireAnyAuth, requireEmailLogin, import_middlewares2.requireVerified, (0, import_middlewares.validate)(mfaTokenSchema), mfa.verify],
|
|
1973
|
+
["POST", "/auth/mfa/verify", ipLimit("mfaVerify"), import_middlewares2.requireAnyAuth, requireEmailLogin, import_middlewares2.requireVerified, (0, import_middlewares.validate)(mfaTokenSchema), mfa.verify],
|
|
1933
1974
|
["POST", "/auth/mfa/disable", import_middlewares2.requireAuth, requireEmailLogin, import_middlewares2.requireVerified, (0, import_middlewares.validate)(mfaTokenSchema), mfa.disable],
|
|
1934
1975
|
[
|
|
1935
1976
|
"POST",
|
|
@@ -2014,6 +2055,8 @@ var import_middlewares3 = require("@fonderie/core/middlewares");
|
|
|
2014
2055
|
AUTH_CONFIG_KEYS,
|
|
2015
2056
|
AuthModule,
|
|
2016
2057
|
MESSAGE_KEYS,
|
|
2058
|
+
buildAuthAccountLimiter,
|
|
2059
|
+
buildAuthIpLimiter,
|
|
2017
2060
|
normalizeEmail,
|
|
2018
2061
|
normalizeEmailSafe,
|
|
2019
2062
|
requireAuth,
|