@igxjs/node-components 1.0.9 → 1.0.11

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,15 +1,32 @@
1
+ import crypto from 'node:crypto';
1
2
  import axios from 'axios';
2
3
  import session from 'express-session';
3
4
  import memStore from 'memorystore';
4
5
  import { RedisStore } from 'connect-redis';
5
- import { jwtDecrypt } from 'jose';
6
6
 
7
7
  import { CustomError, httpCodes, httpHelper, httpMessages } from './http-handlers.js';
8
+ import { JwtManager } from './jwt.js';
8
9
  import { RedisManager } from './redis.js';
9
10
 
10
- const MemoryStore = memStore(session);
11
+ /**
12
+ * Session authentication mode constants
13
+ */
14
+ export const SessionMode = {
15
+ SESSION: 'session',
16
+ TOKEN: 'token'
17
+ };
11
18
 
19
+ /**
20
+ * Session configuration options
21
+ */
12
22
  export class SessionConfig {
23
+ /**
24
+ * @type {string}
25
+ * Authentication mode for protected routes
26
+ * Supported values: SessionMode.SESSION | SessionMode.TOKEN
27
+ * @default SessionMode.SESSION
28
+ */
29
+ SESSION_MODE;
13
30
  /** @type {string} */
14
31
  SSO_ENDPOINT_URL;
15
32
  /** @type {string} */
@@ -34,6 +51,23 @@ export class SessionConfig {
34
51
  REDIS_URL;
35
52
  /** @type {string} */
36
53
  REDIS_CERT_PATH;
54
+
55
+ /** @type {string} */
56
+ JWT_ALGORITHM;
57
+ /** @type {string} */
58
+ JWT_ENCRYPTION;
59
+ /** @type {string} */
60
+ JWT_EXPIRATION_TIME;
61
+ /** @type {number} */
62
+ JWT_CLOCK_TOLERANCE;
63
+ /** @type {string} */
64
+ JWT_SECRET_HASH_ALGORITHM;
65
+ /** @type {string} */
66
+ JWT_ISSUER;
67
+ /** @type {string} */
68
+ JWT_AUDIENCE;
69
+ /** @type {string} */
70
+ JWT_SUBJECT;
37
71
  }
38
72
 
39
73
  export class SessionManager {
@@ -45,6 +79,8 @@ export class SessionManager {
45
79
  #redisManager = null;
46
80
  /** @type {import('axios').AxiosInstance} */
47
81
  #idpRequest = null;
82
+ /** @type {import('./jwt.js').JwtManager} */
83
+ #jwtManager = null;
48
84
 
49
85
  /**
50
86
  * Create a new session manager
@@ -52,6 +88,8 @@ export class SessionManager {
52
88
  */
53
89
  constructor(config) {
54
90
  this.#config = {
91
+ // Session Mode
92
+ SESSION_MODE: config.SESSION_MODE || SessionMode.SESSION,
55
93
  // Session
56
94
  SESSION_AGE: config.SESSION_AGE || 64800000,
57
95
  SESSION_COOKIE_PATH: config.SESSION_COOKIE_PATH || '/',
@@ -66,6 +104,15 @@ export class SessionManager {
66
104
  // Redis
67
105
  REDIS_URL: config.REDIS_URL,
68
106
  REDIS_CERT_PATH: config.REDIS_CERT_PATH,
107
+ // JWT Manager
108
+ JWT_ALGORITHM: config.JWT_ALGORITHM || 'dir',
109
+ JWT_ENCRYPTION: config.JWT_ENCRYPTION || 'A256GCM',
110
+ JWT_EXPIRATION_TIME: config.JWT_EXPIRATION_TIME || '10m',
111
+ JWT_CLOCK_TOLERANCE: config.JWT_CLOCK_TOLERANCE ?? 30,
112
+ JWT_SECRET_HASH_ALGORITHM: config.JWT_SECRET_HASH_ALGORITHM || 'SHA-256',
113
+ JWT_ISSUER: config.JWT_ISSUER,
114
+ JWT_AUDIENCE: config.JWT_AUDIENCE,
115
+ JWT_SUBJECT: config.JWT_SUBJECT,
69
116
  };
70
117
  }
71
118
 
@@ -111,6 +158,27 @@ export class SessionManager {
111
158
  return 'user';
112
159
  }
113
160
 
161
+ /**
162
+ * Get Redis key for token storage
163
+ * @param {string} email User email
164
+ * @param {string} tokenId Token ID
165
+ * @returns {string} Returns the Redis key for token storage
166
+ * @private
167
+ */
168
+ #getTokenRedisKey(email, tokenId) {
169
+ return `${this.#config.SESSION_PREFIX}token:${email}:${tokenId}`;
170
+ }
171
+
172
+ /**
173
+ * Get Redis key pattern for all user tokens
174
+ * @param {string} email User email
175
+ * @returns {string} Returns the Redis key pattern for all user tokens
176
+ * @private
177
+ */
178
+ #getTokenRedisPattern(email) {
179
+ return `${this.#config.SESSION_PREFIX}token:${email}:*`;
180
+ }
181
+
114
182
  /**
115
183
  * Get RedisManager instance
116
184
  * @returns {import('./redis.js').RedisManager} Returns the RedisManager instance
@@ -119,6 +187,368 @@ export class SessionManager {
119
187
  return this.#redisManager;
120
188
  }
121
189
 
190
+ /**
191
+ * Generate and store JWT token in Redis
192
+ * @param {object} user User object
193
+ * @returns {Promise<string>} Returns the generated JWT token
194
+ * @private
195
+ */
196
+ async #generateAndStoreToken(user) {
197
+ // Generate unique token ID for this device/session
198
+ const tokenId = crypto.randomUUID();
199
+
200
+ // Create JWT token with email and tokenId
201
+ const token = await this.#jwtManager.encrypt(
202
+ {
203
+ email: user.email,
204
+ tokenId
205
+ },
206
+ this.#config.SESSION_SECRET,
207
+ { expirationTime: this.#config.JWT_EXPIRATION_TIME }
208
+ );
209
+
210
+ // 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
+
214
+ await this.#redisManager.getClient().setEx(
215
+ redisKey,
216
+ ttlSeconds,
217
+ JSON.stringify(user)
218
+ );
219
+
220
+ console.debug(`### TOKEN GENERATED: ${user.email} ###`);
221
+
222
+ return token;
223
+ }
224
+
225
+ /**
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
232
+ * @private
233
+ */
234
+ async #verifyToken(req, res, next, isDebugging, redirectUrl) {
235
+ try {
236
+ // Extract token from Authorization header
237
+ const authHeader = req.headers.authorization;
238
+ if (!authHeader?.startsWith('Bearer ')) {
239
+ throw new CustomError(httpCodes.UNAUTHORIZED, 'Missing or invalid authorization header');
240
+ }
241
+
242
+ const token = authHeader.substring(7); // Remove 'Bearer ' prefix
243
+
244
+ // Decrypt JWT token
245
+ const { payload } = await this.#jwtManager.decrypt(
246
+ token,
247
+ this.#config.SESSION_SECRET
248
+ );
249
+
250
+ // Extract email and tokenId
251
+ const { email, tokenId } = payload;
252
+ if (!email || !tokenId) {
253
+ throw new CustomError(httpCodes.UNAUTHORIZED, 'Invalid token payload');
254
+ }
255
+
256
+ // Lookup user in Redis
257
+ const redisKey = this.#getTokenRedisKey(email, tokenId);
258
+ const userData = await this.#redisManager.getClient().get(redisKey);
259
+
260
+ if (!userData) {
261
+ throw new CustomError(httpCodes.UNAUTHORIZED, 'Token not found or expired');
262
+ }
263
+
264
+ // Parse and attach user to request
265
+ req.user = JSON.parse(userData);
266
+ res.locals.user = req.user;
267
+
268
+ // Validate authorization
269
+ const { authorized = isDebugging } = req.user ?? { authorized: isDebugging };
270
+ if (!authorized && !isDebugging) {
271
+ throw new CustomError(httpCodes.FORBIDDEN, 'User is not authorized');
272
+ }
273
+
274
+ return next();
275
+
276
+ } catch (error) {
277
+ if (isDebugging) {
278
+ console.warn('### TOKEN VERIFICATION FAILED (debugging mode) ###', error.message);
279
+ return next();
280
+ }
281
+
282
+ if (redirectUrl) {
283
+ return res.redirect(redirectUrl);
284
+ }
285
+
286
+ // Handle specific JWT errors
287
+ if (error.code === 'ERR_JWT_EXPIRED') {
288
+ return next(new CustomError(httpCodes.UNAUTHORIZED, 'Token expired'));
289
+ }
290
+
291
+ return next(error instanceof CustomError ? error :
292
+ new CustomError(httpCodes.UNAUTHORIZED, 'Token verification failed'));
293
+ }
294
+ }
295
+
296
+ /**
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
303
+ * @private
304
+ */
305
+ async #verifySession(req, res, next, isDebugging, redirectUrl) {
306
+ const { authorized = isDebugging } = req.user ?? { authorized: isDebugging };
307
+ if (authorized) {
308
+ return next();
309
+ }
310
+ if (redirectUrl) {
311
+ return res.redirect(redirectUrl);
312
+ }
313
+ return next(new CustomError(httpCodes.UNAUTHORIZED, httpMessages.UNAUTHORIZED));
314
+ }
315
+
316
+ /**
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
323
+ * @private
324
+ */
325
+ async #refreshToken(req, res, next, initUser, idpUrl) {
326
+ try {
327
+ // Get current user from verifyToken middleware
328
+ const { email, attributes } = req.user || {};
329
+
330
+ if (!email) {
331
+ throw new CustomError(httpCodes.UNAUTHORIZED, 'User not authenticated');
332
+ }
333
+
334
+ // Extract tokenId from current token
335
+ const authHeader = req.headers.authorization;
336
+ const token = authHeader?.substring(7);
337
+ const { payload } = await this.#jwtManager.decrypt(token, this.#config.SESSION_SECRET);
338
+ const oldTokenId = payload.tokenId;
339
+
340
+ // Check refresh lock
341
+ if (this.hasLock(email)) {
342
+ throw new CustomError(httpCodes.CONFLICT, 'Token refresh is locked');
343
+ }
344
+ this.lock(email);
345
+
346
+ // Call SSO refresh endpoint
347
+ const response = await this.#idpRequest.post(idpUrl, {
348
+ user: {
349
+ email,
350
+ attributes: {
351
+ idp: attributes?.idp,
352
+ refresh_token: attributes?.refresh_token
353
+ }
354
+ }
355
+ });
356
+
357
+ if (response.status !== httpCodes.OK) {
358
+ throw new CustomError(response.status, response.statusText);
359
+ }
360
+
361
+ // Decrypt new user data from SSO
362
+ const { jwt } = response.data;
363
+ const { payload: newPayload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
364
+
365
+ if (!newPayload?.user) {
366
+ throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload from SSO');
367
+ }
368
+
369
+ // Initialize user with new data
370
+ const user = initUser(newPayload.user);
371
+
372
+ // Generate new token
373
+ const newToken = await this.#generateAndStoreToken(user);
374
+
375
+ // Remove old token from Redis
376
+ const oldRedisKey = this.#getTokenRedisKey(email, oldTokenId);
377
+ await this.#redisManager.getClient().del(oldRedisKey);
378
+
379
+ console.debug('### TOKEN REFRESHED SUCCESSFULLY ###');
380
+
381
+ // Return new token
382
+ return res.json({
383
+ token: newToken,
384
+ user,
385
+ expiresIn: Math.floor(this.#config.SESSION_AGE / 1000),
386
+ tokenType: 'Bearer'
387
+ });
388
+ } catch (error) {
389
+ return next(httpHelper.handleAxiosError(error));
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Refresh session authentication
395
+ * @param {import('@types/express').Request} req Request
396
+ * @param {import('@types/express').Response} res Response
397
+ * @param {import('@types/express').NextFunction} next Next function
398
+ * @param {(user: object) => object} initUser Initialize user function
399
+ * @param {string} idpUrl Identity provider URL
400
+ * @private
401
+ */
402
+ async #refreshSession(req, res, next, initUser, idpUrl) {
403
+ try {
404
+ const { email, attributes } = req.user || { email: '', attributes: {} };
405
+ if (this.hasLock(email)) {
406
+ throw new CustomError(httpCodes.CONFLICT, 'User refresh is locked');
407
+ }
408
+ this.lock(email);
409
+ const response = await this.#idpRequest.post(idpUrl, {
410
+ user: {
411
+ email,
412
+ attributes: {
413
+ idp: attributes?.idp,
414
+ refresh_token: attributes?.refresh_token
415
+ }
416
+ }
417
+ });
418
+ if (response.status === httpCodes.OK) {
419
+ const { jwt } = response.data;
420
+ const payload = await this.#saveSession(req, jwt, initUser);
421
+ return res.json(payload);
422
+ }
423
+ throw new CustomError(response.status, response.statusText);
424
+ } catch (error) {
425
+ return next(httpHelper.handleAxiosError(error));
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Logout all tokens for a user
431
+ * @param {import('@types/express').Request} req Request
432
+ * @param {import('@types/express').Response} res Response
433
+ * @param {boolean} isRedirect Whether to redirect
434
+ * @private
435
+ */
436
+ async #logoutAllTokens(req, res, isRedirect) {
437
+ try {
438
+ const { email } = req.user || {};
439
+
440
+ if (!email) {
441
+ throw new CustomError(httpCodes.UNAUTHORIZED, 'User not authenticated');
442
+ }
443
+
444
+ // Find all tokens for this user
445
+ const pattern = this.#getTokenRedisPattern(email);
446
+ const keys = await this.#redisManager.getClient().keys(pattern);
447
+
448
+ // Delete all tokens
449
+ if (keys.length > 0) {
450
+ await this.#redisManager.getClient().del(keys);
451
+ }
452
+
453
+ console.info(`### ALL TOKENS LOGGED OUT: ${email} (${keys.length} tokens) ###`);
454
+
455
+ if (isRedirect) {
456
+ return res.redirect(this.#config.SSO_SUCCESS_URL);
457
+ }
458
+ return res.json({
459
+ message: 'All tokens logged out successfully',
460
+ tokensRemoved: keys.length,
461
+ redirect_url: this.#config.SSO_SUCCESS_URL
462
+ });
463
+ } catch (error) {
464
+ console.error('### LOGOUT ALL TOKENS ERROR ###', error);
465
+ if (isRedirect) {
466
+ return res.redirect(this.#config.SSO_FAILURE_URL);
467
+ }
468
+ return res.status(httpCodes.SYSTEM_FAILURE).json({
469
+ error: 'Logout all failed',
470
+ redirect_url: this.#config.SSO_FAILURE_URL
471
+ });
472
+ }
473
+ }
474
+
475
+ /**
476
+ * Logout token authentication
477
+ * @param {import('@types/express').Request} req Request
478
+ * @param {import('@types/express').Response} res Response
479
+ * @param {boolean} isRedirect Whether to redirect
480
+ * @param {boolean} logoutAll Whether to logout all tokens
481
+ * @private
482
+ */
483
+ async #logoutToken(req, res, isRedirect, logoutAll = false) {
484
+ // If logoutAll is true, delegate to the all tokens logout method
485
+ if (logoutAll) {
486
+ return this.#logoutAllTokens(req, res, isRedirect);
487
+ }
488
+
489
+ try {
490
+ // Extract tokenId from current token
491
+ const authHeader = req.headers.authorization;
492
+ const token = authHeader?.substring(7);
493
+
494
+ if (!token) {
495
+ throw new CustomError(httpCodes.BAD_REQUEST, 'No token provided');
496
+ }
497
+
498
+ const { payload } = await this.#jwtManager.decrypt(token, this.#config.SESSION_SECRET);
499
+ const { email, tokenId } = payload;
500
+
501
+ // Remove token from Redis
502
+ const redisKey = this.#getTokenRedisKey(email, tokenId);
503
+ await this.#redisManager.getClient().del(redisKey);
504
+
505
+ console.info('### TOKEN LOGOUT SUCCESSFULLY ###');
506
+
507
+ if (isRedirect) {
508
+ return res.redirect(this.#config.SSO_SUCCESS_URL);
509
+ }
510
+ return res.json({
511
+ message: 'Logout successful',
512
+ redirect_url: this.#config.SSO_SUCCESS_URL
513
+ });
514
+
515
+ } catch (error) {
516
+ console.error('### TOKEN LOGOUT ERROR ###', error);
517
+ if (isRedirect) {
518
+ return res.redirect(this.#config.SSO_FAILURE_URL);
519
+ }
520
+ return res.status(httpCodes.SYSTEM_FAILURE).json({
521
+ error: 'Logout failed',
522
+ redirect_url: this.#config.SSO_FAILURE_URL
523
+ });
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Logout session authentication
529
+ * @param {import('@types/express').Request} req Request
530
+ * @param {import('@types/express').Response} res Response
531
+ * @param {Function} callback Callback function
532
+ * @private
533
+ */
534
+ #logoutSession(req, res, callback) {
535
+ try {
536
+ res.clearCookie('connect.sid');
537
+ } catch (error) {
538
+ console.error('### CLEAR COOKIE ERROR ###');
539
+ console.error(error);
540
+ }
541
+ return req.session.destroy((sessionError) => {
542
+ if (sessionError) {
543
+ console.error('### SESSION DESTROY CALLBACK ERROR ###');
544
+ console.error(sessionError);
545
+ return callback(sessionError);
546
+ }
547
+ console.info('### SESSION LOGOUT SUCCESSFULLY ###');
548
+ return callback(null);
549
+ });
550
+ }
551
+
122
552
  /**
123
553
  * Setup the session/user handlers with configurations
124
554
  * @param {import('@types/express').Application} app Express application
@@ -126,6 +556,7 @@ export class SessionManager {
126
556
  */
127
557
  async setup(app, updateUser) {
128
558
  this.#redisManager = new RedisManager();
559
+ this.#jwtManager = new JwtManager(this.#config);
129
560
  // Identity Provider Request
130
561
  this.#idpRequest = axios.create({
131
562
  baseURL: this.#config.SSO_ENDPOINT_URL,
@@ -158,6 +589,7 @@ export class SessionManager {
158
589
  #memorySession() {
159
590
  // Memory Session
160
591
  console.log('### Using Memory as the Session Store ###');
592
+ const MemoryStore = memStore(session);
161
593
  return session({
162
594
  cookie: { maxAge: this.#config.SESSION_AGE, path: this.#config.SESSION_COOKIE_PATH, sameSite: false },
163
595
  store: new MemoryStore({}),
@@ -193,24 +625,44 @@ export class SessionManager {
193
625
  }
194
626
 
195
627
  /**
196
- * Resource protection
628
+ * Resource protection based on configured SESSION_MODE
197
629
  * @param {boolean} [isDebugging=false] Debugging flag
198
- * @param {boolean} [redirectUrl=''] Redirect flag
630
+ * @param {string} [redirectUrl=''] Redirect URL
199
631
  * @returns {import('@types/express').RequestHandler} Returns express Request Handler
200
632
  */
201
- authenticate (isDebugging = false, redirectUrl = '') {
633
+ authenticate(isDebugging = false, redirectUrl = '') {
202
634
  return async (req, res, next) => {
203
- /** @type {{ authorized: boolean }} */
204
- const { authorized = isDebugging } = req.user ?? { authorized: isDebugging };
205
- if (authorized) {
206
- return next();
635
+ const mode = this.#config.SESSION_MODE || SessionMode.SESSION;
636
+ if (mode === SessionMode.TOKEN) {
637
+ return this.#verifyToken(req, res, next, isDebugging, redirectUrl);
207
638
  }
208
- if(redirectUrl) {
209
- return res.redirect(redirectUrl);
210
- }
211
- return next(new CustomError(httpCodes.UNAUTHORIZED, httpMessages.UNAUTHORIZED));
639
+ return this.#verifySession(req, res, next, isDebugging, redirectUrl);
212
640
  };
213
- };
641
+ }
642
+
643
+ /**
644
+ * Resource protection by token (explicit token verification)
645
+ * @param {boolean} [isDebugging=false] Debugging flag
646
+ * @param {string} [redirectUrl=''] Redirect URL
647
+ * @returns {import('@types/express').RequestHandler} Returns express Request Handler
648
+ */
649
+ verifyToken(isDebugging = false, redirectUrl = '') {
650
+ return async (req, res, next) => {
651
+ return this.#verifyToken(req, res, next, isDebugging, redirectUrl);
652
+ };
653
+ }
654
+
655
+ /**
656
+ * Resource protection by session (explicit session verification)
657
+ * @param {boolean} [isDebugging=false] Debugging flag
658
+ * @param {string} [redirectUrl=''] Redirect URL
659
+ * @returns {import('@types/express').RequestHandler} Returns express Request Handler
660
+ */
661
+ verifySession(isDebugging = false, redirectUrl = '') {
662
+ return async (req, res, next) => {
663
+ return this.#verifySession(req, res, next, isDebugging, redirectUrl);
664
+ };
665
+ }
214
666
 
215
667
  /**
216
668
  * Save session
@@ -221,7 +673,7 @@ export class SessionManager {
221
673
  */
222
674
  #saveSession = async (request, jwt, initUser) => {
223
675
  /** @type {{ payload: { user: import('../models/types/user').UserModel, redirect_url: string } }} */
224
- const { payload } = await this.#decryptJWT(jwt, this.#config.SSO_CLIENT_SECRET);
676
+ const { payload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
225
677
  if (payload?.user) {
226
678
  console.debug('### CALLBACK USER ###');
227
679
  request.session[this.#getSessionKey()] = initUser(payload.user);
@@ -247,16 +699,77 @@ export class SessionManager {
247
699
  callback(initUser) {
248
700
  return async (req, res, next) => {
249
701
  const { jwt = '' } = req.query;
250
- if(!jwt) {
702
+ if (!jwt) {
251
703
  return next(new CustomError(httpCodes.BAD_REQUEST, 'Missing `jwt` in query parameters'));
252
704
  }
705
+
253
706
  try {
254
- const payload = await this.#saveSession(req, jwt, initUser);
255
- return res.redirect(payload?.redirect_url ? payload.redirect_url : this.#config.SSO_SUCCESS_URL);
707
+ // Decrypt JWT from Identity Adapter
708
+ const { payload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
709
+
710
+ if (!payload?.user) {
711
+ throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload');
712
+ }
713
+
714
+ const user = initUser(payload.user);
715
+ const redirectUrl = payload.redirect_url || this.#config.SSO_SUCCESS_URL;
716
+
717
+ // Check SESSION_MODE to determine response type
718
+ if (this.#config.SESSION_MODE === SessionMode.TOKEN) {
719
+ // Token-based: Generate token and return HTML page that stores it
720
+ const token = await this.#generateAndStoreToken(user);
721
+
722
+ console.debug('### CALLBACK TOKEN GENERATED ###');
723
+
724
+ // 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);
769
+ }
256
770
  }
257
771
  catch (error) {
258
- console.error('### LOGIN ERROR ###');
259
- console.error(error);
772
+ console.error('### CALLBACK ERROR ###', error);
260
773
  return res.redirect(this.#config.SSO_FAILURE_URL.concat('?message=').concat(encodeURIComponent(error.message)));
261
774
  }
262
775
  };
@@ -283,100 +796,57 @@ export class SessionManager {
283
796
  }
284
797
 
285
798
  /**
286
- * Application logout (NOT SSO)
287
- * @returns {import('@types/express').RequestHandler} Returns express Request Handler
288
- */
289
- logout() {
290
- return (req, res) => {
291
- const { redirect = false } = req.query;
292
- const isRedirect = (redirect === 'true' || redirect === true);
293
- return this.#logout(req, res, (error => {
294
- if (error) {
295
- console.error('### LOGOUT CALLBACK ERROR ###');
296
- console.error(error);
297
- if (isRedirect)
298
- return res.redirect(this.#config.SSO_FAILURE_URL);
299
- return res.status(httpCodes.AUTHORIZATION_FAILED).send({ redirect_url: this.#config.SSO_FAILURE_URL });
300
- }
301
- if (isRedirect)
302
- return res.redirect(this.#config.SSO_SUCCESS_URL);
303
- return res.send({ redirect_url: this.#config.SSO_SUCCESS_URL });
304
- }));
305
- };
306
- }
307
-
308
- /**
309
- * Refresh user session
799
+ * Refresh user authentication based on configured SESSION_MODE
310
800
  * @param {(user: object) => object} initUser Initialize user object function
311
801
  * @returns {import('@types/express').RequestHandler} Returns express Request Handler
312
802
  */
313
803
  refresh(initUser) {
314
804
  const idpUrl = '/auth/refresh'.concat('?client_id=').concat(this.#config.SSO_CLIENT_ID);
315
805
  return async (req, res, next) => {
316
- try {
317
- const { email, attributes } = req.user || { email: '', attributes: {} };
318
- if (this.hasLock(email)) {
319
- throw new CustomError(httpCodes.CONFLICT, 'User refresh is locked');
320
- }
321
- this.lock(email);
322
- const response = await this.#idpRequest.post(idpUrl, {
323
- user: {
324
- email,
325
- attributes: {
326
- idp: attributes?.idp,
327
- refresh_token: attributes?.refresh_token
328
- },
329
- }
330
- });
331
- if(response.status === httpCodes.OK) {
332
- /** @type {{ jwt: string }} */
333
- const { jwt } = response.data;
334
- const payload = await this.#saveSession(req, jwt, initUser);
335
- return res.json(payload);
336
- }
337
- throw new CustomError(response.status, response.statusText);
338
- }
339
- catch(error) {
340
- return next(httpHelper.handleAxiosError(error));
806
+ const mode = this.#config.SESSION_MODE || SessionMode.SESSION;
807
+
808
+ if (mode === SessionMode.TOKEN) {
809
+ return this.#refreshToken(req, res, next, initUser, idpUrl);
810
+ } else {
811
+ return this.#refreshSession(req, res, next, initUser, idpUrl);
341
812
  }
342
813
  };
343
814
  }
344
815
 
345
816
  /**
346
- * Logout
347
- * @param {import('@types/express').Request} req Request
348
- * @param {import('@types/express').Response} res Response
349
- * @param {(error: Error)} callback Callback
817
+ * Application logout based on configured SESSION_MODE (NOT SSO)
818
+ * @returns {import('@types/express').RequestHandler} Returns express Request Handler
350
819
  */
351
- #logout(req, res, callback) {
352
- try {
353
- res.clearCookie('connect.sid');
354
- } catch (error) {
355
- console.error('### CLEAR COOKIE ERROR ###');
356
- console.error(error);
357
- }
358
- return req.session.destroy((sessionError) => {
359
- if (sessionError) {
360
- console.error('### SESSION DESTROY CALLBACK ERROR ###');
361
- console.error(sessionError);
362
- return callback(sessionError);
820
+ logout() {
821
+ return async (req, res) => {
822
+ const { redirect = false, all = false } = req.query;
823
+ const isRedirect = (redirect === 'true' || redirect === true);
824
+ const logoutAll = (all === 'true' || all === true);
825
+ const mode = this.#config.SESSION_MODE || SessionMode.SESSION;
826
+
827
+ if (mode === SessionMode.TOKEN) {
828
+ return this.#logoutToken(req, res, isRedirect, logoutAll);
363
829
  }
364
- console.info('### LOGOUT SUCCESSFULLY ###');
365
- return callback(null);
366
- });
367
- }
368
830
 
369
- /**
370
- * Decrypt JWT data for user session
371
- * @param {string} data JWT data
372
- * @param {string} input Input string for encryption
373
- * @returns {Promise<import('jose').JWTDecryptResult<import('jose').EncryptJWT>>} Returns decrypted JWT payload
374
- */
375
- async #decryptJWT(data, input) {
376
- const secret = await crypto.subtle.digest(
377
- 'SHA-256',
378
- new TextEncoder().encode(input)
379
- );
380
- return await jwtDecrypt(data, new Uint8Array(secret), { clockTolerance: 30 });
831
+ // Note: 'all' parameter is only applicable for token-based authentication
832
+ // Session-based authentication is already single-instance per cookie
833
+ return this.#logoutSession(req, res, (error) => {
834
+ if (error) {
835
+ console.error('### LOGOUT CALLBACK ERROR ###');
836
+ console.error(error);
837
+ if (isRedirect) {
838
+ return res.redirect(this.#config.SSO_FAILURE_URL);
839
+ }
840
+ return res.status(httpCodes.AUTHORIZATION_FAILED).send({
841
+ redirect_url: this.#config.SSO_FAILURE_URL
842
+ });
843
+ }
844
+ if (isRedirect) {
845
+ return res.redirect(this.#config.SSO_SUCCESS_URL);
846
+ }
847
+ return res.send({ redirect_url: this.#config.SSO_SUCCESS_URL });
848
+ });
849
+ };
381
850
  }
851
+
382
852
  }