@awesome-lang-auth/node 1.10.1 → 1.10.3

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.
@@ -1,9 +1,43 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.resolveApiPrefix = resolveApiPrefix;
4
37
  exports.buildUiLink = buildUiLink;
5
38
  exports.createAuthRouter = createAuthRouter;
6
39
  const express_1 = require("express");
40
+ const crypto = __importStar(require("crypto"));
7
41
  const token_service_1 = require("../services/token.service");
8
42
  const password_service_1 = require("../services/password.service");
9
43
  const mailer_service_1 = require("../services/mailer.service");
@@ -146,6 +180,55 @@ function decodeOAuthStateOrigin(state) {
146
180
  catch { /* plain nonce or invalid base64 — ignore */ }
147
181
  return null;
148
182
  }
183
+ /**
184
+ * Extracts the random nonce from a structured or plain OAuth state parameter.
185
+ * Returns `null` when state is undefined or empty.
186
+ */
187
+ function decodeOAuthStateNonce(state) {
188
+ if (!state)
189
+ return null;
190
+ try {
191
+ const parsed = JSON.parse(Buffer.from(state, 'base64url').toString());
192
+ if (isOAuthState(parsed))
193
+ return parsed.n;
194
+ }
195
+ catch { /* plain nonce or invalid base64 */ }
196
+ return typeof state === 'string' && state.trim() ? state.trim() : null;
197
+ }
198
+ function oauthCallbackPath(authUrl, fallback) {
199
+ try {
200
+ const r = new URL(authUrl).searchParams.get('redirect_uri');
201
+ if (r)
202
+ return new URL(r).pathname;
203
+ }
204
+ catch { /* custom URL */ }
205
+ return fallback;
206
+ }
207
+ function oauthStateCookieOpts(path, config, req) {
208
+ return {
209
+ httpOnly: true,
210
+ sameSite: 'lax',
211
+ secure: config.cookieOptions?.secure ?? (req.secure || req.headers['x-forwarded-proto'] === 'https'),
212
+ path,
213
+ };
214
+ }
215
+ function verifyOAuthState(req, res, cookieName, path, config) {
216
+ const cookieVal = req.cookies?.[cookieName]
217
+ ?? req.cookies?.['oauth_state']
218
+ ?? tokenService.extractTokenFromCookie(req, cookieName)
219
+ ?? tokenService.extractTokenFromCookie(req, 'oauth_state');
220
+ res.clearCookie(cookieName, oauthStateCookieOpts(path, config, req));
221
+ res.clearCookie(cookieName, { path: '/' });
222
+ res.clearCookie('oauth_state', oauthStateCookieOpts(path, config, req));
223
+ res.clearCookie('oauth_state', { path: '/' });
224
+ const rawState = req.query['state'];
225
+ const stateNonce = decodeOAuthStateNonce(typeof rawState === 'string' ? rawState : undefined);
226
+ const a = Buffer.from(cookieVal ?? '');
227
+ const b = Buffer.from(stateNonce ?? '');
228
+ if (!cookieVal || !stateNonce || a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
229
+ throw new errors_1.AuthError('Invalid OAuth state', 'INVALID_OAUTH_STATE', 400);
230
+ }
231
+ }
149
232
  /**
150
233
  * Resolves the post-OAuth redirect target from the encoded state.
151
234
  * Falls back to `getDefaultSiteUrl` when the state carries no origin or the
@@ -242,23 +325,47 @@ function sendTokens(req, res, tokens, config) {
242
325
  async function issueTokens(req, res, user, config, options, userStore, redirectTo, oldSid) {
243
326
  const payload = buildPayload(user, config);
244
327
  const refreshExpiryMs = parseExpiryMs(config.refreshTokenExpiresIn);
328
+ let createdSession;
245
329
  if (options.sessionStore) {
330
+ const isSingleSession = config.session?.singleSessionPerUser ?? config.session?.singleSession ?? false;
331
+ if (isSingleSession && !oldSid) {
332
+ await options.sessionStore.revokeAllSessionsForUser(user.id, user.tenantId).catch(() => { });
333
+ }
334
+ // Revoke the previous session before creating new one (token rotation)
335
+ // Store failures during /refresh must reject (500) so old tokens cannot be replayed
336
+ if (oldSid) {
337
+ await options.sessionStore.revokeSession(oldSid);
338
+ }
246
339
  const session = await options.sessionStore.createSession({
247
340
  userId: user.id,
341
+ tenantId: user.tenantId,
248
342
  userAgent: req.headers['user-agent'],
249
343
  ipAddress: req.ip || req.socket.remoteAddress,
250
344
  expiresAt: new Date(Date.now() + refreshExpiryMs),
251
345
  createdAt: new Date(),
252
346
  });
253
347
  payload.sid = session.sessionHandle;
254
- // Revoke the previous session now that the new one is live (token rotation)
255
- if (oldSid) {
256
- await options.sessionStore.revokeSession(oldSid).catch(() => { });
257
- }
348
+ // Save session in local variable for updating hash below
349
+ createdSession = session;
258
350
  }
259
351
  const tokens = tokenService.generateTokenPair(payload, config);
260
352
  const refreshExpiry = new Date(Date.now() + refreshExpiryMs);
261
- await userStore.updateRefreshToken(user.id, tokens.refreshToken, refreshExpiry);
353
+ // Only write refresh token to user record when no session store is configured (single-device fallback)
354
+ if (!options.sessionStore) {
355
+ await userStore.updateRefreshToken(user.id, tokens.refreshToken, refreshExpiry);
356
+ }
357
+ if (options.sessionStore && payload.sid) {
358
+ const refreshTokenHash = crypto.createHash('sha256').update(tokens.refreshToken).digest('hex');
359
+ if (createdSession) {
360
+ createdSession.refreshTokenHash = refreshTokenHash;
361
+ if (!createdSession.data)
362
+ createdSession.data = {};
363
+ createdSession.data.refreshTokenHash = refreshTokenHash;
364
+ }
365
+ if (options.sessionStore.updateSessionRefreshTokenHash) {
366
+ await options.sessionStore.updateSessionRefreshTokenHash(payload.sid, refreshTokenHash).catch(() => { });
367
+ }
368
+ }
262
369
  if (redirectTo) {
263
370
  tokenService.setTokenCookies(res, tokens, config);
264
371
  res.redirect(redirectTo);
@@ -270,6 +377,9 @@ async function issueTokens(req, res, user, config, options, userStore, redirectT
270
377
  }
271
378
  function createAuthRouter(userStore, config, options = {}) {
272
379
  const router = (0, express_1.Router)();
380
+ if (options.sessionStore && config.refreshTokenSecret === config.accessTokenSecret) {
381
+ throw new Error('refreshTokenSecret must differ from accessTokenSecret when a sessionStore is configured');
382
+ }
273
383
  // Parameterize refresh token path by default based on apiPrefix
274
384
  config.cookieOptions = {
275
385
  ...config.cookieOptions,
@@ -472,13 +582,23 @@ function createAuthRouter(userStore, config, options = {}) {
472
582
  if (typeof bodyRefreshToken === 'string' && bodyRefreshToken) {
473
583
  try {
474
584
  const payload = tokenService.verifyRefreshToken(bodyRefreshToken, config);
475
- const user = await userStore.findById(payload.sub);
476
- if (user && user.refreshToken === bodyRefreshToken) {
477
- if (options.sessionStore && payload.sid) {
478
- await options.sessionStore.revokeSession(payload.sid).catch(() => { });
585
+ if (options.sessionStore && payload.sid) {
586
+ const session = await options.sessionStore.getSession(payload.sid);
587
+ if (session) {
588
+ const expectedHash = session.refreshTokenHash ?? session.data?.refreshTokenHash;
589
+ if (!expectedHash || crypto.createHash('sha256').update(bodyRefreshToken).digest('hex') === expectedHash) {
590
+ await options.sessionStore.revokeSession(payload.sid).catch(() => { });
591
+ if (!req.user)
592
+ req.user = payload;
593
+ }
594
+ }
595
+ }
596
+ else {
597
+ const user = await userStore.findById(payload.sub);
598
+ if (user && user.refreshToken === bodyRefreshToken) {
599
+ if (!req.user)
600
+ req.user = payload;
479
601
  }
480
- if (!req.user)
481
- req.user = payload;
482
602
  }
483
603
  }
484
604
  catch {
@@ -491,7 +611,7 @@ function createAuthRouter(userStore, config, options = {}) {
491
611
  const userId = req.user?.sub;
492
612
  const sessionId = req.user?.sid;
493
613
  if (req.user?.sub) {
494
- await userStore.updateRefreshToken(req.user.sub, null, null);
614
+ await userStore.updateRefreshToken(req.user.sub, null, null).catch(() => { });
495
615
  }
496
616
  tokenService.clearTokenCookies(res, config);
497
617
  (0, router_events_1.publishRequestEvent)(eventBus, auth_event_names_1.AuthEventNames.AUTH_LOGOUT, req, {
@@ -518,20 +638,39 @@ function createAuthRouter(userStore, config, options = {}) {
518
638
  return;
519
639
  }
520
640
  const payload = tokenService.verifyRefreshToken(refreshToken, config);
521
- // Real-time Session Validation
522
- const checkOn = config.session?.checkOn ?? 'refresh';
523
- if (options.sessionStore && payload.sid && checkOn !== 'none') {
641
+ // Real-time Session Validation (always checked on /refresh, regardless of checkOn)
642
+ if (options.sessionStore && payload.sid) {
524
643
  const session = await options.sessionStore.getSession(payload.sid);
525
644
  if (!session) {
526
645
  res.status(401).json({ error: 'Session has been revoked', code: 'SESSION_REVOKED' });
527
646
  return;
528
647
  }
648
+ if (session.userId !== payload.sub) {
649
+ res.status(401).json({ error: 'Invalid refresh token' });
650
+ return;
651
+ }
652
+ // Per-session refresh token validation (multi-device support)
653
+ const expectedHash = session.refreshTokenHash ?? session.data?.refreshTokenHash;
654
+ if (expectedHash) {
655
+ const presentedHash = crypto.createHash('sha256').update(refreshToken).digest('hex');
656
+ if (presentedHash !== expectedHash) {
657
+ res.status(401).json({ error: 'Invalid refresh token' });
658
+ return;
659
+ }
660
+ }
529
661
  }
530
662
  const user = await userStore.findById(payload.sub);
531
- if (!user || user.refreshToken !== refreshToken) {
663
+ if (!user) {
532
664
  res.status(401).json({ error: 'Invalid refresh token' });
533
665
  return;
534
666
  }
667
+ // If no session store is used or payload has no sid, validate against user.refreshToken
668
+ if (!options.sessionStore || !payload.sid) {
669
+ if (user.refreshToken !== refreshToken) {
670
+ res.status(401).json({ error: 'Invalid refresh token' });
671
+ return;
672
+ }
673
+ }
535
674
  const { sessionId } = await issueTokens(req, res, user, config, options, userStore, /* redirectTo */ undefined, payload.sid);
536
675
  (0, router_events_1.publishRequestEvent)(eventBus, auth_event_names_1.AuthEventNames.SESSION_ROTATED, req, {
537
676
  userId: user.id,
@@ -1244,10 +1383,24 @@ function createAuthRouter(userStore, config, options = {}) {
1244
1383
  const returnPath = typeof req.query['return_path'] === 'string' ? req.query['return_path'] : undefined;
1245
1384
  const state = resolved ? encodeOAuthState(nonce, resolved, returnPath) : nonce;
1246
1385
  const url = googleStrategy.getAuthorizationUrl(state);
1386
+ const callbackPath = oauthCallbackPath(url, `${req.baseUrl}/oauth/google/callback`);
1387
+ res.cookie('oauth_nonce_google', nonce, {
1388
+ ...oauthStateCookieOpts(callbackPath, config, req),
1389
+ maxAge: 10 * 60 * 1000,
1390
+ });
1391
+ res.cookie('oauth_nonce_google', nonce, {
1392
+ httpOnly: true,
1393
+ sameSite: 'lax',
1394
+ secure: config.cookieOptions?.secure ?? (req.secure || req.headers['x-forwarded-proto'] === 'https'),
1395
+ maxAge: 10 * 60 * 1000,
1396
+ path: '/',
1397
+ });
1247
1398
  res.redirect(url);
1248
1399
  });
1249
1400
  router.get('/oauth/google/callback', ...rl, async (req, res) => {
1250
1401
  try {
1402
+ const callbackPath = oauthCallbackPath(googleStrategy.getAuthorizationUrl(''), `${req.baseUrl}${req.path}`);
1403
+ verifyOAuthState(req, res, 'oauth_nonce_google', callbackPath, config);
1251
1404
  const { code, state } = req.query;
1252
1405
  const redirectTo = resolveOAuthRedirect(state, config, allowedOrigins);
1253
1406
  const user = await googleStrategy.handleCallback(code, state);
@@ -1268,14 +1421,17 @@ function createAuthRouter(userStore, config, options = {}) {
1268
1421
  (0, router_events_1.publishRequestEvent)(eventBus, auth_event_names_1.AuthEventNames.AUTH_OAUTH_CONFLICT, req, {
1269
1422
  data: (0, router_events_1.oauthConflictEventData)('google', err.data),
1270
1423
  });
1271
- const siteUrl = resolveOAuthRedirect(req.query.state, config, allowedOrigins);
1424
+ const rawOrigin = decodeOAuthStateOrigin(req.query.state);
1425
+ const conflictOrigin = (rawOrigin && (allowedOrigins.length === 0 || allowedOrigins.includes(rawOrigin)))
1426
+ ? rawOrigin
1427
+ : getDefaultSiteUrl(config);
1272
1428
  const { email, providerAccountId } = (err.data ?? {});
1273
1429
  if (options.pendingLinkStore && email && providerAccountId) {
1274
1430
  await options.pendingLinkStore.stash(email, 'google', providerAccountId).catch((e) => { console.error('[node-auth] pendingLinkStore.stash error (google):', e); });
1275
1431
  }
1276
1432
  const emailParam = email ? `&email=${encodeURIComponent(email)}` : '';
1277
1433
  const paramStr = `?provider=google&code=OAUTH_ACCOUNT_CONFLICT${emailParam}`;
1278
- res.redirect(buildUiLink(siteUrl, `/account-conflict${paramStr}`, config, options));
1434
+ res.redirect(buildUiLink(conflictOrigin, `/account-conflict${paramStr}`, config, options));
1279
1435
  return;
1280
1436
  }
1281
1437
  handleError(res, err);
@@ -1295,10 +1451,24 @@ function createAuthRouter(userStore, config, options = {}) {
1295
1451
  const returnPath = typeof req.query['return_path'] === 'string' ? req.query['return_path'] : undefined;
1296
1452
  const state = resolved ? encodeOAuthState(nonce, resolved, returnPath) : nonce;
1297
1453
  const url = githubStrategy.getAuthorizationUrl(state);
1454
+ const callbackPath = oauthCallbackPath(url, `${req.baseUrl}/oauth/github/callback`);
1455
+ res.cookie('oauth_nonce_github', nonce, {
1456
+ ...oauthStateCookieOpts(callbackPath, config, req),
1457
+ maxAge: 10 * 60 * 1000,
1458
+ });
1459
+ res.cookie('oauth_nonce_github', nonce, {
1460
+ httpOnly: true,
1461
+ sameSite: 'lax',
1462
+ secure: config.cookieOptions?.secure ?? (req.secure || req.headers['x-forwarded-proto'] === 'https'),
1463
+ maxAge: 10 * 60 * 1000,
1464
+ path: '/',
1465
+ });
1298
1466
  res.redirect(url);
1299
1467
  });
1300
1468
  router.get('/oauth/github/callback', ...rl, async (req, res) => {
1301
1469
  try {
1470
+ const callbackPath = oauthCallbackPath(githubStrategy.getAuthorizationUrl(''), `${req.baseUrl}${req.path}`);
1471
+ verifyOAuthState(req, res, 'oauth_nonce_github', callbackPath, config);
1302
1472
  const { code, state } = req.query;
1303
1473
  const redirectTo = resolveOAuthRedirect(state, config, allowedOrigins);
1304
1474
  const user = await githubStrategy.handleCallback(code, state);
@@ -1319,14 +1489,17 @@ function createAuthRouter(userStore, config, options = {}) {
1319
1489
  (0, router_events_1.publishRequestEvent)(eventBus, auth_event_names_1.AuthEventNames.AUTH_OAUTH_CONFLICT, req, {
1320
1490
  data: (0, router_events_1.oauthConflictEventData)('github', err.data),
1321
1491
  });
1322
- const siteUrl = resolveOAuthRedirect(req.query.state, config, allowedOrigins);
1492
+ const rawOrigin = decodeOAuthStateOrigin(req.query.state);
1493
+ const conflictOrigin = (rawOrigin && (allowedOrigins.length === 0 || allowedOrigins.includes(rawOrigin)))
1494
+ ? rawOrigin
1495
+ : getDefaultSiteUrl(config);
1323
1496
  const { email, providerAccountId } = (err.data ?? {});
1324
1497
  if (options.pendingLinkStore && email && providerAccountId) {
1325
1498
  await options.pendingLinkStore.stash(email, 'github', providerAccountId).catch((e) => { console.error('[node-auth] pendingLinkStore.stash error (github):', e); });
1326
1499
  }
1327
1500
  const emailParam = email ? `&email=${encodeURIComponent(email)}` : '';
1328
1501
  const paramStr = `?provider=github&code=OAUTH_ACCOUNT_CONFLICT${emailParam}`;
1329
- res.redirect(buildUiLink(siteUrl, `/account-conflict${paramStr}`, config, options));
1502
+ res.redirect(buildUiLink(conflictOrigin, `/account-conflict${paramStr}`, config, options));
1330
1503
  return;
1331
1504
  }
1332
1505
  handleError(res, err);
@@ -1346,10 +1519,26 @@ function createAuthRouter(userStore, config, options = {}) {
1346
1519
  const resolved = resolveSiteUrl(req, config, allowedOrigins);
1347
1520
  const returnPath = typeof req.query['return_path'] === 'string' ? req.query['return_path'] : undefined;
1348
1521
  const state = resolved ? encodeOAuthState(nonce, resolved, returnPath) : nonce;
1349
- res.redirect(s.getAuthorizationUrl(state));
1522
+ const url = s.getAuthorizationUrl(state);
1523
+ const callbackPath = oauthCallbackPath(url, `${req.baseUrl}/oauth/${s.name}/callback`);
1524
+ res.cookie(`oauth_nonce_${s.name}`, nonce, {
1525
+ ...oauthStateCookieOpts(callbackPath, config, req),
1526
+ maxAge: 10 * 60 * 1000,
1527
+ });
1528
+ res.cookie(`oauth_nonce_${s.name}`, nonce, {
1529
+ httpOnly: true,
1530
+ sameSite: 'lax',
1531
+ secure: config.cookieOptions?.secure ?? (req.secure || req.headers['x-forwarded-proto'] === 'https'),
1532
+ maxAge: 10 * 60 * 1000,
1533
+ path: '/',
1534
+ });
1535
+ res.redirect(url);
1350
1536
  });
1351
1537
  router.get(`/oauth/${s.name}/callback`, ...rl, async (req, res) => {
1352
1538
  try {
1539
+ const cookieName = `oauth_nonce_${s.name}`;
1540
+ const callbackPath = oauthCallbackPath(s.getAuthorizationUrl(''), `${req.baseUrl}${req.path}`);
1541
+ verifyOAuthState(req, res, cookieName, callbackPath, config);
1353
1542
  const { code, state } = req.query;
1354
1543
  const redirectTo = resolveOAuthRedirect(state, config, allowedOrigins);
1355
1544
  const user = await s.handleCallback(code, state);
@@ -1370,14 +1559,17 @@ function createAuthRouter(userStore, config, options = {}) {
1370
1559
  (0, router_events_1.publishRequestEvent)(eventBus, auth_event_names_1.AuthEventNames.AUTH_OAUTH_CONFLICT, req, {
1371
1560
  data: (0, router_events_1.oauthConflictEventData)(s.name, err.data),
1372
1561
  });
1373
- const siteUrl = resolveOAuthRedirect(req.query.state, config, allowedOrigins);
1562
+ const rawOrigin = decodeOAuthStateOrigin(req.query.state);
1563
+ const conflictOrigin = (rawOrigin && (allowedOrigins.length === 0 || allowedOrigins.includes(rawOrigin)))
1564
+ ? rawOrigin
1565
+ : getDefaultSiteUrl(config);
1374
1566
  const { email, providerAccountId } = (err.data ?? {});
1375
1567
  if (options.pendingLinkStore && email && providerAccountId) {
1376
1568
  await options.pendingLinkStore.stash(email, s.name, providerAccountId).catch((e) => { console.error(`[node-auth] pendingLinkStore.stash error (${s.name}):`, e); });
1377
1569
  }
1378
1570
  const emailParam = email ? `&email=${encodeURIComponent(email)}` : '';
1379
1571
  const paramStr = `?provider=${encodeURIComponent(s.name)}&code=OAUTH_ACCOUNT_CONFLICT${emailParam}`;
1380
- res.redirect(buildUiLink(siteUrl, `/account-conflict${paramStr}`, config, options));
1572
+ res.redirect(buildUiLink(conflictOrigin, `/account-conflict${paramStr}`, config, options));
1381
1573
  return;
1382
1574
  }
1383
1575
  handleError(res, err);