@igxjs/node-components 1.0.14 → 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -7
- package/components/assets/template.html +7 -2
- package/components/session.js +301 -240
- package/index.d.ts +90 -23
- package/package.json +3 -17
package/README.md
CHANGED
|
@@ -46,12 +46,18 @@ export const tokenSession = new SessionManager({
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
// Setup in your app
|
|
49
|
-
await session.setup(app
|
|
49
|
+
await session.setup(app);
|
|
50
50
|
|
|
51
|
-
// Protect routes
|
|
52
|
-
app.get('/protected', session.authenticate(), (req, res) => {
|
|
51
|
+
// Protect routes - user data automatically loaded into req.user
|
|
52
|
+
app.get('/protected', session.authenticate(), session.requireUser(), (req, res) => {
|
|
53
53
|
res.json({ user: req.user });
|
|
54
54
|
});
|
|
55
|
+
|
|
56
|
+
// SSO callback with user transformation
|
|
57
|
+
app.get('/auth/callback', session.callback((user) => ({
|
|
58
|
+
...user,
|
|
59
|
+
displayName: user.email
|
|
60
|
+
})));
|
|
55
61
|
```
|
|
56
62
|
|
|
57
63
|
[📖 Full SessionManager Documentation](./docs/session-manager.md)
|
|
@@ -131,8 +137,9 @@ Uses traditional server-side session cookies. When a user authenticates via SSO,
|
|
|
131
137
|
- `REDIS_URL`: Redis connection string for session storage
|
|
132
138
|
|
|
133
139
|
**Auth Methods:**
|
|
134
|
-
- `session.authenticate()` - Protect routes with SSO session verification
|
|
135
|
-
- `session.verifySession(
|
|
140
|
+
- `session.authenticate(errorRedirectUrl)` - Protect routes with SSO session verification
|
|
141
|
+
- `session.verifySession(errorRedirectUrl)` - Explicit session verification method
|
|
142
|
+
- `session.requireUser()` - Middleware to load user data into `req.user` from session store
|
|
136
143
|
- `session.logout(redirect?, all?)` - Logout current session (or logout all for token mode)
|
|
137
144
|
|
|
138
145
|
### TOKEN Mode
|
|
@@ -148,10 +155,11 @@ Uses JWT bearer tokens instead of session cookies. When a user authenticates via
|
|
|
148
155
|
- `JWT_CLOCK_TOLERANCE`: Clock skew tolerance in seconds (default: 30)
|
|
149
156
|
|
|
150
157
|
**Auth Methods:**
|
|
151
|
-
- `session.verifyToken(
|
|
158
|
+
- `session.verifyToken(errorRedirectUrl)` - Protect routes with token verification
|
|
159
|
+
- `session.requireUser()` - Middleware to load user data into `req.user` from Redis using JWT token
|
|
152
160
|
- `session.callback(initUser)` - SSO callback handler for token generation
|
|
153
161
|
- `session.refresh(initUser)` - Refresh user authentication based on auth mode
|
|
154
|
-
- `session.logout(redirect?, all?)` - Logout current or all tokens
|
|
162
|
+
- `session.logout(redirect?, all?)` - Logout current token or all tokens for user
|
|
155
163
|
|
|
156
164
|
**Token Storage (Client-Side):**
|
|
157
165
|
|
|
@@ -186,6 +194,9 @@ fetch('/api/protected', {
|
|
|
186
194
|
| `SESSION_COOKIE_PATH` | string | `'/'` | Session cookie path |
|
|
187
195
|
| `SESSION_SECRET` | string | - | Session/JWT secret key |
|
|
188
196
|
| `SESSION_PREFIX` | string | `'ibmid:'` | Redis session/key prefix |
|
|
197
|
+
| `SESSION_KEY` | string | `'session_token'` | Redis key for session data (SESSION mode) or localStorage key for token (TOKEN mode) |
|
|
198
|
+
| `SESSION_EXPIRY_KEY` | string | `'session_expires_at'` | localStorage key for session expiry timestamp (TOKEN mode) |
|
|
199
|
+
| `TOKEN_STORAGE_TEMPLATE_PATH` | string | - | Path to custom HTML template for TOKEN mode callback |
|
|
189
200
|
| `REDIS_URL` | string | - | Redis connection URL (optional) |
|
|
190
201
|
| `REDIS_CERT_PATH` | string | - | Path to Redis TLS certificate |
|
|
191
202
|
| `JWT_ALGORITHM` | string | `'dir'` | JWT signing algorithm |
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<title>Sign in IBM Garage</title>
|
|
5
|
-
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no,viewport-fit=cover,minimum-scale=1,maximum-scale=
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no,viewport-fit=cover,minimum-scale=1,maximum-scale=2">
|
|
6
6
|
<meta name="robots" content="noindex, nofollow" />
|
|
7
7
|
<style>
|
|
8
8
|
:root {
|
|
@@ -94,9 +94,14 @@
|
|
|
94
94
|
localStorage.setItem('{{SESSION_EXPIRY_KEY}}', '{{SESSION_EXPIRY_VALUE}}');
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Note: You can also request full user informlation by using ajax request or loading server side page.
|
|
99
|
+
*/
|
|
100
|
+
|
|
97
101
|
// Fall back to simple navigation
|
|
98
102
|
location.href = '{{SSO_SUCCESS_URL}}';
|
|
99
|
-
}
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
100
105
|
console.error('Redirect failed:', e);
|
|
101
106
|
success = false;
|
|
102
107
|
}
|
package/components/session.js
CHANGED
|
@@ -137,6 +137,8 @@ export class SessionManager {
|
|
|
137
137
|
#jwtManager = null;
|
|
138
138
|
/** @type {import('./logger.js').Logger} */
|
|
139
139
|
#logger = Logger.getInstance('SessionManager');
|
|
140
|
+
/** @type {string?} Cached HTML template for token storage */
|
|
141
|
+
#htmlTemplate = null;
|
|
140
142
|
|
|
141
143
|
/**
|
|
142
144
|
* Create a new session manager
|
|
@@ -270,7 +272,7 @@ export class SessionManager {
|
|
|
270
272
|
* @private
|
|
271
273
|
*/
|
|
272
274
|
#getTokenRedisKey(email, tid) {
|
|
273
|
-
return `${this.#config.SESSION_KEY}
|
|
275
|
+
return `${this.#config.SESSION_KEY}:${email}:${tid}`;
|
|
274
276
|
}
|
|
275
277
|
|
|
276
278
|
/**
|
|
@@ -280,7 +282,7 @@ export class SessionManager {
|
|
|
280
282
|
* @private
|
|
281
283
|
*/
|
|
282
284
|
#getTokenRedisPattern(email) {
|
|
283
|
-
return `${this.#config.SESSION_KEY}
|
|
285
|
+
return `${this.#config.SESSION_KEY}:${email}:*`;
|
|
284
286
|
}
|
|
285
287
|
|
|
286
288
|
/**
|
|
@@ -291,75 +293,103 @@ export class SessionManager {
|
|
|
291
293
|
return this.#redisManager;
|
|
292
294
|
}
|
|
293
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Setup the session/user handlers with configurations
|
|
298
|
+
* @param {import('@types/express').Application} app Express application
|
|
299
|
+
*/
|
|
300
|
+
async setup(app) {
|
|
301
|
+
this.#redisManager = new RedisManager();
|
|
302
|
+
this.#jwtManager = new JwtManager({
|
|
303
|
+
...this.#config,
|
|
304
|
+
JWT_EXPIRATION_TIME: this.#config.SESSION_AGE, // SESSION_AGE is already in seconds
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Identity Provider Request
|
|
308
|
+
this.#idpRequest = axios.create({
|
|
309
|
+
baseURL: this.#config.SSO_ENDPOINT_URL,
|
|
310
|
+
timeout: 30000,
|
|
311
|
+
});
|
|
312
|
+
app.set('trust proxy', 1);
|
|
313
|
+
const isOK = await this.#redisManager.connect(this.#config.REDIS_URL, this.#config.REDIS_CERT_PATH);
|
|
314
|
+
if (this.getSessionMode() === SessionMode.SESSION) {
|
|
315
|
+
app.use(this.#sessionHandler(isOK));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Cache HTML template for TOKEN mode
|
|
319
|
+
if (this.getSessionMode() === SessionMode.TOKEN) {
|
|
320
|
+
const templatePath = this.#config.TOKEN_STORAGE_TEMPLATE_PATH ||
|
|
321
|
+
path.resolve(__dirname, 'assets', 'template.html');
|
|
322
|
+
this.#htmlTemplate = fs.readFileSync(templatePath, 'utf8');
|
|
323
|
+
this.#logger.debug('### HTML TEMPLATE CACHED ###');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Generate lightweight JWT token
|
|
329
|
+
* @param {string} email User email
|
|
330
|
+
* @param {string} tokenId Token ID
|
|
331
|
+
* @param {number} expirationTime Expiration time in seconds
|
|
332
|
+
* @returns {Promise<string>} Returns the generated JWT token
|
|
333
|
+
* @private
|
|
334
|
+
*/
|
|
335
|
+
async #getLightweightToken(email, tokenId, expirationTime) {
|
|
336
|
+
return await this.#jwtManager.encrypt({ email, tid: tokenId }, this.#config.SSO_CLIENT_SECRET, { expirationTime });
|
|
337
|
+
}
|
|
338
|
+
|
|
294
339
|
/**
|
|
295
340
|
* Generate and store JWT token in Redis
|
|
296
341
|
* - JWT payload contains only { email, tid } for minimal size
|
|
297
342
|
* - Full user data is stored in Redis as single source of truth
|
|
298
|
-
* @param {
|
|
343
|
+
* @param {string} tid Token ID
|
|
344
|
+
* @param {Record<string, any> & { email: string, tid: string }} user User object with email and attributes
|
|
299
345
|
* @returns {Promise<string>} Returns the generated JWT token
|
|
300
346
|
* @throws {Error} If JWT encryption fails
|
|
301
347
|
* @throws {Error} If Redis storage fails
|
|
302
348
|
* @private
|
|
303
349
|
* @example
|
|
304
|
-
* const token = await this.#generateAndStoreToken({
|
|
350
|
+
* const token = await this.#generateAndStoreToken('tid', {
|
|
305
351
|
* email: 'user@example.com',
|
|
306
352
|
* attributes: { /* user data * / }
|
|
307
353
|
* });
|
|
308
354
|
*/
|
|
309
|
-
async #generateAndStoreToken(user) {
|
|
310
|
-
//
|
|
311
|
-
const
|
|
312
|
-
// SESSION_AGE is already in seconds
|
|
313
|
-
const ttlSeconds = this.#config.SESSION_AGE;
|
|
314
|
-
// Create JWT token with only email and tid (minimal payload)
|
|
315
|
-
const token = await this.#jwtManager.encrypt(
|
|
316
|
-
{ email: user.email, tid },
|
|
317
|
-
this.#config.SESSION_SECRET,
|
|
318
|
-
{ expirationTime: ttlSeconds }
|
|
319
|
-
);
|
|
355
|
+
async #generateAndStoreToken(tid, user) {
|
|
356
|
+
// Create JWT token with only email, tid and idp (minimal payload)
|
|
357
|
+
const token = await this.#getLightweightToken(user.email, tid, this.#config.SESSION_AGE);
|
|
320
358
|
|
|
321
359
|
// Store user data in Redis with TTL
|
|
322
360
|
const redisKey = this.#getTokenRedisKey(user.email, tid);
|
|
323
361
|
|
|
324
|
-
await this.#redisManager.getClient().setEx(
|
|
325
|
-
redisKey,
|
|
326
|
-
ttlSeconds,
|
|
327
|
-
JSON.stringify(user)
|
|
328
|
-
);
|
|
362
|
+
await this.#redisManager.getClient().setEx(redisKey, this.#config.SESSION_AGE, JSON.stringify(user));
|
|
329
363
|
this.#logger.debug(`### TOKEN GENERATED: ${user.email} ###`);
|
|
330
364
|
return token;
|
|
331
365
|
}
|
|
332
366
|
|
|
333
367
|
/**
|
|
334
|
-
*
|
|
335
|
-
* @param {
|
|
336
|
-
* @param {
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
* @
|
|
340
|
-
*
|
|
368
|
+
* Extract and validate user data from Authorization header (TOKEN mode only)
|
|
369
|
+
* @param {string} authHeader Authorization header in format "Bearer {token}"
|
|
370
|
+
* @param {boolean} [includeUserData=true] Whether to include full user data in response
|
|
371
|
+
* - true: Returns { tid, user } with full user data (default)
|
|
372
|
+
* - false: Returns JWT payload only (lightweight validation)
|
|
373
|
+
* @returns {Promise<{ tid: string, user: { email: string, attributes: { expires_at: number, sub: string } } } & object>}
|
|
374
|
+
* - When includeUserData=true: { tid: string, user: object }
|
|
375
|
+
* - When includeUserData=false: JWT payload object
|
|
376
|
+
* @throws {CustomError} UNAUTHORIZED (401) if:
|
|
377
|
+
* - Authorization header is missing or invalid format
|
|
378
|
+
* - Token decryption fails
|
|
379
|
+
* - Token payload is invalid (missing email/tid)
|
|
380
|
+
* - Token not found in Redis (when includeUserData=true)
|
|
341
381
|
* @private
|
|
342
|
-
* @example
|
|
343
|
-
* // Authorization header format: "Bearer {jwt_token}"
|
|
344
|
-
* await this.#verifyToken(req, res, next, false, '/login');
|
|
345
382
|
*/
|
|
346
|
-
async #
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const token = authHeader.substring(7); // Remove 'Bearer ' prefix
|
|
355
|
-
|
|
356
|
-
// Decrypt JWT token
|
|
357
|
-
const { payload } = await this.#jwtManager.decrypt(
|
|
358
|
-
token,
|
|
359
|
-
this.#config.SESSION_SECRET
|
|
360
|
-
);
|
|
383
|
+
async #getUserFromToken(authHeader, includeUserData = true) {
|
|
384
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
385
|
+
throw new CustomError(httpCodes.UNAUTHORIZED, 'Missing or invalid authorization header');
|
|
386
|
+
}
|
|
387
|
+
const token = authHeader.substring(7); // Remove 'Bearer ' prefix
|
|
388
|
+
/** @type {{ payload: { email: string, tid: string } & import('jose').JWTPayload }} */
|
|
389
|
+
const { payload } = await this.#jwtManager.decrypt(token, this.#config.SSO_CLIENT_SECRET);
|
|
361
390
|
|
|
362
|
-
|
|
391
|
+
if (includeUserData) {
|
|
392
|
+
/** @type {{ email: string, tid: string }} Extract email and token ID */
|
|
363
393
|
const { email, tid } = payload;
|
|
364
394
|
if (!email || !tid) {
|
|
365
395
|
throw new CustomError(httpCodes.UNAUTHORIZED, 'Invalid token payload');
|
|
@@ -372,26 +402,59 @@ export class SessionManager {
|
|
|
372
402
|
if (!userData) {
|
|
373
403
|
throw new CustomError(httpCodes.UNAUTHORIZED, 'Token not found or expired');
|
|
374
404
|
}
|
|
405
|
+
return { tid, user: { ...JSON.parse(userData) } };
|
|
406
|
+
}
|
|
407
|
+
return { tid: payload.tid, user: { email: payload.email, attributes: { sub: payload.sub, expires_at: payload.exp ? payload.exp * 1000 : 0 } } };
|
|
408
|
+
}
|
|
375
409
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
410
|
+
/**
|
|
411
|
+
* Get authenticated user data (works for both SESSION and TOKEN modes)
|
|
412
|
+
* @param {import('@types/express').Request} req Express request object
|
|
413
|
+
* @param {boolean} [includeUserData=false] Whether to include full user data in response
|
|
414
|
+
* @returns {Promise<object>} Full user data object
|
|
415
|
+
* @throws {CustomError} If user is not authenticated
|
|
416
|
+
* @public
|
|
417
|
+
* @example
|
|
418
|
+
* // Use in custom middleware
|
|
419
|
+
* app.use(async (req, res, next) => {
|
|
420
|
+
* try {
|
|
421
|
+
* const user = await sessionManager.getUser(req);
|
|
422
|
+
* req.customUser = user;
|
|
423
|
+
* next();
|
|
424
|
+
* } catch (error) {
|
|
425
|
+
* next(error);
|
|
426
|
+
* }
|
|
427
|
+
* });
|
|
428
|
+
*/
|
|
429
|
+
async getUser(req, includeUserData = false) {
|
|
430
|
+
if (this.getSessionMode() === SessionMode.TOKEN) {
|
|
431
|
+
const { user } = await this.#getUserFromToken(req.headers.authorization, includeUserData);
|
|
432
|
+
return user;
|
|
433
|
+
}
|
|
434
|
+
// Session mode
|
|
435
|
+
return req.session[this.#getSessionKey()];
|
|
436
|
+
}
|
|
384
437
|
|
|
438
|
+
/**
|
|
439
|
+
* Verify token authentication - extracts and validates JWT from Authorization header
|
|
440
|
+
* @param {import('@types/express').Request} req Request with Authorization header
|
|
441
|
+
* @param {import('@types/express').Response} res Response object
|
|
442
|
+
* @param {import('@types/express').NextFunction} next Next middleware function
|
|
443
|
+
* @param {string} errorRedirectUrl URL to redirect to on authentication failure
|
|
444
|
+
* @throws {CustomError} If token is missing, invalid, or expired
|
|
445
|
+
* @private
|
|
446
|
+
* @example
|
|
447
|
+
* // Authorization header format: "Bearer {jwt_token}"
|
|
448
|
+
* await this.#verifyToken(req, res, next, '/login');
|
|
449
|
+
*/
|
|
450
|
+
async #verifyToken(req, res, next, errorRedirectUrl) {
|
|
451
|
+
try {
|
|
452
|
+
// Lightweight token validation (no Redis lookup)
|
|
453
|
+
await this.#getUserFromToken(req.headers.authorization, false);
|
|
385
454
|
return next();
|
|
386
|
-
|
|
387
455
|
} catch (error) {
|
|
388
|
-
if (
|
|
389
|
-
|
|
390
|
-
return next();
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (redirectUrl) {
|
|
394
|
-
return res.redirect(redirectUrl);
|
|
456
|
+
if (errorRedirectUrl) {
|
|
457
|
+
return res.redirect(errorRedirectUrl);
|
|
395
458
|
}
|
|
396
459
|
|
|
397
460
|
// Handle specific JWT errors
|
|
@@ -409,18 +472,19 @@ export class SessionManager {
|
|
|
409
472
|
* @param {import('@types/express').Request} req Request with session data
|
|
410
473
|
* @param {import('@types/express').Response} res Response object
|
|
411
474
|
* @param {import('@types/express').NextFunction} next Next middleware function
|
|
412
|
-
* @param {
|
|
413
|
-
* @param {string} redirectUrl URL to redirect to if user is unauthorized
|
|
475
|
+
* @param {string} errorRedirectUrl URL to redirect to if user is unauthorized
|
|
414
476
|
* @throws {CustomError} If user is not authorized
|
|
415
477
|
* @private
|
|
416
478
|
*/
|
|
417
|
-
async #verifySession(req, res, next,
|
|
418
|
-
|
|
479
|
+
async #verifySession(req, res, next, errorRedirectUrl) {
|
|
480
|
+
// Fix: Check session data directly, not req.user (which is only populated by requireUser())
|
|
481
|
+
const user = req.session[this.#getSessionKey()];
|
|
482
|
+
const { authorized = false } = user ?? { authorized: false };
|
|
419
483
|
if (authorized) {
|
|
420
484
|
return next();
|
|
421
485
|
}
|
|
422
|
-
if (
|
|
423
|
-
return res.redirect(
|
|
486
|
+
if (errorRedirectUrl) {
|
|
487
|
+
return res.redirect(errorRedirectUrl);
|
|
424
488
|
}
|
|
425
489
|
return next(new CustomError(httpCodes.UNAUTHORIZED, httpMessages.UNAUTHORIZED));
|
|
426
490
|
}
|
|
@@ -431,43 +495,31 @@ export class SessionManager {
|
|
|
431
495
|
* @param {import('@types/express').Request} req Request with Authorization header
|
|
432
496
|
* @param {import('@types/express').Response} res Response object
|
|
433
497
|
* @param {import('@types/express').NextFunction} next Next middleware function
|
|
434
|
-
* @param {(user: object) => object} initUser Function to initialize/transform user object
|
|
435
|
-
* @param {string}
|
|
498
|
+
* @param {(user: object) => object & { email: string }} initUser Function to initialize/transform user object
|
|
499
|
+
* @param {string} idpRefreshUrl Identity provider refresh endpoint URL
|
|
436
500
|
* @throws {CustomError} If refresh lock is active or SSO refresh fails
|
|
437
501
|
* @private
|
|
438
502
|
* @example
|
|
439
503
|
* // Response format:
|
|
440
|
-
* // {
|
|
504
|
+
* // { jwt: "new_jwt", user: {...} }
|
|
441
505
|
*/
|
|
442
|
-
async #refreshToken(req, res, next, initUser,
|
|
506
|
+
async #refreshToken(req, res, next, initUser, idpRefreshUrl) {
|
|
443
507
|
try {
|
|
444
|
-
|
|
445
|
-
const { email, attributes } = req.user || {};
|
|
446
|
-
|
|
447
|
-
if (!email) {
|
|
448
|
-
throw new CustomError(httpCodes.UNAUTHORIZED, 'User not authenticated');
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Extract Token ID from current token
|
|
452
|
-
const authHeader = req.headers.authorization;
|
|
453
|
-
const token = authHeader?.substring(7);
|
|
454
|
-
const { payload } = await this.#jwtManager.decrypt(token, this.#config.SESSION_SECRET);
|
|
455
|
-
const { tid: oldTokenId } = payload;
|
|
508
|
+
const { tid, user } = await this.#getUserFromToken(req.headers.authorization, true);
|
|
456
509
|
|
|
457
510
|
// Check refresh lock
|
|
458
|
-
if (this.hasLock(email)) {
|
|
511
|
+
if (this.hasLock(user?.email)) {
|
|
459
512
|
throw new CustomError(httpCodes.CONFLICT, 'Token refresh is locked');
|
|
460
513
|
}
|
|
461
|
-
this.lock(email);
|
|
514
|
+
this.lock(user?.email);
|
|
462
515
|
|
|
463
516
|
// Call SSO refresh endpoint
|
|
464
|
-
const response = await this.#idpRequest.post(
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
517
|
+
const response = await this.#idpRequest.post(idpRefreshUrl, {
|
|
518
|
+
idp: user?.attributes?.idp,
|
|
519
|
+
refresh_token: user?.attributes?.refresh_token
|
|
520
|
+
}, {
|
|
521
|
+
headers: {
|
|
522
|
+
Authorization: req.headers.authorization
|
|
471
523
|
}
|
|
472
524
|
});
|
|
473
525
|
|
|
@@ -484,24 +536,15 @@ export class SessionManager {
|
|
|
484
536
|
}
|
|
485
537
|
|
|
486
538
|
// Initialize user with new data
|
|
487
|
-
const
|
|
539
|
+
const newUser = initUser(newPayload.user);
|
|
488
540
|
|
|
489
541
|
// Generate new token
|
|
490
|
-
const newToken = await this.#generateAndStoreToken(
|
|
491
|
-
|
|
492
|
-
// Remove old token from Redis
|
|
493
|
-
const oldRedisKey = this.#getTokenRedisKey(email, oldTokenId);
|
|
494
|
-
await this.#redisManager.getClient().del(oldRedisKey);
|
|
542
|
+
const newToken = await this.#generateAndStoreToken(tid, newUser);
|
|
495
543
|
|
|
496
544
|
this.#logger.debug('### TOKEN REFRESHED SUCCESSFULLY ###');
|
|
497
545
|
|
|
498
546
|
// Return new token
|
|
499
|
-
return res.json({
|
|
500
|
-
token: newToken,
|
|
501
|
-
user,
|
|
502
|
-
expiresIn: this.#config.SESSION_AGE, // Already in seconds
|
|
503
|
-
tokenType: 'Bearer'
|
|
504
|
-
});
|
|
547
|
+
return res.json({ jwt: newToken, user: newUser });
|
|
505
548
|
} catch (error) {
|
|
506
549
|
return next(httpHelper.handleAxiosError(error));
|
|
507
550
|
}
|
|
@@ -513,29 +556,36 @@ export class SessionManager {
|
|
|
513
556
|
* @param {import('@types/express').Response} res Response
|
|
514
557
|
* @param {import('@types/express').NextFunction} next Next function
|
|
515
558
|
* @param {(user: object) => object} initUser Initialize user function
|
|
516
|
-
* @param {string}
|
|
559
|
+
* @param {string} idpRefreshUrl Token Refresh URL
|
|
517
560
|
* @private
|
|
518
561
|
*/
|
|
519
|
-
async #refreshSession(req, res, next, initUser,
|
|
562
|
+
async #refreshSession(req, res, next, initUser, idpRefreshUrl) {
|
|
520
563
|
try {
|
|
521
564
|
const { email, attributes } = req.user || { email: '', attributes: {} };
|
|
565
|
+
// Check refresh lock
|
|
522
566
|
if (this.hasLock(email)) {
|
|
523
|
-
throw new CustomError(httpCodes.CONFLICT, '
|
|
567
|
+
throw new CustomError(httpCodes.CONFLICT, 'Session refresh is locked');
|
|
524
568
|
}
|
|
525
569
|
this.lock(email);
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
570
|
+
|
|
571
|
+
/** @type {string} */
|
|
572
|
+
const token = await this.#getLightweightToken(email, req.sessionID, req.user.attributes.expires_at);
|
|
573
|
+
|
|
574
|
+
// Call SSO refresh endpoint
|
|
575
|
+
const response = await this.#idpRequest.post(idpRefreshUrl, {
|
|
576
|
+
idp: attributes?.idp,
|
|
577
|
+
refresh_token: attributes?.refresh_token,
|
|
578
|
+
}, {
|
|
579
|
+
headers: {
|
|
580
|
+
Authorization: `Bearer ${token}`
|
|
533
581
|
}
|
|
534
582
|
});
|
|
535
583
|
if (response.status === httpCodes.OK) {
|
|
584
|
+
/** @type {{ jwt: string }} */
|
|
536
585
|
const { jwt } = response.data;
|
|
537
|
-
const payload = await this.#
|
|
538
|
-
|
|
586
|
+
const { payload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
|
|
587
|
+
const result = await this.#saveSession(req, payload, initUser);
|
|
588
|
+
return res.json(result);
|
|
539
589
|
}
|
|
540
590
|
throw new CustomError(response.status, response.statusText);
|
|
541
591
|
} catch (error) {
|
|
@@ -604,16 +654,9 @@ export class SessionManager {
|
|
|
604
654
|
}
|
|
605
655
|
|
|
606
656
|
try {
|
|
607
|
-
// Extract Token ID from current token
|
|
608
|
-
const
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
if (!token) {
|
|
612
|
-
throw new CustomError(httpCodes.BAD_REQUEST, 'No token provided');
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
const { payload } = await this.#jwtManager.decrypt(token, this.#config.SESSION_SECRET);
|
|
616
|
-
const { email, tid } = payload;
|
|
657
|
+
// Extract Token ID and email from current token
|
|
658
|
+
const { tid, user } = await this.#getUserFromToken(req.headers.authorization, false);
|
|
659
|
+
const { email } = user;
|
|
617
660
|
|
|
618
661
|
if (!email || !tid) {
|
|
619
662
|
throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid token payload');
|
|
@@ -670,27 +713,6 @@ export class SessionManager {
|
|
|
670
713
|
});
|
|
671
714
|
}
|
|
672
715
|
|
|
673
|
-
/**
|
|
674
|
-
* Setup the session/user handlers with configurations
|
|
675
|
-
* @param {import('@types/express').Application} app Express application
|
|
676
|
-
* @param {(user: object) => object} updateUser Update user object if user should have proper attributes, e.g. permissions, avatar URL
|
|
677
|
-
*/
|
|
678
|
-
async setup(app, updateUser) {
|
|
679
|
-
this.#redisManager = new RedisManager();
|
|
680
|
-
this.#jwtManager = new JwtManager({
|
|
681
|
-
...this.#config,
|
|
682
|
-
JWT_EXPIRATION_TIME: this.#config.SESSION_AGE, // SESSION_AGE is already in seconds
|
|
683
|
-
});
|
|
684
|
-
// Identity Provider Request
|
|
685
|
-
this.#idpRequest = axios.create({
|
|
686
|
-
baseURL: this.#config.SSO_ENDPOINT_URL,
|
|
687
|
-
timeout: 30000,
|
|
688
|
-
});
|
|
689
|
-
app.set('trust proxy', 1);
|
|
690
|
-
app.use(await this.#sessionHandler());
|
|
691
|
-
app.use(this.#userHandler(updateUser));
|
|
692
|
-
}
|
|
693
|
-
|
|
694
716
|
/**
|
|
695
717
|
* Get Redis session RequestHandler
|
|
696
718
|
* @returns {import('@types/express').RequestHandler} Returns RequestHandler instance of Express
|
|
@@ -724,80 +746,96 @@ export class SessionManager {
|
|
|
724
746
|
|
|
725
747
|
/**
|
|
726
748
|
* Get session RequestHandler
|
|
727
|
-
* @
|
|
749
|
+
* @param {boolean} isRedisReady Is Redis Ready
|
|
750
|
+
* @returns {import('@types/express').RequestHandler} Returns RequestHandler instance of Express
|
|
728
751
|
*/
|
|
729
|
-
|
|
730
|
-
if(
|
|
731
|
-
await this.#redisManager.connect(this.#config.REDIS_URL, this.#config.REDIS_CERT_PATH);
|
|
752
|
+
#sessionHandler(isRedisReady) {
|
|
753
|
+
if(isRedisReady) {
|
|
732
754
|
return this.#redisSession();
|
|
733
755
|
}
|
|
734
756
|
return this.#memorySession();
|
|
735
757
|
}
|
|
736
758
|
|
|
737
759
|
/**
|
|
738
|
-
*
|
|
739
|
-
* @param {(user: object) => object} updateUser User wrapper
|
|
760
|
+
* Middleware to load full user data (works for both SESSION and TOKEN modes)
|
|
740
761
|
* @returns {import('@types/express').RequestHandler} Returns express Request Handler
|
|
741
762
|
*/
|
|
742
|
-
|
|
743
|
-
return (req,
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
763
|
+
requireUser = () => {
|
|
764
|
+
return async (req, _res, next) => {
|
|
765
|
+
try {
|
|
766
|
+
req.user = await this.getUser(req, true);
|
|
767
|
+
return next();
|
|
768
|
+
}
|
|
769
|
+
catch (error) {
|
|
770
|
+
return next(error);
|
|
771
|
+
}
|
|
748
772
|
};
|
|
749
|
-
}
|
|
773
|
+
};
|
|
750
774
|
|
|
751
775
|
/**
|
|
752
776
|
* Resource protection based on configured SESSION_MODE
|
|
753
|
-
*
|
|
754
|
-
*
|
|
777
|
+
* - SESSION mode: Verifies user exists in session store and is authorized (checks req.session data)
|
|
778
|
+
* - TOKEN mode: Validates JWT token from Authorization header (lightweight validation)
|
|
779
|
+
*
|
|
780
|
+
* Note: This method verifies authentication only. Use requireUser() after this to populate req.user.
|
|
781
|
+
*
|
|
782
|
+
* @param {string} [errorRedirectUrl=''] Redirect URL on authentication failure
|
|
755
783
|
* @returns {import('@types/express').RequestHandler} Returns express Request Handler
|
|
756
|
-
|
|
757
|
-
|
|
784
|
+
* @example
|
|
785
|
+
* // Option 1: Just verify authentication (user data remains in req.session or token)
|
|
786
|
+
* app.get('/api/check', session.authenticate(), (req, res) => {
|
|
787
|
+
* res.json({ authenticated: true });
|
|
788
|
+
* });
|
|
789
|
+
*
|
|
790
|
+
* // Option 2: Verify authentication AND load user data into req.user
|
|
791
|
+
* app.get('/api/profile',
|
|
792
|
+
* session.authenticate(), // Verifies session/token
|
|
793
|
+
* session.requireUser(), // Loads user data into req.user
|
|
794
|
+
* (req, res) => {
|
|
795
|
+
* res.json({ user: req.user }); // User data available here
|
|
796
|
+
* }
|
|
797
|
+
* );
|
|
798
|
+
*/
|
|
799
|
+
authenticate(errorRedirectUrl = '') {
|
|
758
800
|
return async (req, res, next) => {
|
|
759
|
-
const mode = this
|
|
801
|
+
const mode = this.getSessionMode() || SessionMode.SESSION;
|
|
760
802
|
if (mode === SessionMode.TOKEN) {
|
|
761
|
-
return this.#verifyToken(req, res, next,
|
|
803
|
+
return this.#verifyToken(req, res, next, errorRedirectUrl);
|
|
762
804
|
}
|
|
763
|
-
return this.#verifySession(req, res, next,
|
|
805
|
+
return this.#verifySession(req, res, next, errorRedirectUrl);
|
|
764
806
|
};
|
|
765
807
|
}
|
|
766
808
|
|
|
767
809
|
/**
|
|
768
810
|
* Resource protection by token (explicit token verification)
|
|
769
|
-
* @param {
|
|
770
|
-
* @param {string} [redirectUrl=''] Redirect URL
|
|
811
|
+
* @param {string} [errorRedirectUrl=''] Redirect URL
|
|
771
812
|
* @returns {import('@types/express').RequestHandler} Returns express Request Handler
|
|
772
813
|
*/
|
|
773
|
-
verifyToken(
|
|
814
|
+
verifyToken(errorRedirectUrl = '') {
|
|
774
815
|
return async (req, res, next) => {
|
|
775
|
-
return this.#verifyToken(req, res, next,
|
|
816
|
+
return this.#verifyToken(req, res, next, errorRedirectUrl);
|
|
776
817
|
};
|
|
777
818
|
}
|
|
778
819
|
|
|
779
820
|
/**
|
|
780
821
|
* Resource protection by session (explicit session verification)
|
|
781
|
-
* @param {
|
|
782
|
-
* @param {string} [redirectUrl=''] Redirect URL
|
|
822
|
+
* @param {string} [errorRedirectUrl=''] Redirect URL
|
|
783
823
|
* @returns {import('@types/express').RequestHandler} Returns express Request Handler
|
|
784
824
|
*/
|
|
785
|
-
verifySession(
|
|
825
|
+
verifySession(errorRedirectUrl = '') {
|
|
786
826
|
return async (req, res, next) => {
|
|
787
|
-
return this.#verifySession(req, res, next,
|
|
827
|
+
return this.#verifySession(req, res, next, errorRedirectUrl);
|
|
788
828
|
};
|
|
789
829
|
}
|
|
790
830
|
|
|
791
831
|
/**
|
|
792
832
|
* Save session
|
|
793
833
|
* @param {import('@types/express').Request} request Request object
|
|
794
|
-
* @param {
|
|
834
|
+
* @param {import('jose').JWTPayload} payload JWT
|
|
795
835
|
* @param {(user: object) => object} initUser Redirect URL
|
|
796
836
|
* @returns {Promise<{ user: import('../models/types/user').UserModel, redirect_url: string }>} Promise
|
|
797
837
|
*/
|
|
798
|
-
#saveSession = async (request,
|
|
799
|
-
/** @type {{ payload: { user: import('../models/types/user').UserModel, redirect_url: string } }} */
|
|
800
|
-
const { payload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
|
|
838
|
+
#saveSession = async (request, payload, initUser) => {
|
|
801
839
|
if (payload?.user) {
|
|
802
840
|
this.#logger.debug('### CALLBACK USER ###');
|
|
803
841
|
request.session[this.#getSessionKey()] = initUser(payload.user);
|
|
@@ -813,62 +851,77 @@ export class SessionManager {
|
|
|
813
851
|
});
|
|
814
852
|
}
|
|
815
853
|
throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload');
|
|
816
|
-
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Render HTML template for token storage
|
|
858
|
+
* @param {string} token JWT token
|
|
859
|
+
* @param {string} expiresAt Expiry timestamp
|
|
860
|
+
* @param {string} sucessRedirectUrl Success redirect URL
|
|
861
|
+
* @returns {string} Rendered HTML
|
|
862
|
+
* @private
|
|
863
|
+
*/
|
|
864
|
+
#renderTokenStorageHtml(token, expiresAt, sucessRedirectUrl) {
|
|
865
|
+
return this.#htmlTemplate
|
|
866
|
+
.replaceAll('{{SESSION_DATA_KEY}}', this.#config.SESSION_KEY)
|
|
867
|
+
.replaceAll('{{SESSION_DATA_VALUE}}', token)
|
|
868
|
+
.replaceAll('{{SESSION_EXPIRY_KEY}}', this.#config.SESSION_EXPIRY_KEY)
|
|
869
|
+
.replaceAll('{{SESSION_EXPIRY_VALUE}}', expiresAt)
|
|
870
|
+
.replaceAll('{{SSO_SUCCESS_URL}}', sucessRedirectUrl)
|
|
871
|
+
.replaceAll('{{SSO_FAILURE_URL}}', this.#config.SSO_FAILURE_URL);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* SSO callback for successful login
|
|
876
|
+
* @param {(user: object) => object} initUser Initialize user object function
|
|
877
|
+
* @returns {import('@types/express').RequestHandler} Returns express Request Handler
|
|
878
|
+
*/
|
|
879
|
+
callback(initUser) {
|
|
880
|
+
return async (req, res, next) => {
|
|
881
|
+
const { jwt = '' } = req.query;
|
|
882
|
+
if (!jwt) {
|
|
883
|
+
return next(new CustomError(httpCodes.BAD_REQUEST, 'Missing `jwt` in query parameters'));
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
try {
|
|
887
|
+
// Decrypt JWT from Identity Adapter
|
|
888
|
+
const { payload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
|
|
817
889
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
890
|
+
if (!payload?.user) {
|
|
891
|
+
throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload');
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/** @type {string} */
|
|
895
|
+
const callbackRedirectUrl = payload.redirect_url || this.#config.SSO_SUCCESS_URL;
|
|
896
|
+
|
|
897
|
+
// Token mode: Generate token and return HTML page
|
|
898
|
+
if (this.getSessionMode() === SessionMode.TOKEN) {
|
|
899
|
+
/** @type {import('../index.js').SessionUser} */
|
|
900
|
+
const user = initUser(payload.user);
|
|
901
|
+
// Generate unique token ID for this device/session
|
|
902
|
+
const tid = crypto.randomUUID();
|
|
903
|
+
const token = await this.#generateAndStoreToken(tid, user);
|
|
904
|
+
this.#logger.debug('### CALLBACK TOKEN GENERATED ###');
|
|
905
|
+
const html = this.#renderTokenStorageHtml(token, user.attributes.expires_at, callbackRedirectUrl);
|
|
906
|
+
return res.send(html);
|
|
907
|
+
}
|
|
829
908
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
if (!payload?.user) {
|
|
835
|
-
throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload');
|
|
909
|
+
// Session mode: Save to session and redirect
|
|
910
|
+
await this.#saveSession(req, payload, initUser);
|
|
911
|
+
return res.redirect(callbackRedirectUrl);
|
|
836
912
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
const token = await this.#generateAndStoreToken(user);
|
|
847
|
-
|
|
848
|
-
this.#logger.debug('### CALLBACK TOKEN GENERATED ###');
|
|
849
|
-
|
|
850
|
-
const templatePath = this.#config.TOKEN_STORAGE_TEMPLATE_PATH || path.resolve(__dirname, 'assets', 'template.html');
|
|
851
|
-
// Return HTML page that stores token in localStorage and redirects
|
|
852
|
-
const template = fs.readFileSync(templatePath, 'utf8');
|
|
853
|
-
const html = template
|
|
854
|
-
.replaceAll('{{SESSION_DATA_KEY}}', this.#config.SESSION_KEY)
|
|
855
|
-
.replaceAll('{{SESSION_DATA_VALUE}}', token)
|
|
856
|
-
.replaceAll('{{SESSION_EXPIRY_KEY}}', this.#config.SESSION_EXPIRY_KEY)
|
|
857
|
-
.replaceAll('{{SESSION_EXPIRY_VALUE}}', user.attributes.expires_at)
|
|
858
|
-
.replaceAll('{{SSO_SUCCESS_URL}}', redirectUrl)
|
|
859
|
-
.replaceAll('{{SSO_FAILURE_URL}}', this.#config.SSO_FAILURE_URL);
|
|
860
|
-
return res.send(html);
|
|
913
|
+
catch (error) {
|
|
914
|
+
this.#logger.error('### CALLBACK ERROR ###', error);
|
|
915
|
+
let errorMessage = error.message;
|
|
916
|
+
if (error.code === 'ERR_JWT_EXPIRED') {
|
|
917
|
+
errorMessage = 'Authentication token expired';
|
|
918
|
+
} else if (error.code === 'ERR_JWT_INVALID') {
|
|
919
|
+
errorMessage = 'Invalid authentication token';
|
|
920
|
+
}
|
|
921
|
+
return res.redirect(this.#config.SSO_FAILURE_URL.concat('?message=').concat(encodeURIComponent(errorMessage)));
|
|
861
922
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
return res.redirect(redirectUrl);
|
|
865
|
-
}
|
|
866
|
-
catch (error) {
|
|
867
|
-
this.#logger.error('### CALLBACK ERROR ###', error);
|
|
868
|
-
return res.redirect(this.#config.SSO_FAILURE_URL.concat('?message=').concat(encodeURIComponent(error.message)));
|
|
869
|
-
}
|
|
870
|
-
};
|
|
871
|
-
}
|
|
923
|
+
};
|
|
924
|
+
}
|
|
872
925
|
|
|
873
926
|
/**
|
|
874
927
|
* Get Identity Providers
|
|
@@ -896,14 +949,15 @@ export class SessionManager {
|
|
|
896
949
|
* @returns {import('@types/express').RequestHandler} Returns express Request Handler
|
|
897
950
|
*/
|
|
898
951
|
refresh(initUser) {
|
|
899
|
-
const
|
|
952
|
+
const idpRefreshUrl = '/auth/refresh'.concat('?client_id=').concat(this.#config.SSO_CLIENT_ID);
|
|
900
953
|
return async (req, res, next) => {
|
|
901
|
-
const mode = this
|
|
902
|
-
|
|
954
|
+
const mode = this.getSessionMode() || SessionMode.SESSION;
|
|
955
|
+
|
|
903
956
|
if (mode === SessionMode.TOKEN) {
|
|
904
|
-
return this.#refreshToken(req, res, next, initUser,
|
|
905
|
-
}
|
|
906
|
-
|
|
957
|
+
return this.#refreshToken(req, res, next, initUser, idpRefreshUrl);
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
return this.#refreshSession(req, res, next, initUser, idpRefreshUrl);
|
|
907
961
|
}
|
|
908
962
|
};
|
|
909
963
|
}
|
|
@@ -917,8 +971,8 @@ export class SessionManager {
|
|
|
917
971
|
const { redirect = false, all = false } = req.query;
|
|
918
972
|
const isRedirect = (redirect === 'true' || redirect === true);
|
|
919
973
|
const logoutAll = (all === 'true' || all === true);
|
|
920
|
-
const mode = this
|
|
921
|
-
|
|
974
|
+
const mode = this.getSessionMode() || SessionMode.SESSION;
|
|
975
|
+
|
|
922
976
|
if (mode === SessionMode.TOKEN) {
|
|
923
977
|
return this.#logoutToken(req, res, isRedirect, logoutAll);
|
|
924
978
|
}
|
|
@@ -944,4 +998,11 @@ export class SessionManager {
|
|
|
944
998
|
};
|
|
945
999
|
}
|
|
946
1000
|
|
|
1001
|
+
/**
|
|
1002
|
+
* Get session mode
|
|
1003
|
+
* @returns {string} Session mode
|
|
1004
|
+
*/
|
|
1005
|
+
getSessionMode() {
|
|
1006
|
+
return this.#config.SESSION_MODE;
|
|
1007
|
+
}
|
|
947
1008
|
}
|
package/index.d.ts
CHANGED
|
@@ -142,24 +142,27 @@ export interface SessionConfig {
|
|
|
142
142
|
*/
|
|
143
143
|
SESSION_SECRET?: string;
|
|
144
144
|
|
|
145
|
-
/**
|
|
145
|
+
/**
|
|
146
146
|
* Redis key prefix for storing session data
|
|
147
147
|
* @example 'myapp:session:' (will result in keys like 'myapp:session:user@example.com')
|
|
148
|
-
* @default '
|
|
148
|
+
* @default 'ibmid:' (legacy default, consider changing for your use case)
|
|
149
149
|
*/
|
|
150
150
|
SESSION_PREFIX?: string;
|
|
151
151
|
|
|
152
|
-
/**
|
|
152
|
+
/**
|
|
153
153
|
* Redis key name for storing session data
|
|
154
|
+
* - In SESSION mode: key used to store the user in the session
|
|
155
|
+
* - In TOKEN mode: key of localStorage where the token is stored
|
|
154
156
|
* @example 'user' (results in session.user containing user data)
|
|
155
|
-
* @default '
|
|
157
|
+
* @default 'session_token'
|
|
156
158
|
*/
|
|
157
159
|
SESSION_KEY?: string;
|
|
158
160
|
|
|
159
|
-
/**
|
|
161
|
+
/**
|
|
160
162
|
* Redis key name for storing session expiry timestamp
|
|
163
|
+
* - In TOKEN mode: key of localStorage where the session expiry timestamp is stored
|
|
161
164
|
* @example 'expires' (results in session.expires containing expiry time)
|
|
162
|
-
* @default '
|
|
165
|
+
* @default 'session_expires_at'
|
|
163
166
|
*/
|
|
164
167
|
SESSION_EXPIRY_KEY?: string;
|
|
165
168
|
|
|
@@ -311,41 +314,99 @@ export class SessionManager {
|
|
|
311
314
|
redisManager(): RedisManager;
|
|
312
315
|
|
|
313
316
|
/**
|
|
314
|
-
*
|
|
317
|
+
* Get authenticated user data (works for both SESSION and TOKEN modes)
|
|
318
|
+
* @param req Express request object
|
|
319
|
+
* @param includeUserData Include user data in the response (default: false)
|
|
320
|
+
* @returns Promise resolving to full user data object
|
|
321
|
+
* @throws CustomError If user is not authenticated
|
|
322
|
+
* @example
|
|
323
|
+
* ```javascript
|
|
324
|
+
* // Use in custom middleware
|
|
325
|
+
* app.use(async (req, res, next) => {
|
|
326
|
+
* try {
|
|
327
|
+
* const user = await sessionManager.getUser(req, true);
|
|
328
|
+
* req.customUser = user;
|
|
329
|
+
* next();
|
|
330
|
+
* } catch (error) {
|
|
331
|
+
* next(error);
|
|
332
|
+
* }
|
|
333
|
+
* });
|
|
334
|
+
* ```
|
|
335
|
+
*/
|
|
336
|
+
getUser(req: Request, includeUserData: boolean?): Promise<SessionUser>;
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Initialize the session configurations and middleware
|
|
315
340
|
* @param app Express application
|
|
316
|
-
* @param config Session configurations
|
|
317
|
-
* @param updateUser Process user object to compute attributes like permissions, avatar URL, etc.
|
|
318
341
|
*/
|
|
319
|
-
setup(
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
342
|
+
setup(app: Application): Promise<void>;
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Middleware to load full user data into req.user
|
|
346
|
+
* - SESSION mode: Loads user from session store (req.session[SESSION_KEY])
|
|
347
|
+
* - TOKEN mode: Loads user from Redis using JWT token
|
|
348
|
+
* - Provides request-level caching to avoid redundant lookups
|
|
349
|
+
* - Should be used after authenticate() middleware
|
|
350
|
+
* @returns Returns express Request Handler
|
|
351
|
+
* @example
|
|
352
|
+
* ```javascript
|
|
353
|
+
* app.get('/api/profile',
|
|
354
|
+
* session.authenticate(), // Verifies authentication
|
|
355
|
+
* session.requireUser(), // Loads user data into req.user
|
|
356
|
+
* (req, res) => {
|
|
357
|
+
* res.json({ user: req.user }); // User data available here
|
|
358
|
+
* }
|
|
359
|
+
* );
|
|
360
|
+
* ```
|
|
361
|
+
*/
|
|
362
|
+
requireUser(): RequestHandler;
|
|
323
363
|
|
|
324
364
|
/**
|
|
325
365
|
* Resource protection middleware based on configured SESSION_MODE
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
366
|
+
* - SESSION mode: Verifies user exists in session store and is authorized (checks req.session data)
|
|
367
|
+
* - TOKEN mode: Validates JWT token from Authorization header (lightweight validation)
|
|
368
|
+
*
|
|
369
|
+
* Note: This method verifies authentication only and does NOT populate req.user.
|
|
370
|
+
* Use requireUser() after this middleware to load user data into req.user.
|
|
371
|
+
*
|
|
372
|
+
* @param errorRedirectUrl Redirect URL on authentication failure (default: '')
|
|
329
373
|
* @returns Returns express Request Handler
|
|
374
|
+
* @example
|
|
375
|
+
* ```javascript
|
|
376
|
+
* // Option 1: Just verify authentication (user data remains in req.session or token)
|
|
377
|
+
* app.get('/api/check',
|
|
378
|
+
* session.authenticate(),
|
|
379
|
+
* (req, res) => {
|
|
380
|
+
* res.json({ authenticated: true });
|
|
381
|
+
* }
|
|
382
|
+
* );
|
|
383
|
+
*
|
|
384
|
+
* // Option 2: Verify authentication AND populate req.user (recommended for most use cases)
|
|
385
|
+
* app.get('/api/profile',
|
|
386
|
+
* session.authenticate(), // Verifies session/token validity
|
|
387
|
+
* session.requireUser(), // Loads user data into req.user
|
|
388
|
+
* (req, res) => {
|
|
389
|
+
* res.json({ user: req.user }); // User data now available
|
|
390
|
+
* }
|
|
391
|
+
* );
|
|
392
|
+
* ```
|
|
330
393
|
*/
|
|
331
|
-
authenticate(
|
|
394
|
+
authenticate(errorRedirectUrl?: string): RequestHandler;
|
|
332
395
|
|
|
333
396
|
/**
|
|
334
397
|
* Resource protection by token (explicit token verification)
|
|
335
398
|
* Requires Authorization: Bearer {token} header
|
|
336
|
-
* @param
|
|
337
|
-
* @param redirectUrl Redirect URL (default: '')
|
|
399
|
+
* @param errorRedirectUrl Redirect URL (default: '')
|
|
338
400
|
* @returns Returns express Request Handler
|
|
339
401
|
*/
|
|
340
|
-
verifyToken(
|
|
402
|
+
verifyToken(errorRedirectUrl?: string): RequestHandler;
|
|
341
403
|
|
|
342
404
|
/**
|
|
343
405
|
* Resource protection by session (explicit session verification)
|
|
344
|
-
* @param
|
|
345
|
-
* @param redirectUrl Redirect URL (default: '')
|
|
406
|
+
* @param errorRedirectUrl Redirect URL (default: '')
|
|
346
407
|
* @returns Returns express Request Handler
|
|
347
408
|
*/
|
|
348
|
-
verifySession(
|
|
409
|
+
verifySession(errorRedirectUrl?: string): RequestHandler;
|
|
349
410
|
|
|
350
411
|
/**
|
|
351
412
|
* SSO callback for successful login
|
|
@@ -379,6 +440,12 @@ export class SessionManager {
|
|
|
379
440
|
* @returns Returns express Request Handler
|
|
380
441
|
*/
|
|
381
442
|
logout(): RequestHandler;
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Get the current session mode
|
|
446
|
+
* @returns Returns 'session' or 'token' based on configuration
|
|
447
|
+
*/
|
|
448
|
+
getSessionMode(): string;
|
|
382
449
|
}
|
|
383
450
|
|
|
384
451
|
// Custom Error class
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@igxjs/node-components",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "Node components for igxjs",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -12,21 +12,7 @@
|
|
|
12
12
|
"url": "git+https://github.com/igxjs/node-components.git"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
|
-
"igxjs"
|
|
16
|
-
"express",
|
|
17
|
-
"session-management",
|
|
18
|
-
"jwt",
|
|
19
|
-
"jwe",
|
|
20
|
-
"redis",
|
|
21
|
-
"authentication",
|
|
22
|
-
"sso",
|
|
23
|
-
"oauth",
|
|
24
|
-
"middleware",
|
|
25
|
-
"node-components",
|
|
26
|
-
"session",
|
|
27
|
-
"token-auth",
|
|
28
|
-
"bearer-token",
|
|
29
|
-
"express-middleware"
|
|
15
|
+
"igxjs"
|
|
30
16
|
],
|
|
31
17
|
"author": "Michael",
|
|
32
18
|
"license": "Apache-2.0",
|
|
@@ -43,7 +29,7 @@
|
|
|
43
29
|
"axios": "^1.13.6",
|
|
44
30
|
"connect-redis": "^9.0.0",
|
|
45
31
|
"express-session": "^1.19.0",
|
|
46
|
-
"jose": "^6.2.
|
|
32
|
+
"jose": "^6.2.2",
|
|
47
33
|
"memorystore": "^1.6.7"
|
|
48
34
|
},
|
|
49
35
|
"devDependencies": {
|