@igxjs/node-components 1.0.14 → 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)
@@ -131,8 +137,9 @@ Uses traditional server-side session cookies. When a user authenticates via SSO,
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
 
@@ -186,6 +194,9 @@ fetch('/api/protected', {
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 |
@@ -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
@@ -291,6 +293,37 @@ export class SessionManager {
291
293
  return this.#redisManager;
292
294
  }
293
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
+
294
327
  /**
295
328
  * Generate and store JWT token in Redis
296
329
  * - JWT payload contains only { email, tid } for minimal size
@@ -309,57 +342,44 @@ export class SessionManager {
309
342
  async #generateAndStoreToken(user) {
310
343
  // Generate unique token ID for this device/session
311
344
  const tid = crypto.randomUUID();
312
- // SESSION_AGE is already in seconds
313
- const ttlSeconds = this.#config.SESSION_AGE;
314
345
  // Create JWT token with only email and tid (minimal payload)
315
- const token = await this.#jwtManager.encrypt(
316
- { email: user.email, tid },
317
- this.#config.SESSION_SECRET,
318
- { expirationTime: ttlSeconds }
319
- );
346
+ const payload = { email: user.email, tid };
347
+ const token = await this.#jwtManager.encrypt(payload, this.#config.SESSION_SECRET, { expirationTime: this.#config.SESSION_AGE });
320
348
 
321
349
  // Store user data in Redis with TTL
322
350
  const redisKey = this.#getTokenRedisKey(user.email, tid);
323
351
 
324
- await this.#redisManager.getClient().setEx(
325
- redisKey,
326
- ttlSeconds,
327
- JSON.stringify(user)
328
- );
352
+ await this.#redisManager.getClient().setEx(redisKey, this.#config.SESSION_AGE, JSON.stringify(user));
329
353
  this.#logger.debug(`### TOKEN GENERATED: ${user.email} ###`);
330
354
  return token;
331
355
  }
332
356
 
333
357
  /**
334
- * Verify token authentication - extracts and validates JWT from Authorization header
335
- * @param {import('@types/express').Request} req Request with Authorization header
336
- * @param {import('@types/express').Response} res Response object
337
- * @param {import('@types/express').NextFunction} next Next middleware function
338
- * @param {boolean} isDebugging If true, allows unauthenticated requests
339
- * @param {string} redirectUrl URL to redirect to on authentication failure
340
- * @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)
341
371
  * @private
342
- * @example
343
- * // Authorization header format: "Bearer {jwt_token}"
344
- * await this.#verifyToken(req, res, next, false, '/login');
345
372
  */
346
- async #verifyToken(req, res, next, isDebugging, redirectUrl) {
347
- try {
348
- // Extract token from Authorization header
349
- const authHeader = req.headers.authorization;
350
- if (!authHeader?.startsWith('Bearer ')) {
351
- throw new CustomError(httpCodes.UNAUTHORIZED, 'Missing or invalid authorization header');
352
- }
353
-
354
- const token = authHeader.substring(7); // Remove 'Bearer ' prefix
355
-
356
- // Decrypt JWT token
357
- const { payload } = await this.#jwtManager.decrypt(
358
- token,
359
- this.#config.SESSION_SECRET
360
- );
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);
361
380
 
362
- // Extract email and token ID
381
+ if (fetchFromRedis) {
382
+ /** @type {{ email: string, tid: string }} Extract email and token ID */
363
383
  const { email, tid } = payload;
364
384
  if (!email || !tid) {
365
385
  throw new CustomError(httpCodes.UNAUTHORIZED, 'Invalid token payload');
@@ -372,26 +392,58 @@ export class SessionManager {
372
392
  if (!userData) {
373
393
  throw new CustomError(httpCodes.UNAUTHORIZED, 'Token not found or expired');
374
394
  }
395
+ return { tid, user: JSON.parse(userData) };
396
+ }
397
+ return payload;
398
+ }
375
399
 
376
- // Parse and attach user to request
377
- req.user = JSON.parse(userData);
378
-
379
- // Validate authorization
380
- const { authorized = isDebugging } = req.user ?? { authorized: isDebugging };
381
- if (!authorized && !isDebugging) {
382
- throw new CustomError(httpCodes.FORBIDDEN, 'User is not authorized');
383
- }
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
+ }
384
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);
385
443
  return next();
386
-
387
444
  } catch (error) {
388
- if (isDebugging) {
389
- this.#logger.warn('### TOKEN VERIFICATION FAILED (debugging mode) ###', error.message);
390
- return next();
391
- }
392
-
393
- if (redirectUrl) {
394
- return res.redirect(redirectUrl);
445
+ if (errorRedirectUrl) {
446
+ return res.redirect(errorRedirectUrl);
395
447
  }
396
448
 
397
449
  // Handle specific JWT errors
@@ -409,18 +461,19 @@ export class SessionManager {
409
461
  * @param {import('@types/express').Request} req Request with session data
410
462
  * @param {import('@types/express').Response} res Response object
411
463
  * @param {import('@types/express').NextFunction} next Next middleware function
412
- * @param {boolean} isDebugging If true, allows unauthorized users
413
- * @param {string} redirectUrl URL to redirect to if user is unauthorized
464
+ * @param {string} errorRedirectUrl URL to redirect to if user is unauthorized
414
465
  * @throws {CustomError} If user is not authorized
415
466
  * @private
416
467
  */
417
- async #verifySession(req, res, next, isDebugging, redirectUrl) {
418
- 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 };
419
472
  if (authorized) {
420
473
  return next();
421
474
  }
422
- if (redirectUrl) {
423
- return res.redirect(redirectUrl);
475
+ if (errorRedirectUrl) {
476
+ return res.redirect(errorRedirectUrl);
424
477
  }
425
478
  return next(new CustomError(httpCodes.UNAUTHORIZED, httpMessages.UNAUTHORIZED));
426
479
  }
@@ -432,41 +485,31 @@ export class SessionManager {
432
485
  * @param {import('@types/express').Response} res Response object
433
486
  * @param {import('@types/express').NextFunction} next Next middleware function
434
487
  * @param {(user: object) => object} initUser Function to initialize/transform user object
435
- * @param {string} idpUrl Identity provider refresh endpoint URL
488
+ * @param {string} idpRefreshUrl Identity provider refresh endpoint URL
436
489
  * @throws {CustomError} If refresh lock is active or SSO refresh fails
437
490
  * @private
438
491
  * @example
439
492
  * // Response format:
440
- * // { token: "new_jwt", user: {...}, expiresIn: 64800, tokenType: "Bearer" }
493
+ * // { jwt: "new_jwt", user: {...}, expires_at: 64800, token_type: "Bearer" }
441
494
  */
442
- async #refreshToken(req, res, next, initUser, idpUrl) {
495
+ async #refreshToken(req, res, next, initUser, idpRefreshUrl) {
443
496
  try {
444
- // Get current user from verifyToken middleware
445
- const { email, attributes } = req.user || {};
446
-
447
- if (!email) {
448
- throw new CustomError(httpCodes.UNAUTHORIZED, 'User not authenticated');
449
- }
450
-
451
- // Extract Token ID from current token
452
- const authHeader = req.headers.authorization;
453
- const token = authHeader?.substring(7);
454
- const { payload } = await this.#jwtManager.decrypt(token, this.#config.SESSION_SECRET);
455
- 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);
456
499
 
457
500
  // Check refresh lock
458
- if (this.hasLock(email)) {
501
+ if (this.hasLock(user?.email)) {
459
502
  throw new CustomError(httpCodes.CONFLICT, 'Token refresh is locked');
460
503
  }
461
- this.lock(email);
504
+ this.lock(user?.email);
462
505
 
463
506
  // Call SSO refresh endpoint
464
- const response = await this.#idpRequest.post(idpUrl, {
507
+ const response = await this.#idpRequest.post(idpRefreshUrl, {
465
508
  user: {
466
- email,
509
+ email: user?.email,
467
510
  attributes: {
468
- idp: attributes?.idp,
469
- refresh_token: attributes?.refresh_token
511
+ idp: user?.attributes?.idp,
512
+ refresh_token: user?.attributes?.refresh_token
470
513
  }
471
514
  }
472
515
  });
@@ -484,24 +527,19 @@ export class SessionManager {
484
527
  }
485
528
 
486
529
  // Initialize user with new data
487
- const user = initUser(newPayload.user);
530
+ const newUser = initUser(newPayload.user);
488
531
 
489
532
  // Generate new token
490
- const newToken = await this.#generateAndStoreToken(user);
533
+ const newToken = await this.#generateAndStoreToken(newUser);
491
534
 
492
535
  // Remove old token from Redis
493
- const oldRedisKey = this.#getTokenRedisKey(email, oldTokenId);
536
+ const oldRedisKey = this.#getTokenRedisKey(user.email, tid);
494
537
  await this.#redisManager.getClient().del(oldRedisKey);
495
538
 
496
539
  this.#logger.debug('### TOKEN REFRESHED SUCCESSFULLY ###');
497
540
 
498
541
  // Return new token
499
- return res.json({
500
- token: newToken,
501
- user,
502
- expiresIn: this.#config.SESSION_AGE, // Already in seconds
503
- tokenType: 'Bearer'
504
- });
542
+ return res.json({ jwt: newToken, user: newUser, expires_at: this.#config.SESSION_AGE, token_type: 'Bearer' });
505
543
  } catch (error) {
506
544
  return next(httpHelper.handleAxiosError(error));
507
545
  }
@@ -513,17 +551,17 @@ export class SessionManager {
513
551
  * @param {import('@types/express').Response} res Response
514
552
  * @param {import('@types/express').NextFunction} next Next function
515
553
  * @param {(user: object) => object} initUser Initialize user function
516
- * @param {string} idpUrl Identity provider URL
554
+ * @param {string} idpRefreshUrl Token Refresh URL
517
555
  * @private
518
556
  */
519
- async #refreshSession(req, res, next, initUser, idpUrl) {
557
+ async #refreshSession(req, res, next, initUser, idpRefreshUrl) {
520
558
  try {
521
559
  const { email, attributes } = req.user || { email: '', attributes: {} };
522
560
  if (this.hasLock(email)) {
523
561
  throw new CustomError(httpCodes.CONFLICT, 'User refresh is locked');
524
562
  }
525
563
  this.lock(email);
526
- const response = await this.#idpRequest.post(idpUrl, {
564
+ const response = await this.#idpRequest.post(idpRefreshUrl, {
527
565
  user: {
528
566
  email,
529
567
  attributes: {
@@ -604,15 +642,8 @@ export class SessionManager {
604
642
  }
605
643
 
606
644
  try {
607
- // Extract Token ID from current token
608
- const authHeader = req.headers.authorization;
609
- const token = authHeader?.substring(7);
610
-
611
- if (!token) {
612
- throw new CustomError(httpCodes.BAD_REQUEST, 'No token provided');
613
- }
614
-
615
- 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);
616
647
  const { email, tid } = payload;
617
648
 
618
649
  if (!email || !tid) {
@@ -670,27 +701,6 @@ export class SessionManager {
670
701
  });
671
702
  }
672
703
 
673
- /**
674
- * Setup the session/user handlers with configurations
675
- * @param {import('@types/express').Application} app Express application
676
- * @param {(user: object) => object} updateUser Update user object if user should have proper attributes, e.g. permissions, avatar URL
677
- */
678
- async setup(app, updateUser) {
679
- this.#redisManager = new RedisManager();
680
- this.#jwtManager = new JwtManager({
681
- ...this.#config,
682
- JWT_EXPIRATION_TIME: this.#config.SESSION_AGE, // SESSION_AGE is already in seconds
683
- });
684
- // Identity Provider Request
685
- this.#idpRequest = axios.create({
686
- baseURL: this.#config.SSO_ENDPOINT_URL,
687
- timeout: 30000,
688
- });
689
- app.set('trust proxy', 1);
690
- app.use(await this.#sessionHandler());
691
- app.use(this.#userHandler(updateUser));
692
- }
693
-
694
704
  /**
695
705
  * Get Redis session RequestHandler
696
706
  * @returns {import('@types/express').RequestHandler} Returns RequestHandler instance of Express
@@ -724,67 +734,85 @@ export class SessionManager {
724
734
 
725
735
  /**
726
736
  * Get session RequestHandler
727
- * @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
728
739
  */
729
- async #sessionHandler() {
730
- if(this.#config.REDIS_URL?.length > 0) {
731
- await this.#redisManager.connect(this.#config.REDIS_URL, this.#config.REDIS_CERT_PATH);
740
+ #sessionHandler(isRedisReady) {
741
+ if(isRedisReady) {
732
742
  return this.#redisSession();
733
743
  }
734
744
  return this.#memorySession();
735
745
  }
736
746
 
737
747
  /**
738
- * User HTTP Handler
739
- * @param {(user: object) => object} updateUser User wrapper
748
+ * Middleware to load full user data (works for both SESSION and TOKEN modes)
740
749
  * @returns {import('@types/express').RequestHandler} Returns express Request Handler
741
750
  */
742
- #userHandler (updateUser) {
743
- return (req, res, next) => {
744
- req.user = req.session[this.#getSessionKey()];
745
- /** @type {import('@types/express').Request & { user: object }} Session user */
746
- res.locals.user = updateUser(req.user);
747
- 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
+ }
748
760
  };
749
- }
761
+ };
750
762
 
751
763
  /**
752
764
  * Resource protection based on configured SESSION_MODE
753
- * @param {boolean} [isDebugging=false] Debugging flag
754
- * @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
755
771
  * @returns {import('@types/express').RequestHandler} Returns express Request Handler
756
- */
757
- 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 = '') {
758
788
  return async (req, res, next) => {
759
789
  const mode = this.#config.SESSION_MODE || SessionMode.SESSION;
760
790
  if (mode === SessionMode.TOKEN) {
761
- return this.#verifyToken(req, res, next, isDebugging, redirectUrl);
791
+ return this.#verifyToken(req, res, next, errorRedirectUrl);
762
792
  }
763
- return this.#verifySession(req, res, next, isDebugging, redirectUrl);
793
+ return this.#verifySession(req, res, next, errorRedirectUrl);
764
794
  };
765
795
  }
766
796
 
767
797
  /**
768
798
  * Resource protection by token (explicit token verification)
769
- * @param {boolean} [isDebugging=false] Debugging flag
770
- * @param {string} [redirectUrl=''] Redirect URL
799
+ * @param {string} [errorRedirectUrl=''] Redirect URL
771
800
  * @returns {import('@types/express').RequestHandler} Returns express Request Handler
772
801
  */
773
- verifyToken(isDebugging = false, redirectUrl = '') {
802
+ verifyToken(errorRedirectUrl = '') {
774
803
  return async (req, res, next) => {
775
- return this.#verifyToken(req, res, next, isDebugging, redirectUrl);
804
+ return this.#verifyToken(req, res, next, errorRedirectUrl);
776
805
  };
777
806
  }
778
807
 
779
808
  /**
780
809
  * Resource protection by session (explicit session verification)
781
- * @param {boolean} [isDebugging=false] Debugging flag
782
- * @param {string} [redirectUrl=''] Redirect URL
810
+ * @param {string} [errorRedirectUrl=''] Redirect URL
783
811
  * @returns {import('@types/express').RequestHandler} Returns express Request Handler
784
812
  */
785
- verifySession(isDebugging = false, redirectUrl = '') {
813
+ verifySession(errorRedirectUrl = '') {
786
814
  return async (req, res, next) => {
787
- return this.#verifySession(req, res, next, isDebugging, redirectUrl);
815
+ return this.#verifySession(req, res, next, errorRedirectUrl);
788
816
  };
789
817
  }
790
818
 
@@ -813,62 +841,75 @@ export class SessionManager {
813
841
  });
814
842
  }
815
843
  throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload');
816
- };
817
-
818
- /**
819
- * SSO callback for successful login
820
- * @param {(user: object) => object} initUser Initialize user object function
821
- * @returns {import('@types/express').RequestHandler} Returns express Request Handler
822
- */
823
- callback(initUser) {
824
- return async (req, res, next) => {
825
- const { jwt = '' } = req.query;
826
- if (!jwt) {
827
- return next(new CustomError(httpCodes.BAD_REQUEST, 'Missing `jwt` in query parameters'));
828
- }
829
-
830
- try {
831
- // Decrypt JWT from Identity Adapter
832
- const { payload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
833
-
834
- if (!payload?.user) {
835
- 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'));
836
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
+ }
837
896
 
838
- /** @type {import('../index.js').SessionUser} */
839
- const user = initUser(payload.user);
840
- /** @type {string} */
841
- const redirectUrl = payload.redirect_url || this.#config.SSO_SUCCESS_URL;
842
-
843
- // Check SESSION_MODE to determine response type
844
- if (this.#config.SESSION_MODE === SessionMode.TOKEN) {
845
- // Token-based: Generate token and return HTML page that stores it
846
- const token = await this.#generateAndStoreToken(user);
847
-
848
- this.#logger.debug('### CALLBACK TOKEN GENERATED ###');
849
-
850
- const templatePath = this.#config.TOKEN_STORAGE_TEMPLATE_PATH || path.resolve(__dirname, 'assets', 'template.html');
851
- // Return HTML page that stores token in localStorage and redirects
852
- const template = fs.readFileSync(templatePath, 'utf8');
853
- const html = template
854
- .replaceAll('{{SESSION_DATA_KEY}}', this.#config.SESSION_KEY)
855
- .replaceAll('{{SESSION_DATA_VALUE}}', token)
856
- .replaceAll('{{SESSION_EXPIRY_KEY}}', this.#config.SESSION_EXPIRY_KEY)
857
- .replaceAll('{{SESSION_EXPIRY_VALUE}}', user.attributes.expires_at)
858
- .replaceAll('{{SSO_SUCCESS_URL}}', redirectUrl)
859
- .replaceAll('{{SSO_FAILURE_URL}}', this.#config.SSO_FAILURE_URL);
860
- return res.send(html);
897
+ // Session mode: Save to session and redirect
898
+ await this.#saveSession(req, jwt, initUser);
899
+ return res.redirect(callbackRedirectUrl);
861
900
  }
862
- // Session-based: Save to session and redirect
863
- await this.#saveSession(req, jwt, initUser);
864
- return res.redirect(redirectUrl);
865
- }
866
- catch (error) {
867
- this.#logger.error('### CALLBACK ERROR ###', error);
868
- return res.redirect(this.#config.SSO_FAILURE_URL.concat('?message=').concat(encodeURIComponent(error.message)));
869
- }
870
- };
871
- }
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
+ }
872
913
 
873
914
  /**
874
915
  * Get Identity Providers
@@ -896,14 +937,14 @@ export class SessionManager {
896
937
  * @returns {import('@types/express').RequestHandler} Returns express Request Handler
897
938
  */
898
939
  refresh(initUser) {
899
- 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);
900
941
  return async (req, res, next) => {
901
942
  const mode = this.#config.SESSION_MODE || SessionMode.SESSION;
902
-
943
+
903
944
  if (mode === SessionMode.TOKEN) {
904
- return this.#refreshToken(req, res, next, initUser, idpUrl);
945
+ return this.#refreshToken(req, res, next, initUser, idpRefreshUrl);
905
946
  } else {
906
- return this.#refreshSession(req, res, next, initUser, idpUrl);
947
+ return this.#refreshSession(req, res, next, initUser, idpRefreshUrl);
907
948
  }
908
949
  };
909
950
  }
package/index.d.ts CHANGED
@@ -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.14",
3
+ "version": "1.0.15",
4
4
  "description": "Node components for igxjs",
5
5
  "main": "index.js",
6
6
  "type": "module",