@awesome-lang-auth/node 1.10.1 → 1.10.2

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,21 @@ 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
+ }
149
198
  /**
150
199
  * Resolves the post-OAuth redirect target from the encoded state.
151
200
  * Falls back to `getDefaultSiteUrl` when the state carries no origin or the
@@ -242,9 +291,15 @@ function sendTokens(req, res, tokens, config) {
242
291
  async function issueTokens(req, res, user, config, options, userStore, redirectTo, oldSid) {
243
292
  const payload = buildPayload(user, config);
244
293
  const refreshExpiryMs = parseExpiryMs(config.refreshTokenExpiresIn);
294
+ let createdSession;
245
295
  if (options.sessionStore) {
296
+ const isSingleSession = config.session?.singleSessionPerUser ?? config.session?.singleSession ?? false;
297
+ if (isSingleSession && !oldSid) {
298
+ await options.sessionStore.revokeAllSessionsForUser(user.id, user.tenantId).catch(() => { });
299
+ }
246
300
  const session = await options.sessionStore.createSession({
247
301
  userId: user.id,
302
+ tenantId: user.tenantId,
248
303
  userAgent: req.headers['user-agent'],
249
304
  ipAddress: req.ip || req.socket.remoteAddress,
250
305
  expiresAt: new Date(Date.now() + refreshExpiryMs),
@@ -255,10 +310,24 @@ async function issueTokens(req, res, user, config, options, userStore, redirectT
255
310
  if (oldSid) {
256
311
  await options.sessionStore.revokeSession(oldSid).catch(() => { });
257
312
  }
313
+ // Save session in local variable for updating hash below
314
+ createdSession = session;
258
315
  }
259
316
  const tokens = tokenService.generateTokenPair(payload, config);
260
317
  const refreshExpiry = new Date(Date.now() + refreshExpiryMs);
261
318
  await userStore.updateRefreshToken(user.id, tokens.refreshToken, refreshExpiry);
319
+ if (options.sessionStore && payload.sid) {
320
+ const refreshTokenHash = crypto.createHash('sha256').update(tokens.refreshToken).digest('hex');
321
+ if (createdSession) {
322
+ createdSession.refreshTokenHash = refreshTokenHash;
323
+ if (!createdSession.data)
324
+ createdSession.data = {};
325
+ createdSession.data.refreshTokenHash = refreshTokenHash;
326
+ }
327
+ if (options.sessionStore.updateSessionRefreshTokenHash) {
328
+ await options.sessionStore.updateSessionRefreshTokenHash(payload.sid, refreshTokenHash).catch(() => { });
329
+ }
330
+ }
262
331
  if (redirectTo) {
263
332
  tokenService.setTokenCookies(res, tokens, config);
264
333
  res.redirect(redirectTo);
@@ -472,13 +541,23 @@ function createAuthRouter(userStore, config, options = {}) {
472
541
  if (typeof bodyRefreshToken === 'string' && bodyRefreshToken) {
473
542
  try {
474
543
  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(() => { });
544
+ if (options.sessionStore && payload.sid) {
545
+ const session = await options.sessionStore.getSession(payload.sid);
546
+ if (session) {
547
+ const expectedHash = session.refreshTokenHash ?? session.data?.refreshTokenHash;
548
+ if (!expectedHash || crypto.createHash('sha256').update(bodyRefreshToken).digest('hex') === expectedHash) {
549
+ await options.sessionStore.revokeSession(payload.sid).catch(() => { });
550
+ if (!req.user)
551
+ req.user = payload;
552
+ }
553
+ }
554
+ }
555
+ else {
556
+ const user = await userStore.findById(payload.sub);
557
+ if (user && user.refreshToken === bodyRefreshToken) {
558
+ if (!req.user)
559
+ req.user = payload;
479
560
  }
480
- if (!req.user)
481
- req.user = payload;
482
561
  }
483
562
  }
484
563
  catch {
@@ -491,7 +570,14 @@ function createAuthRouter(userStore, config, options = {}) {
491
570
  const userId = req.user?.sub;
492
571
  const sessionId = req.user?.sid;
493
572
  if (req.user?.sub) {
494
- await userStore.updateRefreshToken(req.user.sub, null, null);
573
+ let hasOtherSessions = false;
574
+ if (options.sessionStore?.getSessionsForUser) {
575
+ const remaining = await options.sessionStore.getSessionsForUser(req.user.sub).catch(() => []);
576
+ hasOtherSessions = remaining.length > 0;
577
+ }
578
+ if (!hasOtherSessions) {
579
+ await userStore.updateRefreshToken(req.user.sub, null, null);
580
+ }
495
581
  }
496
582
  tokenService.clearTokenCookies(res, config);
497
583
  (0, router_events_1.publishRequestEvent)(eventBus, auth_event_names_1.AuthEventNames.AUTH_LOGOUT, req, {
@@ -520,18 +606,40 @@ function createAuthRouter(userStore, config, options = {}) {
520
606
  const payload = tokenService.verifyRefreshToken(refreshToken, config);
521
607
  // Real-time Session Validation
522
608
  const checkOn = config.session?.checkOn ?? 'refresh';
523
- if (options.sessionStore && payload.sid && checkOn !== 'none') {
524
- const session = await options.sessionStore.getSession(payload.sid);
609
+ let session = null;
610
+ if (options.sessionStore && payload.sid) {
611
+ if (checkOn !== 'none') {
612
+ session = await options.sessionStore.getSession(payload.sid);
613
+ if (!session) {
614
+ res.status(401).json({ error: 'Session has been revoked', code: 'SESSION_REVOKED' });
615
+ return;
616
+ }
617
+ }
525
618
  if (!session) {
526
- res.status(401).json({ error: 'Session has been revoked', code: 'SESSION_REVOKED' });
527
- return;
619
+ session = await options.sessionStore.getSession(payload.sid);
620
+ }
621
+ // Per-session refresh token validation (multi-device support)
622
+ const expectedHash = session?.refreshTokenHash ?? session?.data?.refreshTokenHash;
623
+ if (expectedHash) {
624
+ const presentedHash = crypto.createHash('sha256').update(refreshToken).digest('hex');
625
+ if (presentedHash !== expectedHash) {
626
+ res.status(401).json({ error: 'Invalid refresh token' });
627
+ return;
628
+ }
528
629
  }
529
630
  }
530
631
  const user = await userStore.findById(payload.sub);
531
- if (!user || user.refreshToken !== refreshToken) {
632
+ if (!user) {
532
633
  res.status(401).json({ error: 'Invalid refresh token' });
533
634
  return;
534
635
  }
636
+ // If no session store is used or payload has no sid, validate against user.refreshToken
637
+ if (!options.sessionStore || !payload.sid) {
638
+ if (user.refreshToken !== refreshToken) {
639
+ res.status(401).json({ error: 'Invalid refresh token' });
640
+ return;
641
+ }
642
+ }
535
643
  const { sessionId } = await issueTokens(req, res, user, config, options, userStore, /* redirectTo */ undefined, payload.sid);
536
644
  (0, router_events_1.publishRequestEvent)(eventBus, auth_event_names_1.AuthEventNames.SESSION_ROTATED, req, {
537
645
  userId: user.id,
@@ -1243,12 +1351,29 @@ function createAuthRouter(userStore, config, options = {}) {
1243
1351
  const resolved = resolveSiteUrl(req, config, allowedOrigins);
1244
1352
  const returnPath = typeof req.query['return_path'] === 'string' ? req.query['return_path'] : undefined;
1245
1353
  const state = resolved ? encodeOAuthState(nonce, resolved, returnPath) : nonce;
1354
+ res.cookie('oauth_nonce_google', nonce, {
1355
+ httpOnly: true,
1356
+ sameSite: 'lax',
1357
+ secure: config.cookieOptions?.secure ?? (req.secure || req.headers['x-forwarded-proto'] === 'https'),
1358
+ maxAge: 10 * 60 * 1000,
1359
+ path: '/',
1360
+ });
1246
1361
  const url = googleStrategy.getAuthorizationUrl(state);
1247
1362
  res.redirect(url);
1248
1363
  });
1249
1364
  router.get('/oauth/google/callback', ...rl, async (req, res) => {
1250
1365
  try {
1251
1366
  const { code, state } = req.query;
1367
+ const cookieNonce = req.cookies?.['oauth_nonce_google']
1368
+ ?? tokenService.extractTokenFromCookie(req, 'oauth_nonce_google');
1369
+ res.clearCookie('oauth_nonce_google', { path: '/' });
1370
+ const stateNonce = decodeOAuthStateNonce(state);
1371
+ if (cookieNonce || stateNonce) {
1372
+ if (!cookieNonce || !stateNonce || cookieNonce !== stateNonce) {
1373
+ res.status(400).json({ error: 'Invalid or missing OAuth state nonce', code: 'INVALID_OAUTH_STATE' });
1374
+ return;
1375
+ }
1376
+ }
1252
1377
  const redirectTo = resolveOAuthRedirect(state, config, allowedOrigins);
1253
1378
  const user = await googleStrategy.handleCallback(code, state);
1254
1379
  // Link the account if linkedAccountsStore is configured
@@ -1268,14 +1393,17 @@ function createAuthRouter(userStore, config, options = {}) {
1268
1393
  (0, router_events_1.publishRequestEvent)(eventBus, auth_event_names_1.AuthEventNames.AUTH_OAUTH_CONFLICT, req, {
1269
1394
  data: (0, router_events_1.oauthConflictEventData)('google', err.data),
1270
1395
  });
1271
- const siteUrl = resolveOAuthRedirect(req.query.state, config, allowedOrigins);
1396
+ const rawOrigin = decodeOAuthStateOrigin(req.query.state);
1397
+ const conflictOrigin = (rawOrigin && (allowedOrigins.length === 0 || allowedOrigins.includes(rawOrigin)))
1398
+ ? rawOrigin
1399
+ : getDefaultSiteUrl(config);
1272
1400
  const { email, providerAccountId } = (err.data ?? {});
1273
1401
  if (options.pendingLinkStore && email && providerAccountId) {
1274
1402
  await options.pendingLinkStore.stash(email, 'google', providerAccountId).catch((e) => { console.error('[node-auth] pendingLinkStore.stash error (google):', e); });
1275
1403
  }
1276
1404
  const emailParam = email ? `&email=${encodeURIComponent(email)}` : '';
1277
1405
  const paramStr = `?provider=google&code=OAUTH_ACCOUNT_CONFLICT${emailParam}`;
1278
- res.redirect(buildUiLink(siteUrl, `/account-conflict${paramStr}`, config, options));
1406
+ res.redirect(buildUiLink(conflictOrigin, `/account-conflict${paramStr}`, config, options));
1279
1407
  return;
1280
1408
  }
1281
1409
  handleError(res, err);
@@ -1294,12 +1422,29 @@ function createAuthRouter(userStore, config, options = {}) {
1294
1422
  const resolved = resolveSiteUrl(req, config, allowedOrigins);
1295
1423
  const returnPath = typeof req.query['return_path'] === 'string' ? req.query['return_path'] : undefined;
1296
1424
  const state = resolved ? encodeOAuthState(nonce, resolved, returnPath) : nonce;
1425
+ res.cookie('oauth_nonce_github', nonce, {
1426
+ httpOnly: true,
1427
+ sameSite: 'lax',
1428
+ secure: config.cookieOptions?.secure ?? (req.secure || req.headers['x-forwarded-proto'] === 'https'),
1429
+ maxAge: 10 * 60 * 1000,
1430
+ path: '/',
1431
+ });
1297
1432
  const url = githubStrategy.getAuthorizationUrl(state);
1298
1433
  res.redirect(url);
1299
1434
  });
1300
1435
  router.get('/oauth/github/callback', ...rl, async (req, res) => {
1301
1436
  try {
1302
1437
  const { code, state } = req.query;
1438
+ const cookieNonce = req.cookies?.['oauth_nonce_github']
1439
+ ?? tokenService.extractTokenFromCookie(req, 'oauth_nonce_github');
1440
+ res.clearCookie('oauth_nonce_github', { path: '/' });
1441
+ const stateNonce = decodeOAuthStateNonce(state);
1442
+ if (cookieNonce || stateNonce) {
1443
+ if (!cookieNonce || !stateNonce || cookieNonce !== stateNonce) {
1444
+ res.status(400).json({ error: 'Invalid or missing OAuth state nonce', code: 'INVALID_OAUTH_STATE' });
1445
+ return;
1446
+ }
1447
+ }
1303
1448
  const redirectTo = resolveOAuthRedirect(state, config, allowedOrigins);
1304
1449
  const user = await githubStrategy.handleCallback(code, state);
1305
1450
  // Link the account if linkedAccountsStore is configured
@@ -1319,14 +1464,17 @@ function createAuthRouter(userStore, config, options = {}) {
1319
1464
  (0, router_events_1.publishRequestEvent)(eventBus, auth_event_names_1.AuthEventNames.AUTH_OAUTH_CONFLICT, req, {
1320
1465
  data: (0, router_events_1.oauthConflictEventData)('github', err.data),
1321
1466
  });
1322
- const siteUrl = resolveOAuthRedirect(req.query.state, config, allowedOrigins);
1467
+ const rawOrigin = decodeOAuthStateOrigin(req.query.state);
1468
+ const conflictOrigin = (rawOrigin && (allowedOrigins.length === 0 || allowedOrigins.includes(rawOrigin)))
1469
+ ? rawOrigin
1470
+ : getDefaultSiteUrl(config);
1323
1471
  const { email, providerAccountId } = (err.data ?? {});
1324
1472
  if (options.pendingLinkStore && email && providerAccountId) {
1325
1473
  await options.pendingLinkStore.stash(email, 'github', providerAccountId).catch((e) => { console.error('[node-auth] pendingLinkStore.stash error (github):', e); });
1326
1474
  }
1327
1475
  const emailParam = email ? `&email=${encodeURIComponent(email)}` : '';
1328
1476
  const paramStr = `?provider=github&code=OAUTH_ACCOUNT_CONFLICT${emailParam}`;
1329
- res.redirect(buildUiLink(siteUrl, `/account-conflict${paramStr}`, config, options));
1477
+ res.redirect(buildUiLink(conflictOrigin, `/account-conflict${paramStr}`, config, options));
1330
1478
  return;
1331
1479
  }
1332
1480
  handleError(res, err);
@@ -1346,11 +1494,27 @@ function createAuthRouter(userStore, config, options = {}) {
1346
1494
  const resolved = resolveSiteUrl(req, config, allowedOrigins);
1347
1495
  const returnPath = typeof req.query['return_path'] === 'string' ? req.query['return_path'] : undefined;
1348
1496
  const state = resolved ? encodeOAuthState(nonce, resolved, returnPath) : nonce;
1497
+ res.cookie(`oauth_nonce_${s.name}`, nonce, {
1498
+ httpOnly: true,
1499
+ sameSite: 'lax',
1500
+ secure: config.cookieOptions?.secure ?? (req.secure || req.headers['x-forwarded-proto'] === 'https'),
1501
+ maxAge: 10 * 60 * 1000,
1502
+ path: '/',
1503
+ });
1349
1504
  res.redirect(s.getAuthorizationUrl(state));
1350
1505
  });
1351
1506
  router.get(`/oauth/${s.name}/callback`, ...rl, async (req, res) => {
1352
1507
  try {
1353
1508
  const { code, state } = req.query;
1509
+ const cookieName = `oauth_nonce_${s.name}`;
1510
+ const cookieNonce = req.cookies?.[cookieName]
1511
+ ?? tokenService.extractTokenFromCookie(req, cookieName);
1512
+ res.clearCookie(cookieName, { path: '/' });
1513
+ const stateNonce = decodeOAuthStateNonce(state);
1514
+ if (cookieNonce && (!stateNonce || cookieNonce !== stateNonce)) {
1515
+ res.status(400).json({ error: 'Invalid or missing OAuth state nonce', code: 'INVALID_OAUTH_STATE' });
1516
+ return;
1517
+ }
1354
1518
  const redirectTo = resolveOAuthRedirect(state, config, allowedOrigins);
1355
1519
  const user = await s.handleCallback(code, state);
1356
1520
  // Link the account if linkedAccountsStore is configured
@@ -1370,14 +1534,17 @@ function createAuthRouter(userStore, config, options = {}) {
1370
1534
  (0, router_events_1.publishRequestEvent)(eventBus, auth_event_names_1.AuthEventNames.AUTH_OAUTH_CONFLICT, req, {
1371
1535
  data: (0, router_events_1.oauthConflictEventData)(s.name, err.data),
1372
1536
  });
1373
- const siteUrl = resolveOAuthRedirect(req.query.state, config, allowedOrigins);
1537
+ const rawOrigin = decodeOAuthStateOrigin(req.query.state);
1538
+ const conflictOrigin = (rawOrigin && (allowedOrigins.length === 0 || allowedOrigins.includes(rawOrigin)))
1539
+ ? rawOrigin
1540
+ : getDefaultSiteUrl(config);
1374
1541
  const { email, providerAccountId } = (err.data ?? {});
1375
1542
  if (options.pendingLinkStore && email && providerAccountId) {
1376
1543
  await options.pendingLinkStore.stash(email, s.name, providerAccountId).catch((e) => { console.error(`[node-auth] pendingLinkStore.stash error (${s.name}):`, e); });
1377
1544
  }
1378
1545
  const emailParam = email ? `&email=${encodeURIComponent(email)}` : '';
1379
1546
  const paramStr = `?provider=${encodeURIComponent(s.name)}&code=OAUTH_ACCOUNT_CONFLICT${emailParam}`;
1380
- res.redirect(buildUiLink(siteUrl, `/account-conflict${paramStr}`, config, options));
1547
+ res.redirect(buildUiLink(conflictOrigin, `/account-conflict${paramStr}`, config, options));
1381
1548
  return;
1382
1549
  }
1383
1550
  handleError(res, err);