@igxjs/node-components 1.0.15 → 1.0.16

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.
@@ -2,7 +2,7 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <title>Sign in IBM Garage</title>
5
- <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no,viewport-fit=cover,minimum-scale=1,maximum-scale=1,user-scalable=no">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no,viewport-fit=cover,minimum-scale=1,maximum-scale=2">
6
6
  <meta name="robots" content="noindex, nofollow" />
7
7
  <style>
8
8
  :root {
@@ -94,9 +94,14 @@
94
94
  localStorage.setItem('{{SESSION_EXPIRY_KEY}}', '{{SESSION_EXPIRY_VALUE}}');
95
95
  }
96
96
 
97
+ /**
98
+ * Note: You can also request full user informlation by using ajax request or loading server side page.
99
+ */
100
+
97
101
  // Fall back to simple navigation
98
102
  location.href = '{{SSO_SUCCESS_URL}}';
99
- } catch (e) {
103
+ }
104
+ catch (e) {
100
105
  console.error('Redirect failed:', e);
101
106
  success = false;
102
107
  }
@@ -272,7 +272,7 @@ export class SessionManager {
272
272
  * @private
273
273
  */
274
274
  #getTokenRedisKey(email, tid) {
275
- return `${this.#config.SESSION_KEY}:t:${email}:${tid}`;
275
+ return `${this.#config.SESSION_KEY}:${email}:${tid}`;
276
276
  }
277
277
 
278
278
  /**
@@ -282,7 +282,7 @@ export class SessionManager {
282
282
  * @private
283
283
  */
284
284
  #getTokenRedisPattern(email) {
285
- return `${this.#config.SESSION_KEY}:t:${email}:*`;
285
+ return `${this.#config.SESSION_KEY}:${email}:*`;
286
286
  }
287
287
 
288
288
  /**
@@ -311,12 +311,12 @@ export class SessionManager {
311
311
  });
312
312
  app.set('trust proxy', 1);
313
313
  const isOK = await this.#redisManager.connect(this.#config.REDIS_URL, this.#config.REDIS_CERT_PATH);
314
- if (this.#config.SESSION_MODE === SessionMode.SESSION) {
314
+ if (this.getSessionMode() === SessionMode.SESSION) {
315
315
  app.use(this.#sessionHandler(isOK));
316
316
  }
317
317
 
318
318
  // Cache HTML template for TOKEN mode
319
- if (this.#config.SESSION_MODE === SessionMode.TOKEN) {
319
+ if (this.getSessionMode() === SessionMode.TOKEN) {
320
320
  const templatePath = this.#config.TOKEN_STORAGE_TEMPLATE_PATH ||
321
321
  path.resolve(__dirname, 'assets', 'template.html');
322
322
  this.#htmlTemplate = fs.readFileSync(templatePath, 'utf8');
@@ -324,27 +324,37 @@ export class SessionManager {
324
324
  }
325
325
  }
326
326
 
327
+ /**
328
+ * Generate lightweight JWT token
329
+ * @param {string} email User email
330
+ * @param {string} tokenId Token ID
331
+ * @param {number} expirationTime Expiration time in seconds
332
+ * @returns {Promise<string>} Returns the generated JWT token
333
+ * @private
334
+ */
335
+ async #getLightweightToken(email, tokenId, expirationTime) {
336
+ return await this.#jwtManager.encrypt({ email, tid: tokenId }, this.#config.SSO_CLIENT_SECRET, { expirationTime });
337
+ }
338
+
327
339
  /**
328
340
  * Generate and store JWT token in Redis
329
341
  * - JWT payload contains only { email, tid } for minimal size
330
342
  * - Full user data is stored in Redis as single source of truth
331
- * @param {object} user User object with email and attributes
343
+ * @param {string} tid Token ID
344
+ * @param {Record<string, any> & { email: string, tid: string }} user User object with email and attributes
332
345
  * @returns {Promise<string>} Returns the generated JWT token
333
346
  * @throws {Error} If JWT encryption fails
334
347
  * @throws {Error} If Redis storage fails
335
348
  * @private
336
349
  * @example
337
- * const token = await this.#generateAndStoreToken({
350
+ * const token = await this.#generateAndStoreToken('tid', {
338
351
  * email: 'user@example.com',
339
352
  * attributes: { /* user data * / }
340
353
  * });
341
354
  */
342
- async #generateAndStoreToken(user) {
343
- // Generate unique token ID for this device/session
344
- const tid = crypto.randomUUID();
345
- // Create JWT token with only email and tid (minimal payload)
346
- const payload = { email: user.email, tid };
347
- const token = await this.#jwtManager.encrypt(payload, this.#config.SESSION_SECRET, { expirationTime: this.#config.SESSION_AGE });
355
+ async #generateAndStoreToken(tid, user) {
356
+ // Create JWT token with only email, tid and idp (minimal payload)
357
+ const token = await this.#getLightweightToken(user.email, tid, this.#config.SESSION_AGE);
348
358
 
349
359
  // Store user data in Redis with TTL
350
360
  const redisKey = this.#getTokenRedisKey(user.email, tid);
@@ -357,28 +367,28 @@ export class SessionManager {
357
367
  /**
358
368
  * Extract and validate user data from Authorization header (TOKEN mode only)
359
369
  * @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)
370
+ * @param {boolean} [includeUserData=true] Whether to include full user data in response
371
+ * - true: Returns { tid, user } with full user data (default)
362
372
  * - 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
373
+ * @returns {Promise<{ tid: string, user: { email: string, attributes: { expires_at: number, sub: string } } } & object>}
374
+ * - When includeUserData=true: { tid: string, user: object }
375
+ * - When includeUserData=false: JWT payload object
366
376
  * @throws {CustomError} UNAUTHORIZED (401) if:
367
377
  * - Authorization header is missing or invalid format
368
378
  * - Token decryption fails
369
379
  * - Token payload is invalid (missing email/tid)
370
- * - Token not found in Redis (when fetchFromRedis=true)
380
+ * - Token not found in Redis (when includeUserData=true)
371
381
  * @private
372
382
  */
373
- async #getUserFromToken(authHeader, fetchFromRedis = true) {
383
+ async #getUserFromToken(authHeader, includeUserData = true) {
374
384
  if (!authHeader?.startsWith('Bearer ')) {
375
385
  throw new CustomError(httpCodes.UNAUTHORIZED, 'Missing or invalid authorization header');
376
386
  }
377
387
  const token = authHeader.substring(7); // Remove 'Bearer ' prefix
378
- // Decrypt JWT token
379
- const { payload } = await this.#jwtManager.decrypt(token, this.#config.SESSION_SECRET);
388
+ /** @type {{ payload: { email: string, tid: string } & import('jose').JWTPayload }} */
389
+ const { payload } = await this.#jwtManager.decrypt(token, this.#config.SSO_CLIENT_SECRET);
380
390
 
381
- if (fetchFromRedis) {
391
+ if (includeUserData) {
382
392
  /** @type {{ email: string, tid: string }} Extract email and token ID */
383
393
  const { email, tid } = payload;
384
394
  if (!email || !tid) {
@@ -392,14 +402,15 @@ export class SessionManager {
392
402
  if (!userData) {
393
403
  throw new CustomError(httpCodes.UNAUTHORIZED, 'Token not found or expired');
394
404
  }
395
- return { tid, user: JSON.parse(userData) };
405
+ return { tid, user: { ...JSON.parse(userData) } };
396
406
  }
397
- return payload;
407
+ return { tid: payload.tid, user: { email: payload.email, attributes: { sub: payload.sub, expires_at: payload.exp ? payload.exp * 1000 : 0 } } };
398
408
  }
399
409
 
400
410
  /**
401
411
  * Get authenticated user data (works for both SESSION and TOKEN modes)
402
412
  * @param {import('@types/express').Request} req Express request object
413
+ * @param {boolean} [includeUserData=false] Whether to include full user data in response
403
414
  * @returns {Promise<object>} Full user data object
404
415
  * @throws {CustomError} If user is not authenticated
405
416
  * @public
@@ -415,9 +426,9 @@ export class SessionManager {
415
426
  * }
416
427
  * });
417
428
  */
418
- async getUser(req) {
419
- if (this.#config.SESSION_MODE === SessionMode.TOKEN) {
420
- const { user } = await this.#getUserFromToken(req.headers.authorization, true);
429
+ async getUser(req, includeUserData = false) {
430
+ if (this.getSessionMode() === SessionMode.TOKEN) {
431
+ const { user } = await this.#getUserFromToken(req.headers.authorization, includeUserData);
421
432
  return user;
422
433
  }
423
434
  // Session mode
@@ -484,17 +495,16 @@ export class SessionManager {
484
495
  * @param {import('@types/express').Request} req Request with Authorization header
485
496
  * @param {import('@types/express').Response} res Response object
486
497
  * @param {import('@types/express').NextFunction} next Next middleware function
487
- * @param {(user: object) => object} initUser Function to initialize/transform user object
498
+ * @param {(user: object) => object & { email: string }} initUser Function to initialize/transform user object
488
499
  * @param {string} idpRefreshUrl Identity provider refresh endpoint URL
489
500
  * @throws {CustomError} If refresh lock is active or SSO refresh fails
490
501
  * @private
491
502
  * @example
492
503
  * // Response format:
493
- * // { jwt: "new_jwt", user: {...}, expires_at: 64800, token_type: "Bearer" }
504
+ * // { jwt: "new_jwt", user: {...} }
494
505
  */
495
506
  async #refreshToken(req, res, next, initUser, idpRefreshUrl) {
496
507
  try {
497
- /** @type {{ tid: string, user: { email: string, attributes: { idp: string, refresh_token: string }? } }} */
498
508
  const { tid, user } = await this.#getUserFromToken(req.headers.authorization, true);
499
509
 
500
510
  // Check refresh lock
@@ -505,12 +515,11 @@ export class SessionManager {
505
515
 
506
516
  // Call SSO refresh endpoint
507
517
  const response = await this.#idpRequest.post(idpRefreshUrl, {
508
- user: {
509
- email: user?.email,
510
- attributes: {
511
- idp: user?.attributes?.idp,
512
- refresh_token: user?.attributes?.refresh_token
513
- }
518
+ idp: user?.attributes?.idp,
519
+ refresh_token: user?.attributes?.refresh_token
520
+ }, {
521
+ headers: {
522
+ Authorization: req.headers.authorization
514
523
  }
515
524
  });
516
525
 
@@ -530,16 +539,12 @@ export class SessionManager {
530
539
  const newUser = initUser(newPayload.user);
531
540
 
532
541
  // Generate new token
533
- const newToken = await this.#generateAndStoreToken(newUser);
534
-
535
- // Remove old token from Redis
536
- const oldRedisKey = this.#getTokenRedisKey(user.email, tid);
537
- await this.#redisManager.getClient().del(oldRedisKey);
542
+ const newToken = await this.#generateAndStoreToken(tid, newUser);
538
543
 
539
544
  this.#logger.debug('### TOKEN REFRESHED SUCCESSFULLY ###');
540
545
 
541
546
  // Return new token
542
- return res.json({ jwt: newToken, user: newUser, expires_at: this.#config.SESSION_AGE, token_type: 'Bearer' });
547
+ return res.json({ jwt: newToken, user: newUser });
543
548
  } catch (error) {
544
549
  return next(httpHelper.handleAxiosError(error));
545
550
  }
@@ -557,23 +562,30 @@ export class SessionManager {
557
562
  async #refreshSession(req, res, next, initUser, idpRefreshUrl) {
558
563
  try {
559
564
  const { email, attributes } = req.user || { email: '', attributes: {} };
565
+ // Check refresh lock
560
566
  if (this.hasLock(email)) {
561
- throw new CustomError(httpCodes.CONFLICT, 'User refresh is locked');
567
+ throw new CustomError(httpCodes.CONFLICT, 'Session refresh is locked');
562
568
  }
563
569
  this.lock(email);
570
+
571
+ /** @type {string} */
572
+ const token = await this.#getLightweightToken(email, req.sessionID, req.user.attributes.expires_at);
573
+
574
+ // Call SSO refresh endpoint
564
575
  const response = await this.#idpRequest.post(idpRefreshUrl, {
565
- user: {
566
- email,
567
- attributes: {
568
- idp: attributes?.idp,
569
- refresh_token: attributes?.refresh_token
570
- }
576
+ idp: attributes?.idp,
577
+ refresh_token: attributes?.refresh_token,
578
+ }, {
579
+ headers: {
580
+ Authorization: `Bearer ${token}`
571
581
  }
572
582
  });
573
583
  if (response.status === httpCodes.OK) {
584
+ /** @type {{ jwt: string }} */
574
585
  const { jwt } = response.data;
575
- const payload = await this.#saveSession(req, jwt, initUser);
576
- return res.json(payload);
586
+ const { payload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
587
+ const result = await this.#saveSession(req, payload, initUser);
588
+ return res.json(result);
577
589
  }
578
590
  throw new CustomError(response.status, response.statusText);
579
591
  } catch (error) {
@@ -643,8 +655,8 @@ export class SessionManager {
643
655
 
644
656
  try {
645
657
  // Extract Token ID and email from current token
646
- const payload = await this.#getUserFromToken(req.headers.authorization, false);
647
- const { email, tid } = payload;
658
+ const { tid, user } = await this.#getUserFromToken(req.headers.authorization, false);
659
+ const { email } = user;
648
660
 
649
661
  if (!email || !tid) {
650
662
  throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid token payload');
@@ -749,9 +761,9 @@ export class SessionManager {
749
761
  * @returns {import('@types/express').RequestHandler} Returns express Request Handler
750
762
  */
751
763
  requireUser = () => {
752
- return async (req, res, next) => {
764
+ return async (req, _res, next) => {
753
765
  try {
754
- req.user = await this.getUser(req);
766
+ req.user = await this.getUser(req, true);
755
767
  return next();
756
768
  }
757
769
  catch (error) {
@@ -786,7 +798,7 @@ export class SessionManager {
786
798
  */
787
799
  authenticate(errorRedirectUrl = '') {
788
800
  return async (req, res, next) => {
789
- const mode = this.#config.SESSION_MODE || SessionMode.SESSION;
801
+ const mode = this.getSessionMode() || SessionMode.SESSION;
790
802
  if (mode === SessionMode.TOKEN) {
791
803
  return this.#verifyToken(req, res, next, errorRedirectUrl);
792
804
  }
@@ -819,13 +831,11 @@ export class SessionManager {
819
831
  /**
820
832
  * Save session
821
833
  * @param {import('@types/express').Request} request Request object
822
- * @param {string} jwt JWT
834
+ * @param {import('jose').JWTPayload} payload JWT
823
835
  * @param {(user: object) => object} initUser Redirect URL
824
836
  * @returns {Promise<{ user: import('../models/types/user').UserModel, redirect_url: string }>} Promise
825
837
  */
826
- #saveSession = async (request, jwt, initUser) => {
827
- /** @type {{ payload: { user: import('../models/types/user').UserModel, redirect_url: string } }} */
828
- const { payload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
838
+ #saveSession = async (request, payload, initUser) => {
829
839
  if (payload?.user) {
830
840
  this.#logger.debug('### CALLBACK USER ###');
831
841
  request.session[this.#getSessionKey()] = initUser(payload.user);
@@ -876,26 +886,28 @@ export class SessionManager {
876
886
  try {
877
887
  // Decrypt JWT from Identity Adapter
878
888
  const { payload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
879
-
889
+
880
890
  if (!payload?.user) {
881
891
  throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload');
882
892
  }
883
893
 
884
- /** @type {import('../index.js').SessionUser} */
885
- const user = initUser(payload.user);
886
894
  /** @type {string} */
887
895
  const callbackRedirectUrl = payload.redirect_url || this.#config.SSO_SUCCESS_URL;
888
896
 
889
897
  // Token mode: Generate token and return HTML page
890
- if (this.#config.SESSION_MODE === SessionMode.TOKEN) {
891
- const token = await this.#generateAndStoreToken(user);
898
+ if (this.getSessionMode() === SessionMode.TOKEN) {
899
+ /** @type {import('../index.js').SessionUser} */
900
+ const user = initUser(payload.user);
901
+ // Generate unique token ID for this device/session
902
+ const tid = crypto.randomUUID();
903
+ const token = await this.#generateAndStoreToken(tid, user);
892
904
  this.#logger.debug('### CALLBACK TOKEN GENERATED ###');
893
905
  const html = this.#renderTokenStorageHtml(token, user.attributes.expires_at, callbackRedirectUrl);
894
906
  return res.send(html);
895
907
  }
896
908
 
897
909
  // Session mode: Save to session and redirect
898
- await this.#saveSession(req, jwt, initUser);
910
+ await this.#saveSession(req, payload, initUser);
899
911
  return res.redirect(callbackRedirectUrl);
900
912
  }
901
913
  catch (error) {
@@ -939,11 +951,12 @@ export class SessionManager {
939
951
  refresh(initUser) {
940
952
  const idpRefreshUrl = '/auth/refresh'.concat('?client_id=').concat(this.#config.SSO_CLIENT_ID);
941
953
  return async (req, res, next) => {
942
- const mode = this.#config.SESSION_MODE || SessionMode.SESSION;
954
+ const mode = this.getSessionMode() || SessionMode.SESSION;
943
955
 
944
956
  if (mode === SessionMode.TOKEN) {
945
957
  return this.#refreshToken(req, res, next, initUser, idpRefreshUrl);
946
- } else {
958
+ }
959
+ else {
947
960
  return this.#refreshSession(req, res, next, initUser, idpRefreshUrl);
948
961
  }
949
962
  };
@@ -958,8 +971,8 @@ export class SessionManager {
958
971
  const { redirect = false, all = false } = req.query;
959
972
  const isRedirect = (redirect === 'true' || redirect === true);
960
973
  const logoutAll = (all === 'true' || all === true);
961
- const mode = this.#config.SESSION_MODE || SessionMode.SESSION;
962
-
974
+ const mode = this.getSessionMode() || SessionMode.SESSION;
975
+
963
976
  if (mode === SessionMode.TOKEN) {
964
977
  return this.#logoutToken(req, res, isRedirect, logoutAll);
965
978
  }
@@ -985,4 +998,11 @@ export class SessionManager {
985
998
  };
986
999
  }
987
1000
 
1001
+ /**
1002
+ * Get session mode
1003
+ * @returns {string} Session mode
1004
+ */
1005
+ getSessionMode() {
1006
+ return this.#config.SESSION_MODE;
1007
+ }
988
1008
  }
package/index.d.ts CHANGED
@@ -316,6 +316,7 @@ export class SessionManager {
316
316
  /**
317
317
  * Get authenticated user data (works for both SESSION and TOKEN modes)
318
318
  * @param req Express request object
319
+ * @param includeUserData Include user data in the response (default: false)
319
320
  * @returns Promise resolving to full user data object
320
321
  * @throws CustomError If user is not authenticated
321
322
  * @example
@@ -323,7 +324,7 @@ export class SessionManager {
323
324
  * // Use in custom middleware
324
325
  * app.use(async (req, res, next) => {
325
326
  * try {
326
- * const user = await sessionManager.getUser(req);
327
+ * const user = await sessionManager.getUser(req, true);
327
328
  * req.customUser = user;
328
329
  * next();
329
330
  * } catch (error) {
@@ -332,7 +333,7 @@ export class SessionManager {
332
333
  * });
333
334
  * ```
334
335
  */
335
- getUser(req: Request): Promise<SessionUser>;
336
+ getUser(req: Request, includeUserData: boolean?): Promise<SessionUser>;
336
337
 
337
338
  /**
338
339
  * Initialize the session configurations and middleware
@@ -439,6 +440,12 @@ export class SessionManager {
439
440
  * @returns Returns express Request Handler
440
441
  */
441
442
  logout(): RequestHandler;
443
+
444
+ /**
445
+ * Get the current session mode
446
+ * @returns Returns 'session' or 'token' based on configuration
447
+ */
448
+ getSessionMode(): string;
442
449
  }
443
450
 
444
451
  // Custom Error class
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igxjs/node-components",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "description": "Node components for igxjs",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -12,21 +12,7 @@
12
12
  "url": "git+https://github.com/igxjs/node-components.git"
13
13
  },
14
14
  "keywords": [
15
- "igxjs",
16
- "express",
17
- "session-management",
18
- "jwt",
19
- "jwe",
20
- "redis",
21
- "authentication",
22
- "sso",
23
- "oauth",
24
- "middleware",
25
- "node-components",
26
- "session",
27
- "token-auth",
28
- "bearer-token",
29
- "express-middleware"
15
+ "igxjs"
30
16
  ],
31
17
  "author": "Michael",
32
18
  "license": "Apache-2.0",
@@ -43,7 +29,7 @@
43
29
  "axios": "^1.13.6",
44
30
  "connect-redis": "^9.0.0",
45
31
  "express-session": "^1.19.0",
46
- "jose": "^6.2.1",
32
+ "jose": "^6.2.2",
47
33
  "memorystore": "^1.6.7"
48
34
  },
49
35
  "devDependencies": {