@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.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-DbtCnbiG.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-DbtCnbiG.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-DbtCnbiG.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-DbtCnbiG.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, {
|
|
@@ -165,6 +219,7 @@ var EVENT_KEYS = {
|
|
|
165
219
|
};
|
|
166
220
|
|
|
167
221
|
// src/services/jwt.ts
|
|
222
|
+
import { randomUUID } from "crypto";
|
|
168
223
|
import jwt from "jsonwebtoken";
|
|
169
224
|
function issueMfaPendingToken(userId, config, loginMethod) {
|
|
170
225
|
return jwt.sign(
|
|
@@ -181,19 +236,21 @@ function issueMfaPendingToken(userId, config, loginMethod) {
|
|
|
181
236
|
}
|
|
182
237
|
function issueTokenPair(userId, config, options) {
|
|
183
238
|
const duration = config.sessionDuration ?? "7d";
|
|
239
|
+
const accessDuration = config.accessTokenDuration ?? "24h";
|
|
184
240
|
const loginMethod = options.loginMethod;
|
|
185
241
|
const phoneVerified = options.phoneVerified ?? false;
|
|
242
|
+
const sid = randomUUID();
|
|
186
243
|
const accessToken = jwt.sign(
|
|
187
|
-
{ sub: userId, type: "access", loginMethod, phoneVerified },
|
|
244
|
+
{ sub: userId, type: "access", loginMethod, phoneVerified, sid },
|
|
188
245
|
config.jwtSecret,
|
|
189
|
-
{ expiresIn:
|
|
246
|
+
{ expiresIn: accessDuration }
|
|
190
247
|
);
|
|
191
248
|
const refreshToken = jwt.sign(
|
|
192
|
-
{ sub: userId, type: "refresh", loginMethod, phoneVerified },
|
|
249
|
+
{ sub: userId, type: "refresh", loginMethod, phoneVerified, sid },
|
|
193
250
|
config.jwtSecret,
|
|
194
251
|
{ expiresIn: duration }
|
|
195
252
|
);
|
|
196
|
-
return { accessToken, refreshToken };
|
|
253
|
+
return { accessToken, refreshToken, sid };
|
|
197
254
|
}
|
|
198
255
|
function refreshTokenExpiry(token) {
|
|
199
256
|
const decoded = jwt.decode(token);
|
|
@@ -549,12 +606,12 @@ var SessionModel = class {
|
|
|
549
606
|
this.store = store;
|
|
550
607
|
}
|
|
551
608
|
store;
|
|
552
|
-
async create(userId, token, expiresAt) {
|
|
609
|
+
async create(userId, token, expiresAt, sid) {
|
|
553
610
|
await this.store.query(
|
|
554
|
-
`INSERT INTO fonderie_sessions (user_id, token, expires_at)
|
|
555
|
-
VALUES ($1, $2, $3)
|
|
611
|
+
`INSERT INTO fonderie_sessions (user_id, token, expires_at, sid)
|
|
612
|
+
VALUES ($1, $2, $3, $4)
|
|
556
613
|
ON CONFLICT (token) DO NOTHING`,
|
|
557
|
-
[userId, token, expiresAt]
|
|
614
|
+
[userId, token, expiresAt, sid ?? null]
|
|
558
615
|
);
|
|
559
616
|
}
|
|
560
617
|
async delete(token) {
|
|
@@ -567,6 +624,14 @@ var SessionModel = class {
|
|
|
567
624
|
);
|
|
568
625
|
return rows.length > 0;
|
|
569
626
|
}
|
|
627
|
+
// Liveness check for session-bound access tokens (by the sid claim).
|
|
628
|
+
async aliveBySid(sid) {
|
|
629
|
+
const rows = await this.store.query(
|
|
630
|
+
`SELECT id FROM fonderie_sessions WHERE sid = $1 AND expires_at > now()`,
|
|
631
|
+
[sid]
|
|
632
|
+
);
|
|
633
|
+
return rows.length > 0;
|
|
634
|
+
}
|
|
570
635
|
};
|
|
571
636
|
|
|
572
637
|
// src/models/backup-code.model.ts
|
|
@@ -697,10 +762,10 @@ function mfaController(store, config, issuer, bus) {
|
|
|
697
762
|
await users.enableMfa(ctx.user.id);
|
|
698
763
|
}
|
|
699
764
|
}
|
|
700
|
-
const { accessToken, refreshToken } = issueTokenPair(ctx.user.id, config, {
|
|
765
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(ctx.user.id, config, {
|
|
701
766
|
loginMethod: ctx.user.loginMethod
|
|
702
767
|
});
|
|
703
|
-
await sessions.create(ctx.user.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
768
|
+
await sessions.create(ctx.user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
704
769
|
const fullUser = await users.findById(ctx.user.id);
|
|
705
770
|
if (!fullUser) {
|
|
706
771
|
return setApiResponse2(HTTP2.SERVER_ERROR, "SERVER_ERROR", "User not found after MFA verify");
|
|
@@ -1032,10 +1097,10 @@ function authController(store, config, bus) {
|
|
|
1032
1097
|
reqOpts
|
|
1033
1098
|
).catch(() => {
|
|
1034
1099
|
});
|
|
1035
|
-
const { accessToken, refreshToken } = issueTokenPair(user.id, config, {
|
|
1100
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
1036
1101
|
loginMethod: "email"
|
|
1037
1102
|
});
|
|
1038
|
-
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
1103
|
+
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1039
1104
|
const resolvedRegister = { ...config, ...config.resolve?.(ctx) };
|
|
1040
1105
|
const requiresVerification = !!resolvedRegister.requireVerification && !user.emailVerifiedAt;
|
|
1041
1106
|
return Response.json(
|
|
@@ -1097,10 +1162,10 @@ function authController(store, config, bus) {
|
|
|
1097
1162
|
reqOpts2
|
|
1098
1163
|
).catch(() => {
|
|
1099
1164
|
});
|
|
1100
|
-
const { accessToken, refreshToken } = issueTokenPair(user.id, config, {
|
|
1165
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
1101
1166
|
loginMethod: "phone"
|
|
1102
1167
|
});
|
|
1103
|
-
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
1168
|
+
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1104
1169
|
return Response.json(
|
|
1105
1170
|
{
|
|
1106
1171
|
reason: "USER_PHONE_REGISTERED",
|
|
@@ -1153,10 +1218,10 @@ function authController(store, config, bus) {
|
|
|
1153
1218
|
mfaToken
|
|
1154
1219
|
});
|
|
1155
1220
|
}
|
|
1156
|
-
const { accessToken, refreshToken } = issueTokenPair(user.id, config, {
|
|
1221
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
1157
1222
|
loginMethod: "email"
|
|
1158
1223
|
});
|
|
1159
|
-
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
1224
|
+
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1160
1225
|
const resolvedLogin = { ...config, ...config.resolve?.(ctx) };
|
|
1161
1226
|
const requiresVerification = !!resolvedLogin.requireVerification && !user.emailVerifiedAt;
|
|
1162
1227
|
return Response.json(
|
|
@@ -1199,10 +1264,10 @@ function authController(store, config, bus) {
|
|
|
1199
1264
|
recipient: { email: null, phone: normalizePhone(phone2), deviceToken: null }
|
|
1200
1265
|
}).catch(() => {
|
|
1201
1266
|
});
|
|
1202
|
-
const { accessToken, refreshToken } = issueTokenPair(user.id, config, {
|
|
1267
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
1203
1268
|
loginMethod: "phone"
|
|
1204
1269
|
});
|
|
1205
|
-
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
1270
|
+
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1206
1271
|
return Response.json(
|
|
1207
1272
|
{
|
|
1208
1273
|
reason: "USER_PHONE_OTP_SENT",
|
|
@@ -1263,11 +1328,11 @@ function authController(store, config, bus) {
|
|
|
1263
1328
|
return setApiResponse3(HTTP3.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized");
|
|
1264
1329
|
}
|
|
1265
1330
|
await sessions.delete(token);
|
|
1266
|
-
const { accessToken, refreshToken } = issueTokenPair(user.id, config, {
|
|
1331
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(user.id, config, {
|
|
1267
1332
|
loginMethod: payload.loginMethod ?? "email",
|
|
1268
1333
|
phoneVerified: payload.phoneVerified ?? false
|
|
1269
1334
|
});
|
|
1270
|
-
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
1335
|
+
await sessions.create(user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1271
1336
|
return Response.json(
|
|
1272
1337
|
{
|
|
1273
1338
|
reason: "TOKENS_REFRESHED",
|
|
@@ -1398,11 +1463,11 @@ function authController(store, config, bus) {
|
|
|
1398
1463
|
);
|
|
1399
1464
|
}
|
|
1400
1465
|
await phoneVerif.deleteByUser(ctx.user.id);
|
|
1401
|
-
const { accessToken, refreshToken } = issueTokenPair(ctx.user.id, config, {
|
|
1466
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(ctx.user.id, config, {
|
|
1402
1467
|
loginMethod: "phone",
|
|
1403
1468
|
phoneVerified: true
|
|
1404
1469
|
});
|
|
1405
|
-
await sessions.create(ctx.user.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
1470
|
+
await sessions.create(ctx.user.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1406
1471
|
const verifiedUser = await users.findById(ctx.user.id);
|
|
1407
1472
|
if (!verifiedUser) {
|
|
1408
1473
|
return setApiResponse3(HTTP3.NOT_FOUND, "NOT_FOUND", "User not found");
|
|
@@ -1820,10 +1885,10 @@ function oauthController(store, config) {
|
|
|
1820
1885
|
if (!fullUser) {
|
|
1821
1886
|
return setApiResponse5(HTTP5.SERVER_ERROR, "SERVER_ERROR", "OAuth login failed");
|
|
1822
1887
|
}
|
|
1823
|
-
const { accessToken, refreshToken } = issueTokenPair(upserted.id, config, {
|
|
1888
|
+
const { accessToken, refreshToken, sid } = issueTokenPair(upserted.id, config, {
|
|
1824
1889
|
loginMethod: "google"
|
|
1825
1890
|
});
|
|
1826
|
-
await sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken));
|
|
1891
|
+
await sessions.create(upserted.id, refreshToken, refreshTokenExpiry(refreshToken), sid);
|
|
1827
1892
|
return Response.json(
|
|
1828
1893
|
{
|
|
1829
1894
|
reason: "GOOGLE_AUTH_SUCCESS",
|
|
@@ -1851,14 +1916,17 @@ function buildAuthRoutes(store, config, bus) {
|
|
|
1851
1916
|
const oauth = oauthController(store, config);
|
|
1852
1917
|
const mfa = mfaController(store, config, config.appName ?? "Fonderie", bus);
|
|
1853
1918
|
const verifyGate = config.requireVerification ? requireVerified : (_ctx, next) => next();
|
|
1919
|
+
const passthrough = (_ctx, next) => next();
|
|
1920
|
+
const ipLimit = (route) => buildAuthIpLimiter(route, store, config.rateLimit) ?? passthrough;
|
|
1921
|
+
const acctLimit = (route) => buildAuthAccountLimiter(route, store, config.rateLimit) ?? passthrough;
|
|
1854
1922
|
const routes = [
|
|
1855
1923
|
// Registration & Login (Public)
|
|
1856
|
-
["POST", "/auth/register", validate(registerSchema), auth.register],
|
|
1857
|
-
["POST", "/auth/login", validate(loginSchema), auth.login],
|
|
1924
|
+
["POST", "/auth/register", ipLimit("register"), validate(registerSchema), auth.register],
|
|
1925
|
+
["POST", "/auth/login", ipLimit("login"), validate(loginSchema), acctLimit("login"), auth.login],
|
|
1858
1926
|
// Token Management (Public)
|
|
1859
1927
|
["POST", "/auth/refresh", validate(refreshSchema), auth.refresh],
|
|
1860
1928
|
// Email — Password Recovery (Public)
|
|
1861
|
-
["POST", "/auth/email/forgot", validate(forgotPasswordSchema), auth.forgotPassword],
|
|
1929
|
+
["POST", "/auth/email/forgot", ipLimit("forgot"), validate(forgotPasswordSchema), acctLimit("forgot"), auth.forgotPassword],
|
|
1862
1930
|
["POST", "/auth/email/reset", validate(resetPasswordSchema), auth.resetPassword],
|
|
1863
1931
|
// Verification (Protected — email or phone, determined by loginMethod)
|
|
1864
1932
|
["POST", "/auth/verify", requireAuth, validate(verifySchema), auth.verify],
|
|
@@ -1878,7 +1946,7 @@ function buildAuthRoutes(store, config, bus) {
|
|
|
1878
1946
|
["POST", "/auth/mfa/setup", requireAuth, requireEmailLogin, requireVerified, mfa.setup],
|
|
1879
1947
|
// /auth/mfa/verify accepts both mfaPending tokens (TOTP/backup-code login)
|
|
1880
1948
|
// and full tokens (setup confirmation), so requireAnyAuth is used here.
|
|
1881
|
-
["POST", "/auth/mfa/verify", requireAnyAuth, requireEmailLogin, requireVerified, validate(mfaTokenSchema), mfa.verify],
|
|
1949
|
+
["POST", "/auth/mfa/verify", ipLimit("mfaVerify"), requireAnyAuth, requireEmailLogin, requireVerified, validate(mfaTokenSchema), mfa.verify],
|
|
1882
1950
|
["POST", "/auth/mfa/disable", requireAuth, requireEmailLogin, requireVerified, validate(mfaTokenSchema), mfa.disable],
|
|
1883
1951
|
[
|
|
1884
1952
|
"POST",
|
|
@@ -1902,6 +1970,7 @@ function buildAuthRoutes(store, config, bus) {
|
|
|
1902
1970
|
// src/middlewares/session.ts
|
|
1903
1971
|
function withSession(store, config) {
|
|
1904
1972
|
const users = new UserModel(store);
|
|
1973
|
+
const sessions = new SessionModel(store);
|
|
1905
1974
|
return async (ctx, next) => {
|
|
1906
1975
|
const token = extractToken(ctx.request);
|
|
1907
1976
|
if (!token) {
|
|
@@ -1911,6 +1980,9 @@ function withSession(store, config) {
|
|
|
1911
1980
|
if (!payload || payload.type !== "access") {
|
|
1912
1981
|
return next();
|
|
1913
1982
|
}
|
|
1983
|
+
if (payload.sid && !await sessions.aliveBySid(payload.sid)) {
|
|
1984
|
+
return next();
|
|
1985
|
+
}
|
|
1914
1986
|
const user = await users.findById(payload.sub);
|
|
1915
1987
|
if (!user || user.suspended || user.deletedAt) {
|
|
1916
1988
|
return next();
|
|
@@ -1962,6 +2034,8 @@ export {
|
|
|
1962
2034
|
AUTH_CONFIG_KEYS,
|
|
1963
2035
|
AuthModule,
|
|
1964
2036
|
MESSAGE_KEYS,
|
|
2037
|
+
buildAuthAccountLimiter,
|
|
2038
|
+
buildAuthIpLimiter,
|
|
1965
2039
|
normalizeEmail,
|
|
1966
2040
|
normalizeEmailSafe,
|
|
1967
2041
|
requireAuth2 as requireAuth,
|