@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.d.cts
CHANGED
|
@@ -2,11 +2,12 @@ export { IMfaChallenge, ISession, IUser } from './types.cjs';
|
|
|
2
2
|
import { IFonderieModule, IFonderieApp } from '@fonderie/core';
|
|
3
3
|
import { IStoreAdapter } from '@fonderie/store';
|
|
4
4
|
import { EventBus } from '@fonderie/events';
|
|
5
|
-
import { I as IAuthConfig } from './session-
|
|
6
|
-
export { A as AUTH_CONFIG_KEYS, a as
|
|
5
|
+
import { I as IAuthConfig } from './session-C54ksYE8.cjs';
|
|
6
|
+
export { A as AUTH_CONFIG_KEYS, a as AuthLimitedRoute, b as AuthMessageKey, c as IAuthRateLimitConfig, d as IAuthRuntimeConfig, e as IAuthSecrets, M as MESSAGE_KEYS, f as buildAuthAccountLimiter, g as buildAuthIpLimiter, w as withSession } from './session-C54ksYE8.cjs';
|
|
7
7
|
export { IUserDTO, toUserDTO } from './dtos/user.cjs';
|
|
8
8
|
export { requireAuth, validate } from '@fonderie/core/middlewares';
|
|
9
9
|
import { z } from 'zod';
|
|
10
|
+
import '@fonderie/rate-limit';
|
|
10
11
|
|
|
11
12
|
declare class AuthModule implements IFonderieModule {
|
|
12
13
|
private store;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,11 +2,12 @@ export { IMfaChallenge, ISession, IUser } from './types.js';
|
|
|
2
2
|
import { IFonderieModule, IFonderieApp } from '@fonderie/core';
|
|
3
3
|
import { IStoreAdapter } from '@fonderie/store';
|
|
4
4
|
import { EventBus } from '@fonderie/events';
|
|
5
|
-
import { I as IAuthConfig } from './session-
|
|
6
|
-
export { A as AUTH_CONFIG_KEYS, a as
|
|
5
|
+
import { I as IAuthConfig } from './session-C54ksYE8.js';
|
|
6
|
+
export { A as AUTH_CONFIG_KEYS, a as AuthLimitedRoute, b as AuthMessageKey, c as IAuthRateLimitConfig, d as IAuthRuntimeConfig, e as IAuthSecrets, M as MESSAGE_KEYS, f as buildAuthAccountLimiter, g as buildAuthIpLimiter, w as withSession } from './session-C54ksYE8.js';
|
|
7
7
|
export { IUserDTO, toUserDTO } from './dtos/user.js';
|
|
8
8
|
export { requireAuth, validate } from '@fonderie/core/middlewares';
|
|
9
9
|
import { z } from 'zod';
|
|
10
|
+
import '@fonderie/rate-limit';
|
|
10
11
|
|
|
11
12
|
declare class AuthModule implements IFonderieModule {
|
|
12
13
|
private store;
|
package/dist/index.js
CHANGED
|
@@ -48,6 +48,60 @@ var requireEmailLogin = async (ctx, next) => {
|
|
|
48
48
|
// src/middlewares/validate.ts
|
|
49
49
|
import { validate } from "@fonderie/core/middlewares";
|
|
50
50
|
|
|
51
|
+
// src/services/rate-limit.ts
|
|
52
|
+
import {
|
|
53
|
+
byBodyField,
|
|
54
|
+
byIp,
|
|
55
|
+
rateLimit,
|
|
56
|
+
StoreAdapterStore
|
|
57
|
+
} from "@fonderie/rate-limit";
|
|
58
|
+
var min = (n) => n * 60;
|
|
59
|
+
var DEFAULTS = {
|
|
60
|
+
// login: 10/15min per IP AND 5/15min per account (credential stuffing
|
|
61
|
+
// rotates IPs, so the per-account bucket is the one that bites).
|
|
62
|
+
login: {
|
|
63
|
+
ip: { capacity: 10, refillPerSec: 10 / min(15) },
|
|
64
|
+
account: { capacity: 5, refillPerSec: 5 / min(15) },
|
|
65
|
+
accountField: "email"
|
|
66
|
+
},
|
|
67
|
+
// register: 5/hour per IP — signup abuse is IP-shaped.
|
|
68
|
+
register: { ip: { capacity: 5, refillPerSec: 5 / min(60) } },
|
|
69
|
+
// forgot: 5/hour per IP AND 3/hour per account (email-bombing protection).
|
|
70
|
+
forgot: {
|
|
71
|
+
ip: { capacity: 5, refillPerSec: 5 / min(60) },
|
|
72
|
+
account: { capacity: 3, refillPerSec: 3 / min(60) },
|
|
73
|
+
accountField: "email"
|
|
74
|
+
},
|
|
75
|
+
// mfaVerify: 10/15min per IP — TOTP brute-force.
|
|
76
|
+
mfaVerify: { ip: { capacity: 10, refillPerSec: 10 / min(15) } }
|
|
77
|
+
};
|
|
78
|
+
function resolve(route, store, config) {
|
|
79
|
+
if (config === false) return null;
|
|
80
|
+
const override = config?.rules?.[route];
|
|
81
|
+
if (override === false) return null;
|
|
82
|
+
const backing = config?.store ?? new StoreAdapterStore(store);
|
|
83
|
+
const def = DEFAULTS[route];
|
|
84
|
+
return {
|
|
85
|
+
store: backing,
|
|
86
|
+
ipRule: override ?? def.ip,
|
|
87
|
+
...def.account && def.accountField ? { account: { rule: def.account, field: def.accountField } } : {}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function buildAuthIpLimiter(route, store, config) {
|
|
91
|
+
const r = resolve(route, store, config);
|
|
92
|
+
if (!r) return null;
|
|
93
|
+
return rateLimit({ store: r.store, rule: r.ipRule, key: byIp(`auth:${route}`) });
|
|
94
|
+
}
|
|
95
|
+
function buildAuthAccountLimiter(route, store, config) {
|
|
96
|
+
const r = resolve(route, store, config);
|
|
97
|
+
if (!r || !r.account) return null;
|
|
98
|
+
return rateLimit({
|
|
99
|
+
store: r.store,
|
|
100
|
+
rule: r.account.rule,
|
|
101
|
+
key: byBodyField(`auth:${route}`, r.account.field)
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
51
105
|
// src/schemas.ts
|
|
52
106
|
var schemas_exports = {};
|
|
53
107
|
__export(schemas_exports, {
|
|
@@ -113,6 +167,26 @@ var changePasswordSchema = z.object({
|
|
|
113
167
|
});
|
|
114
168
|
var mfaTokenSchema = z.object({ token: z.string().trim().min(6).max(64) });
|
|
115
169
|
|
|
170
|
+
// src/services/cookies.ts
|
|
171
|
+
function secureAttr(config) {
|
|
172
|
+
const secure = config.secureCookies ?? process.env["NODE_ENV"] === "production";
|
|
173
|
+
return secure ? "; Secure" : "";
|
|
174
|
+
}
|
|
175
|
+
function tokenPairCookies(accessToken, refreshToken, config) {
|
|
176
|
+
const secure = secureAttr(config);
|
|
177
|
+
return [
|
|
178
|
+
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/${secure}`,
|
|
179
|
+
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh${secure}`
|
|
180
|
+
].join(", ");
|
|
181
|
+
}
|
|
182
|
+
function clearedTokenCookies(config) {
|
|
183
|
+
const secure = secureAttr(config);
|
|
184
|
+
return [
|
|
185
|
+
`access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0${secure}`,
|
|
186
|
+
`refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0${secure}`
|
|
187
|
+
].join(", ");
|
|
188
|
+
}
|
|
189
|
+
|
|
116
190
|
// src/controllers/mfa.controller.ts
|
|
117
191
|
import QRCode from "qrcode";
|
|
118
192
|
import { setApiResponse as setApiResponse2, HTTP as HTTP2 } from "@fonderie/core";
|
|
@@ -697,10 +771,7 @@ function mfaController(store, config, issuer, bus) {
|
|
|
697
771
|
{
|
|
698
772
|
status: 200,
|
|
699
773
|
headers: {
|
|
700
|
-
"Set-Cookie":
|
|
701
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
702
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
703
|
-
].join(", ")
|
|
774
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
704
775
|
}
|
|
705
776
|
}
|
|
706
777
|
);
|
|
@@ -1034,10 +1105,7 @@ function authController(store, config, bus) {
|
|
|
1034
1105
|
{
|
|
1035
1106
|
status: 201,
|
|
1036
1107
|
headers: {
|
|
1037
|
-
"Set-Cookie":
|
|
1038
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1039
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1040
|
-
].join(", ")
|
|
1108
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1041
1109
|
}
|
|
1042
1110
|
}
|
|
1043
1111
|
);
|
|
@@ -1099,10 +1167,7 @@ function authController(store, config, bus) {
|
|
|
1099
1167
|
{
|
|
1100
1168
|
status: 202,
|
|
1101
1169
|
headers: {
|
|
1102
|
-
"Set-Cookie":
|
|
1103
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1104
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1105
|
-
].join(", ")
|
|
1170
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1106
1171
|
}
|
|
1107
1172
|
}
|
|
1108
1173
|
);
|
|
@@ -1161,10 +1226,7 @@ function authController(store, config, bus) {
|
|
|
1161
1226
|
{
|
|
1162
1227
|
status: 200,
|
|
1163
1228
|
headers: {
|
|
1164
|
-
"Set-Cookie":
|
|
1165
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1166
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1167
|
-
].join(", ")
|
|
1229
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1168
1230
|
}
|
|
1169
1231
|
}
|
|
1170
1232
|
);
|
|
@@ -1207,10 +1269,7 @@ function authController(store, config, bus) {
|
|
|
1207
1269
|
{
|
|
1208
1270
|
status: 202,
|
|
1209
1271
|
headers: {
|
|
1210
|
-
"Set-Cookie":
|
|
1211
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1212
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1213
|
-
].join(", ")
|
|
1272
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1214
1273
|
}
|
|
1215
1274
|
}
|
|
1216
1275
|
);
|
|
@@ -1231,10 +1290,7 @@ function authController(store, config, bus) {
|
|
|
1231
1290
|
{
|
|
1232
1291
|
status: 200,
|
|
1233
1292
|
headers: {
|
|
1234
|
-
"Set-Cookie":
|
|
1235
|
-
"access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0",
|
|
1236
|
-
"refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0"
|
|
1237
|
-
].join(", ")
|
|
1293
|
+
"Set-Cookie": clearedTokenCookies(config)
|
|
1238
1294
|
}
|
|
1239
1295
|
}
|
|
1240
1296
|
);
|
|
@@ -1275,10 +1331,7 @@ function authController(store, config, bus) {
|
|
|
1275
1331
|
{
|
|
1276
1332
|
status: 200,
|
|
1277
1333
|
headers: {
|
|
1278
|
-
"Set-Cookie":
|
|
1279
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1280
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1281
|
-
].join(", ")
|
|
1334
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1282
1335
|
}
|
|
1283
1336
|
}
|
|
1284
1337
|
);
|
|
@@ -1306,12 +1359,9 @@ function authController(store, config, bus) {
|
|
|
1306
1359
|
const remaining = checkCooldown(await passwordReset.findLastSentAt(user.id), cooldown);
|
|
1307
1360
|
if (remaining > 0) {
|
|
1308
1361
|
return setApiResponse3(
|
|
1309
|
-
HTTP3.
|
|
1310
|
-
"
|
|
1311
|
-
"
|
|
1312
|
-
{
|
|
1313
|
-
retryAfter: Math.ceil(remaining / 1e3)
|
|
1314
|
-
}
|
|
1362
|
+
HTTP3.OK,
|
|
1363
|
+
"PASSWORD_RESET_EMAIL_SENT",
|
|
1364
|
+
"Password reset email sent (if account exists)."
|
|
1315
1365
|
);
|
|
1316
1366
|
}
|
|
1317
1367
|
const pin = randomInt(1e5, 1e6).toString();
|
|
@@ -1430,10 +1480,7 @@ function authController(store, config, bus) {
|
|
|
1430
1480
|
{
|
|
1431
1481
|
status: 200,
|
|
1432
1482
|
headers: {
|
|
1433
|
-
"Set-Cookie":
|
|
1434
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1435
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1436
|
-
].join(", ")
|
|
1483
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1437
1484
|
}
|
|
1438
1485
|
}
|
|
1439
1486
|
);
|
|
@@ -1550,7 +1597,7 @@ function normalizePhone2(phone2) {
|
|
|
1550
1597
|
function isValidPhone2(phone2) {
|
|
1551
1598
|
return typeof phone2 === "string" && /^\+?[1-9]\d{6,14}$/.test(normalizePhone2(phone2));
|
|
1552
1599
|
}
|
|
1553
|
-
function userController(store, bus) {
|
|
1600
|
+
function userController(store, config, bus) {
|
|
1554
1601
|
const users = new UserModel(store);
|
|
1555
1602
|
const emailVerif = new EmailVerificationModel(store);
|
|
1556
1603
|
const phoneVerif = new PhoneVerificationModel(store);
|
|
@@ -1739,10 +1786,7 @@ function userController(store, bus) {
|
|
|
1739
1786
|
{
|
|
1740
1787
|
status: 200,
|
|
1741
1788
|
headers: {
|
|
1742
|
-
"Set-Cookie":
|
|
1743
|
-
"access_token=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0",
|
|
1744
|
-
"refresh_token=; HttpOnly; SameSite=Strict; Path=/auth/refresh; Max-Age=0"
|
|
1745
|
-
].join(", ")
|
|
1789
|
+
"Set-Cookie": clearedTokenCookies(config)
|
|
1746
1790
|
}
|
|
1747
1791
|
}
|
|
1748
1792
|
);
|
|
@@ -1846,10 +1890,7 @@ function oauthController(store, config) {
|
|
|
1846
1890
|
{
|
|
1847
1891
|
status: 200,
|
|
1848
1892
|
headers: {
|
|
1849
|
-
"Set-Cookie":
|
|
1850
|
-
`access_token=${accessToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
1851
|
-
`refresh_token=${refreshToken}; HttpOnly; SameSite=Strict; Path=/auth/refresh`
|
|
1852
|
-
].join(", ")
|
|
1893
|
+
"Set-Cookie": tokenPairCookies(accessToken, refreshToken, config)
|
|
1853
1894
|
}
|
|
1854
1895
|
}
|
|
1855
1896
|
);
|
|
@@ -1859,19 +1900,22 @@ function oauthController(store, config) {
|
|
|
1859
1900
|
|
|
1860
1901
|
// src/routes.ts
|
|
1861
1902
|
function buildAuthRoutes(store, config, bus) {
|
|
1862
|
-
const user = userController(store, bus);
|
|
1903
|
+
const user = userController(store, config, bus);
|
|
1863
1904
|
const auth = authController(store, config, bus);
|
|
1864
1905
|
const oauth = oauthController(store, config);
|
|
1865
1906
|
const mfa = mfaController(store, config, config.appName ?? "Fonderie", bus);
|
|
1866
1907
|
const verifyGate = config.requireVerification ? requireVerified : (_ctx, next) => next();
|
|
1908
|
+
const passthrough = (_ctx, next) => next();
|
|
1909
|
+
const ipLimit = (route) => buildAuthIpLimiter(route, store, config.rateLimit) ?? passthrough;
|
|
1910
|
+
const acctLimit = (route) => buildAuthAccountLimiter(route, store, config.rateLimit) ?? passthrough;
|
|
1867
1911
|
const routes = [
|
|
1868
1912
|
// Registration & Login (Public)
|
|
1869
|
-
["POST", "/auth/register", validate(registerSchema), auth.register],
|
|
1870
|
-
["POST", "/auth/login", validate(loginSchema), auth.login],
|
|
1913
|
+
["POST", "/auth/register", ipLimit("register"), validate(registerSchema), auth.register],
|
|
1914
|
+
["POST", "/auth/login", ipLimit("login"), validate(loginSchema), acctLimit("login"), auth.login],
|
|
1871
1915
|
// Token Management (Public)
|
|
1872
1916
|
["POST", "/auth/refresh", validate(refreshSchema), auth.refresh],
|
|
1873
1917
|
// Email — Password Recovery (Public)
|
|
1874
|
-
["POST", "/auth/email/forgot", validate(forgotPasswordSchema), auth.forgotPassword],
|
|
1918
|
+
["POST", "/auth/email/forgot", ipLimit("forgot"), validate(forgotPasswordSchema), acctLimit("forgot"), auth.forgotPassword],
|
|
1875
1919
|
["POST", "/auth/email/reset", validate(resetPasswordSchema), auth.resetPassword],
|
|
1876
1920
|
// Verification (Protected — email or phone, determined by loginMethod)
|
|
1877
1921
|
["POST", "/auth/verify", requireAuth, validate(verifySchema), auth.verify],
|
|
@@ -1891,7 +1935,7 @@ function buildAuthRoutes(store, config, bus) {
|
|
|
1891
1935
|
["POST", "/auth/mfa/setup", requireAuth, requireEmailLogin, requireVerified, mfa.setup],
|
|
1892
1936
|
// /auth/mfa/verify accepts both mfaPending tokens (TOTP/backup-code login)
|
|
1893
1937
|
// and full tokens (setup confirmation), so requireAnyAuth is used here.
|
|
1894
|
-
["POST", "/auth/mfa/verify", requireAnyAuth, requireEmailLogin, requireVerified, validate(mfaTokenSchema), mfa.verify],
|
|
1938
|
+
["POST", "/auth/mfa/verify", ipLimit("mfaVerify"), requireAnyAuth, requireEmailLogin, requireVerified, validate(mfaTokenSchema), mfa.verify],
|
|
1895
1939
|
["POST", "/auth/mfa/disable", requireAuth, requireEmailLogin, requireVerified, validate(mfaTokenSchema), mfa.disable],
|
|
1896
1940
|
[
|
|
1897
1941
|
"POST",
|
|
@@ -1975,6 +2019,8 @@ export {
|
|
|
1975
2019
|
AUTH_CONFIG_KEYS,
|
|
1976
2020
|
AuthModule,
|
|
1977
2021
|
MESSAGE_KEYS,
|
|
2022
|
+
buildAuthAccountLimiter,
|
|
2023
|
+
buildAuthIpLimiter,
|
|
1978
2024
|
normalizeEmail,
|
|
1979
2025
|
normalizeEmailSafe,
|
|
1980
2026
|
requireAuth2 as requireAuth,
|