@igxjs/node-components 1.0.12 → 1.0.14

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
@@ -78,7 +78,7 @@ flexRouter.mount(app, '');
78
78
  import { JwtManager } from '@igxjs/node-components';
79
79
 
80
80
  // Constructor uses UPPERCASE naming with JWT_ prefix
81
- const jwt = new JwtManager({ SESSION_AGE: 64800000 });
81
+ const jwt = new JwtManager({ JWT_EXPIRATION_TIME: 64800 });
82
82
  const SECRET = process.env.JWT_SECRET;
83
83
 
84
84
  // Create token (encrypt method uses camelCase for per-call options)
@@ -127,7 +127,7 @@ Uses traditional server-side session cookies. When a user authenticates via SSO,
127
127
 
128
128
  **Configuration:**
129
129
  - `SESSION_MODE`: `SessionMode.SESSION` (default) - Uses session-based authentication
130
- - `SESSION_AGE`: Session timeout in milliseconds (default: 64800000)
130
+ - `SESSION_AGE`: Session timeout in seconds (default: 64800 = 18 hours)
131
131
  - `REDIS_URL`: Redis connection string for session storage
132
132
 
133
133
  **Auth Methods:**
@@ -182,7 +182,7 @@ fetch('/api/protected', {
182
182
  | `SSO_SUCCESS_URL` | string | - | Redirect URL after successful login (token mode) |
183
183
  | `SSO_FAILURE_URL` | string | - | Redirect URL after failed login (token mode) |
184
184
  | `SESSION_MODE` | string | `SessionMode.SESSION` | Authentication mode: `SessionMode.SESSION` or `SessionMode.TOKEN` |
185
- | `SESSION_AGE` | number | 64800000 | Session timeout in milliseconds |
185
+ | `SESSION_AGE` | number | 64800 | Session timeout in seconds (default: 64800 = 18 hours) |
186
186
  | `SESSION_COOKIE_PATH` | string | `'/'` | Session cookie path |
187
187
  | `SESSION_SECRET` | string | - | Session/JWT secret key |
188
188
  | `SESSION_PREFIX` | string | `'ibmid:'` | Redis session/key prefix |
package/components/jwt.js CHANGED
@@ -1,4 +1,4 @@
1
- import crypto from 'node:crypto';
1
+ import { webcrypto as crypto } from 'node:crypto';
2
2
 
3
3
  import { jwtDecrypt, EncryptJWT } from 'jose';
4
4
 
@@ -37,7 +37,7 @@ export class JwtManager {
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')
40
- * @property {number} [SESSION_AGE=64800000] Token expiration time in milliseconds (default: 64800000 = 18 hours)
40
+ * @property {number|string} [JWT_EXPIRATION_TIME=64800] Token expiration time - number in seconds (e.g., 64800) or string with time suffix (e.g., '18h', '7d', '1080m') (default: 64800 = 18 hours)
41
41
  * @property {string} [JWT_SECRET_HASH_ALGORITHM='SHA-256'] Hash algorithm (default: 'SHA-256')
42
42
  * @property {string?} [JWT_ISSUER] Optional JWT issuer claim
43
43
  * @property {string?} [JWT_AUDIENCE] Optional JWT audience claim
@@ -48,8 +48,8 @@ export class JwtManager {
48
48
  constructor(options = {}) {
49
49
  this.algorithm = options.JWT_ALGORITHM || 'dir';
50
50
  this.encryption = options.JWT_ENCRYPTION || 'A256GCM';
51
- // SESSION_AGE is in milliseconds, convert to seconds for expirationTime
52
- this.expirationTime = Math.floor((options.SESSION_AGE || 64800000) / 1000);
51
+ // JWT_EXPIRATION_TIME is in seconds
52
+ this.expirationTime = options.JWT_EXPIRATION_TIME || 64800;
53
53
  this.secretHashAlgorithm = options.JWT_SECRET_HASH_ALGORITHM || 'SHA-256';
54
54
  this.issuer = options.JWT_ISSUER;
55
55
  this.audience = options.JWT_AUDIENCE;
@@ -63,7 +63,7 @@ export class JwtManager {
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)
66
- * @property {number} [expirationTime] Token expiration time in seconds (overrides instance expirationTime derived from SESSION_AGE)
66
+ * @property {number|string} [expirationTime] Token expiration time - number in seconds or string with time suffix like '1h', '30m', '7d' (overrides instance JWT_EXPIRATION_TIME)
67
67
  * @property {string} [secretHashAlgorithm='SHA-256'] Hash algorithm for secret derivation (overrides instance JWT_SECRET_HASH_ALGORITHM)
68
68
  * @property {string?} [issuer] Optional JWT issuer claim (overrides instance JWT_ISSUER)
69
69
  * @property {string?} [audience] Optional JWT audience claim (overrides instance JWT_AUDIENCE)
@@ -91,6 +91,9 @@ export class JwtManager {
91
91
  new TextEncoder().encode(secret)
92
92
  );
93
93
 
94
+ // Convert number to string with 's' suffix, pass strings directly
95
+ const expTime = typeof expirationTime === 'number' ? `${expirationTime}s` : expirationTime;
96
+
94
97
  const jwt = new EncryptJWT(data)
95
98
  .setProtectedHeader({
96
99
  alg: algorithm,
@@ -98,7 +101,7 @@ export class JwtManager {
98
101
  typ: 'JWT',
99
102
  })
100
103
  .setIssuedAt()
101
- .setExpirationTime(`${expirationTime}s`); // Pass as string with 's' suffix for seconds
104
+ .setExpirationTime(expTime); // Accepts: number (as seconds) or string (e.g., '18h', '7d')
102
105
 
103
106
  // Add optional claims if provided
104
107
  if (issuer) jwt.setIssuer(issuer);
@@ -124,7 +127,7 @@ export class JwtManager {
124
127
  * @param {string} token JWT token to decrypt
125
128
  * @param {string} secret Secret key or password for decryption
126
129
  * @param {JwtDecryptOptions} [options] Per-call configuration overrides (camelCase naming convention)
127
- * @returns {Promise<import('jose').JWTDecryptResult<import('jose').EncryptJWT>} Returns decrypted JWT token
130
+ * @returns {Promise<import('jose').JWTDecryptResult>} Returns decrypted JWT token
128
131
  */
129
132
  async decrypt(token, secret, options = {}) {
130
133
  const clockTolerance = options.clockTolerance ?? this.clockTolerance;
@@ -147,4 +150,4 @@ export class JwtManager {
147
150
 
148
151
  return await jwtDecrypt(token, new Uint8Array(secretHash), decryptOptions);
149
152
  }
150
- }
153
+ }
@@ -11,17 +11,36 @@ export class RedisManager {
11
11
  #client = null;
12
12
  /**
13
13
  * Connect with Redis
14
+ * @param {string} redisUrl Redis connection URL
15
+ * @param {string} certPath Path to TLS certificate (required when using rediss:// protocol)
14
16
  * @returns {Promise<boolean>} Returns `true` if Redis server is connected
17
+ * @throws {TypeError} If redisUrl is not a string
18
+ * @throws {Error} If TLS is enabled but certPath is invalid
15
19
  */
16
20
  async connect(redisUrl, certPath) {
17
21
  if (redisUrl?.length > 0) {
18
22
  try {
23
+ // Validate redisUrl is a string
24
+ if (typeof redisUrl !== 'string') {
25
+ throw new TypeError(`Invalid redisUrl: expected string but received ${typeof redisUrl}`);
26
+ }
27
+
19
28
  /** @type {import('@redis/client').RedisClientOptions} */
20
29
  const options = {
21
30
  url: redisUrl,
22
31
  socket: { tls: redisUrl.includes('rediss:'), ca: null },
23
32
  };
24
- if(options.socket.tls) {
33
+
34
+ if (options.socket.tls) {
35
+ // Validate certPath when TLS is enabled
36
+ if (!certPath || typeof certPath !== 'string') {
37
+ throw new Error('TLS certificate path is required when using rediss:// protocol');
38
+ }
39
+
40
+ if (!fs.existsSync(certPath)) {
41
+ throw new Error(`TLS certificate file not found at path: ${certPath}`);
42
+ }
43
+
25
44
  const caCert = fs.readFileSync(certPath);
26
45
  options.socket.ca = caCert;
27
46
  }
@@ -43,11 +43,23 @@ export class FlexRouter {
43
43
  }
44
44
 
45
45
  /**
46
- * Mount router
46
+ * Mount router to Express application
47
47
  * @param {import('@types/express').Express} app Express app
48
48
  * @param {string} basePath Base path
49
+ * @throws {TypeError} If app is not a valid Express instance
50
+ * @throws {TypeError} If basePath is not a string
49
51
  */
50
52
  mount(app, basePath) {
53
+ // Validate app is an Express instance (has 'use' method)
54
+ if (!app || typeof app.use !== 'function') {
55
+ throw new TypeError('Invalid Express app: app must be an Express application instance with a "use" method');
56
+ }
57
+
58
+ // Validate basePath is a string
59
+ if (typeof basePath !== 'string') {
60
+ throw new TypeError(`Invalid basePath: expected string but received ${typeof basePath}`);
61
+ }
62
+
51
63
  const path = basePath.concat(this.context);
52
64
  app.use(path, this.handlers, this.router);
53
65
  }
@@ -34,7 +34,20 @@ export class SessionConfig {
34
34
  * @default SessionMode.SESSION
35
35
  */
36
36
  SESSION_MODE;
37
- /** @type {string} */
37
+ /**
38
+ * @type {string} Identity Provider microservice endpoint URL
39
+ *
40
+ * This is a fully customized, independent microservice that provides SSO authentication.
41
+ * The endpoint serves multiple applications and provides the following APIs:
42
+ * - GET /auth/providers - List supported identity providers
43
+ * - POST /auth/login/:idp - Generate login URL for specific identity provider
44
+ * - POST /auth/verify - Verify JWT token validity
45
+ * - GET /auth/callback/:idp - Validate authentication and return user data
46
+ * - POST /auth/refresh - Refresh access tokens
47
+ *
48
+ * @example
49
+ * SSO_ENDPOINT_URL: 'https://idp.example.com/open/api/v1'
50
+ */
38
51
  SSO_ENDPOINT_URL;
39
52
  /** @type {string} */
40
53
  SSO_CLIENT_ID;
@@ -45,7 +58,7 @@ export class SessionConfig {
45
58
  /** @type {string} */
46
59
  SSO_FAILURE_URL;
47
60
 
48
- /** @type {number} Session age in milliseconds */
61
+ /** @type {number} Session age in seconds (default: 64800 = 18 hours) */
49
62
  SESSION_AGE;
50
63
  /**
51
64
  * @type {string} Session cookie path
@@ -128,13 +141,48 @@ export class SessionManager {
128
141
  /**
129
142
  * Create a new session manager
130
143
  * @param {SessionConfig} config Session configuration
144
+ * @throws {TypeError} If config is not an object
145
+ * @throws {Error} If required configuration fields are missing
131
146
  */
132
147
  constructor(config) {
148
+ // Validate config is an object
149
+ if (!config || typeof config !== 'object') {
150
+ throw new TypeError('SessionManager configuration must be an object');
151
+ }
152
+
153
+ // Validate required fields based on SESSION_MODE
154
+ const mode = config.SESSION_MODE || SessionMode.SESSION;
155
+
156
+ // SESSION_SECRET is always required for both modes
157
+ if (!config.SESSION_SECRET) {
158
+ throw new Error('SESSION_SECRET is required for SessionManager');
159
+ }
160
+
161
+ // Validate SSO configuration if SSO endpoints are used
162
+ if (config.SSO_ENDPOINT_URL) {
163
+ if (!config.SSO_CLIENT_ID) {
164
+ throw new Error('SSO_CLIENT_ID is required when SSO_ENDPOINT_URL is provided');
165
+ }
166
+ if (!config.SSO_CLIENT_SECRET) {
167
+ throw new Error('SSO_CLIENT_SECRET is required when SSO_ENDPOINT_URL is provided');
168
+ }
169
+ }
170
+
171
+ // Validate TOKEN mode specific requirements
172
+ if (mode === SessionMode.TOKEN) {
173
+ if (!config.SSO_SUCCESS_URL) {
174
+ throw new Error('SSO_SUCCESS_URL is required for TOKEN authentication mode');
175
+ }
176
+ if (!config.SSO_FAILURE_URL) {
177
+ throw new Error('SSO_FAILURE_URL is required for TOKEN authentication mode');
178
+ }
179
+ }
180
+
133
181
  this.#config = {
134
182
  // Session Mode
135
183
  SESSION_MODE: config.SESSION_MODE || SessionMode.SESSION,
136
- // Session
137
- SESSION_AGE: config.SESSION_AGE || 64800000,
184
+ // Session - SESSION_AGE is now in seconds (default: 64800 = 18 hours)
185
+ SESSION_AGE: config.SESSION_AGE || 64800,
138
186
  SESSION_COOKIE_PATH: config.SESSION_COOKIE_PATH || '/',
139
187
  SESSION_SECRET: config.SESSION_SECRET,
140
188
  SESSION_PREFIX: config.SESSION_PREFIX || 'ibmid:',
@@ -205,6 +253,15 @@ export class SessionManager {
205
253
  return this.#config.SESSION_KEY;
206
254
  }
207
255
 
256
+ /**
257
+ * Get session age in milliseconds (for express-session cookie maxAge)
258
+ * @returns {number} Returns the session age in milliseconds
259
+ * @private
260
+ */
261
+ #getSessionAgeInMilliseconds() {
262
+ return Math.round(this.#config.SESSION_AGE * 1000);
263
+ }
264
+
208
265
  /**
209
266
  * Get Redis key for token storage
210
267
  * @param {string} email User email
@@ -238,14 +295,22 @@ export class SessionManager {
238
295
  * Generate and store JWT token in Redis
239
296
  * - JWT payload contains only { email, tid } for minimal size
240
297
  * - Full user data is stored in Redis as single source of truth
241
- * @param {object} user User object
298
+ * @param {object} user User object with email and attributes
242
299
  * @returns {Promise<string>} Returns the generated JWT token
300
+ * @throws {Error} If JWT encryption fails
301
+ * @throws {Error} If Redis storage fails
243
302
  * @private
303
+ * @example
304
+ * const token = await this.#generateAndStoreToken({
305
+ * email: 'user@example.com',
306
+ * attributes: { /* user data * / }
307
+ * });
244
308
  */
245
309
  async #generateAndStoreToken(user) {
246
310
  // Generate unique token ID for this device/session
247
311
  const tid = crypto.randomUUID();
248
- const ttlSeconds = Math.floor(this.#config.SESSION_AGE / 1000);
312
+ // SESSION_AGE is already in seconds
313
+ const ttlSeconds = this.#config.SESSION_AGE;
249
314
  // Create JWT token with only email and tid (minimal payload)
250
315
  const token = await this.#jwtManager.encrypt(
251
316
  { email: user.email, tid },
@@ -266,13 +331,17 @@ export class SessionManager {
266
331
  }
267
332
 
268
333
  /**
269
- * Verify token authentication
270
- * @param {import('@types/express').Request} req Request
271
- * @param {import('@types/express').Response} res Response
272
- * @param {import('@types/express').NextFunction} next Next function
273
- * @param {boolean} isDebugging Debugging flag
274
- * @param {string} redirectUrl Redirect URL
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
275
341
  * @private
342
+ * @example
343
+ * // Authorization header format: "Bearer {jwt_token}"
344
+ * await this.#verifyToken(req, res, next, false, '/login');
276
345
  */
277
346
  async #verifyToken(req, res, next, isDebugging, redirectUrl) {
278
347
  try {
@@ -306,7 +375,6 @@ export class SessionManager {
306
375
 
307
376
  // Parse and attach user to request
308
377
  req.user = JSON.parse(userData);
309
- res.locals.user = req.user;
310
378
 
311
379
  // Validate authorization
312
380
  const { authorized = isDebugging } = req.user ?? { authorized: isDebugging };
@@ -337,12 +405,13 @@ export class SessionManager {
337
405
  }
338
406
 
339
407
  /**
340
- * Verify session authentication
341
- * @param {import('@types/express').Request} req Request
342
- * @param {import('@types/express').Response} res Response
343
- * @param {import('@types/express').NextFunction} next Next function
344
- * @param {boolean} isDebugging Debugging flag
345
- * @param {string} redirectUrl Redirect URL
408
+ * Verify session authentication - checks if user is authorized in session
409
+ * @param {import('@types/express').Request} req Request with session data
410
+ * @param {import('@types/express').Response} res Response object
411
+ * @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
414
+ * @throws {CustomError} If user is not authorized
346
415
  * @private
347
416
  */
348
417
  async #verifySession(req, res, next, isDebugging, redirectUrl) {
@@ -357,13 +426,18 @@ export class SessionManager {
357
426
  }
358
427
 
359
428
  /**
360
- * Refresh token authentication
361
- * @param {import('@types/express').Request} req Request
362
- * @param {import('@types/express').Response} res Response
363
- * @param {import('@types/express').NextFunction} next Next function
364
- * @param {(user: object) => object} initUser Initialize user function
365
- * @param {string} idpUrl Identity provider URL
429
+ * Refresh token authentication - exchanges refresh token for new JWT
430
+ * Invalidates old token and generates a new one with updated user data
431
+ * @param {import('@types/express').Request} req Request with Authorization header
432
+ * @param {import('@types/express').Response} res Response object
433
+ * @param {import('@types/express').NextFunction} next Next middleware function
434
+ * @param {(user: object) => object} initUser Function to initialize/transform user object
435
+ * @param {string} idpUrl Identity provider refresh endpoint URL
436
+ * @throws {CustomError} If refresh lock is active or SSO refresh fails
366
437
  * @private
438
+ * @example
439
+ * // Response format:
440
+ * // { token: "new_jwt", user: {...}, expiresIn: 64800, tokenType: "Bearer" }
367
441
  */
368
442
  async #refreshToken(req, res, next, initUser, idpUrl) {
369
443
  try {
@@ -425,7 +499,7 @@ export class SessionManager {
425
499
  return res.json({
426
500
  token: newToken,
427
501
  user,
428
- expiresIn: Math.floor(this.#config.SESSION_AGE / 1000),
502
+ expiresIn: this.#config.SESSION_AGE, // Already in seconds
429
503
  tokenType: 'Bearer'
430
504
  });
431
505
  } catch (error) {
@@ -603,7 +677,10 @@ export class SessionManager {
603
677
  */
604
678
  async setup(app, updateUser) {
605
679
  this.#redisManager = new RedisManager();
606
- this.#jwtManager = new JwtManager(this.#config);
680
+ this.#jwtManager = new JwtManager({
681
+ ...this.#config,
682
+ JWT_EXPIRATION_TIME: this.#config.SESSION_AGE, // SESSION_AGE is already in seconds
683
+ });
607
684
  // Identity Provider Request
608
685
  this.#idpRequest = axios.create({
609
686
  baseURL: this.#config.SSO_ENDPOINT_URL,
@@ -622,7 +699,7 @@ export class SessionManager {
622
699
  // Redis Session
623
700
  this.#logger.log('### Using Redis as the Session Store ###');
624
701
  return session({
625
- cookie: { maxAge: this.#config.SESSION_AGE, path: this.#config.SESSION_COOKIE_PATH, sameSite: false },
702
+ cookie: { maxAge: this.#getSessionAgeInMilliseconds(), path: this.#config.SESSION_COOKIE_PATH, sameSite: false },
626
703
  store: new RedisStore({ client: this.#redisManager.getClient(), prefix: this.#config.SESSION_PREFIX, disableTouch: true }),
627
704
  resave: false, saveUninitialized: false,
628
705
  secret: this.#config.SESSION_SECRET,
@@ -638,7 +715,7 @@ export class SessionManager {
638
715
  this.#logger.log('### Using Memory as the Session Store ###');
639
716
  const MemoryStore = memStore(session);
640
717
  return session({
641
- cookie: { maxAge: this.#config.SESSION_AGE, path: this.#config.SESSION_COOKIE_PATH, sameSite: false },
718
+ cookie: { maxAge: this.#getSessionAgeInMilliseconds(), path: this.#config.SESSION_COOKIE_PATH, sameSite: false },
642
719
  store: new MemoryStore({}),
643
720
  resave: false, saveUninitialized: false,
644
721
  secret: this.#config.SESSION_SECRET,
package/index.d.ts CHANGED
@@ -79,33 +79,160 @@ export const SessionMode: {
79
79
 
80
80
  // Session Configuration - uses strict UPPERCASE naming convention for all property names
81
81
  export interface SessionConfig {
82
- /** Identity Provider */
82
+ /**
83
+ * SSO Identity Provider endpoint URL
84
+ * @example 'https://idp.example.com/open/api/v1'
85
+ * @required Required when using SSO authentication
86
+ */
83
87
  SSO_ENDPOINT_URL?: string;
88
+
89
+ /**
90
+ * OAuth2 Client ID registered with the Identity Provider
91
+ * @example 'my-app-client-id'
92
+ * @required Required when using SSO authentication
93
+ */
84
94
  SSO_CLIENT_ID?: string;
95
+
96
+ /**
97
+ * OAuth2 Client Secret for authentication
98
+ * @example 'super-secret-client-key'
99
+ * @required Required when using SSO authentication
100
+ */
85
101
  SSO_CLIENT_SECRET?: string;
102
+
103
+ /**
104
+ * Redirect URL after successful SSO login
105
+ * @example '/dashboard' or 'https://myapp.com/home'
106
+ * @default '/'
107
+ */
86
108
  SSO_SUCCESS_URL?: string;
109
+
110
+ /**
111
+ * Redirect URL after failed SSO login
112
+ * @example '/login?error=auth_failed'
113
+ * @default '/login'
114
+ */
87
115
  SSO_FAILURE_URL?: string;
88
116
 
89
- /** Authentication mode: 'session' or 'token' (default: 'session') */
117
+ /**
118
+ * Authentication mode: 'session' for cookie-based sessions, 'token' for JWT token-based auth
119
+ * @example 'session' | 'token'
120
+ * @default 'session'
121
+ */
90
122
  SESSION_MODE?: string;
91
123
 
124
+ /**
125
+ * Session expiration time in seconds
126
+ * @example 3600 (1 hour) or 86400 (24 hours) or 64800 (18 hours)
127
+ * @default 64800 (18 hours)
128
+ */
92
129
  SESSION_AGE?: number;
130
+
131
+ /**
132
+ * Cookie path for session cookies
133
+ * @example '/' for entire site or '/app' for specific path
134
+ * @default '/'
135
+ */
93
136
  SESSION_COOKIE_PATH?: string;
137
+
138
+ /**
139
+ * Secret key used for signing session cookies (should be a strong random string)
140
+ * @example 'your-super-secret-session-key-change-this-in-production'
141
+ * @required Required for session-based authentication
142
+ */
94
143
  SESSION_SECRET?: string;
144
+
145
+ /**
146
+ * Redis key prefix for storing session data
147
+ * @example 'myapp:session:' (will result in keys like 'myapp:session:user@example.com')
148
+ * @default 'sess:'
149
+ */
95
150
  SESSION_PREFIX?: string;
151
+
152
+ /**
153
+ * Redis key name for storing session data
154
+ * @example 'user' (results in session.user containing user data)
155
+ * @default 'user'
156
+ */
96
157
  SESSION_KEY?: string;
158
+
159
+ /**
160
+ * Redis key name for storing session expiry timestamp
161
+ * @example 'expires' (results in session.expires containing expiry time)
162
+ * @default 'expires'
163
+ */
97
164
  SESSION_EXPIRY_KEY?: string;
165
+
166
+ /**
167
+ * Path to custom HTML template for token storage page (TOKEN mode only)
168
+ * @example './templates/token-storage.html'
169
+ * @default Built-in template included in the library
170
+ */
98
171
  TOKEN_STORAGE_TEMPLATE_PATH?: string;
99
172
 
173
+ /**
174
+ * Redis connection URL
175
+ * @example 'redis://localhost:6379' or 'rediss://user:pass@host:6380/0' (rediss for TLS)
176
+ * @required Required when using Redis for session storage
177
+ */
100
178
  REDIS_URL?: string;
179
+
180
+ /**
181
+ * Path to TLS certificate file for secure Redis connections
182
+ * @example '/path/to/redis-ca-cert.pem'
183
+ * @optional Only needed for TLS-enabled Redis connections
184
+ */
101
185
  REDIS_CERT_PATH?: string;
102
186
 
187
+ /**
188
+ * JWE algorithm for token encryption
189
+ * @example 'dir' (Direct Key Agreement) or 'RSA-OAEP' or 'A256KW'
190
+ * @default 'dir'
191
+ * @see https://tools.ietf.org/html/rfc7518#section-4.1
192
+ */
103
193
  JWT_ALGORITHM?: string;
194
+
195
+ /**
196
+ * JWE encryption method
197
+ * @example 'A256GCM' (AES-256 GCM) or 'A128GCM' or 'A192GCM'
198
+ * @default 'A256GCM'
199
+ * @see https://tools.ietf.org/html/rfc7518#section-5.1
200
+ */
104
201
  JWT_ENCRYPTION?: string;
202
+
203
+ /**
204
+ * Clock tolerance in seconds for JWT token validation (allows for time drift between servers)
205
+ * @example 30 (allows 30 seconds of clock skew)
206
+ * @default 30
207
+ */
105
208
  JWT_CLOCK_TOLERANCE?: number;
209
+
210
+ /**
211
+ * Hash algorithm for deriving encryption key from secret
212
+ * @example 'SHA-256' or 'SHA-384' or 'SHA-512'
213
+ * @default 'SHA-256'
214
+ */
106
215
  JWT_SECRET_HASH_ALGORITHM?: string;
216
+
217
+ /**
218
+ * JWT issuer claim (iss) - identifies the principal that issued the token
219
+ * @example 'https://myapp.com' or 'my-auth-service'
220
+ * @optional Adds additional security validation
221
+ */
107
222
  JWT_ISSUER?: string;
223
+
224
+ /**
225
+ * JWT audience claim (aud) - identifies the recipients that the token is intended for
226
+ * @example 'https://api.myapp.com' or 'my-api-service'
227
+ * @optional Adds additional security validation
228
+ */
108
229
  JWT_AUDIENCE?: string;
230
+
231
+ /**
232
+ * JWT subject claim (sub) - identifies the principal that is the subject of the token
233
+ * @example 'user-authentication' or 'api-access'
234
+ * @optional Adds additional context to tokens
235
+ */
109
236
  JWT_SUBJECT?: string;
110
237
  }
111
238
 
@@ -144,8 +271,19 @@ export interface SessionUser {
144
271
  // Session Manager
145
272
  export class SessionManager {
146
273
  /**
147
- * Constructor
148
- * @param config - Session configuration (uses strict UPPERCASE property names)
274
+ * Create a new SessionManager instance
275
+ * @param config Session configuration object with UPPERCASE property names
276
+ * @example
277
+ * ```javascript
278
+ * const sessionManager = new SessionManager({
279
+ * SSO_ENDPOINT_URL: 'https://idp.example.com/open/api/v1',
280
+ * SSO_CLIENT_ID: 'my-app-client-id',
281
+ * SSO_CLIENT_SECRET: 'secret-key',
282
+ * SESSION_MODE: 'session', // or 'token' for JWT-based auth
283
+ * SESSION_SECRET: 'your-session-secret',
284
+ * REDIS_URL: 'redis://localhost:6379'
285
+ * });
286
+ * ```
149
287
  */
150
288
  constructor(config: SessionConfig);
151
289
 
@@ -312,75 +450,191 @@ export class RedisManager {
312
450
 
313
451
  // JWT Manager Configuration - uses strict UPPERCASE naming convention with JWT_ prefix for all property names
314
452
  export interface JwtManagerOptions {
315
- /** JWE algorithm (default: 'dir') */
453
+ /**
454
+ * JWE algorithm for token encryption
455
+ * @example 'dir' (Direct Key Agreement - symmetric encryption, recommended for most cases)
456
+ * @example 'RSA-OAEP' (RSA with OAEP padding - for asymmetric encryption)
457
+ * @example 'A256KW' (AES Key Wrap with 256-bit key)
458
+ * @default 'dir'
459
+ * @see https://tools.ietf.org/html/rfc7518#section-4.1
460
+ */
316
461
  JWT_ALGORITHM?: string;
317
462
 
318
- /** JWE encryption method (default: 'A256GCM') */
463
+ /**
464
+ * JWE content encryption method
465
+ * @example 'A256GCM' (AES-256 GCM - recommended, highest security)
466
+ * @example 'A128GCM' (AES-128 GCM - faster, lower security)
467
+ * @example 'A192GCM' (AES-192 GCM - balanced)
468
+ * @default 'A256GCM'
469
+ * @see https://tools.ietf.org/html/rfc7518#section-5.1
470
+ */
319
471
  JWT_ENCRYPTION?: string;
320
472
 
321
- /** Clock tolerance in seconds for token validation (default: 30) */
473
+ /**
474
+ * Token expiration time - accepts number (seconds) or string with time suffix
475
+ * @example 64800 (18 hours as number)
476
+ * @example '18h' (18 hours)
477
+ * @example '7d' (7 days)
478
+ * @example '1080m' (1080 minutes = 18 hours)
479
+ * @example '30s' (30 seconds)
480
+ * @default 64800 (18 hours)
481
+ */
482
+ JWT_EXPIRATION_TIME?: number | string;
483
+
484
+ /**
485
+ * Clock tolerance in seconds for token validation
486
+ * Allows for small time differences between client and server clocks
487
+ * @example 30 (allows 30 seconds of clock drift)
488
+ * @example 60 (more lenient, allows 1 minute of drift)
489
+ * @default 30
490
+ */
322
491
  JWT_CLOCK_TOLERANCE?: number;
323
492
 
324
- /** Hash algorithm for secret derivation (default: 'SHA-256') */
493
+ /**
494
+ * Hash algorithm used for deriving encryption key from secret string
495
+ * @example 'SHA-256' (recommended for most cases)
496
+ * @example 'SHA-384' (higher security)
497
+ * @example 'SHA-512' (highest security, slower)
498
+ * @default 'SHA-256'
499
+ */
325
500
  JWT_SECRET_HASH_ALGORITHM?: string;
326
501
 
327
- /** Optional JWT issuer claim */
502
+ /**
503
+ * JWT issuer claim (iss) - identifies who issued the token
504
+ * Used for token validation to ensure tokens are from expected source
505
+ * @example 'https://myapp.com'
506
+ * @example 'my-auth-service'
507
+ * @optional Recommended for production environments
508
+ */
328
509
  JWT_ISSUER?: string;
329
510
 
330
- /** Optional JWT audience claim */
511
+ /**
512
+ * JWT audience claim (aud) - identifies the intended recipients
513
+ * Used for token validation to ensure tokens are for the correct service
514
+ * @example 'https://api.myapp.com'
515
+ * @example ['api.myapp.com', 'admin.myapp.com'] (can be array)
516
+ * @optional Recommended for production environments
517
+ */
331
518
  JWT_AUDIENCE?: string;
332
519
 
333
- /** Optional JWT subject claim */
520
+ /**
521
+ * JWT subject claim (sub) - identifies the principal (subject) of the token
522
+ * Provides context about what the token represents
523
+ * @example 'user-authentication'
524
+ * @example 'api-access-token'
525
+ * @optional Adds semantic meaning to tokens
526
+ */
334
527
  JWT_SUBJECT?: string;
335
528
  }
336
529
 
337
530
  /**
338
531
  * Options for encrypt() method - uses camelCase naming convention
532
+ * These options allow you to override the defaults set in JwtManagerOptions for specific encryption operations
339
533
  */
340
534
  export interface JwtEncryptOptions {
341
- /** Override default algorithm */
535
+ /**
536
+ * Override default JWE algorithm for this specific token
537
+ * @example 'dir' or 'RSA-OAEP'
538
+ * @use-case Use when you need different encryption algorithms for different token types
539
+ */
342
540
  algorithm?: string;
343
541
 
344
- /** Override default encryption method */
542
+ /**
543
+ * Override default encryption method for this specific token
544
+ * @example 'A256GCM' or 'A128GCM'
545
+ * @use-case Use when you need different encryption strength for different token types
546
+ */
345
547
  encryption?: string;
346
548
 
347
- /** Override default expiration time */
348
- expirationTime?: number;
549
+ /**
550
+ * Override default expiration time for this specific token
551
+ * @example '1h' (short-lived token for sensitive operations)
552
+ * @example '30m' (half hour)
553
+ * @example '7d' (long-lived token for remember-me functionality)
554
+ * @example 3600 (1 hour as number in seconds)
555
+ * @use-case Use when different tokens need different lifetimes (e.g., access token vs refresh token)
556
+ */
557
+ expirationTime?: number | string;
349
558
 
350
- /** Override default hash algorithm */
559
+ /**
560
+ * Override default hash algorithm for deriving encryption key
561
+ * @example 'SHA-256' or 'SHA-512'
562
+ * @use-case Use when you need stronger hashing for specific high-security tokens
563
+ */
351
564
  secretHashAlgorithm?: string;
352
565
 
353
- /** Override default issuer claim */
566
+ /**
567
+ * Override default issuer claim for this specific token
568
+ * @example 'https://admin.myapp.com' (for admin tokens)
569
+ * @use-case Use when tokens are issued by different services
570
+ */
354
571
  issuer?: string;
355
572
 
356
- /** Override default audience claim */
573
+ /**
574
+ * Override default audience claim for this specific token
575
+ * @example 'https://api.myapp.com' (for API access tokens)
576
+ * @example ['service1.myapp.com', 'service2.myapp.com'] (multiple audiences)
577
+ * @use-case Use when tokens are intended for different services
578
+ */
357
579
  audience?: string;
358
580
 
359
- /** Override default subject claim */
581
+ /**
582
+ * Override default subject claim for this specific token
583
+ * @example 'password-reset' (for password reset tokens)
584
+ * @example 'email-verification' (for verification tokens)
585
+ * @use-case Use when different token types serve different purposes
586
+ */
360
587
  subject?: string;
361
588
  }
362
589
 
363
590
  /**
364
591
  * Options for decrypt() method - uses camelCase naming convention
592
+ * These options allow you to override validation settings and verify specific claims during decryption
365
593
  */
366
594
  export interface JwtDecryptOptions {
367
- /** Override default clock tolerance */
595
+ /**
596
+ * Override default clock tolerance for this specific token validation
597
+ * @example 30 (seconds) - allows 30 seconds of clock drift
598
+ * @example 60 (seconds) - more lenient for distributed systems
599
+ * @use-case Use when validating tokens from systems with known clock drift issues
600
+ */
368
601
  clockTolerance?: number;
369
602
 
370
- /** Override default hash algorithm */
603
+ /**
604
+ * Override default hash algorithm for deriving decryption key
605
+ * Must match the algorithm used during encryption
606
+ * @example 'SHA-256' or 'SHA-512'
607
+ * @use-case Use when tokens were encrypted with different hash algorithms
608
+ */
371
609
  secretHashAlgorithm?: string;
372
610
 
373
- /** Expected issuer claim for validation */
611
+ /**
612
+ * Expected issuer claim (iss) for validation
613
+ * Token will be rejected if issuer doesn't match
614
+ * @example 'https://myapp.com'
615
+ * @use-case Use to ensure tokens come from a specific trusted source
616
+ */
374
617
  issuer?: string;
375
618
 
376
- /** Expected audience claim for validation */
619
+ /**
620
+ * Expected audience claim (aud) for validation
621
+ * Token will be rejected if audience doesn't match
622
+ * @example 'https://api.myapp.com'
623
+ * @example ['service1.myapp.com', 'service2.myapp.com'] (multiple valid audiences)
624
+ * @use-case Use to ensure tokens are intended for your service
625
+ */
377
626
  audience?: string;
378
627
 
379
- /** Expected subject claim for validation */
628
+ /**
629
+ * Expected subject claim (sub) for validation
630
+ * Token will be rejected if subject doesn't match
631
+ * @example 'user-authentication'
632
+ * @use-case Use to validate tokens are of the expected type/purpose
633
+ */
380
634
  subject?: string;
381
635
  }
382
636
 
383
- export type JwtDecryptResult = JWTDecryptResult<EncryptJWT>;
637
+ export type JwtDecryptResult = JWTDecryptResult;
384
638
 
385
639
  // JwtManager class for JWT encryption and decryption
386
640
  export class JwtManager {
@@ -396,6 +650,16 @@ export class JwtManager {
396
650
  /**
397
651
  * Create a new JwtManager instance with configurable defaults
398
652
  * @param options Configuration options (uses strict UPPERCASE with JWT_ prefix property names)
653
+ * @example
654
+ * ```javascript
655
+ * const jwtManager = new JwtManager({
656
+ * JWT_ALGORITHM: 'dir',
657
+ * JWT_ENCRYPTION: 'A256GCM',
658
+ * JWT_EXPIRATION_TIME: '18h', // or 64800 seconds
659
+ * JWT_ISSUER: 'https://myapp.com',
660
+ * JWT_AUDIENCE: 'https://api.myapp.com'
661
+ * });
662
+ * ```
399
663
  */
400
664
  constructor(options?: JwtManagerOptions);
401
665
 
@@ -403,17 +667,31 @@ export class JwtManager {
403
667
  * Generate JWT token for user session
404
668
  * @param data User data payload
405
669
  * @param input Secret key or password for encryption
406
- * @param options Per-call configuration overrides (uses strict UPPERCASE with JWT_ prefix property names)
670
+ * @param options Per-call configuration overrides (uses camelCase property names)
407
671
  * @returns Returns encrypted JWT token
408
672
  */
409
673
  encrypt(data: JWTPayload, input: string, options?: JwtEncryptOptions): Promise<string>;
410
674
 
411
675
  /**
412
- * Decrypt JWT token for user session
413
- * @param token JWT token to decrypt
414
- * @param input Secret key or password for decryption
415
- * @param options Per-call configuration overrides (uses strict UPPERCASE with JWT_ prefix property names)
416
- * @returns Returns decrypted JWT token
676
+ * Decrypt and validate JWT token for user session
677
+ * @param token JWT token string to decrypt and validate
678
+ * @param input Secret key or password for decryption (must match the key used for encryption)
679
+ * @param options Per-call validation overrides (uses camelCase property names)
680
+ * @returns Returns decrypted JWT token result containing payload and protected header
681
+ * @throws Will throw error if token is invalid, expired, or validation fails
682
+ * @example
683
+ * ```javascript
684
+ * try {
685
+ * const result = await jwtManager.decrypt(
686
+ * tokenString,
687
+ * 'my-secret-key',
688
+ * { issuer: 'https://myapp.com' } // Optional: validate issuer claim
689
+ * );
690
+ * console.log(result.payload); // Access decrypted data
691
+ * } catch (error) {
692
+ * console.error('Token validation failed:', error.message);
693
+ * }
694
+ * ```
417
695
  */
418
696
  decrypt(token: string, input: string, options?: JwtDecryptOptions): Promise<JwtDecryptResult>;
419
697
  }
@@ -533,4 +811,4 @@ declare module 'express-session' {
533
811
  [key: string]: any;
534
812
  user?: SessionUser;
535
813
  }
536
- }
814
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igxjs/node-components",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "Node components for igxjs",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -12,7 +12,21 @@
12
12
  "url": "git+https://github.com/igxjs/node-components.git"
13
13
  },
14
14
  "keywords": [
15
- "igxjs"
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"
16
30
  ],
17
31
  "author": "Michael",
18
32
  "license": "Apache-2.0",
@@ -20,6 +34,9 @@
20
34
  "url": "https://github.com/igxjs/node-components/issues"
21
35
  },
22
36
  "homepage": "https://github.com/igxjs/node-components#readme",
37
+ "engines": {
38
+ "node": ">=18.0.0"
39
+ },
23
40
  "dependencies": {
24
41
  "@redis/client": "^5.11.0",
25
42
  "@types/express": "^5.0.6",
@@ -31,11 +48,18 @@
31
48
  },
32
49
  "devDependencies": {
33
50
  "chai": "^6.2.2",
34
- "express": "^5.2.1",
35
51
  "mocha": "^12.0.0-beta-10",
36
52
  "sinon": "^21.0.3",
37
53
  "supertest": "^7.0.0"
38
54
  },
55
+ "peerDependencies": {
56
+ "express": "^4.0.0 || ^5.0.0"
57
+ },
58
+ "peerDependenciesMeta": {
59
+ "express": {
60
+ "optional": false
61
+ }
62
+ },
39
63
  "files": [
40
64
  "index.js",
41
65
  "index.d.ts",