@igxjs/node-components 1.0.13 → 1.0.15

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/README.md CHANGED
@@ -46,12 +46,18 @@ export const tokenSession = new SessionManager({
46
46
  });
47
47
 
48
48
  // Setup in your app
49
- await session.setup(app, (user) => ({ ...user, displayName: user.email }));
49
+ await session.setup(app);
50
50
 
51
- // Protect routes
52
- app.get('/protected', session.authenticate(), (req, res) => {
51
+ // Protect routes - user data automatically loaded into req.user
52
+ app.get('/protected', session.authenticate(), session.requireUser(), (req, res) => {
53
53
  res.json({ user: req.user });
54
54
  });
55
+
56
+ // SSO callback with user transformation
57
+ app.get('/auth/callback', session.callback((user) => ({
58
+ ...user,
59
+ displayName: user.email
60
+ })));
55
61
  ```
56
62
 
57
63
  [📖 Full SessionManager Documentation](./docs/session-manager.md)
@@ -78,7 +84,7 @@ flexRouter.mount(app, '');
78
84
  import { JwtManager } from '@igxjs/node-components';
79
85
 
80
86
  // Constructor uses UPPERCASE naming with JWT_ prefix
81
- const jwt = new JwtManager({ SESSION_AGE: 64800000 });
87
+ const jwt = new JwtManager({ JWT_EXPIRATION_TIME: 64800 });
82
88
  const SECRET = process.env.JWT_SECRET;
83
89
 
84
90
  // Create token (encrypt method uses camelCase for per-call options)
@@ -127,12 +133,13 @@ Uses traditional server-side session cookies. When a user authenticates via SSO,
127
133
 
128
134
  **Configuration:**
129
135
  - `SESSION_MODE`: `SessionMode.SESSION` (default) - Uses session-based authentication
130
- - `SESSION_AGE`: Session timeout in milliseconds (default: 64800000)
136
+ - `SESSION_AGE`: Session timeout in seconds (default: 64800 = 18 hours)
131
137
  - `REDIS_URL`: Redis connection string for session storage
132
138
 
133
139
  **Auth Methods:**
134
- - `session.authenticate()` - Protect routes with SSO session verification
135
- - `session.verifySession(isDebugging, redirectUrl)` - Explicit session verification method
140
+ - `session.authenticate(errorRedirectUrl)` - Protect routes with SSO session verification
141
+ - `session.verifySession(errorRedirectUrl)` - Explicit session verification method
142
+ - `session.requireUser()` - Middleware to load user data into `req.user` from session store
136
143
  - `session.logout(redirect?, all?)` - Logout current session (or logout all for token mode)
137
144
 
138
145
  ### TOKEN Mode
@@ -148,10 +155,11 @@ Uses JWT bearer tokens instead of session cookies. When a user authenticates via
148
155
  - `JWT_CLOCK_TOLERANCE`: Clock skew tolerance in seconds (default: 30)
149
156
 
150
157
  **Auth Methods:**
151
- - `session.verifyToken(isDebugging, redirectUrl)` - Protect routes with token verification
158
+ - `session.verifyToken(errorRedirectUrl)` - Protect routes with token verification
159
+ - `session.requireUser()` - Middleware to load user data into `req.user` from Redis using JWT token
152
160
  - `session.callback(initUser)` - SSO callback handler for token generation
153
161
  - `session.refresh(initUser)` - Refresh user authentication based on auth mode
154
- - `session.logout(redirect?, all?)` - Logout current or all tokens
162
+ - `session.logout(redirect?, all?)` - Logout current token or all tokens for user
155
163
 
156
164
  **Token Storage (Client-Side):**
157
165
 
@@ -182,10 +190,13 @@ fetch('/api/protected', {
182
190
  | `SSO_SUCCESS_URL` | string | - | Redirect URL after successful login (token mode) |
183
191
  | `SSO_FAILURE_URL` | string | - | Redirect URL after failed login (token mode) |
184
192
  | `SESSION_MODE` | string | `SessionMode.SESSION` | Authentication mode: `SessionMode.SESSION` or `SessionMode.TOKEN` |
185
- | `SESSION_AGE` | number | 64800000 | Session timeout in milliseconds |
193
+ | `SESSION_AGE` | number | 64800 | Session timeout in seconds (default: 64800 = 18 hours) |
186
194
  | `SESSION_COOKIE_PATH` | string | `'/'` | Session cookie path |
187
195
  | `SESSION_SECRET` | string | - | Session/JWT secret key |
188
196
  | `SESSION_PREFIX` | string | `'ibmid:'` | Redis session/key prefix |
197
+ | `SESSION_KEY` | string | `'session_token'` | Redis key for session data (SESSION mode) or localStorage key for token (TOKEN mode) |
198
+ | `SESSION_EXPIRY_KEY` | string | `'session_expires_at'` | localStorage key for session expiry timestamp (TOKEN mode) |
199
+ | `TOKEN_STORAGE_TEMPLATE_PATH` | string | - | Path to custom HTML template for TOKEN mode callback |
189
200
  | `REDIS_URL` | string | - | Redis connection URL (optional) |
190
201
  | `REDIS_CERT_PATH` | string | - | Path to Redis TLS certificate |
191
202
  | `JWT_ALGORITHM` | string | `'dir'` | JWT signing algorithm |
@@ -58,7 +58,7 @@ export class SessionConfig {
58
58
  /** @type {string} */
59
59
  SSO_FAILURE_URL;
60
60
 
61
- /** @type {number} Session age in milliseconds */
61
+ /** @type {number} Session age in seconds (default: 64800 = 18 hours) */
62
62
  SESSION_AGE;
63
63
  /**
64
64
  * @type {string} Session cookie path
@@ -137,6 +137,8 @@ export class SessionManager {
137
137
  #jwtManager = null;
138
138
  /** @type {import('./logger.js').Logger} */
139
139
  #logger = Logger.getInstance('SessionManager');
140
+ /** @type {string?} Cached HTML template for token storage */
141
+ #htmlTemplate = null;
140
142
 
141
143
  /**
142
144
  * Create a new session manager
@@ -181,8 +183,8 @@ export class SessionManager {
181
183
  this.#config = {
182
184
  // Session Mode
183
185
  SESSION_MODE: config.SESSION_MODE || SessionMode.SESSION,
184
- // Session
185
- SESSION_AGE: config.SESSION_AGE || 64800000,
186
+ // Session - SESSION_AGE is now in seconds (default: 64800 = 18 hours)
187
+ SESSION_AGE: config.SESSION_AGE || 64800,
186
188
  SESSION_COOKIE_PATH: config.SESSION_COOKIE_PATH || '/',
187
189
  SESSION_SECRET: config.SESSION_SECRET,
188
190
  SESSION_PREFIX: config.SESSION_PREFIX || 'ibmid:',
@@ -253,6 +255,15 @@ export class SessionManager {
253
255
  return this.#config.SESSION_KEY;
254
256
  }
255
257
 
258
+ /**
259
+ * Get session age in milliseconds (for express-session cookie maxAge)
260
+ * @returns {number} Returns the session age in milliseconds
261
+ * @private
262
+ */
263
+ #getSessionAgeInMilliseconds() {
264
+ return Math.round(this.#config.SESSION_AGE * 1000);
265
+ }
266
+
256
267
  /**
257
268
  * Get Redis key for token storage
258
269
  * @param {string} email User email
@@ -282,6 +293,37 @@ export class SessionManager {
282
293
  return this.#redisManager;
283
294
  }
284
295
 
296
+ /**
297
+ * Setup the session/user handlers with configurations
298
+ * @param {import('@types/express').Application} app Express application
299
+ */
300
+ async setup(app) {
301
+ this.#redisManager = new RedisManager();
302
+ this.#jwtManager = new JwtManager({
303
+ ...this.#config,
304
+ JWT_EXPIRATION_TIME: this.#config.SESSION_AGE, // SESSION_AGE is already in seconds
305
+ });
306
+
307
+ // Identity Provider Request
308
+ this.#idpRequest = axios.create({
309
+ baseURL: this.#config.SSO_ENDPOINT_URL,
310
+ timeout: 30000,
311
+ });
312
+ app.set('trust proxy', 1);
313
+ const isOK = await this.#redisManager.connect(this.#config.REDIS_URL, this.#config.REDIS_CERT_PATH);
314
+ if (this.#config.SESSION_MODE === SessionMode.SESSION) {
315
+ app.use(this.#sessionHandler(isOK));
316
+ }
317
+
318
+ // Cache HTML template for TOKEN mode
319
+ if (this.#config.SESSION_MODE === SessionMode.TOKEN) {
320
+ const templatePath = this.#config.TOKEN_STORAGE_TEMPLATE_PATH ||
321
+ path.resolve(__dirname, 'assets', 'template.html');
322
+ this.#htmlTemplate = fs.readFileSync(templatePath, 'utf8');
323
+ this.#logger.debug('### HTML TEMPLATE CACHED ###');
324
+ }
325
+ }
326
+
285
327
  /**
286
328
  * Generate and store JWT token in Redis
287
329
  * - JWT payload contains only { email, tid } for minimal size
@@ -300,56 +342,44 @@ export class SessionManager {
300
342
  async #generateAndStoreToken(user) {
301
343
  // Generate unique token ID for this device/session
302
344
  const tid = crypto.randomUUID();
303
- const ttlSeconds = Math.floor(this.#config.SESSION_AGE / 1000);
304
345
  // Create JWT token with only email and tid (minimal payload)
305
- const token = await this.#jwtManager.encrypt(
306
- { email: user.email, tid },
307
- this.#config.SESSION_SECRET,
308
- { expirationTime: ttlSeconds }
309
- );
346
+ const payload = { email: user.email, tid };
347
+ const token = await this.#jwtManager.encrypt(payload, this.#config.SESSION_SECRET, { expirationTime: this.#config.SESSION_AGE });
310
348
 
311
349
  // Store user data in Redis with TTL
312
350
  const redisKey = this.#getTokenRedisKey(user.email, tid);
313
351
 
314
- await this.#redisManager.getClient().setEx(
315
- redisKey,
316
- ttlSeconds,
317
- JSON.stringify(user)
318
- );
352
+ await this.#redisManager.getClient().setEx(redisKey, this.#config.SESSION_AGE, JSON.stringify(user));
319
353
  this.#logger.debug(`### TOKEN GENERATED: ${user.email} ###`);
320
354
  return token;
321
355
  }
322
356
 
323
357
  /**
324
- * Verify token authentication - extracts and validates JWT from Authorization header
325
- * @param {import('@types/express').Request} req Request with Authorization header
326
- * @param {import('@types/express').Response} res Response object
327
- * @param {import('@types/express').NextFunction} next Next middleware function
328
- * @param {boolean} isDebugging If true, allows unauthenticated requests
329
- * @param {string} redirectUrl URL to redirect to on authentication failure
330
- * @throws {CustomError} If token is missing, invalid, or expired
358
+ * Extract and validate user data from Authorization header (TOKEN mode only)
359
+ * @param {string} authHeader Authorization header in format "Bearer {token}"
360
+ * @param {boolean} [fetchFromRedis=true] Whether to fetch full user data from Redis
361
+ * - true: Returns { tid, user } with full user data from Redis (default)
362
+ * - false: Returns JWT payload only (lightweight validation)
363
+ * @returns {Promise<{ tid: string?, email: string?, user: object? } | object>}
364
+ * - When fetchFromRedis=true: { tid: string, user: object }
365
+ * - When fetchFromRedis=false: JWT payload object
366
+ * @throws {CustomError} UNAUTHORIZED (401) if:
367
+ * - Authorization header is missing or invalid format
368
+ * - Token decryption fails
369
+ * - Token payload is invalid (missing email/tid)
370
+ * - Token not found in Redis (when fetchFromRedis=true)
331
371
  * @private
332
- * @example
333
- * // Authorization header format: "Bearer {jwt_token}"
334
- * await this.#verifyToken(req, res, next, false, '/login');
335
372
  */
336
- async #verifyToken(req, res, next, isDebugging, redirectUrl) {
337
- try {
338
- // Extract token from Authorization header
339
- const authHeader = req.headers.authorization;
340
- if (!authHeader?.startsWith('Bearer ')) {
341
- throw new CustomError(httpCodes.UNAUTHORIZED, 'Missing or invalid authorization header');
342
- }
343
-
344
- const token = authHeader.substring(7); // Remove 'Bearer ' prefix
345
-
346
- // Decrypt JWT token
347
- const { payload } = await this.#jwtManager.decrypt(
348
- token,
349
- this.#config.SESSION_SECRET
350
- );
373
+ async #getUserFromToken(authHeader, fetchFromRedis = true) {
374
+ if (!authHeader?.startsWith('Bearer ')) {
375
+ throw new CustomError(httpCodes.UNAUTHORIZED, 'Missing or invalid authorization header');
376
+ }
377
+ const token = authHeader.substring(7); // Remove 'Bearer ' prefix
378
+ // Decrypt JWT token
379
+ const { payload } = await this.#jwtManager.decrypt(token, this.#config.SESSION_SECRET);
351
380
 
352
- // Extract email and token ID
381
+ if (fetchFromRedis) {
382
+ /** @type {{ email: string, tid: string }} Extract email and token ID */
353
383
  const { email, tid } = payload;
354
384
  if (!email || !tid) {
355
385
  throw new CustomError(httpCodes.UNAUTHORIZED, 'Invalid token payload');
@@ -362,26 +392,58 @@ export class SessionManager {
362
392
  if (!userData) {
363
393
  throw new CustomError(httpCodes.UNAUTHORIZED, 'Token not found or expired');
364
394
  }
395
+ return { tid, user: JSON.parse(userData) };
396
+ }
397
+ return payload;
398
+ }
365
399
 
366
- // Parse and attach user to request
367
- req.user = JSON.parse(userData);
368
-
369
- // Validate authorization
370
- const { authorized = isDebugging } = req.user ?? { authorized: isDebugging };
371
- if (!authorized && !isDebugging) {
372
- throw new CustomError(httpCodes.FORBIDDEN, 'User is not authorized');
373
- }
400
+ /**
401
+ * Get authenticated user data (works for both SESSION and TOKEN modes)
402
+ * @param {import('@types/express').Request} req Express request object
403
+ * @returns {Promise<object>} Full user data object
404
+ * @throws {CustomError} If user is not authenticated
405
+ * @public
406
+ * @example
407
+ * // Use in custom middleware
408
+ * app.use(async (req, res, next) => {
409
+ * try {
410
+ * const user = await sessionManager.getUser(req);
411
+ * req.customUser = user;
412
+ * next();
413
+ * } catch (error) {
414
+ * next(error);
415
+ * }
416
+ * });
417
+ */
418
+ async getUser(req) {
419
+ if (this.#config.SESSION_MODE === SessionMode.TOKEN) {
420
+ const { user } = await this.#getUserFromToken(req.headers.authorization, true);
421
+ return user;
422
+ }
423
+ // Session mode
424
+ return req.session[this.#getSessionKey()];
425
+ }
374
426
 
427
+ /**
428
+ * Verify token authentication - extracts and validates JWT from Authorization header
429
+ * @param {import('@types/express').Request} req Request with Authorization header
430
+ * @param {import('@types/express').Response} res Response object
431
+ * @param {import('@types/express').NextFunction} next Next middleware function
432
+ * @param {string} errorRedirectUrl URL to redirect to on authentication failure
433
+ * @throws {CustomError} If token is missing, invalid, or expired
434
+ * @private
435
+ * @example
436
+ * // Authorization header format: "Bearer {jwt_token}"
437
+ * await this.#verifyToken(req, res, next, '/login');
438
+ */
439
+ async #verifyToken(req, res, next, errorRedirectUrl) {
440
+ try {
441
+ // Lightweight token validation (no Redis lookup)
442
+ await this.#getUserFromToken(req.headers.authorization, false);
375
443
  return next();
376
-
377
444
  } catch (error) {
378
- if (isDebugging) {
379
- this.#logger.warn('### TOKEN VERIFICATION FAILED (debugging mode) ###', error.message);
380
- return next();
381
- }
382
-
383
- if (redirectUrl) {
384
- return res.redirect(redirectUrl);
445
+ if (errorRedirectUrl) {
446
+ return res.redirect(errorRedirectUrl);
385
447
  }
386
448
 
387
449
  // Handle specific JWT errors
@@ -399,18 +461,19 @@ export class SessionManager {
399
461
  * @param {import('@types/express').Request} req Request with session data
400
462
  * @param {import('@types/express').Response} res Response object
401
463
  * @param {import('@types/express').NextFunction} next Next middleware function
402
- * @param {boolean} isDebugging If true, allows unauthorized users
403
- * @param {string} redirectUrl URL to redirect to if user is unauthorized
464
+ * @param {string} errorRedirectUrl URL to redirect to if user is unauthorized
404
465
  * @throws {CustomError} If user is not authorized
405
466
  * @private
406
467
  */
407
- async #verifySession(req, res, next, isDebugging, redirectUrl) {
408
- const { authorized = isDebugging } = req.user ?? { authorized: isDebugging };
468
+ async #verifySession(req, res, next, errorRedirectUrl) {
469
+ // Fix: Check session data directly, not req.user (which is only populated by requireUser())
470
+ const user = req.session[this.#getSessionKey()];
471
+ const { authorized = false } = user ?? { authorized: false };
409
472
  if (authorized) {
410
473
  return next();
411
474
  }
412
- if (redirectUrl) {
413
- return res.redirect(redirectUrl);
475
+ if (errorRedirectUrl) {
476
+ return res.redirect(errorRedirectUrl);
414
477
  }
415
478
  return next(new CustomError(httpCodes.UNAUTHORIZED, httpMessages.UNAUTHORIZED));
416
479
  }
@@ -422,41 +485,31 @@ export class SessionManager {
422
485
  * @param {import('@types/express').Response} res Response object
423
486
  * @param {import('@types/express').NextFunction} next Next middleware function
424
487
  * @param {(user: object) => object} initUser Function to initialize/transform user object
425
- * @param {string} idpUrl Identity provider refresh endpoint URL
488
+ * @param {string} idpRefreshUrl Identity provider refresh endpoint URL
426
489
  * @throws {CustomError} If refresh lock is active or SSO refresh fails
427
490
  * @private
428
491
  * @example
429
492
  * // Response format:
430
- * // { token: "new_jwt", user: {...}, expiresIn: 64800, tokenType: "Bearer" }
493
+ * // { jwt: "new_jwt", user: {...}, expires_at: 64800, token_type: "Bearer" }
431
494
  */
432
- async #refreshToken(req, res, next, initUser, idpUrl) {
495
+ async #refreshToken(req, res, next, initUser, idpRefreshUrl) {
433
496
  try {
434
- // Get current user from verifyToken middleware
435
- const { email, attributes } = req.user || {};
436
-
437
- if (!email) {
438
- throw new CustomError(httpCodes.UNAUTHORIZED, 'User not authenticated');
439
- }
440
-
441
- // Extract Token ID from current token
442
- const authHeader = req.headers.authorization;
443
- const token = authHeader?.substring(7);
444
- const { payload } = await this.#jwtManager.decrypt(token, this.#config.SESSION_SECRET);
445
- const { tid: oldTokenId } = payload;
497
+ /** @type {{ tid: string, user: { email: string, attributes: { idp: string, refresh_token: string }? } }} */
498
+ const { tid, user } = await this.#getUserFromToken(req.headers.authorization, true);
446
499
 
447
500
  // Check refresh lock
448
- if (this.hasLock(email)) {
501
+ if (this.hasLock(user?.email)) {
449
502
  throw new CustomError(httpCodes.CONFLICT, 'Token refresh is locked');
450
503
  }
451
- this.lock(email);
504
+ this.lock(user?.email);
452
505
 
453
506
  // Call SSO refresh endpoint
454
- const response = await this.#idpRequest.post(idpUrl, {
507
+ const response = await this.#idpRequest.post(idpRefreshUrl, {
455
508
  user: {
456
- email,
509
+ email: user?.email,
457
510
  attributes: {
458
- idp: attributes?.idp,
459
- refresh_token: attributes?.refresh_token
511
+ idp: user?.attributes?.idp,
512
+ refresh_token: user?.attributes?.refresh_token
460
513
  }
461
514
  }
462
515
  });
@@ -474,24 +527,19 @@ export class SessionManager {
474
527
  }
475
528
 
476
529
  // Initialize user with new data
477
- const user = initUser(newPayload.user);
530
+ const newUser = initUser(newPayload.user);
478
531
 
479
532
  // Generate new token
480
- const newToken = await this.#generateAndStoreToken(user);
533
+ const newToken = await this.#generateAndStoreToken(newUser);
481
534
 
482
535
  // Remove old token from Redis
483
- const oldRedisKey = this.#getTokenRedisKey(email, oldTokenId);
536
+ const oldRedisKey = this.#getTokenRedisKey(user.email, tid);
484
537
  await this.#redisManager.getClient().del(oldRedisKey);
485
538
 
486
539
  this.#logger.debug('### TOKEN REFRESHED SUCCESSFULLY ###');
487
540
 
488
541
  // Return new token
489
- return res.json({
490
- token: newToken,
491
- user,
492
- expiresIn: Math.floor(this.#config.SESSION_AGE / 1000),
493
- tokenType: 'Bearer'
494
- });
542
+ return res.json({ jwt: newToken, user: newUser, expires_at: this.#config.SESSION_AGE, token_type: 'Bearer' });
495
543
  } catch (error) {
496
544
  return next(httpHelper.handleAxiosError(error));
497
545
  }
@@ -503,17 +551,17 @@ export class SessionManager {
503
551
  * @param {import('@types/express').Response} res Response
504
552
  * @param {import('@types/express').NextFunction} next Next function
505
553
  * @param {(user: object) => object} initUser Initialize user function
506
- * @param {string} idpUrl Identity provider URL
554
+ * @param {string} idpRefreshUrl Token Refresh URL
507
555
  * @private
508
556
  */
509
- async #refreshSession(req, res, next, initUser, idpUrl) {
557
+ async #refreshSession(req, res, next, initUser, idpRefreshUrl) {
510
558
  try {
511
559
  const { email, attributes } = req.user || { email: '', attributes: {} };
512
560
  if (this.hasLock(email)) {
513
561
  throw new CustomError(httpCodes.CONFLICT, 'User refresh is locked');
514
562
  }
515
563
  this.lock(email);
516
- const response = await this.#idpRequest.post(idpUrl, {
564
+ const response = await this.#idpRequest.post(idpRefreshUrl, {
517
565
  user: {
518
566
  email,
519
567
  attributes: {
@@ -594,15 +642,8 @@ export class SessionManager {
594
642
  }
595
643
 
596
644
  try {
597
- // Extract Token ID from current token
598
- const authHeader = req.headers.authorization;
599
- const token = authHeader?.substring(7);
600
-
601
- if (!token) {
602
- throw new CustomError(httpCodes.BAD_REQUEST, 'No token provided');
603
- }
604
-
605
- const { payload } = await this.#jwtManager.decrypt(token, this.#config.SESSION_SECRET);
645
+ // Extract Token ID and email from current token
646
+ const payload = await this.#getUserFromToken(req.headers.authorization, false);
606
647
  const { email, tid } = payload;
607
648
 
608
649
  if (!email || !tid) {
@@ -660,24 +701,6 @@ export class SessionManager {
660
701
  });
661
702
  }
662
703
 
663
- /**
664
- * Setup the session/user handlers with configurations
665
- * @param {import('@types/express').Application} app Express application
666
- * @param {(user: object) => object} updateUser Update user object if user should have proper attributes, e.g. permissions, avatar URL
667
- */
668
- async setup(app, updateUser) {
669
- this.#redisManager = new RedisManager();
670
- this.#jwtManager = new JwtManager(this.#config);
671
- // Identity Provider Request
672
- this.#idpRequest = axios.create({
673
- baseURL: this.#config.SSO_ENDPOINT_URL,
674
- timeout: 30000,
675
- });
676
- app.set('trust proxy', 1);
677
- app.use(await this.#sessionHandler());
678
- app.use(this.#userHandler(updateUser));
679
- }
680
-
681
704
  /**
682
705
  * Get Redis session RequestHandler
683
706
  * @returns {import('@types/express').RequestHandler} Returns RequestHandler instance of Express
@@ -686,7 +709,7 @@ export class SessionManager {
686
709
  // Redis Session
687
710
  this.#logger.log('### Using Redis as the Session Store ###');
688
711
  return session({
689
- cookie: { maxAge: this.#config.SESSION_AGE, path: this.#config.SESSION_COOKIE_PATH, sameSite: false },
712
+ cookie: { maxAge: this.#getSessionAgeInMilliseconds(), path: this.#config.SESSION_COOKIE_PATH, sameSite: false },
690
713
  store: new RedisStore({ client: this.#redisManager.getClient(), prefix: this.#config.SESSION_PREFIX, disableTouch: true }),
691
714
  resave: false, saveUninitialized: false,
692
715
  secret: this.#config.SESSION_SECRET,
@@ -702,7 +725,7 @@ export class SessionManager {
702
725
  this.#logger.log('### Using Memory as the Session Store ###');
703
726
  const MemoryStore = memStore(session);
704
727
  return session({
705
- cookie: { maxAge: this.#config.SESSION_AGE, path: this.#config.SESSION_COOKIE_PATH, sameSite: false },
728
+ cookie: { maxAge: this.#getSessionAgeInMilliseconds(), path: this.#config.SESSION_COOKIE_PATH, sameSite: false },
706
729
  store: new MemoryStore({}),
707
730
  resave: false, saveUninitialized: false,
708
731
  secret: this.#config.SESSION_SECRET,
@@ -711,67 +734,85 @@ export class SessionManager {
711
734
 
712
735
  /**
713
736
  * Get session RequestHandler
714
- * @returns {Promise<import('@types/express').RequestHandler>} Returns RequestHandler instance of Express
737
+ * @param {boolean} isRedisReady Is Redis Ready
738
+ * @returns {import('@types/express').RequestHandler} Returns RequestHandler instance of Express
715
739
  */
716
- async #sessionHandler() {
717
- if(this.#config.REDIS_URL?.length > 0) {
718
- await this.#redisManager.connect(this.#config.REDIS_URL, this.#config.REDIS_CERT_PATH);
740
+ #sessionHandler(isRedisReady) {
741
+ if(isRedisReady) {
719
742
  return this.#redisSession();
720
743
  }
721
744
  return this.#memorySession();
722
745
  }
723
746
 
724
747
  /**
725
- * User HTTP Handler
726
- * @param {(user: object) => object} updateUser User wrapper
748
+ * Middleware to load full user data (works for both SESSION and TOKEN modes)
727
749
  * @returns {import('@types/express').RequestHandler} Returns express Request Handler
728
750
  */
729
- #userHandler (updateUser) {
730
- return (req, res, next) => {
731
- req.user = req.session[this.#getSessionKey()];
732
- /** @type {import('@types/express').Request & { user: object }} Session user */
733
- res.locals.user = updateUser(req.user);
734
- return next();
751
+ requireUser = () => {
752
+ return async (req, res, next) => {
753
+ try {
754
+ req.user = await this.getUser(req);
755
+ return next();
756
+ }
757
+ catch (error) {
758
+ return next(error);
759
+ }
735
760
  };
736
- }
761
+ };
737
762
 
738
763
  /**
739
764
  * Resource protection based on configured SESSION_MODE
740
- * @param {boolean} [isDebugging=false] Debugging flag
741
- * @param {string} [redirectUrl=''] Redirect URL
765
+ * - SESSION mode: Verifies user exists in session store and is authorized (checks req.session data)
766
+ * - TOKEN mode: Validates JWT token from Authorization header (lightweight validation)
767
+ *
768
+ * Note: This method verifies authentication only. Use requireUser() after this to populate req.user.
769
+ *
770
+ * @param {string} [errorRedirectUrl=''] Redirect URL on authentication failure
742
771
  * @returns {import('@types/express').RequestHandler} Returns express Request Handler
743
- */
744
- authenticate(isDebugging = false, redirectUrl = '') {
772
+ * @example
773
+ * // Option 1: Just verify authentication (user data remains in req.session or token)
774
+ * app.get('/api/check', session.authenticate(), (req, res) => {
775
+ * res.json({ authenticated: true });
776
+ * });
777
+ *
778
+ * // Option 2: Verify authentication AND load user data into req.user
779
+ * app.get('/api/profile',
780
+ * session.authenticate(), // Verifies session/token
781
+ * session.requireUser(), // Loads user data into req.user
782
+ * (req, res) => {
783
+ * res.json({ user: req.user }); // User data available here
784
+ * }
785
+ * );
786
+ */
787
+ authenticate(errorRedirectUrl = '') {
745
788
  return async (req, res, next) => {
746
789
  const mode = this.#config.SESSION_MODE || SessionMode.SESSION;
747
790
  if (mode === SessionMode.TOKEN) {
748
- return this.#verifyToken(req, res, next, isDebugging, redirectUrl);
791
+ return this.#verifyToken(req, res, next, errorRedirectUrl);
749
792
  }
750
- return this.#verifySession(req, res, next, isDebugging, redirectUrl);
793
+ return this.#verifySession(req, res, next, errorRedirectUrl);
751
794
  };
752
795
  }
753
796
 
754
797
  /**
755
798
  * Resource protection by token (explicit token verification)
756
- * @param {boolean} [isDebugging=false] Debugging flag
757
- * @param {string} [redirectUrl=''] Redirect URL
799
+ * @param {string} [errorRedirectUrl=''] Redirect URL
758
800
  * @returns {import('@types/express').RequestHandler} Returns express Request Handler
759
801
  */
760
- verifyToken(isDebugging = false, redirectUrl = '') {
802
+ verifyToken(errorRedirectUrl = '') {
761
803
  return async (req, res, next) => {
762
- return this.#verifyToken(req, res, next, isDebugging, redirectUrl);
804
+ return this.#verifyToken(req, res, next, errorRedirectUrl);
763
805
  };
764
806
  }
765
807
 
766
808
  /**
767
809
  * Resource protection by session (explicit session verification)
768
- * @param {boolean} [isDebugging=false] Debugging flag
769
- * @param {string} [redirectUrl=''] Redirect URL
810
+ * @param {string} [errorRedirectUrl=''] Redirect URL
770
811
  * @returns {import('@types/express').RequestHandler} Returns express Request Handler
771
812
  */
772
- verifySession(isDebugging = false, redirectUrl = '') {
813
+ verifySession(errorRedirectUrl = '') {
773
814
  return async (req, res, next) => {
774
- return this.#verifySession(req, res, next, isDebugging, redirectUrl);
815
+ return this.#verifySession(req, res, next, errorRedirectUrl);
775
816
  };
776
817
  }
777
818
 
@@ -800,62 +841,75 @@ export class SessionManager {
800
841
  });
801
842
  }
802
843
  throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload');
803
- };
804
-
805
- /**
806
- * SSO callback for successful login
807
- * @param {(user: object) => object} initUser Initialize user object function
808
- * @returns {import('@types/express').RequestHandler} Returns express Request Handler
809
- */
810
- callback(initUser) {
811
- return async (req, res, next) => {
812
- const { jwt = '' } = req.query;
813
- if (!jwt) {
814
- return next(new CustomError(httpCodes.BAD_REQUEST, 'Missing `jwt` in query parameters'));
815
- }
816
-
817
- try {
818
- // Decrypt JWT from Identity Adapter
819
- const { payload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
820
-
821
- if (!payload?.user) {
822
- throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload');
844
+ };
845
+
846
+ /**
847
+ * Render HTML template for token storage
848
+ * @param {string} token JWT token
849
+ * @param {string} expiresAt Expiry timestamp
850
+ * @param {string} sucessRedirectUrl Success redirect URL
851
+ * @returns {string} Rendered HTML
852
+ * @private
853
+ */
854
+ #renderTokenStorageHtml(token, expiresAt, sucessRedirectUrl) {
855
+ return this.#htmlTemplate
856
+ .replaceAll('{{SESSION_DATA_KEY}}', this.#config.SESSION_KEY)
857
+ .replaceAll('{{SESSION_DATA_VALUE}}', token)
858
+ .replaceAll('{{SESSION_EXPIRY_KEY}}', this.#config.SESSION_EXPIRY_KEY)
859
+ .replaceAll('{{SESSION_EXPIRY_VALUE}}', expiresAt)
860
+ .replaceAll('{{SSO_SUCCESS_URL}}', sucessRedirectUrl)
861
+ .replaceAll('{{SSO_FAILURE_URL}}', this.#config.SSO_FAILURE_URL);
862
+ }
863
+
864
+ /**
865
+ * SSO callback for successful login
866
+ * @param {(user: object) => object} initUser Initialize user object function
867
+ * @returns {import('@types/express').RequestHandler} Returns express Request Handler
868
+ */
869
+ callback(initUser) {
870
+ return async (req, res, next) => {
871
+ const { jwt = '' } = req.query;
872
+ if (!jwt) {
873
+ return next(new CustomError(httpCodes.BAD_REQUEST, 'Missing `jwt` in query parameters'));
823
874
  }
875
+
876
+ try {
877
+ // Decrypt JWT from Identity Adapter
878
+ const { payload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
879
+
880
+ if (!payload?.user) {
881
+ throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload');
882
+ }
883
+
884
+ /** @type {import('../index.js').SessionUser} */
885
+ const user = initUser(payload.user);
886
+ /** @type {string} */
887
+ const callbackRedirectUrl = payload.redirect_url || this.#config.SSO_SUCCESS_URL;
888
+
889
+ // Token mode: Generate token and return HTML page
890
+ if (this.#config.SESSION_MODE === SessionMode.TOKEN) {
891
+ const token = await this.#generateAndStoreToken(user);
892
+ this.#logger.debug('### CALLBACK TOKEN GENERATED ###');
893
+ const html = this.#renderTokenStorageHtml(token, user.attributes.expires_at, callbackRedirectUrl);
894
+ return res.send(html);
895
+ }
824
896
 
825
- /** @type {import('../index.js').SessionUser} */
826
- const user = initUser(payload.user);
827
- /** @type {string} */
828
- const redirectUrl = payload.redirect_url || this.#config.SSO_SUCCESS_URL;
829
-
830
- // Check SESSION_MODE to determine response type
831
- if (this.#config.SESSION_MODE === SessionMode.TOKEN) {
832
- // Token-based: Generate token and return HTML page that stores it
833
- const token = await this.#generateAndStoreToken(user);
834
-
835
- this.#logger.debug('### CALLBACK TOKEN GENERATED ###');
836
-
837
- const templatePath = this.#config.TOKEN_STORAGE_TEMPLATE_PATH || path.resolve(__dirname, 'assets', 'template.html');
838
- // Return HTML page that stores token in localStorage and redirects
839
- const template = fs.readFileSync(templatePath, 'utf8');
840
- const html = template
841
- .replaceAll('{{SESSION_DATA_KEY}}', this.#config.SESSION_KEY)
842
- .replaceAll('{{SESSION_DATA_VALUE}}', token)
843
- .replaceAll('{{SESSION_EXPIRY_KEY}}', this.#config.SESSION_EXPIRY_KEY)
844
- .replaceAll('{{SESSION_EXPIRY_VALUE}}', user.attributes.expires_at)
845
- .replaceAll('{{SSO_SUCCESS_URL}}', redirectUrl)
846
- .replaceAll('{{SSO_FAILURE_URL}}', this.#config.SSO_FAILURE_URL);
847
- return res.send(html);
897
+ // Session mode: Save to session and redirect
898
+ await this.#saveSession(req, jwt, initUser);
899
+ return res.redirect(callbackRedirectUrl);
848
900
  }
849
- // Session-based: Save to session and redirect
850
- await this.#saveSession(req, jwt, initUser);
851
- return res.redirect(redirectUrl);
852
- }
853
- catch (error) {
854
- this.#logger.error('### CALLBACK ERROR ###', error);
855
- return res.redirect(this.#config.SSO_FAILURE_URL.concat('?message=').concat(encodeURIComponent(error.message)));
856
- }
857
- };
858
- }
901
+ catch (error) {
902
+ this.#logger.error('### CALLBACK ERROR ###', error);
903
+ let errorMessage = error.message;
904
+ if (error.code === 'ERR_JWT_EXPIRED') {
905
+ errorMessage = 'Authentication token expired';
906
+ } else if (error.code === 'ERR_JWT_INVALID') {
907
+ errorMessage = 'Invalid authentication token';
908
+ }
909
+ return res.redirect(this.#config.SSO_FAILURE_URL.concat('?message=').concat(encodeURIComponent(errorMessage)));
910
+ }
911
+ };
912
+ }
859
913
 
860
914
  /**
861
915
  * Get Identity Providers
@@ -883,14 +937,14 @@ export class SessionManager {
883
937
  * @returns {import('@types/express').RequestHandler} Returns express Request Handler
884
938
  */
885
939
  refresh(initUser) {
886
- const idpUrl = '/auth/refresh'.concat('?client_id=').concat(this.#config.SSO_CLIENT_ID);
940
+ const idpRefreshUrl = '/auth/refresh'.concat('?client_id=').concat(this.#config.SSO_CLIENT_ID);
887
941
  return async (req, res, next) => {
888
942
  const mode = this.#config.SESSION_MODE || SessionMode.SESSION;
889
-
943
+
890
944
  if (mode === SessionMode.TOKEN) {
891
- return this.#refreshToken(req, res, next, initUser, idpUrl);
945
+ return this.#refreshToken(req, res, next, initUser, idpRefreshUrl);
892
946
  } else {
893
- return this.#refreshSession(req, res, next, initUser, idpUrl);
947
+ return this.#refreshSession(req, res, next, initUser, idpRefreshUrl);
894
948
  }
895
949
  };
896
950
  }
package/index.d.ts CHANGED
@@ -122,9 +122,9 @@ export interface SessionConfig {
122
122
  SESSION_MODE?: string;
123
123
 
124
124
  /**
125
- * Session expiration time in milliseconds
126
- * @example 3600000 (1 hour) or 86400000 (24 hours)
127
- * @default 3600000 (1 hour)
125
+ * Session expiration time in seconds
126
+ * @example 3600 (1 hour) or 86400 (24 hours) or 64800 (18 hours)
127
+ * @default 64800 (18 hours)
128
128
  */
129
129
  SESSION_AGE?: number;
130
130
 
@@ -142,24 +142,27 @@ export interface SessionConfig {
142
142
  */
143
143
  SESSION_SECRET?: string;
144
144
 
145
- /**
145
+ /**
146
146
  * Redis key prefix for storing session data
147
147
  * @example 'myapp:session:' (will result in keys like 'myapp:session:user@example.com')
148
- * @default 'sess:'
148
+ * @default 'ibmid:' (legacy default, consider changing for your use case)
149
149
  */
150
150
  SESSION_PREFIX?: string;
151
151
 
152
- /**
152
+ /**
153
153
  * Redis key name for storing session data
154
+ * - In SESSION mode: key used to store the user in the session
155
+ * - In TOKEN mode: key of localStorage where the token is stored
154
156
  * @example 'user' (results in session.user containing user data)
155
- * @default 'user'
157
+ * @default 'session_token'
156
158
  */
157
159
  SESSION_KEY?: string;
158
160
 
159
- /**
161
+ /**
160
162
  * Redis key name for storing session expiry timestamp
163
+ * - In TOKEN mode: key of localStorage where the session expiry timestamp is stored
161
164
  * @example 'expires' (results in session.expires containing expiry time)
162
- * @default 'expires'
165
+ * @default 'session_expires_at'
163
166
  */
164
167
  SESSION_EXPIRY_KEY?: string;
165
168
 
@@ -311,41 +314,98 @@ export class SessionManager {
311
314
  redisManager(): RedisManager;
312
315
 
313
316
  /**
314
- * Initialize the session configurations
317
+ * Get authenticated user data (works for both SESSION and TOKEN modes)
318
+ * @param req Express request object
319
+ * @returns Promise resolving to full user data object
320
+ * @throws CustomError If user is not authenticated
321
+ * @example
322
+ * ```javascript
323
+ * // Use in custom middleware
324
+ * app.use(async (req, res, next) => {
325
+ * try {
326
+ * const user = await sessionManager.getUser(req);
327
+ * req.customUser = user;
328
+ * next();
329
+ * } catch (error) {
330
+ * next(error);
331
+ * }
332
+ * });
333
+ * ```
334
+ */
335
+ getUser(req: Request): Promise<SessionUser>;
336
+
337
+ /**
338
+ * Initialize the session configurations and middleware
315
339
  * @param app Express application
316
- * @param config Session configurations
317
- * @param updateUser Process user object to compute attributes like permissions, avatar URL, etc.
318
340
  */
319
- setup(
320
- app: Application,
321
- updateUser: (user: SessionUser | undefined) => any
322
- ): Promise<void>;
341
+ setup(app: Application): Promise<void>;
342
+
343
+ /**
344
+ * Middleware to load full user data into req.user
345
+ * - SESSION mode: Loads user from session store (req.session[SESSION_KEY])
346
+ * - TOKEN mode: Loads user from Redis using JWT token
347
+ * - Provides request-level caching to avoid redundant lookups
348
+ * - Should be used after authenticate() middleware
349
+ * @returns Returns express Request Handler
350
+ * @example
351
+ * ```javascript
352
+ * app.get('/api/profile',
353
+ * session.authenticate(), // Verifies authentication
354
+ * session.requireUser(), // Loads user data into req.user
355
+ * (req, res) => {
356
+ * res.json({ user: req.user }); // User data available here
357
+ * }
358
+ * );
359
+ * ```
360
+ */
361
+ requireUser(): RequestHandler;
323
362
 
324
363
  /**
325
364
  * Resource protection middleware based on configured SESSION_MODE
326
- * Uses verifySession() for SESSION mode and verifyToken() for TOKEN mode
327
- * @param isDebugging Debugging flag (default: false)
328
- * @param redirectUrl Redirect URL (default: '')
365
+ * - SESSION mode: Verifies user exists in session store and is authorized (checks req.session data)
366
+ * - TOKEN mode: Validates JWT token from Authorization header (lightweight validation)
367
+ *
368
+ * Note: This method verifies authentication only and does NOT populate req.user.
369
+ * Use requireUser() after this middleware to load user data into req.user.
370
+ *
371
+ * @param errorRedirectUrl Redirect URL on authentication failure (default: '')
329
372
  * @returns Returns express Request Handler
373
+ * @example
374
+ * ```javascript
375
+ * // Option 1: Just verify authentication (user data remains in req.session or token)
376
+ * app.get('/api/check',
377
+ * session.authenticate(),
378
+ * (req, res) => {
379
+ * res.json({ authenticated: true });
380
+ * }
381
+ * );
382
+ *
383
+ * // Option 2: Verify authentication AND populate req.user (recommended for most use cases)
384
+ * app.get('/api/profile',
385
+ * session.authenticate(), // Verifies session/token validity
386
+ * session.requireUser(), // Loads user data into req.user
387
+ * (req, res) => {
388
+ * res.json({ user: req.user }); // User data now available
389
+ * }
390
+ * );
391
+ * ```
330
392
  */
331
- authenticate(isDebugging?: boolean, redirectUrl?: string): RequestHandler;
393
+ authenticate(errorRedirectUrl?: string): RequestHandler;
332
394
 
333
395
  /**
334
396
  * Resource protection by token (explicit token verification)
335
397
  * Requires Authorization: Bearer {token} header
336
- * @param isDebugging Debugging flag (default: false)
337
- * @param redirectUrl Redirect URL (default: '')
398
+ * @param errorRedirectUrl Redirect URL (default: '')
338
399
  * @returns Returns express Request Handler
339
400
  */
340
- verifyToken(isDebugging?: boolean, redirectUrl?: string): RequestHandler;
401
+ verifyToken(errorRedirectUrl?: string): RequestHandler;
341
402
 
342
403
  /**
343
404
  * Resource protection by session (explicit session verification)
344
- * @param isDebugging Debugging flag (default: false)
345
- * @param redirectUrl Redirect URL (default: '')
405
+ * @param errorRedirectUrl Redirect URL (default: '')
346
406
  * @returns Returns express Request Handler
347
407
  */
348
- verifySession(isDebugging?: boolean, redirectUrl?: string): RequestHandler;
408
+ verifySession(errorRedirectUrl?: string): RequestHandler;
349
409
 
350
410
  /**
351
411
  * SSO callback for successful login
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igxjs/node-components",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "Node components for igxjs",
5
5
  "main": "index.js",
6
6
  "type": "module",