@igxjs/node-components 1.0.15 → 1.0.17

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
  }
@@ -161,12 +161,13 @@ export const httpHelper = {
161
161
  * @returns {string} Formatted string
162
162
  */
163
163
  format (str, ...args) {
164
+ let result = str;
164
165
  const matched = str.match(/{\d}/ig);
165
166
  matched.forEach((element, index) => {
166
167
  if(args.length > index)
167
- str = str.replace(element, args[index]);
168
+ result = result.replace(element, args[index]);
168
169
  });
169
- return str;
170
+ return result;
170
171
  },
171
172
 
172
173
  /**
@@ -209,4 +210,4 @@ export const httpHelper = {
209
210
  */
210
211
  export const httpError = (code, message, error, data) => {
211
212
  return new CustomError(code, message, error, data);
212
- };
213
+ };
package/components/jwt.js CHANGED
@@ -9,31 +9,31 @@ import { jwtDecrypt, EncryptJWT } from 'jose';
9
9
  export class JwtManager {
10
10
  /** @type {string} JWE algorithm */
11
11
  algorithm;
12
-
12
+
13
13
  /** @type {string} Encryption method */
14
14
  encryption;
15
-
15
+
16
16
  /** @type {number} Token expiration time */
17
17
  expirationTime;
18
-
18
+
19
19
  /** @type {number} Clock tolerance in seconds */
20
20
  clockTolerance;
21
-
21
+
22
22
  /** @type {string} Hash algorithm for secret derivation */
23
23
  secretHashAlgorithm;
24
-
24
+
25
25
  /** @type {string|null} Optional JWT issuer claim */
26
26
  issuer;
27
-
27
+
28
28
  /** @type {string|null} Optional JWT audience claim */
29
29
  audience;
30
-
30
+
31
31
  /** @type {string|null} Optional JWT subject claim */
32
32
  subject;
33
33
  /**
34
34
  * Create a new JwtManager instance with configurable defaults
35
35
  * Constructor options use UPPERCASE naming convention with JWT_ prefix (e.g., JWT_ALGORITHM).
36
- *
36
+ *
37
37
  * @typedef {Object} JwtManagerOptions JwtManager configuration options
38
38
  * @property {string} [JWT_ALGORITHM='dir'] JWE algorithm (default: 'dir')
39
39
  * @property {string} [JWT_ENCRYPTION='A256GCM'] Encryption method (default: 'A256GCM')
@@ -59,7 +59,7 @@ export class JwtManager {
59
59
 
60
60
  /**
61
61
  * Encrypt method options (camelCase naming convention, uses instance defaults when not provided)
62
- *
62
+ *
63
63
  * @typedef {Object} JwtEncryptOptions Encryption method options
64
64
  * @property {string} [algorithm='dir'] JWE algorithm (overrides instance JWT_ALGORITHM)
65
65
  * @property {string} [encryption='A256GCM'] Encryption method (overrides instance JWT_ENCRYPTION)
@@ -71,7 +71,7 @@ export class JwtManager {
71
71
  */
72
72
  /**
73
73
  * Generate JWT token for user session
74
- *
74
+ *
75
75
  * @param {import('jose').JWTPayload} data User data payload
76
76
  * @param {string} secret Secret key or password for encryption
77
77
  * @param {JwtEncryptOptions} [options] Per-call configuration overrides (camelCase naming convention)
@@ -113,7 +113,7 @@ export class JwtManager {
113
113
 
114
114
  /**
115
115
  * Decrypt method options (camelCase naming convention, uses instance defaults when not provided)
116
- *
116
+ *
117
117
  * @typedef {Object} JwtDecryptOptions Decryption method options
118
118
  * @property {number} [clockTolerance=30] Clock tolerance in seconds (overrides instance JWT_CLOCK_TOLERANCE)
119
119
  * @property {string} [secretHashAlgorithm='SHA-256'] Hash algorithm for secret derivation (overrides instance JWT_SECRET_HASH_ALGORITHM)
@@ -123,7 +123,7 @@ export class JwtManager {
123
123
  **/
124
124
  /**
125
125
  * Decrypt JWT
126
- *
126
+ *
127
127
  * @param {string} token JWT token to decrypt
128
128
  * @param {string} secret Secret key or password for decryption
129
129
  * @param {JwtDecryptOptions} [options] Per-call configuration overrides (camelCase naming convention)
@@ -2,20 +2,20 @@
2
2
  * Logger utility for node-components
3
3
  * Provides configurable logging with enable/disable functionality
4
4
  * Uses singleton pattern to manage logger instances per component
5
- *
5
+ *
6
6
  * @example
7
7
  * import { Logger } from './logger.js';
8
- *
8
+ *
9
9
  * // Recommended: Get logger instance (singleton pattern)
10
10
  * const logger = Logger.getInstance('ComponentName');
11
- *
11
+ *
12
12
  * // With explicit enable/disable
13
13
  * const logger = Logger.getInstance('ComponentName', true); // Always enabled
14
14
  * const logger = Logger.getInstance('ComponentName', false); // Always disabled
15
- *
15
+ *
16
16
  * // Backward compatibility: Constructor still works
17
17
  * const logger = new Logger('ComponentName');
18
- *
18
+ *
19
19
  * // Use logger
20
20
  * logger.info('Operation completed');
21
21
  * logger.error('Error occurred', error);
@@ -23,10 +23,10 @@
23
23
  export class Logger {
24
24
  /** @type {Map<string, Logger>} */
25
25
  static #instances = new Map();
26
-
26
+
27
27
  /** @type {boolean} - Global flag to enable/disable colors */
28
28
  static #colorsEnabled = true;
29
-
29
+
30
30
  /** ANSI color codes for different log levels */
31
31
  static #colors = {
32
32
  reset: '\x1b[0m',
@@ -35,7 +35,7 @@ export class Logger {
35
35
  yellow: '\x1b[33m', // warn - yellow
36
36
  red: '\x1b[31m', // error - red
37
37
  };
38
-
38
+
39
39
  /** @type {boolean} */
40
40
  #enabled;
41
41
  /** @type {string} */
@@ -51,11 +51,11 @@ export class Logger {
51
51
  */
52
52
  static getInstance(componentName, enableLogging) {
53
53
  const key = `${componentName}:${enableLogging ?? 'default'}`;
54
-
54
+
55
55
  if (!Logger.#instances.has(key)) {
56
56
  Logger.#instances.set(key, new Logger(componentName, enableLogging));
57
57
  }
58
-
58
+
59
59
  return Logger.#instances.get(key);
60
60
  }
61
61
 
@@ -90,20 +90,20 @@ export class Logger {
90
90
  // Otherwise, default to enabled in development, disabled in production
91
91
  this.#enabled = enableLogging ?? (process.env.NODE_ENV !== 'production');
92
92
  this.#prefix = `[${componentName}]`;
93
-
93
+
94
94
  // Determine if colors should be used:
95
95
  // - Colors are globally enabled
96
96
  // - Output is a terminal (TTY)
97
97
  // - NO_COLOR environment variable is not set
98
- this.#useColors = Logger.#colorsEnabled &&
99
- process.stdout.isTTY &&
98
+ this.#useColors = Logger.#colorsEnabled &&
99
+ process.stdout.isTTY &&
100
100
  !process.env.NO_COLOR;
101
-
101
+
102
102
  // Assign methods based on enabled flag to eliminate runtime checks
103
103
  // This improves performance by avoiding conditional checks on every log call
104
104
  if (this.#enabled) {
105
105
  const colors = Logger.#colors;
106
-
106
+
107
107
  if (this.#useColors) {
108
108
  // Logging enabled with colors: colorize the prefix
109
109
  this.debug = (...args) => console.debug(colors.dim + this.#prefix + colors.reset, ...args);
@@ -128,4 +128,4 @@ export class Logger {
128
128
  this.log = () => {};
129
129
  }
130
130
  }
131
- }
131
+ }
@@ -100,4 +100,4 @@ export class RedisManager {
100
100
  await this.#client.quit();
101
101
  this.#logger.info('### REDIS DISCONNECTED ###');
102
102
  }
103
- }
103
+ }
@@ -1,22 +1,22 @@
1
1
  /**
2
2
  * FlexRouter for expressjs
3
- *
3
+ *
4
4
  * @example
5
5
  * import { Router } from 'express';
6
6
  * import { FlexRouter } from '../models/router.js';
7
7
  * import { authenticate } from '../middlewares/common.js';
8
- *
8
+ *
9
9
  * export const publicRouter = Router();
10
10
  * export const privateRouter = Router();
11
- *
11
+ *
12
12
  * publicRouter.get('/health', (req, res) => {
13
13
  * res.send('OK');
14
14
  * });
15
- *
15
+ *
16
16
  * privateRouter.get('/', (req, res) => {
17
17
  * res.send('Hello World!');
18
18
  * });
19
- *
19
+ *
20
20
  * export const routers = [
21
21
  * new FlexRouter('/api/v1/protected', privateRouter, [authenticate]),
22
22
  * new FlexRouter('/api/v1/public', healthRouter),
@@ -28,15 +28,15 @@ export const SessionMode = {
28
28
  * Session configuration options
29
29
  */
30
30
  export class SessionConfig {
31
- /**
31
+ /**
32
32
  * @type {string} Authentication mode for protected routes
33
33
  * - Supported values: SessionMode.SESSION | SessionMode.TOKEN
34
34
  * @default SessionMode.SESSION
35
35
  */
36
36
  SESSION_MODE;
37
- /**
37
+ /**
38
38
  * @type {string} Identity Provider microservice endpoint URL
39
- *
39
+ *
40
40
  * This is a fully customized, independent microservice that provides SSO authentication.
41
41
  * The endpoint serves multiple applications and provides the following APIs:
42
42
  * - GET /auth/providers - List supported identity providers
@@ -44,7 +44,7 @@ export class SessionConfig {
44
44
  * - POST /auth/verify - Verify JWT token validity
45
45
  * - GET /auth/callback/:idp - Validate authentication and return user data
46
46
  * - POST /auth/refresh - Refresh access tokens
47
- *
47
+ *
48
48
  * @example
49
49
  * SSO_ENDPOINT_URL: 'https://idp.example.com/open/api/v1'
50
50
  */
@@ -67,19 +67,19 @@ export class SessionConfig {
67
67
  SESSION_COOKIE_PATH;
68
68
  /** @type {string} Session secret */
69
69
  SESSION_SECRET;
70
- /**
71
- * @type {string}
70
+ /**
71
+ * @type {string}
72
72
  * @default 'ibmid:'
73
73
  */
74
74
  SESSION_PREFIX;
75
- /**
75
+ /**
76
76
  * @type {string} Session key
77
77
  * - In the `SessionMode.SESSION` mode, this is the key used to store the user in the session.
78
78
  * - In the `SessionMode.TOKEN` mode, this is the key of localStorage where the user is stored.
79
79
  * @default 'session_token'
80
80
  */
81
81
  SESSION_KEY;
82
- /**
82
+ /**
83
83
  * @type {string} Session expiry key
84
84
  * - In the `SessionMode.TOKEN` mode, this is the key of localStorage where the session expiry timestamp is stored.
85
85
  * @default 'session_expires_at'
@@ -141,26 +141,33 @@ export class SessionManager {
141
141
  #htmlTemplate = null;
142
142
 
143
143
  /**
144
- * Create a new session manager
145
- * @param {SessionConfig} config Session configuration
146
- * @throws {TypeError} If config is not an object
147
- * @throws {Error} If required configuration fields are missing
144
+ * Validate config is an object
145
+ * @param {SessionConfig} config
146
+ * @throws {TypeError}
148
147
  */
149
- constructor(config) {
150
- // Validate config is an object
148
+ #validateConfigType(config) {
151
149
  if (!config || typeof config !== 'object') {
152
150
  throw new TypeError('SessionManager configuration must be an object');
153
151
  }
152
+ }
154
153
 
155
- // Validate required fields based on SESSION_MODE
156
- const mode = config.SESSION_MODE || SessionMode.SESSION;
157
-
158
- // SESSION_SECRET is always required for both modes
154
+ /**
155
+ * Validate required configuration fields
156
+ * @param {SessionConfig} config
157
+ * @throws {Error}
158
+ */
159
+ #validateRequiredFields(config) {
159
160
  if (!config.SESSION_SECRET) {
160
161
  throw new Error('SESSION_SECRET is required for SessionManager');
161
162
  }
163
+ }
162
164
 
163
- // Validate SSO configuration if SSO endpoints are used
165
+ /**
166
+ * Validate SSO configuration
167
+ * @param {SessionConfig} config
168
+ * @throws {Error}
169
+ */
170
+ #validateSsoConfig(config) {
164
171
  if (config.SSO_ENDPOINT_URL) {
165
172
  if (!config.SSO_CLIENT_ID) {
166
173
  throw new Error('SSO_CLIENT_ID is required when SSO_ENDPOINT_URL is provided');
@@ -169,9 +176,15 @@ export class SessionManager {
169
176
  throw new Error('SSO_CLIENT_SECRET is required when SSO_ENDPOINT_URL is provided');
170
177
  }
171
178
  }
179
+ }
172
180
 
173
- // Validate TOKEN mode specific requirements
174
- if (mode === SessionMode.TOKEN) {
181
+ /**
182
+ * Validate TOKEN mode requirements
183
+ * @param {SessionConfig} config
184
+ * @throws {Error}
185
+ */
186
+ #validateTokenMode(config) {
187
+ if (config.SESSION_MODE === SessionMode.TOKEN) {
175
188
  if (!config.SSO_SUCCESS_URL) {
176
189
  throw new Error('SSO_SUCCESS_URL is required for TOKEN authentication mode');
177
190
  }
@@ -179,11 +192,16 @@ export class SessionManager {
179
192
  throw new Error('SSO_FAILURE_URL is required for TOKEN authentication mode');
180
193
  }
181
194
  }
195
+ }
182
196
 
183
- this.#config = {
184
- // Session Mode
197
+ /**
198
+ * Build configuration object
199
+ * @param {SessionConfig} config
200
+ * @returns {object}
201
+ */
202
+ #buildConfig(config) {
203
+ return {
185
204
  SESSION_MODE: config.SESSION_MODE || SessionMode.SESSION,
186
- // Session - SESSION_AGE is now in seconds (default: 64800 = 18 hours)
187
205
  SESSION_AGE: config.SESSION_AGE || 64800,
188
206
  SESSION_COOKIE_PATH: config.SESSION_COOKIE_PATH || '/',
189
207
  SESSION_SECRET: config.SESSION_SECRET,
@@ -191,18 +209,13 @@ export class SessionManager {
191
209
  SESSION_KEY: config.SESSION_KEY || 'session_token',
192
210
  SESSION_EXPIRY_KEY: config.SESSION_EXPIRY_KEY || 'session_expires_at',
193
211
  TOKEN_STORAGE_TEMPLATE_PATH: config.TOKEN_STORAGE_TEMPLATE_PATH,
194
-
195
- // Identity Provider
196
212
  SSO_ENDPOINT_URL: config.SSO_ENDPOINT_URL,
197
213
  SSO_CLIENT_ID: config.SSO_CLIENT_ID,
198
214
  SSO_CLIENT_SECRET: config.SSO_CLIENT_SECRET,
199
215
  SSO_SUCCESS_URL: config.SSO_SUCCESS_URL,
200
216
  SSO_FAILURE_URL: config.SSO_FAILURE_URL,
201
- // Redis
202
217
  REDIS_URL: config.REDIS_URL,
203
218
  REDIS_CERT_PATH: config.REDIS_CERT_PATH,
204
-
205
- // JWT Manager
206
219
  JWT_ALGORITHM: config.JWT_ALGORITHM || 'dir',
207
220
  JWT_ENCRYPTION: config.JWT_ENCRYPTION || 'A256GCM',
208
221
  JWT_CLOCK_TOLERANCE: config.JWT_CLOCK_TOLERANCE ?? 30,
@@ -213,6 +226,20 @@ export class SessionManager {
213
226
  };
214
227
  }
215
228
 
229
+ /**
230
+ * Create a new session manager
231
+ * @param {SessionConfig} config Session configuration
232
+ * @throws {TypeError} If config is not an object
233
+ * @throws {Error} If required configuration fields are missing
234
+ */
235
+ constructor(config) {
236
+ this.#validateConfigType(config);
237
+ this.#validateRequiredFields(config);
238
+ this.#validateSsoConfig(config);
239
+ this.#validateTokenMode(config);
240
+ this.#config = this.#buildConfig(config);
241
+ }
242
+
216
243
  /**
217
244
  * Check if the email has a session refresh lock
218
245
  * @param {string} email Email address
@@ -272,7 +299,7 @@ export class SessionManager {
272
299
  * @private
273
300
  */
274
301
  #getTokenRedisKey(email, tid) {
275
- return `${this.#config.SESSION_KEY}:t:${email}:${tid}`;
302
+ return `${this.#config.SESSION_KEY}:${email}:${tid}`;
276
303
  }
277
304
 
278
305
  /**
@@ -282,7 +309,7 @@ export class SessionManager {
282
309
  * @private
283
310
  */
284
311
  #getTokenRedisPattern(email) {
285
- return `${this.#config.SESSION_KEY}:t:${email}:*`;
312
+ return `${this.#config.SESSION_KEY}:${email}:*`;
286
313
  }
287
314
 
288
315
  /**
@@ -311,12 +338,12 @@ export class SessionManager {
311
338
  });
312
339
  app.set('trust proxy', 1);
313
340
  const isOK = await this.#redisManager.connect(this.#config.REDIS_URL, this.#config.REDIS_CERT_PATH);
314
- if (this.#config.SESSION_MODE === SessionMode.SESSION) {
341
+ if (this.getSessionMode() === SessionMode.SESSION) {
315
342
  app.use(this.#sessionHandler(isOK));
316
343
  }
317
344
 
318
345
  // Cache HTML template for TOKEN mode
319
- if (this.#config.SESSION_MODE === SessionMode.TOKEN) {
346
+ if (this.getSessionMode() === SessionMode.TOKEN) {
320
347
  const templatePath = this.#config.TOKEN_STORAGE_TEMPLATE_PATH ||
321
348
  path.resolve(__dirname, 'assets', 'template.html');
322
349
  this.#htmlTemplate = fs.readFileSync(templatePath, 'utf8');
@@ -324,27 +351,37 @@ export class SessionManager {
324
351
  }
325
352
  }
326
353
 
354
+ /**
355
+ * Generate lightweight JWT token
356
+ * @param {string} email User email
357
+ * @param {string} tokenId Token ID
358
+ * @param {number} expirationTime Expiration time in seconds
359
+ * @returns {Promise<string>} Returns the generated JWT token
360
+ * @private
361
+ */
362
+ async #getLightweightToken(email, tokenId, expirationTime) {
363
+ return await this.#jwtManager.encrypt({ email, tid: tokenId }, this.#config.SSO_CLIENT_SECRET, { expirationTime });
364
+ }
365
+
327
366
  /**
328
367
  * Generate and store JWT token in Redis
329
368
  * - JWT payload contains only { email, tid } for minimal size
330
369
  * - Full user data is stored in Redis as single source of truth
331
- * @param {object} user User object with email and attributes
370
+ * @param {string} tid Token ID
371
+ * @param {Record<string, any> & { email: string, tid: string }} user User object with email and attributes
332
372
  * @returns {Promise<string>} Returns the generated JWT token
333
373
  * @throws {Error} If JWT encryption fails
334
374
  * @throws {Error} If Redis storage fails
335
375
  * @private
336
376
  * @example
337
- * const token = await this.#generateAndStoreToken({
377
+ * const token = await this.#generateAndStoreToken('tid', {
338
378
  * email: 'user@example.com',
339
379
  * attributes: { /* user data * / }
340
380
  * });
341
381
  */
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 });
382
+ async #generateAndStoreToken(tid, user) {
383
+ // Create JWT token with only email, tid and idp (minimal payload)
384
+ const token = await this.#getLightweightToken(user.email, tid, this.#config.SESSION_AGE);
348
385
 
349
386
  // Store user data in Redis with TTL
350
387
  const redisKey = this.#getTokenRedisKey(user.email, tid);
@@ -357,28 +394,28 @@ export class SessionManager {
357
394
  /**
358
395
  * Extract and validate user data from Authorization header (TOKEN mode only)
359
396
  * @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)
397
+ * @param {boolean} [includeUserData=true] Whether to include full user data in response
398
+ * - true: Returns { tid, user } with full user data (default)
362
399
  * - 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
400
+ * @returns {Promise<{ tid: string, user: { email: string, attributes: { expires_at: number, sub: string } } } & object>}
401
+ * - When includeUserData=true: { tid: string, user: object }
402
+ * - When includeUserData=false: JWT payload object
366
403
  * @throws {CustomError} UNAUTHORIZED (401) if:
367
404
  * - Authorization header is missing or invalid format
368
405
  * - Token decryption fails
369
406
  * - Token payload is invalid (missing email/tid)
370
- * - Token not found in Redis (when fetchFromRedis=true)
407
+ * - Token not found in Redis (when includeUserData=true)
371
408
  * @private
372
409
  */
373
- async #getUserFromToken(authHeader, fetchFromRedis = true) {
410
+ async #getUserFromToken(authHeader, includeUserData = true) {
374
411
  if (!authHeader?.startsWith('Bearer ')) {
375
412
  throw new CustomError(httpCodes.UNAUTHORIZED, 'Missing or invalid authorization header');
376
413
  }
377
414
  const token = authHeader.substring(7); // Remove 'Bearer ' prefix
378
- // Decrypt JWT token
379
- const { payload } = await this.#jwtManager.decrypt(token, this.#config.SESSION_SECRET);
415
+ /** @type {{ payload: { email: string, tid: string } & import('jose').JWTPayload }} */
416
+ const { payload } = await this.#jwtManager.decrypt(token, this.#config.SSO_CLIENT_SECRET);
380
417
 
381
- if (fetchFromRedis) {
418
+ if (includeUserData) {
382
419
  /** @type {{ email: string, tid: string }} Extract email and token ID */
383
420
  const { email, tid } = payload;
384
421
  if (!email || !tid) {
@@ -392,14 +429,15 @@ export class SessionManager {
392
429
  if (!userData) {
393
430
  throw new CustomError(httpCodes.UNAUTHORIZED, 'Token not found or expired');
394
431
  }
395
- return { tid, user: JSON.parse(userData) };
432
+ return { tid, user: { ...JSON.parse(userData) } };
396
433
  }
397
- return payload;
434
+ return { tid: payload.tid, user: { email: payload.email, attributes: { sub: payload.sub, expires_at: payload.exp ? payload.exp * 1000 : 0 } } };
398
435
  }
399
436
 
400
437
  /**
401
438
  * Get authenticated user data (works for both SESSION and TOKEN modes)
402
439
  * @param {import('@types/express').Request} req Express request object
440
+ * @param {boolean} [includeUserData=false] Whether to include full user data in response
403
441
  * @returns {Promise<object>} Full user data object
404
442
  * @throws {CustomError} If user is not authenticated
405
443
  * @public
@@ -415,9 +453,9 @@ export class SessionManager {
415
453
  * }
416
454
  * });
417
455
  */
418
- async getUser(req) {
419
- if (this.#config.SESSION_MODE === SessionMode.TOKEN) {
420
- const { user } = await this.#getUserFromToken(req.headers.authorization, true);
456
+ async getUser(req, includeUserData = false) {
457
+ if (this.getSessionMode() === SessionMode.TOKEN) {
458
+ const { user } = await this.#getUserFromToken(req.headers.authorization, includeUserData);
421
459
  return user;
422
460
  }
423
461
  // Session mode
@@ -451,7 +489,7 @@ export class SessionManager {
451
489
  return next(new CustomError(httpCodes.UNAUTHORIZED, 'Token expired'));
452
490
  }
453
491
 
454
- return next(error instanceof CustomError ? error :
492
+ return next(error instanceof CustomError ? error :
455
493
  new CustomError(httpCodes.UNAUTHORIZED, 'Token verification failed'));
456
494
  }
457
495
  }
@@ -484,17 +522,16 @@ export class SessionManager {
484
522
  * @param {import('@types/express').Request} req Request with Authorization header
485
523
  * @param {import('@types/express').Response} res Response object
486
524
  * @param {import('@types/express').NextFunction} next Next middleware function
487
- * @param {(user: object) => object} initUser Function to initialize/transform user object
525
+ * @param {(user: object) => object & { email: string }} initUser Function to initialize/transform user object
488
526
  * @param {string} idpRefreshUrl Identity provider refresh endpoint URL
489
527
  * @throws {CustomError} If refresh lock is active or SSO refresh fails
490
528
  * @private
491
529
  * @example
492
530
  * // Response format:
493
- * // { jwt: "new_jwt", user: {...}, expires_at: 64800, token_type: "Bearer" }
531
+ * // { jwt: "new_jwt", user: {...} }
494
532
  */
495
533
  async #refreshToken(req, res, next, initUser, idpRefreshUrl) {
496
534
  try {
497
- /** @type {{ tid: string, user: { email: string, attributes: { idp: string, refresh_token: string }? } }} */
498
535
  const { tid, user } = await this.#getUserFromToken(req.headers.authorization, true);
499
536
 
500
537
  // Check refresh lock
@@ -505,12 +542,11 @@ export class SessionManager {
505
542
 
506
543
  // Call SSO refresh endpoint
507
544
  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
- }
545
+ idp: user?.attributes?.idp,
546
+ refresh_token: user?.attributes?.refresh_token
547
+ }, {
548
+ headers: {
549
+ Authorization: req.headers.authorization
514
550
  }
515
551
  });
516
552
 
@@ -530,16 +566,12 @@ export class SessionManager {
530
566
  const newUser = initUser(newPayload.user);
531
567
 
532
568
  // 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);
569
+ const newToken = await this.#generateAndStoreToken(tid, newUser);
538
570
 
539
571
  this.#logger.debug('### TOKEN REFRESHED SUCCESSFULLY ###');
540
572
 
541
573
  // Return new token
542
- return res.json({ jwt: newToken, user: newUser, expires_at: this.#config.SESSION_AGE, token_type: 'Bearer' });
574
+ return res.json({ jwt: newToken, user: newUser });
543
575
  } catch (error) {
544
576
  return next(httpHelper.handleAxiosError(error));
545
577
  }
@@ -557,23 +589,30 @@ export class SessionManager {
557
589
  async #refreshSession(req, res, next, initUser, idpRefreshUrl) {
558
590
  try {
559
591
  const { email, attributes } = req.user || { email: '', attributes: {} };
592
+ // Check refresh lock
560
593
  if (this.hasLock(email)) {
561
- throw new CustomError(httpCodes.CONFLICT, 'User refresh is locked');
594
+ throw new CustomError(httpCodes.CONFLICT, 'Session refresh is locked');
562
595
  }
563
596
  this.lock(email);
597
+
598
+ /** @type {string} */
599
+ const token = await this.#getLightweightToken(email, req.sessionID, req.user.attributes.expires_at);
600
+
601
+ // Call SSO refresh endpoint
564
602
  const response = await this.#idpRequest.post(idpRefreshUrl, {
565
- user: {
566
- email,
567
- attributes: {
568
- idp: attributes?.idp,
569
- refresh_token: attributes?.refresh_token
570
- }
603
+ idp: attributes?.idp,
604
+ refresh_token: attributes?.refresh_token,
605
+ }, {
606
+ headers: {
607
+ Authorization: `Bearer ${token}`
571
608
  }
572
609
  });
573
610
  if (response.status === httpCodes.OK) {
611
+ /** @type {{ jwt: string }} */
574
612
  const { jwt } = response.data;
575
- const payload = await this.#saveSession(req, jwt, initUser);
576
- return res.json(payload);
613
+ const { payload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
614
+ const result = await this.#saveSession(req, payload, initUser);
615
+ return res.json(result);
577
616
  }
578
617
  throw new CustomError(response.status, response.statusText);
579
618
  } catch (error) {
@@ -610,7 +649,7 @@ export class SessionManager {
610
649
  if (isRedirect) {
611
650
  return res.redirect(this.#config.SSO_SUCCESS_URL);
612
651
  }
613
- return res.json({
652
+ return res.json({
614
653
  message: 'All tokens logged out successfully',
615
654
  tokensRemoved: keys.length,
616
655
  redirect_url: this.#config.SSO_SUCCESS_URL
@@ -620,7 +659,7 @@ export class SessionManager {
620
659
  if (isRedirect) {
621
660
  return res.redirect(this.#config.SSO_FAILURE_URL);
622
661
  }
623
- return res.status(httpCodes.SYSTEM_FAILURE).json({
662
+ return res.status(httpCodes.SYSTEM_FAILURE).json({
624
663
  error: 'Logout all failed',
625
664
  redirect_url: this.#config.SSO_FAILURE_URL
626
665
  });
@@ -643,8 +682,8 @@ export class SessionManager {
643
682
 
644
683
  try {
645
684
  // Extract Token ID and email from current token
646
- const payload = await this.#getUserFromToken(req.headers.authorization, false);
647
- const { email, tid } = payload;
685
+ const { tid, user } = await this.#getUserFromToken(req.headers.authorization, false);
686
+ const { email } = user;
648
687
 
649
688
  if (!email || !tid) {
650
689
  throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid token payload');
@@ -659,7 +698,7 @@ export class SessionManager {
659
698
  if (isRedirect) {
660
699
  return res.redirect(this.#config.SSO_SUCCESS_URL);
661
700
  }
662
- return res.json({
701
+ return res.json({
663
702
  message: 'Logout successful',
664
703
  redirect_url: this.#config.SSO_SUCCESS_URL
665
704
  });
@@ -669,7 +708,7 @@ export class SessionManager {
669
708
  if (isRedirect) {
670
709
  return res.redirect(this.#config.SSO_FAILURE_URL);
671
710
  }
672
- return res.status(httpCodes.SYSTEM_FAILURE).json({
711
+ return res.status(httpCodes.SYSTEM_FAILURE).json({
673
712
  error: 'Logout failed',
674
713
  redirect_url: this.#config.SSO_FAILURE_URL
675
714
  });
@@ -749,9 +788,9 @@ export class SessionManager {
749
788
  * @returns {import('@types/express').RequestHandler} Returns express Request Handler
750
789
  */
751
790
  requireUser = () => {
752
- return async (req, res, next) => {
791
+ return async (req, _res, next) => {
753
792
  try {
754
- req.user = await this.getUser(req);
793
+ req.user = await this.getUser(req, true);
755
794
  return next();
756
795
  }
757
796
  catch (error) {
@@ -764,9 +803,9 @@ export class SessionManager {
764
803
  * Resource protection based on configured SESSION_MODE
765
804
  * - SESSION mode: Verifies user exists in session store and is authorized (checks req.session data)
766
805
  * - TOKEN mode: Validates JWT token from Authorization header (lightweight validation)
767
- *
806
+ *
768
807
  * Note: This method verifies authentication only. Use requireUser() after this to populate req.user.
769
- *
808
+ *
770
809
  * @param {string} [errorRedirectUrl=''] Redirect URL on authentication failure
771
810
  * @returns {import('@types/express').RequestHandler} Returns express Request Handler
772
811
  * @example
@@ -774,9 +813,9 @@ export class SessionManager {
774
813
  * app.get('/api/check', session.authenticate(), (req, res) => {
775
814
  * res.json({ authenticated: true });
776
815
  * });
777
- *
816
+ *
778
817
  * // Option 2: Verify authentication AND load user data into req.user
779
- * app.get('/api/profile',
818
+ * app.get('/api/profile',
780
819
  * session.authenticate(), // Verifies session/token
781
820
  * session.requireUser(), // Loads user data into req.user
782
821
  * (req, res) => {
@@ -786,7 +825,7 @@ export class SessionManager {
786
825
  */
787
826
  authenticate(errorRedirectUrl = '') {
788
827
  return async (req, res, next) => {
789
- const mode = this.#config.SESSION_MODE || SessionMode.SESSION;
828
+ const mode = this.getSessionMode() || SessionMode.SESSION;
790
829
  if (mode === SessionMode.TOKEN) {
791
830
  return this.#verifyToken(req, res, next, errorRedirectUrl);
792
831
  }
@@ -819,13 +858,11 @@ export class SessionManager {
819
858
  /**
820
859
  * Save session
821
860
  * @param {import('@types/express').Request} request Request object
822
- * @param {string} jwt JWT
861
+ * @param {import('jose').JWTPayload} payload JWT
823
862
  * @param {(user: object) => object} initUser Redirect URL
824
863
  * @returns {Promise<{ user: import('../models/types/user').UserModel, redirect_url: string }>} Promise
825
864
  */
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);
865
+ #saveSession = async (request, payload, initUser) => {
829
866
  if (payload?.user) {
830
867
  this.#logger.debug('### CALLBACK USER ###');
831
868
  request.session[this.#getSessionKey()] = initUser(payload.user);
@@ -842,7 +879,7 @@ export class SessionManager {
842
879
  }
843
880
  throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload');
844
881
  };
845
-
882
+
846
883
  /**
847
884
  * Render HTML template for token storage
848
885
  * @param {string} token JWT token
@@ -860,7 +897,7 @@ export class SessionManager {
860
897
  .replaceAll('{{SSO_SUCCESS_URL}}', sucessRedirectUrl)
861
898
  .replaceAll('{{SSO_FAILURE_URL}}', this.#config.SSO_FAILURE_URL);
862
899
  }
863
-
900
+
864
901
  /**
865
902
  * SSO callback for successful login
866
903
  * @param {(user: object) => object} initUser Initialize user object function
@@ -872,30 +909,32 @@ export class SessionManager {
872
909
  if (!jwt) {
873
910
  return next(new CustomError(httpCodes.BAD_REQUEST, 'Missing `jwt` in query parameters'));
874
911
  }
875
-
912
+
876
913
  try {
877
914
  // Decrypt JWT from Identity Adapter
878
915
  const { payload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
879
-
916
+
880
917
  if (!payload?.user) {
881
918
  throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload');
882
919
  }
883
-
884
- /** @type {import('../index.js').SessionUser} */
885
- const user = initUser(payload.user);
920
+
886
921
  /** @type {string} */
887
922
  const callbackRedirectUrl = payload.redirect_url || this.#config.SSO_SUCCESS_URL;
888
-
923
+
889
924
  // Token mode: Generate token and return HTML page
890
- if (this.#config.SESSION_MODE === SessionMode.TOKEN) {
891
- const token = await this.#generateAndStoreToken(user);
925
+ if (this.getSessionMode() === SessionMode.TOKEN) {
926
+ /** @type {import('../index.js').SessionUser} */
927
+ const user = initUser(payload.user);
928
+ // Generate unique token ID for this device/session
929
+ const tid = crypto.randomUUID();
930
+ const token = await this.#generateAndStoreToken(tid, user);
892
931
  this.#logger.debug('### CALLBACK TOKEN GENERATED ###');
893
932
  const html = this.#renderTokenStorageHtml(token, user.attributes.expires_at, callbackRedirectUrl);
894
933
  return res.send(html);
895
934
  }
896
935
 
897
936
  // Session mode: Save to session and redirect
898
- await this.#saveSession(req, jwt, initUser);
937
+ await this.#saveSession(req, payload, initUser);
899
938
  return res.redirect(callbackRedirectUrl);
900
939
  }
901
940
  catch (error) {
@@ -939,11 +978,12 @@ export class SessionManager {
939
978
  refresh(initUser) {
940
979
  const idpRefreshUrl = '/auth/refresh'.concat('?client_id=').concat(this.#config.SSO_CLIENT_ID);
941
980
  return async (req, res, next) => {
942
- const mode = this.#config.SESSION_MODE || SessionMode.SESSION;
981
+ const mode = this.getSessionMode() || SessionMode.SESSION;
943
982
 
944
983
  if (mode === SessionMode.TOKEN) {
945
984
  return this.#refreshToken(req, res, next, initUser, idpRefreshUrl);
946
- } else {
985
+ }
986
+ else {
947
987
  return this.#refreshSession(req, res, next, initUser, idpRefreshUrl);
948
988
  }
949
989
  };
@@ -958,8 +998,8 @@ export class SessionManager {
958
998
  const { redirect = false, all = false } = req.query;
959
999
  const isRedirect = (redirect === 'true' || redirect === true);
960
1000
  const logoutAll = (all === 'true' || all === true);
961
- const mode = this.#config.SESSION_MODE || SessionMode.SESSION;
962
-
1001
+ const mode = this.getSessionMode() || SessionMode.SESSION;
1002
+
963
1003
  if (mode === SessionMode.TOKEN) {
964
1004
  return this.#logoutToken(req, res, isRedirect, logoutAll);
965
1005
  }
@@ -973,8 +1013,8 @@ export class SessionManager {
973
1013
  if (isRedirect) {
974
1014
  return res.redirect(this.#config.SSO_FAILURE_URL);
975
1015
  }
976
- return res.status(httpCodes.AUTHORIZATION_FAILED).send({
977
- redirect_url: this.#config.SSO_FAILURE_URL
1016
+ return res.status(httpCodes.AUTHORIZATION_FAILED).send({
1017
+ redirect_url: this.#config.SSO_FAILURE_URL
978
1018
  });
979
1019
  }
980
1020
  if (isRedirect) {
@@ -985,4 +1025,11 @@ export class SessionManager {
985
1025
  };
986
1026
  }
987
1027
 
1028
+ /**
1029
+ * Get session mode
1030
+ * @returns {string} Session mode
1031
+ */
1032
+ getSessionMode() {
1033
+ return this.#config.SESSION_MODE;
1034
+ }
988
1035
  }
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/index.js CHANGED
@@ -3,4 +3,4 @@ export { httpCodes, httpMessages, httpErrorHandler, httpNotFoundHandler, CustomE
3
3
  export { RedisManager } from './components/redis.js';
4
4
  export { FlexRouter } from './components/router.js';
5
5
  export { JwtManager } from './components/jwt.js';
6
- export { Logger } from './components/logger.js';
6
+ export { Logger } from './components/logger.js';
package/package.json CHANGED
@@ -1,32 +1,20 @@
1
1
  {
2
2
  "name": "@igxjs/node-components",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "Node components for igxjs",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "scripts": {
8
- "test": "mocha tests/**/*.test.js --timeout 5000"
8
+ "test": "mocha tests/**/*.test.js --timeout 5000",
9
+ "lint": "eslint .",
10
+ "lint:fix": "eslint . --fix"
9
11
  },
10
12
  "repository": {
11
13
  "type": "git",
12
14
  "url": "git+https://github.com/igxjs/node-components.git"
13
15
  },
14
16
  "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"
17
+ "igxjs"
30
18
  ],
31
19
  "author": "Michael",
32
20
  "license": "Apache-2.0",
@@ -43,11 +31,14 @@
43
31
  "axios": "^1.13.6",
44
32
  "connect-redis": "^9.0.0",
45
33
  "express-session": "^1.19.0",
46
- "jose": "^6.2.1",
34
+ "jose": "^6.2.2",
47
35
  "memorystore": "^1.6.7"
48
36
  },
49
37
  "devDependencies": {
38
+ "@eslint/js": "^10.0.1",
50
39
  "chai": "^6.2.2",
40
+ "eslint": "^10.1.0",
41
+ "globals": "^17.4.0",
51
42
  "mocha": "^12.0.0-beta-10",
52
43
  "sinon": "^21.0.3",
53
44
  "supertest": "^7.0.0"
@@ -68,7 +59,8 @@
68
59
  "README.md"
69
60
  ],
70
61
  "publishConfig": {
71
- "access": "public"
62
+ "access": "public",
63
+ "registry": "https://npm.pkg.github.com"
72
64
  },
73
65
  "types": "./index.d.ts"
74
66
  }