@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.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-BGjjwz5_.cjs';
6
- export { A as AUTH_CONFIG_KEYS, a as AuthMessageKey, b as IAuthRuntimeConfig, c as IAuthSecrets, M as MESSAGE_KEYS, w as withSession } from './session-BGjjwz5_.cjs';
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-BGjjwz5_.js';
6
- export { A as AUTH_CONFIG_KEYS, a as AuthMessageKey, b as IAuthRuntimeConfig, c as IAuthSecrets, M as MESSAGE_KEYS, w as withSession } from './session-BGjjwz5_.js';
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: "24h" }
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,