@igxjs/node-components 1.0.11 → 1.0.13

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.
@@ -1,4 +1,8 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
1
3
  import crypto from 'node:crypto';
4
+ import { fileURLToPath } from 'node:url';
5
+
2
6
  import axios from 'axios';
3
7
  import session from 'express-session';
4
8
  import memStore from 'memorystore';
@@ -7,6 +11,10 @@ import { RedisStore } from 'connect-redis';
7
11
  import { CustomError, httpCodes, httpHelper, httpMessages } from './http-handlers.js';
8
12
  import { JwtManager } from './jwt.js';
9
13
  import { RedisManager } from './redis.js';
14
+ import { Logger } from './logger.js';
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
10
18
 
11
19
  /**
12
20
  * Session authentication mode constants
@@ -21,13 +29,25 @@ export const SessionMode = {
21
29
  */
22
30
  export class SessionConfig {
23
31
  /**
24
- * @type {string}
25
- * Authentication mode for protected routes
26
- * Supported values: SessionMode.SESSION | SessionMode.TOKEN
32
+ * @type {string} Authentication mode for protected routes
33
+ * - Supported values: SessionMode.SESSION | SessionMode.TOKEN
27
34
  * @default SessionMode.SESSION
28
35
  */
29
36
  SESSION_MODE;
30
- /** @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
+ */
31
51
  SSO_ENDPOINT_URL;
32
52
  /** @type {string} */
33
53
  SSO_CLIENT_ID;
@@ -38,35 +58,69 @@ export class SessionConfig {
38
58
  /** @type {string} */
39
59
  SSO_FAILURE_URL;
40
60
 
41
- /** @type {number} */
61
+ /** @type {number} Session age in milliseconds */
42
62
  SESSION_AGE;
43
- /** @type {string} */
63
+ /**
64
+ * @type {string} Session cookie path
65
+ * @default '/'
66
+ */
44
67
  SESSION_COOKIE_PATH;
45
- /** @type {string} */
68
+ /** @type {string} Session secret */
46
69
  SESSION_SECRET;
47
- /** @type {string} */
70
+ /**
71
+ * @type {string}
72
+ * @default 'ibmid:'
73
+ */
48
74
  SESSION_PREFIX;
49
-
50
- /** @type {string} */
75
+ /**
76
+ * @type {string} Session key
77
+ * - In the `SessionMode.SESSION` mode, this is the key used to store the user in the session.
78
+ * - In the `SessionMode.TOKEN` mode, this is the key of localStorage where the user is stored.
79
+ * @default 'session_token'
80
+ */
81
+ SESSION_KEY;
82
+ /**
83
+ * @type {string} Session expiry key
84
+ * - In the `SessionMode.TOKEN` mode, this is the key of localStorage where the session expiry timestamp is stored.
85
+ * @default 'session_expires_at'
86
+ */
87
+ SESSION_EXPIRY_KEY;
88
+ /**
89
+ * @type {string} Path to custom HTML template for TOKEN mode callback
90
+ * - Used to customize the redirect page that stores JWT token and expiry in localStorage
91
+ * - Supports placeholders: {{SESSION_DATA_KEY}}, {{SESSION_DATA_VALUE}}, {{SESSION_EXPIRY_KEY}}, {{SESSION_EXPIRY_VALUE}}, {{SSO_SUCCESS_URL}}, {{SSO_FAILURE_URL}}
92
+ * - If not provided, uses default template
93
+ */
94
+ TOKEN_STORAGE_TEMPLATE_PATH;
95
+ /** @type {string} Redis URL */
51
96
  REDIS_URL;
52
- /** @type {string} */
97
+ /** @type {string} Redis certificate path */
53
98
  REDIS_CERT_PATH;
54
-
55
- /** @type {string} */
99
+ /**
100
+ * @type {string} Algorithm used to encrypt the JWT
101
+ * @default 'dir'
102
+ */
56
103
  JWT_ALGORITHM;
57
- /** @type {string} */
104
+ /**
105
+ * @type {string} Encryption algorithm used to encrypt the JWT
106
+ * @default 'A256GCM'
107
+ */
58
108
  JWT_ENCRYPTION;
59
- /** @type {string} */
60
- JWT_EXPIRATION_TIME;
61
- /** @type {number} */
109
+ /**
110
+ * @type {number} Clock tolerance in seconds
111
+ * @default 30
112
+ */
62
113
  JWT_CLOCK_TOLERANCE;
63
- /** @type {string} */
114
+ /**
115
+ * @type {string} Hash algorithm used to hash the JWT secret
116
+ * @default 'SHA-256'
117
+ */
64
118
  JWT_SECRET_HASH_ALGORITHM;
65
- /** @type {string} */
119
+ /** @type {string?} JWT issuer claim */
66
120
  JWT_ISSUER;
67
- /** @type {string} */
121
+ /** @type {string?} JWT audience claim */
68
122
  JWT_AUDIENCE;
69
- /** @type {string} */
123
+ /** @type {string?} JWT subject claim */
70
124
  JWT_SUBJECT;
71
125
  }
72
126
 
@@ -81,12 +135,49 @@ export class SessionManager {
81
135
  #idpRequest = null;
82
136
  /** @type {import('./jwt.js').JwtManager} */
83
137
  #jwtManager = null;
138
+ /** @type {import('./logger.js').Logger} */
139
+ #logger = Logger.getInstance('SessionManager');
84
140
 
85
141
  /**
86
142
  * Create a new session manager
87
143
  * @param {SessionConfig} config Session configuration
144
+ * @throws {TypeError} If config is not an object
145
+ * @throws {Error} If required configuration fields are missing
88
146
  */
89
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
+
90
181
  this.#config = {
91
182
  // Session Mode
92
183
  SESSION_MODE: config.SESSION_MODE || SessionMode.SESSION,
@@ -95,6 +186,10 @@ export class SessionManager {
95
186
  SESSION_COOKIE_PATH: config.SESSION_COOKIE_PATH || '/',
96
187
  SESSION_SECRET: config.SESSION_SECRET,
97
188
  SESSION_PREFIX: config.SESSION_PREFIX || 'ibmid:',
189
+ SESSION_KEY: config.SESSION_KEY || 'session_token',
190
+ SESSION_EXPIRY_KEY: config.SESSION_EXPIRY_KEY || 'session_expires_at',
191
+ TOKEN_STORAGE_TEMPLATE_PATH: config.TOKEN_STORAGE_TEMPLATE_PATH,
192
+
98
193
  // Identity Provider
99
194
  SSO_ENDPOINT_URL: config.SSO_ENDPOINT_URL,
100
195
  SSO_CLIENT_ID: config.SSO_CLIENT_ID,
@@ -104,10 +199,10 @@ export class SessionManager {
104
199
  // Redis
105
200
  REDIS_URL: config.REDIS_URL,
106
201
  REDIS_CERT_PATH: config.REDIS_CERT_PATH,
202
+
107
203
  // JWT Manager
108
204
  JWT_ALGORITHM: config.JWT_ALGORITHM || 'dir',
109
205
  JWT_ENCRYPTION: config.JWT_ENCRYPTION || 'A256GCM',
110
- JWT_EXPIRATION_TIME: config.JWT_EXPIRATION_TIME || '10m',
111
206
  JWT_CLOCK_TOLERANCE: config.JWT_CLOCK_TOLERANCE ?? 30,
112
207
  JWT_SECRET_HASH_ALGORITHM: config.JWT_SECRET_HASH_ALGORITHM || 'SHA-256',
113
208
  JWT_ISSUER: config.JWT_ISSUER,
@@ -155,18 +250,18 @@ export class SessionManager {
155
250
  * @returns {string} Returns the session key
156
251
  */
157
252
  #getSessionKey() {
158
- return 'user';
253
+ return this.#config.SESSION_KEY;
159
254
  }
160
255
 
161
256
  /**
162
257
  * Get Redis key for token storage
163
258
  * @param {string} email User email
164
- * @param {string} tokenId Token ID
259
+ * @param {string} tid Token ID
165
260
  * @returns {string} Returns the Redis key for token storage
166
261
  * @private
167
262
  */
168
- #getTokenRedisKey(email, tokenId) {
169
- return `${this.#config.SESSION_PREFIX}token:${email}:${tokenId}`;
263
+ #getTokenRedisKey(email, tid) {
264
+ return `${this.#config.SESSION_KEY}:t:${email}:${tid}`;
170
265
  }
171
266
 
172
267
  /**
@@ -176,7 +271,7 @@ export class SessionManager {
176
271
  * @private
177
272
  */
178
273
  #getTokenRedisPattern(email) {
179
- return `${this.#config.SESSION_PREFIX}token:${email}:*`;
274
+ return `${this.#config.SESSION_KEY}:t:${email}:*`;
180
275
  }
181
276
 
182
277
  /**
@@ -189,47 +284,54 @@ export class SessionManager {
189
284
 
190
285
  /**
191
286
  * Generate and store JWT token in Redis
192
- * @param {object} user User object
287
+ * - JWT payload contains only { email, tid } for minimal size
288
+ * - Full user data is stored in Redis as single source of truth
289
+ * @param {object} user User object with email and attributes
193
290
  * @returns {Promise<string>} Returns the generated JWT token
291
+ * @throws {Error} If JWT encryption fails
292
+ * @throws {Error} If Redis storage fails
194
293
  * @private
294
+ * @example
295
+ * const token = await this.#generateAndStoreToken({
296
+ * email: 'user@example.com',
297
+ * attributes: { /* user data * / }
298
+ * });
195
299
  */
196
300
  async #generateAndStoreToken(user) {
197
301
  // Generate unique token ID for this device/session
198
- const tokenId = crypto.randomUUID();
199
-
200
- // Create JWT token with email and tokenId
302
+ const tid = crypto.randomUUID();
303
+ const ttlSeconds = Math.floor(this.#config.SESSION_AGE / 1000);
304
+ // Create JWT token with only email and tid (minimal payload)
201
305
  const token = await this.#jwtManager.encrypt(
202
- {
203
- email: user.email,
204
- tokenId
205
- },
306
+ { email: user.email, tid },
206
307
  this.#config.SESSION_SECRET,
207
- { expirationTime: this.#config.JWT_EXPIRATION_TIME }
308
+ { expirationTime: ttlSeconds }
208
309
  );
209
-
310
+
210
311
  // Store user data in Redis with TTL
211
- const redisKey = this.#getTokenRedisKey(user.email, tokenId);
212
- const ttlSeconds = Math.floor(this.#config.SESSION_AGE / 1000);
213
-
312
+ const redisKey = this.#getTokenRedisKey(user.email, tid);
313
+
214
314
  await this.#redisManager.getClient().setEx(
215
315
  redisKey,
216
316
  ttlSeconds,
217
317
  JSON.stringify(user)
218
318
  );
219
-
220
- console.debug(`### TOKEN GENERATED: ${user.email} ###`);
221
-
319
+ this.#logger.debug(`### TOKEN GENERATED: ${user.email} ###`);
222
320
  return token;
223
321
  }
224
322
 
225
323
  /**
226
- * Verify token authentication
227
- * @param {import('@types/express').Request} req Request
228
- * @param {import('@types/express').Response} res Response
229
- * @param {import('@types/express').NextFunction} next Next function
230
- * @param {boolean} isDebugging Debugging flag
231
- * @param {string} redirectUrl Redirect URL
324
+ * Verify token authentication - extracts and validates JWT from Authorization header
325
+ * @param {import('@types/express').Request} req Request with Authorization header
326
+ * @param {import('@types/express').Response} res Response object
327
+ * @param {import('@types/express').NextFunction} next Next middleware function
328
+ * @param {boolean} isDebugging If true, allows unauthenticated requests
329
+ * @param {string} redirectUrl URL to redirect to on authentication failure
330
+ * @throws {CustomError} If token is missing, invalid, or expired
232
331
  * @private
332
+ * @example
333
+ * // Authorization header format: "Bearer {jwt_token}"
334
+ * await this.#verifyToken(req, res, next, false, '/login');
233
335
  */
234
336
  async #verifyToken(req, res, next, isDebugging, redirectUrl) {
235
337
  try {
@@ -247,14 +349,14 @@ export class SessionManager {
247
349
  this.#config.SESSION_SECRET
248
350
  );
249
351
 
250
- // Extract email and tokenId
251
- const { email, tokenId } = payload;
252
- if (!email || !tokenId) {
352
+ // Extract email and token ID
353
+ const { email, tid } = payload;
354
+ if (!email || !tid) {
253
355
  throw new CustomError(httpCodes.UNAUTHORIZED, 'Invalid token payload');
254
356
  }
255
357
 
256
358
  // Lookup user in Redis
257
- const redisKey = this.#getTokenRedisKey(email, tokenId);
359
+ const redisKey = this.#getTokenRedisKey(email, tid);
258
360
  const userData = await this.#redisManager.getClient().get(redisKey);
259
361
 
260
362
  if (!userData) {
@@ -263,7 +365,6 @@ export class SessionManager {
263
365
 
264
366
  // Parse and attach user to request
265
367
  req.user = JSON.parse(userData);
266
- res.locals.user = req.user;
267
368
 
268
369
  // Validate authorization
269
370
  const { authorized = isDebugging } = req.user ?? { authorized: isDebugging };
@@ -275,7 +376,7 @@ export class SessionManager {
275
376
 
276
377
  } catch (error) {
277
378
  if (isDebugging) {
278
- console.warn('### TOKEN VERIFICATION FAILED (debugging mode) ###', error.message);
379
+ this.#logger.warn('### TOKEN VERIFICATION FAILED (debugging mode) ###', error.message);
279
380
  return next();
280
381
  }
281
382
 
@@ -294,12 +395,13 @@ export class SessionManager {
294
395
  }
295
396
 
296
397
  /**
297
- * Verify session authentication
298
- * @param {import('@types/express').Request} req Request
299
- * @param {import('@types/express').Response} res Response
300
- * @param {import('@types/express').NextFunction} next Next function
301
- * @param {boolean} isDebugging Debugging flag
302
- * @param {string} redirectUrl Redirect URL
398
+ * Verify session authentication - checks if user is authorized in session
399
+ * @param {import('@types/express').Request} req Request with session data
400
+ * @param {import('@types/express').Response} res Response object
401
+ * @param {import('@types/express').NextFunction} next Next middleware function
402
+ * @param {boolean} isDebugging If true, allows unauthorized users
403
+ * @param {string} redirectUrl URL to redirect to if user is unauthorized
404
+ * @throws {CustomError} If user is not authorized
303
405
  * @private
304
406
  */
305
407
  async #verifySession(req, res, next, isDebugging, redirectUrl) {
@@ -314,13 +416,18 @@ export class SessionManager {
314
416
  }
315
417
 
316
418
  /**
317
- * Refresh token authentication
318
- * @param {import('@types/express').Request} req Request
319
- * @param {import('@types/express').Response} res Response
320
- * @param {import('@types/express').NextFunction} next Next function
321
- * @param {(user: object) => object} initUser Initialize user function
322
- * @param {string} idpUrl Identity provider URL
419
+ * Refresh token authentication - exchanges refresh token for new JWT
420
+ * Invalidates old token and generates a new one with updated user data
421
+ * @param {import('@types/express').Request} req Request with Authorization header
422
+ * @param {import('@types/express').Response} res Response object
423
+ * @param {import('@types/express').NextFunction} next Next middleware function
424
+ * @param {(user: object) => object} initUser Function to initialize/transform user object
425
+ * @param {string} idpUrl Identity provider refresh endpoint URL
426
+ * @throws {CustomError} If refresh lock is active or SSO refresh fails
323
427
  * @private
428
+ * @example
429
+ * // Response format:
430
+ * // { token: "new_jwt", user: {...}, expiresIn: 64800, tokenType: "Bearer" }
324
431
  */
325
432
  async #refreshToken(req, res, next, initUser, idpUrl) {
326
433
  try {
@@ -331,11 +438,11 @@ export class SessionManager {
331
438
  throw new CustomError(httpCodes.UNAUTHORIZED, 'User not authenticated');
332
439
  }
333
440
 
334
- // Extract tokenId from current token
441
+ // Extract Token ID from current token
335
442
  const authHeader = req.headers.authorization;
336
443
  const token = authHeader?.substring(7);
337
444
  const { payload } = await this.#jwtManager.decrypt(token, this.#config.SESSION_SECRET);
338
- const oldTokenId = payload.tokenId;
445
+ const { tid: oldTokenId } = payload;
339
446
 
340
447
  // Check refresh lock
341
448
  if (this.hasLock(email)) {
@@ -376,7 +483,7 @@ export class SessionManager {
376
483
  const oldRedisKey = this.#getTokenRedisKey(email, oldTokenId);
377
484
  await this.#redisManager.getClient().del(oldRedisKey);
378
485
 
379
- console.debug('### TOKEN REFRESHED SUCCESSFULLY ###');
486
+ this.#logger.debug('### TOKEN REFRESHED SUCCESSFULLY ###');
380
487
 
381
488
  // Return new token
382
489
  return res.json({
@@ -450,7 +557,7 @@ export class SessionManager {
450
557
  await this.#redisManager.getClient().del(keys);
451
558
  }
452
559
 
453
- console.info(`### ALL TOKENS LOGGED OUT: ${email} (${keys.length} tokens) ###`);
560
+ this.#logger.info(`### ALL TOKENS LOGGED OUT: ${email} (${keys.length} tokens) ###`);
454
561
 
455
562
  if (isRedirect) {
456
563
  return res.redirect(this.#config.SSO_SUCCESS_URL);
@@ -461,7 +568,7 @@ export class SessionManager {
461
568
  redirect_url: this.#config.SSO_SUCCESS_URL
462
569
  });
463
570
  } catch (error) {
464
- console.error('### LOGOUT ALL TOKENS ERROR ###', error);
571
+ this.#logger.error('### LOGOUT ALL TOKENS ERROR ###', error);
465
572
  if (isRedirect) {
466
573
  return res.redirect(this.#config.SSO_FAILURE_URL);
467
574
  }
@@ -487,7 +594,7 @@ export class SessionManager {
487
594
  }
488
595
 
489
596
  try {
490
- // Extract tokenId from current token
597
+ // Extract Token ID from current token
491
598
  const authHeader = req.headers.authorization;
492
599
  const token = authHeader?.substring(7);
493
600
 
@@ -496,13 +603,17 @@ export class SessionManager {
496
603
  }
497
604
 
498
605
  const { payload } = await this.#jwtManager.decrypt(token, this.#config.SESSION_SECRET);
499
- const { email, tokenId } = payload;
606
+ const { email, tid } = payload;
607
+
608
+ if (!email || !tid) {
609
+ throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid token payload');
610
+ }
500
611
 
501
612
  // Remove token from Redis
502
- const redisKey = this.#getTokenRedisKey(email, tokenId);
613
+ const redisKey = this.#getTokenRedisKey(email, tid);
503
614
  await this.#redisManager.getClient().del(redisKey);
504
615
 
505
- console.info('### TOKEN LOGOUT SUCCESSFULLY ###');
616
+ this.#logger.info('### TOKEN LOGOUT SUCCESSFULLY ###');
506
617
 
507
618
  if (isRedirect) {
508
619
  return res.redirect(this.#config.SSO_SUCCESS_URL);
@@ -513,7 +624,7 @@ export class SessionManager {
513
624
  });
514
625
 
515
626
  } catch (error) {
516
- console.error('### TOKEN LOGOUT ERROR ###', error);
627
+ this.#logger.error('### TOKEN LOGOUT ERROR ###', error);
517
628
  if (isRedirect) {
518
629
  return res.redirect(this.#config.SSO_FAILURE_URL);
519
630
  }
@@ -535,16 +646,16 @@ export class SessionManager {
535
646
  try {
536
647
  res.clearCookie('connect.sid');
537
648
  } catch (error) {
538
- console.error('### CLEAR COOKIE ERROR ###');
539
- console.error(error);
649
+ this.#logger.error('### CLEAR COOKIE ERROR ###');
650
+ this.#logger.error(error);
540
651
  }
541
652
  return req.session.destroy((sessionError) => {
542
653
  if (sessionError) {
543
- console.error('### SESSION DESTROY CALLBACK ERROR ###');
544
- console.error(sessionError);
654
+ this.#logger.error('### SESSION DESTROY CALLBACK ERROR ###');
655
+ this.#logger.error(sessionError);
545
656
  return callback(sessionError);
546
657
  }
547
- console.info('### SESSION LOGOUT SUCCESSFULLY ###');
658
+ this.#logger.info('### SESSION LOGOUT SUCCESSFULLY ###');
548
659
  return callback(null);
549
660
  });
550
661
  }
@@ -573,7 +684,7 @@ export class SessionManager {
573
684
  */
574
685
  #redisSession() {
575
686
  // Redis Session
576
- console.log('### Using Redis as the Session Store ###');
687
+ this.#logger.log('### Using Redis as the Session Store ###');
577
688
  return session({
578
689
  cookie: { maxAge: this.#config.SESSION_AGE, path: this.#config.SESSION_COOKIE_PATH, sameSite: false },
579
690
  store: new RedisStore({ client: this.#redisManager.getClient(), prefix: this.#config.SESSION_PREFIX, disableTouch: true }),
@@ -588,7 +699,7 @@ export class SessionManager {
588
699
  */
589
700
  #memorySession() {
590
701
  // Memory Session
591
- console.log('### Using Memory as the Session Store ###');
702
+ this.#logger.log('### Using Memory as the Session Store ###');
592
703
  const MemoryStore = memStore(session);
593
704
  return session({
594
705
  cookie: { maxAge: this.#config.SESSION_AGE, path: this.#config.SESSION_COOKIE_PATH, sameSite: false },
@@ -675,13 +786,13 @@ export class SessionManager {
675
786
  /** @type {{ payload: { user: import('../models/types/user').UserModel, redirect_url: string } }} */
676
787
  const { payload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
677
788
  if (payload?.user) {
678
- console.debug('### CALLBACK USER ###');
789
+ this.#logger.debug('### CALLBACK USER ###');
679
790
  request.session[this.#getSessionKey()] = initUser(payload.user);
680
791
  return new Promise((resolve, reject) => {
681
792
  request.session.touch().save((err) => {
682
793
  if (err) {
683
- console.error('### SESSION SAVE ERROR ###');
684
- console.error(err);
794
+ this.#logger.error('### SESSION SAVE ERROR ###');
795
+ this.#logger.error(err);
685
796
  return reject(new CustomError(httpCodes.SYSTEM_FAILURE, 'Session failed to save', err));
686
797
  }
687
798
  return resolve(payload);
@@ -711,7 +822,9 @@ export class SessionManager {
711
822
  throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload');
712
823
  }
713
824
 
825
+ /** @type {import('../index.js').SessionUser} */
714
826
  const user = initUser(payload.user);
827
+ /** @type {string} */
715
828
  const redirectUrl = payload.redirect_url || this.#config.SSO_SUCCESS_URL;
716
829
 
717
830
  // Check SESSION_MODE to determine response type
@@ -719,57 +832,26 @@ export class SessionManager {
719
832
  // Token-based: Generate token and return HTML page that stores it
720
833
  const token = await this.#generateAndStoreToken(user);
721
834
 
722
- console.debug('### CALLBACK TOKEN GENERATED ###');
835
+ this.#logger.debug('### CALLBACK TOKEN GENERATED ###');
723
836
 
837
+ const templatePath = this.#config.TOKEN_STORAGE_TEMPLATE_PATH || path.resolve(__dirname, 'assets', 'template.html');
724
838
  // Return HTML page that stores token in localStorage and redirects
725
- return res.send(`
726
- <!DOCTYPE html>
727
- <html>
728
- <head>
729
- <meta charset="UTF-8">
730
- <title>Authentication Complete</title>
731
- <script>
732
- (function() {
733
- try {
734
- // Store auth data in localStorage
735
- localStorage.setItem('authToken', ${JSON.stringify(token)});
736
- localStorage.setItem('tokenExpiry', ${Date.now() + this.#config.SESSION_AGE});
737
- localStorage.setItem('user', ${JSON.stringify({
738
- email: user.email,
739
- name: user.name,
740
- })});
741
-
742
- // Redirect to original destination
743
- window.location.replace(${JSON.stringify(redirectUrl)});
744
- } catch (error) {
745
- console.error('Failed to store authentication:', error);
746
- document.getElementById('error').style.display = 'block';
747
- }
748
- })();
749
- </script>
750
- <style>
751
- body { font-family: system-ui, sans-serif; text-align: center; padding: 50px; }
752
- #error { display: none; color: #d32f2f; margin-top: 20px; }
753
- </style>
754
- </head>
755
- <body>
756
- <p>Completing authentication...</p>
757
- <div id="error">
758
- <p>Authentication failed. Please try again.</p>
759
- <a href="${this.#config.SSO_FAILURE_URL}">Return to login</a>
760
- </div>
761
- </body>
762
- </html>
763
- `);
764
- }
765
- else {
766
- // Session-based: Save to session and redirect
767
- await this.#saveSession(req, jwt, initUser);
768
- return res.redirect(redirectUrl);
839
+ const template = fs.readFileSync(templatePath, 'utf8');
840
+ const html = template
841
+ .replaceAll('{{SESSION_DATA_KEY}}', this.#config.SESSION_KEY)
842
+ .replaceAll('{{SESSION_DATA_VALUE}}', token)
843
+ .replaceAll('{{SESSION_EXPIRY_KEY}}', this.#config.SESSION_EXPIRY_KEY)
844
+ .replaceAll('{{SESSION_EXPIRY_VALUE}}', user.attributes.expires_at)
845
+ .replaceAll('{{SSO_SUCCESS_URL}}', redirectUrl)
846
+ .replaceAll('{{SSO_FAILURE_URL}}', this.#config.SSO_FAILURE_URL);
847
+ return res.send(html);
769
848
  }
849
+ // Session-based: Save to session and redirect
850
+ await this.#saveSession(req, jwt, initUser);
851
+ return res.redirect(redirectUrl);
770
852
  }
771
853
  catch (error) {
772
- console.error('### CALLBACK ERROR ###', error);
854
+ this.#logger.error('### CALLBACK ERROR ###', error);
773
855
  return res.redirect(this.#config.SSO_FAILURE_URL.concat('?message=').concat(encodeURIComponent(error.message)));
774
856
  }
775
857
  };
@@ -832,8 +914,8 @@ export class SessionManager {
832
914
  // Session-based authentication is already single-instance per cookie
833
915
  return this.#logoutSession(req, res, (error) => {
834
916
  if (error) {
835
- console.error('### LOGOUT CALLBACK ERROR ###');
836
- console.error(error);
917
+ this.#logger.error('### LOGOUT CALLBACK ERROR ###');
918
+ this.#logger.error(error);
837
919
  if (isRedirect) {
838
920
  return res.redirect(this.#config.SSO_FAILURE_URL);
839
921
  }