@igxjs/node-components 1.0.13 → 1.0.15
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 +21 -10
- package/components/session.js +262 -208
- package/index.d.ts +86 -26
- package/package.json +1 -1
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)
|
|
@@ -78,7 +84,7 @@ flexRouter.mount(app, '');
|
|
|
78
84
|
import { JwtManager } from '@igxjs/node-components';
|
|
79
85
|
|
|
80
86
|
// Constructor uses UPPERCASE naming with JWT_ prefix
|
|
81
|
-
const jwt = new JwtManager({
|
|
87
|
+
const jwt = new JwtManager({ JWT_EXPIRATION_TIME: 64800 });
|
|
82
88
|
const SECRET = process.env.JWT_SECRET;
|
|
83
89
|
|
|
84
90
|
// Create token (encrypt method uses camelCase for per-call options)
|
|
@@ -127,12 +133,13 @@ Uses traditional server-side session cookies. When a user authenticates via SSO,
|
|
|
127
133
|
|
|
128
134
|
**Configuration:**
|
|
129
135
|
- `SESSION_MODE`: `SessionMode.SESSION` (default) - Uses session-based authentication
|
|
130
|
-
- `SESSION_AGE`: Session timeout in
|
|
136
|
+
- `SESSION_AGE`: Session timeout in seconds (default: 64800 = 18 hours)
|
|
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
|
|
|
@@ -182,10 +190,13 @@ fetch('/api/protected', {
|
|
|
182
190
|
| `SSO_SUCCESS_URL` | string | - | Redirect URL after successful login (token mode) |
|
|
183
191
|
| `SSO_FAILURE_URL` | string | - | Redirect URL after failed login (token mode) |
|
|
184
192
|
| `SESSION_MODE` | string | `SessionMode.SESSION` | Authentication mode: `SessionMode.SESSION` or `SessionMode.TOKEN` |
|
|
185
|
-
| `SESSION_AGE` | number |
|
|
193
|
+
| `SESSION_AGE` | number | 64800 | Session timeout in seconds (default: 64800 = 18 hours) |
|
|
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 |
|
package/components/session.js
CHANGED
|
@@ -58,7 +58,7 @@ export class SessionConfig {
|
|
|
58
58
|
/** @type {string} */
|
|
59
59
|
SSO_FAILURE_URL;
|
|
60
60
|
|
|
61
|
-
/** @type {number} Session age in
|
|
61
|
+
/** @type {number} Session age in seconds (default: 64800 = 18 hours) */
|
|
62
62
|
SESSION_AGE;
|
|
63
63
|
/**
|
|
64
64
|
* @type {string} Session cookie path
|
|
@@ -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
|
|
@@ -181,8 +183,8 @@ export class SessionManager {
|
|
|
181
183
|
this.#config = {
|
|
182
184
|
// Session Mode
|
|
183
185
|
SESSION_MODE: config.SESSION_MODE || SessionMode.SESSION,
|
|
184
|
-
// Session
|
|
185
|
-
SESSION_AGE: config.SESSION_AGE ||
|
|
186
|
+
// Session - SESSION_AGE is now in seconds (default: 64800 = 18 hours)
|
|
187
|
+
SESSION_AGE: config.SESSION_AGE || 64800,
|
|
186
188
|
SESSION_COOKIE_PATH: config.SESSION_COOKIE_PATH || '/',
|
|
187
189
|
SESSION_SECRET: config.SESSION_SECRET,
|
|
188
190
|
SESSION_PREFIX: config.SESSION_PREFIX || 'ibmid:',
|
|
@@ -253,6 +255,15 @@ export class SessionManager {
|
|
|
253
255
|
return this.#config.SESSION_KEY;
|
|
254
256
|
}
|
|
255
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Get session age in milliseconds (for express-session cookie maxAge)
|
|
260
|
+
* @returns {number} Returns the session age in milliseconds
|
|
261
|
+
* @private
|
|
262
|
+
*/
|
|
263
|
+
#getSessionAgeInMilliseconds() {
|
|
264
|
+
return Math.round(this.#config.SESSION_AGE * 1000);
|
|
265
|
+
}
|
|
266
|
+
|
|
256
267
|
/**
|
|
257
268
|
* Get Redis key for token storage
|
|
258
269
|
* @param {string} email User email
|
|
@@ -282,6 +293,37 @@ export class SessionManager {
|
|
|
282
293
|
return this.#redisManager;
|
|
283
294
|
}
|
|
284
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.#config.SESSION_MODE === SessionMode.SESSION) {
|
|
315
|
+
app.use(this.#sessionHandler(isOK));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Cache HTML template for TOKEN mode
|
|
319
|
+
if (this.#config.SESSION_MODE === 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
|
+
|
|
285
327
|
/**
|
|
286
328
|
* Generate and store JWT token in Redis
|
|
287
329
|
* - JWT payload contains only { email, tid } for minimal size
|
|
@@ -300,56 +342,44 @@ export class SessionManager {
|
|
|
300
342
|
async #generateAndStoreToken(user) {
|
|
301
343
|
// Generate unique token ID for this device/session
|
|
302
344
|
const tid = crypto.randomUUID();
|
|
303
|
-
const ttlSeconds = Math.floor(this.#config.SESSION_AGE / 1000);
|
|
304
345
|
// Create JWT token with only email and tid (minimal payload)
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
this.#config.SESSION_SECRET,
|
|
308
|
-
{ expirationTime: ttlSeconds }
|
|
309
|
-
);
|
|
346
|
+
const payload = { email: user.email, tid };
|
|
347
|
+
const token = await this.#jwtManager.encrypt(payload, this.#config.SESSION_SECRET, { expirationTime: this.#config.SESSION_AGE });
|
|
310
348
|
|
|
311
349
|
// Store user data in Redis with TTL
|
|
312
350
|
const redisKey = this.#getTokenRedisKey(user.email, tid);
|
|
313
351
|
|
|
314
|
-
await this.#redisManager.getClient().setEx(
|
|
315
|
-
redisKey,
|
|
316
|
-
ttlSeconds,
|
|
317
|
-
JSON.stringify(user)
|
|
318
|
-
);
|
|
352
|
+
await this.#redisManager.getClient().setEx(redisKey, this.#config.SESSION_AGE, JSON.stringify(user));
|
|
319
353
|
this.#logger.debug(`### TOKEN GENERATED: ${user.email} ###`);
|
|
320
354
|
return token;
|
|
321
355
|
}
|
|
322
356
|
|
|
323
357
|
/**
|
|
324
|
-
*
|
|
325
|
-
* @param {
|
|
326
|
-
* @param {
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
* @
|
|
330
|
-
*
|
|
358
|
+
* Extract and validate user data from Authorization header (TOKEN mode only)
|
|
359
|
+
* @param {string} authHeader Authorization header in format "Bearer {token}"
|
|
360
|
+
* @param {boolean} [fetchFromRedis=true] Whether to fetch full user data from Redis
|
|
361
|
+
* - true: Returns { tid, user } with full user data from Redis (default)
|
|
362
|
+
* - false: Returns JWT payload only (lightweight validation)
|
|
363
|
+
* @returns {Promise<{ tid: string?, email: string?, user: object? } | object>}
|
|
364
|
+
* - When fetchFromRedis=true: { tid: string, user: object }
|
|
365
|
+
* - When fetchFromRedis=false: JWT payload object
|
|
366
|
+
* @throws {CustomError} UNAUTHORIZED (401) if:
|
|
367
|
+
* - Authorization header is missing or invalid format
|
|
368
|
+
* - Token decryption fails
|
|
369
|
+
* - Token payload is invalid (missing email/tid)
|
|
370
|
+
* - Token not found in Redis (when fetchFromRedis=true)
|
|
331
371
|
* @private
|
|
332
|
-
* @example
|
|
333
|
-
* // Authorization header format: "Bearer {jwt_token}"
|
|
334
|
-
* await this.#verifyToken(req, res, next, false, '/login');
|
|
335
372
|
*/
|
|
336
|
-
async #
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const token = authHeader.substring(7); // Remove 'Bearer ' prefix
|
|
345
|
-
|
|
346
|
-
// Decrypt JWT token
|
|
347
|
-
const { payload } = await this.#jwtManager.decrypt(
|
|
348
|
-
token,
|
|
349
|
-
this.#config.SESSION_SECRET
|
|
350
|
-
);
|
|
373
|
+
async #getUserFromToken(authHeader, fetchFromRedis = true) {
|
|
374
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
375
|
+
throw new CustomError(httpCodes.UNAUTHORIZED, 'Missing or invalid authorization header');
|
|
376
|
+
}
|
|
377
|
+
const token = authHeader.substring(7); // Remove 'Bearer ' prefix
|
|
378
|
+
// Decrypt JWT token
|
|
379
|
+
const { payload } = await this.#jwtManager.decrypt(token, this.#config.SESSION_SECRET);
|
|
351
380
|
|
|
352
|
-
|
|
381
|
+
if (fetchFromRedis) {
|
|
382
|
+
/** @type {{ email: string, tid: string }} Extract email and token ID */
|
|
353
383
|
const { email, tid } = payload;
|
|
354
384
|
if (!email || !tid) {
|
|
355
385
|
throw new CustomError(httpCodes.UNAUTHORIZED, 'Invalid token payload');
|
|
@@ -362,26 +392,58 @@ export class SessionManager {
|
|
|
362
392
|
if (!userData) {
|
|
363
393
|
throw new CustomError(httpCodes.UNAUTHORIZED, 'Token not found or expired');
|
|
364
394
|
}
|
|
395
|
+
return { tid, user: JSON.parse(userData) };
|
|
396
|
+
}
|
|
397
|
+
return payload;
|
|
398
|
+
}
|
|
365
399
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
400
|
+
/**
|
|
401
|
+
* Get authenticated user data (works for both SESSION and TOKEN modes)
|
|
402
|
+
* @param {import('@types/express').Request} req Express request object
|
|
403
|
+
* @returns {Promise<object>} Full user data object
|
|
404
|
+
* @throws {CustomError} If user is not authenticated
|
|
405
|
+
* @public
|
|
406
|
+
* @example
|
|
407
|
+
* // Use in custom middleware
|
|
408
|
+
* app.use(async (req, res, next) => {
|
|
409
|
+
* try {
|
|
410
|
+
* const user = await sessionManager.getUser(req);
|
|
411
|
+
* req.customUser = user;
|
|
412
|
+
* next();
|
|
413
|
+
* } catch (error) {
|
|
414
|
+
* next(error);
|
|
415
|
+
* }
|
|
416
|
+
* });
|
|
417
|
+
*/
|
|
418
|
+
async getUser(req) {
|
|
419
|
+
if (this.#config.SESSION_MODE === SessionMode.TOKEN) {
|
|
420
|
+
const { user } = await this.#getUserFromToken(req.headers.authorization, true);
|
|
421
|
+
return user;
|
|
422
|
+
}
|
|
423
|
+
// Session mode
|
|
424
|
+
return req.session[this.#getSessionKey()];
|
|
425
|
+
}
|
|
374
426
|
|
|
427
|
+
/**
|
|
428
|
+
* Verify token authentication - extracts and validates JWT from Authorization header
|
|
429
|
+
* @param {import('@types/express').Request} req Request with Authorization header
|
|
430
|
+
* @param {import('@types/express').Response} res Response object
|
|
431
|
+
* @param {import('@types/express').NextFunction} next Next middleware function
|
|
432
|
+
* @param {string} errorRedirectUrl URL to redirect to on authentication failure
|
|
433
|
+
* @throws {CustomError} If token is missing, invalid, or expired
|
|
434
|
+
* @private
|
|
435
|
+
* @example
|
|
436
|
+
* // Authorization header format: "Bearer {jwt_token}"
|
|
437
|
+
* await this.#verifyToken(req, res, next, '/login');
|
|
438
|
+
*/
|
|
439
|
+
async #verifyToken(req, res, next, errorRedirectUrl) {
|
|
440
|
+
try {
|
|
441
|
+
// Lightweight token validation (no Redis lookup)
|
|
442
|
+
await this.#getUserFromToken(req.headers.authorization, false);
|
|
375
443
|
return next();
|
|
376
|
-
|
|
377
444
|
} catch (error) {
|
|
378
|
-
if (
|
|
379
|
-
|
|
380
|
-
return next();
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (redirectUrl) {
|
|
384
|
-
return res.redirect(redirectUrl);
|
|
445
|
+
if (errorRedirectUrl) {
|
|
446
|
+
return res.redirect(errorRedirectUrl);
|
|
385
447
|
}
|
|
386
448
|
|
|
387
449
|
// Handle specific JWT errors
|
|
@@ -399,18 +461,19 @@ export class SessionManager {
|
|
|
399
461
|
* @param {import('@types/express').Request} req Request with session data
|
|
400
462
|
* @param {import('@types/express').Response} res Response object
|
|
401
463
|
* @param {import('@types/express').NextFunction} next Next middleware function
|
|
402
|
-
* @param {
|
|
403
|
-
* @param {string} redirectUrl URL to redirect to if user is unauthorized
|
|
464
|
+
* @param {string} errorRedirectUrl URL to redirect to if user is unauthorized
|
|
404
465
|
* @throws {CustomError} If user is not authorized
|
|
405
466
|
* @private
|
|
406
467
|
*/
|
|
407
|
-
async #verifySession(req, res, next,
|
|
408
|
-
|
|
468
|
+
async #verifySession(req, res, next, errorRedirectUrl) {
|
|
469
|
+
// Fix: Check session data directly, not req.user (which is only populated by requireUser())
|
|
470
|
+
const user = req.session[this.#getSessionKey()];
|
|
471
|
+
const { authorized = false } = user ?? { authorized: false };
|
|
409
472
|
if (authorized) {
|
|
410
473
|
return next();
|
|
411
474
|
}
|
|
412
|
-
if (
|
|
413
|
-
return res.redirect(
|
|
475
|
+
if (errorRedirectUrl) {
|
|
476
|
+
return res.redirect(errorRedirectUrl);
|
|
414
477
|
}
|
|
415
478
|
return next(new CustomError(httpCodes.UNAUTHORIZED, httpMessages.UNAUTHORIZED));
|
|
416
479
|
}
|
|
@@ -422,41 +485,31 @@ export class SessionManager {
|
|
|
422
485
|
* @param {import('@types/express').Response} res Response object
|
|
423
486
|
* @param {import('@types/express').NextFunction} next Next middleware function
|
|
424
487
|
* @param {(user: object) => object} initUser Function to initialize/transform user object
|
|
425
|
-
* @param {string}
|
|
488
|
+
* @param {string} idpRefreshUrl Identity provider refresh endpoint URL
|
|
426
489
|
* @throws {CustomError} If refresh lock is active or SSO refresh fails
|
|
427
490
|
* @private
|
|
428
491
|
* @example
|
|
429
492
|
* // Response format:
|
|
430
|
-
* // {
|
|
493
|
+
* // { jwt: "new_jwt", user: {...}, expires_at: 64800, token_type: "Bearer" }
|
|
431
494
|
*/
|
|
432
|
-
async #refreshToken(req, res, next, initUser,
|
|
495
|
+
async #refreshToken(req, res, next, initUser, idpRefreshUrl) {
|
|
433
496
|
try {
|
|
434
|
-
|
|
435
|
-
const {
|
|
436
|
-
|
|
437
|
-
if (!email) {
|
|
438
|
-
throw new CustomError(httpCodes.UNAUTHORIZED, 'User not authenticated');
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Extract Token ID from current token
|
|
442
|
-
const authHeader = req.headers.authorization;
|
|
443
|
-
const token = authHeader?.substring(7);
|
|
444
|
-
const { payload } = await this.#jwtManager.decrypt(token, this.#config.SESSION_SECRET);
|
|
445
|
-
const { tid: oldTokenId } = payload;
|
|
497
|
+
/** @type {{ tid: string, user: { email: string, attributes: { idp: string, refresh_token: string }? } }} */
|
|
498
|
+
const { tid, user } = await this.#getUserFromToken(req.headers.authorization, true);
|
|
446
499
|
|
|
447
500
|
// Check refresh lock
|
|
448
|
-
if (this.hasLock(email)) {
|
|
501
|
+
if (this.hasLock(user?.email)) {
|
|
449
502
|
throw new CustomError(httpCodes.CONFLICT, 'Token refresh is locked');
|
|
450
503
|
}
|
|
451
|
-
this.lock(email);
|
|
504
|
+
this.lock(user?.email);
|
|
452
505
|
|
|
453
506
|
// Call SSO refresh endpoint
|
|
454
|
-
const response = await this.#idpRequest.post(
|
|
507
|
+
const response = await this.#idpRequest.post(idpRefreshUrl, {
|
|
455
508
|
user: {
|
|
456
|
-
email,
|
|
509
|
+
email: user?.email,
|
|
457
510
|
attributes: {
|
|
458
|
-
idp: attributes?.idp,
|
|
459
|
-
refresh_token: attributes?.refresh_token
|
|
511
|
+
idp: user?.attributes?.idp,
|
|
512
|
+
refresh_token: user?.attributes?.refresh_token
|
|
460
513
|
}
|
|
461
514
|
}
|
|
462
515
|
});
|
|
@@ -474,24 +527,19 @@ export class SessionManager {
|
|
|
474
527
|
}
|
|
475
528
|
|
|
476
529
|
// Initialize user with new data
|
|
477
|
-
const
|
|
530
|
+
const newUser = initUser(newPayload.user);
|
|
478
531
|
|
|
479
532
|
// Generate new token
|
|
480
|
-
const newToken = await this.#generateAndStoreToken(
|
|
533
|
+
const newToken = await this.#generateAndStoreToken(newUser);
|
|
481
534
|
|
|
482
535
|
// Remove old token from Redis
|
|
483
|
-
const oldRedisKey = this.#getTokenRedisKey(email,
|
|
536
|
+
const oldRedisKey = this.#getTokenRedisKey(user.email, tid);
|
|
484
537
|
await this.#redisManager.getClient().del(oldRedisKey);
|
|
485
538
|
|
|
486
539
|
this.#logger.debug('### TOKEN REFRESHED SUCCESSFULLY ###');
|
|
487
540
|
|
|
488
541
|
// Return new token
|
|
489
|
-
return res.json({
|
|
490
|
-
token: newToken,
|
|
491
|
-
user,
|
|
492
|
-
expiresIn: Math.floor(this.#config.SESSION_AGE / 1000),
|
|
493
|
-
tokenType: 'Bearer'
|
|
494
|
-
});
|
|
542
|
+
return res.json({ jwt: newToken, user: newUser, expires_at: this.#config.SESSION_AGE, token_type: 'Bearer' });
|
|
495
543
|
} catch (error) {
|
|
496
544
|
return next(httpHelper.handleAxiosError(error));
|
|
497
545
|
}
|
|
@@ -503,17 +551,17 @@ export class SessionManager {
|
|
|
503
551
|
* @param {import('@types/express').Response} res Response
|
|
504
552
|
* @param {import('@types/express').NextFunction} next Next function
|
|
505
553
|
* @param {(user: object) => object} initUser Initialize user function
|
|
506
|
-
* @param {string}
|
|
554
|
+
* @param {string} idpRefreshUrl Token Refresh URL
|
|
507
555
|
* @private
|
|
508
556
|
*/
|
|
509
|
-
async #refreshSession(req, res, next, initUser,
|
|
557
|
+
async #refreshSession(req, res, next, initUser, idpRefreshUrl) {
|
|
510
558
|
try {
|
|
511
559
|
const { email, attributes } = req.user || { email: '', attributes: {} };
|
|
512
560
|
if (this.hasLock(email)) {
|
|
513
561
|
throw new CustomError(httpCodes.CONFLICT, 'User refresh is locked');
|
|
514
562
|
}
|
|
515
563
|
this.lock(email);
|
|
516
|
-
const response = await this.#idpRequest.post(
|
|
564
|
+
const response = await this.#idpRequest.post(idpRefreshUrl, {
|
|
517
565
|
user: {
|
|
518
566
|
email,
|
|
519
567
|
attributes: {
|
|
@@ -594,15 +642,8 @@ export class SessionManager {
|
|
|
594
642
|
}
|
|
595
643
|
|
|
596
644
|
try {
|
|
597
|
-
// Extract Token ID from current token
|
|
598
|
-
const
|
|
599
|
-
const token = authHeader?.substring(7);
|
|
600
|
-
|
|
601
|
-
if (!token) {
|
|
602
|
-
throw new CustomError(httpCodes.BAD_REQUEST, 'No token provided');
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
const { payload } = await this.#jwtManager.decrypt(token, this.#config.SESSION_SECRET);
|
|
645
|
+
// Extract Token ID and email from current token
|
|
646
|
+
const payload = await this.#getUserFromToken(req.headers.authorization, false);
|
|
606
647
|
const { email, tid } = payload;
|
|
607
648
|
|
|
608
649
|
if (!email || !tid) {
|
|
@@ -660,24 +701,6 @@ export class SessionManager {
|
|
|
660
701
|
});
|
|
661
702
|
}
|
|
662
703
|
|
|
663
|
-
/**
|
|
664
|
-
* Setup the session/user handlers with configurations
|
|
665
|
-
* @param {import('@types/express').Application} app Express application
|
|
666
|
-
* @param {(user: object) => object} updateUser Update user object if user should have proper attributes, e.g. permissions, avatar URL
|
|
667
|
-
*/
|
|
668
|
-
async setup(app, updateUser) {
|
|
669
|
-
this.#redisManager = new RedisManager();
|
|
670
|
-
this.#jwtManager = new JwtManager(this.#config);
|
|
671
|
-
// Identity Provider Request
|
|
672
|
-
this.#idpRequest = axios.create({
|
|
673
|
-
baseURL: this.#config.SSO_ENDPOINT_URL,
|
|
674
|
-
timeout: 30000,
|
|
675
|
-
});
|
|
676
|
-
app.set('trust proxy', 1);
|
|
677
|
-
app.use(await this.#sessionHandler());
|
|
678
|
-
app.use(this.#userHandler(updateUser));
|
|
679
|
-
}
|
|
680
|
-
|
|
681
704
|
/**
|
|
682
705
|
* Get Redis session RequestHandler
|
|
683
706
|
* @returns {import('@types/express').RequestHandler} Returns RequestHandler instance of Express
|
|
@@ -686,7 +709,7 @@ export class SessionManager {
|
|
|
686
709
|
// Redis Session
|
|
687
710
|
this.#logger.log('### Using Redis as the Session Store ###');
|
|
688
711
|
return session({
|
|
689
|
-
cookie: { maxAge: this.#
|
|
712
|
+
cookie: { maxAge: this.#getSessionAgeInMilliseconds(), path: this.#config.SESSION_COOKIE_PATH, sameSite: false },
|
|
690
713
|
store: new RedisStore({ client: this.#redisManager.getClient(), prefix: this.#config.SESSION_PREFIX, disableTouch: true }),
|
|
691
714
|
resave: false, saveUninitialized: false,
|
|
692
715
|
secret: this.#config.SESSION_SECRET,
|
|
@@ -702,7 +725,7 @@ export class SessionManager {
|
|
|
702
725
|
this.#logger.log('### Using Memory as the Session Store ###');
|
|
703
726
|
const MemoryStore = memStore(session);
|
|
704
727
|
return session({
|
|
705
|
-
cookie: { maxAge: this.#
|
|
728
|
+
cookie: { maxAge: this.#getSessionAgeInMilliseconds(), path: this.#config.SESSION_COOKIE_PATH, sameSite: false },
|
|
706
729
|
store: new MemoryStore({}),
|
|
707
730
|
resave: false, saveUninitialized: false,
|
|
708
731
|
secret: this.#config.SESSION_SECRET,
|
|
@@ -711,67 +734,85 @@ export class SessionManager {
|
|
|
711
734
|
|
|
712
735
|
/**
|
|
713
736
|
* Get session RequestHandler
|
|
714
|
-
* @
|
|
737
|
+
* @param {boolean} isRedisReady Is Redis Ready
|
|
738
|
+
* @returns {import('@types/express').RequestHandler} Returns RequestHandler instance of Express
|
|
715
739
|
*/
|
|
716
|
-
|
|
717
|
-
if(
|
|
718
|
-
await this.#redisManager.connect(this.#config.REDIS_URL, this.#config.REDIS_CERT_PATH);
|
|
740
|
+
#sessionHandler(isRedisReady) {
|
|
741
|
+
if(isRedisReady) {
|
|
719
742
|
return this.#redisSession();
|
|
720
743
|
}
|
|
721
744
|
return this.#memorySession();
|
|
722
745
|
}
|
|
723
746
|
|
|
724
747
|
/**
|
|
725
|
-
*
|
|
726
|
-
* @param {(user: object) => object} updateUser User wrapper
|
|
748
|
+
* Middleware to load full user data (works for both SESSION and TOKEN modes)
|
|
727
749
|
* @returns {import('@types/express').RequestHandler} Returns express Request Handler
|
|
728
750
|
*/
|
|
729
|
-
|
|
730
|
-
return (req, res, next) => {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
751
|
+
requireUser = () => {
|
|
752
|
+
return async (req, res, next) => {
|
|
753
|
+
try {
|
|
754
|
+
req.user = await this.getUser(req);
|
|
755
|
+
return next();
|
|
756
|
+
}
|
|
757
|
+
catch (error) {
|
|
758
|
+
return next(error);
|
|
759
|
+
}
|
|
735
760
|
};
|
|
736
|
-
}
|
|
761
|
+
};
|
|
737
762
|
|
|
738
763
|
/**
|
|
739
764
|
* Resource protection based on configured SESSION_MODE
|
|
740
|
-
*
|
|
741
|
-
*
|
|
765
|
+
* - SESSION mode: Verifies user exists in session store and is authorized (checks req.session data)
|
|
766
|
+
* - TOKEN mode: Validates JWT token from Authorization header (lightweight validation)
|
|
767
|
+
*
|
|
768
|
+
* Note: This method verifies authentication only. Use requireUser() after this to populate req.user.
|
|
769
|
+
*
|
|
770
|
+
* @param {string} [errorRedirectUrl=''] Redirect URL on authentication failure
|
|
742
771
|
* @returns {import('@types/express').RequestHandler} Returns express Request Handler
|
|
743
|
-
|
|
744
|
-
|
|
772
|
+
* @example
|
|
773
|
+
* // Option 1: Just verify authentication (user data remains in req.session or token)
|
|
774
|
+
* app.get('/api/check', session.authenticate(), (req, res) => {
|
|
775
|
+
* res.json({ authenticated: true });
|
|
776
|
+
* });
|
|
777
|
+
*
|
|
778
|
+
* // Option 2: Verify authentication AND load user data into req.user
|
|
779
|
+
* app.get('/api/profile',
|
|
780
|
+
* session.authenticate(), // Verifies session/token
|
|
781
|
+
* session.requireUser(), // Loads user data into req.user
|
|
782
|
+
* (req, res) => {
|
|
783
|
+
* res.json({ user: req.user }); // User data available here
|
|
784
|
+
* }
|
|
785
|
+
* );
|
|
786
|
+
*/
|
|
787
|
+
authenticate(errorRedirectUrl = '') {
|
|
745
788
|
return async (req, res, next) => {
|
|
746
789
|
const mode = this.#config.SESSION_MODE || SessionMode.SESSION;
|
|
747
790
|
if (mode === SessionMode.TOKEN) {
|
|
748
|
-
return this.#verifyToken(req, res, next,
|
|
791
|
+
return this.#verifyToken(req, res, next, errorRedirectUrl);
|
|
749
792
|
}
|
|
750
|
-
return this.#verifySession(req, res, next,
|
|
793
|
+
return this.#verifySession(req, res, next, errorRedirectUrl);
|
|
751
794
|
};
|
|
752
795
|
}
|
|
753
796
|
|
|
754
797
|
/**
|
|
755
798
|
* Resource protection by token (explicit token verification)
|
|
756
|
-
* @param {
|
|
757
|
-
* @param {string} [redirectUrl=''] Redirect URL
|
|
799
|
+
* @param {string} [errorRedirectUrl=''] Redirect URL
|
|
758
800
|
* @returns {import('@types/express').RequestHandler} Returns express Request Handler
|
|
759
801
|
*/
|
|
760
|
-
verifyToken(
|
|
802
|
+
verifyToken(errorRedirectUrl = '') {
|
|
761
803
|
return async (req, res, next) => {
|
|
762
|
-
return this.#verifyToken(req, res, next,
|
|
804
|
+
return this.#verifyToken(req, res, next, errorRedirectUrl);
|
|
763
805
|
};
|
|
764
806
|
}
|
|
765
807
|
|
|
766
808
|
/**
|
|
767
809
|
* Resource protection by session (explicit session verification)
|
|
768
|
-
* @param {
|
|
769
|
-
* @param {string} [redirectUrl=''] Redirect URL
|
|
810
|
+
* @param {string} [errorRedirectUrl=''] Redirect URL
|
|
770
811
|
* @returns {import('@types/express').RequestHandler} Returns express Request Handler
|
|
771
812
|
*/
|
|
772
|
-
verifySession(
|
|
813
|
+
verifySession(errorRedirectUrl = '') {
|
|
773
814
|
return async (req, res, next) => {
|
|
774
|
-
return this.#verifySession(req, res, next,
|
|
815
|
+
return this.#verifySession(req, res, next, errorRedirectUrl);
|
|
775
816
|
};
|
|
776
817
|
}
|
|
777
818
|
|
|
@@ -800,62 +841,75 @@ export class SessionManager {
|
|
|
800
841
|
});
|
|
801
842
|
}
|
|
802
843
|
throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload');
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Render HTML template for token storage
|
|
848
|
+
* @param {string} token JWT token
|
|
849
|
+
* @param {string} expiresAt Expiry timestamp
|
|
850
|
+
* @param {string} sucessRedirectUrl Success redirect URL
|
|
851
|
+
* @returns {string} Rendered HTML
|
|
852
|
+
* @private
|
|
853
|
+
*/
|
|
854
|
+
#renderTokenStorageHtml(token, expiresAt, sucessRedirectUrl) {
|
|
855
|
+
return this.#htmlTemplate
|
|
856
|
+
.replaceAll('{{SESSION_DATA_KEY}}', this.#config.SESSION_KEY)
|
|
857
|
+
.replaceAll('{{SESSION_DATA_VALUE}}', token)
|
|
858
|
+
.replaceAll('{{SESSION_EXPIRY_KEY}}', this.#config.SESSION_EXPIRY_KEY)
|
|
859
|
+
.replaceAll('{{SESSION_EXPIRY_VALUE}}', expiresAt)
|
|
860
|
+
.replaceAll('{{SSO_SUCCESS_URL}}', sucessRedirectUrl)
|
|
861
|
+
.replaceAll('{{SSO_FAILURE_URL}}', this.#config.SSO_FAILURE_URL);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* SSO callback for successful login
|
|
866
|
+
* @param {(user: object) => object} initUser Initialize user object function
|
|
867
|
+
* @returns {import('@types/express').RequestHandler} Returns express Request Handler
|
|
868
|
+
*/
|
|
869
|
+
callback(initUser) {
|
|
870
|
+
return async (req, res, next) => {
|
|
871
|
+
const { jwt = '' } = req.query;
|
|
872
|
+
if (!jwt) {
|
|
873
|
+
return next(new CustomError(httpCodes.BAD_REQUEST, 'Missing `jwt` in query parameters'));
|
|
823
874
|
}
|
|
875
|
+
|
|
876
|
+
try {
|
|
877
|
+
// Decrypt JWT from Identity Adapter
|
|
878
|
+
const { payload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
|
|
879
|
+
|
|
880
|
+
if (!payload?.user) {
|
|
881
|
+
throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload');
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** @type {import('../index.js').SessionUser} */
|
|
885
|
+
const user = initUser(payload.user);
|
|
886
|
+
/** @type {string} */
|
|
887
|
+
const callbackRedirectUrl = payload.redirect_url || this.#config.SSO_SUCCESS_URL;
|
|
888
|
+
|
|
889
|
+
// Token mode: Generate token and return HTML page
|
|
890
|
+
if (this.#config.SESSION_MODE === SessionMode.TOKEN) {
|
|
891
|
+
const token = await this.#generateAndStoreToken(user);
|
|
892
|
+
this.#logger.debug('### CALLBACK TOKEN GENERATED ###');
|
|
893
|
+
const html = this.#renderTokenStorageHtml(token, user.attributes.expires_at, callbackRedirectUrl);
|
|
894
|
+
return res.send(html);
|
|
895
|
+
}
|
|
824
896
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
const redirectUrl = payload.redirect_url || this.#config.SSO_SUCCESS_URL;
|
|
829
|
-
|
|
830
|
-
// Check SESSION_MODE to determine response type
|
|
831
|
-
if (this.#config.SESSION_MODE === SessionMode.TOKEN) {
|
|
832
|
-
// Token-based: Generate token and return HTML page that stores it
|
|
833
|
-
const token = await this.#generateAndStoreToken(user);
|
|
834
|
-
|
|
835
|
-
this.#logger.debug('### CALLBACK TOKEN GENERATED ###');
|
|
836
|
-
|
|
837
|
-
const templatePath = this.#config.TOKEN_STORAGE_TEMPLATE_PATH || path.resolve(__dirname, 'assets', 'template.html');
|
|
838
|
-
// Return HTML page that stores token in localStorage and redirects
|
|
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);
|
|
897
|
+
// Session mode: Save to session and redirect
|
|
898
|
+
await this.#saveSession(req, jwt, initUser);
|
|
899
|
+
return res.redirect(callbackRedirectUrl);
|
|
848
900
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
901
|
+
catch (error) {
|
|
902
|
+
this.#logger.error('### CALLBACK ERROR ###', error);
|
|
903
|
+
let errorMessage = error.message;
|
|
904
|
+
if (error.code === 'ERR_JWT_EXPIRED') {
|
|
905
|
+
errorMessage = 'Authentication token expired';
|
|
906
|
+
} else if (error.code === 'ERR_JWT_INVALID') {
|
|
907
|
+
errorMessage = 'Invalid authentication token';
|
|
908
|
+
}
|
|
909
|
+
return res.redirect(this.#config.SSO_FAILURE_URL.concat('?message=').concat(encodeURIComponent(errorMessage)));
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
}
|
|
859
913
|
|
|
860
914
|
/**
|
|
861
915
|
* Get Identity Providers
|
|
@@ -883,14 +937,14 @@ export class SessionManager {
|
|
|
883
937
|
* @returns {import('@types/express').RequestHandler} Returns express Request Handler
|
|
884
938
|
*/
|
|
885
939
|
refresh(initUser) {
|
|
886
|
-
const
|
|
940
|
+
const idpRefreshUrl = '/auth/refresh'.concat('?client_id=').concat(this.#config.SSO_CLIENT_ID);
|
|
887
941
|
return async (req, res, next) => {
|
|
888
942
|
const mode = this.#config.SESSION_MODE || SessionMode.SESSION;
|
|
889
|
-
|
|
943
|
+
|
|
890
944
|
if (mode === SessionMode.TOKEN) {
|
|
891
|
-
return this.#refreshToken(req, res, next, initUser,
|
|
945
|
+
return this.#refreshToken(req, res, next, initUser, idpRefreshUrl);
|
|
892
946
|
} else {
|
|
893
|
-
return this.#refreshSession(req, res, next, initUser,
|
|
947
|
+
return this.#refreshSession(req, res, next, initUser, idpRefreshUrl);
|
|
894
948
|
}
|
|
895
949
|
};
|
|
896
950
|
}
|
package/index.d.ts
CHANGED
|
@@ -122,9 +122,9 @@ export interface SessionConfig {
|
|
|
122
122
|
SESSION_MODE?: string;
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
|
-
* Session expiration time in
|
|
126
|
-
* @example
|
|
127
|
-
* @default
|
|
125
|
+
* Session expiration time in seconds
|
|
126
|
+
* @example 3600 (1 hour) or 86400 (24 hours) or 64800 (18 hours)
|
|
127
|
+
* @default 64800 (18 hours)
|
|
128
128
|
*/
|
|
129
129
|
SESSION_AGE?: number;
|
|
130
130
|
|
|
@@ -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,98 @@ 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
|
+
* @returns Promise resolving to full user data object
|
|
320
|
+
* @throws CustomError If user is not authenticated
|
|
321
|
+
* @example
|
|
322
|
+
* ```javascript
|
|
323
|
+
* // Use in custom middleware
|
|
324
|
+
* app.use(async (req, res, next) => {
|
|
325
|
+
* try {
|
|
326
|
+
* const user = await sessionManager.getUser(req);
|
|
327
|
+
* req.customUser = user;
|
|
328
|
+
* next();
|
|
329
|
+
* } catch (error) {
|
|
330
|
+
* next(error);
|
|
331
|
+
* }
|
|
332
|
+
* });
|
|
333
|
+
* ```
|
|
334
|
+
*/
|
|
335
|
+
getUser(req: Request): Promise<SessionUser>;
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Initialize the session configurations and middleware
|
|
315
339
|
* @param app Express application
|
|
316
|
-
* @param config Session configurations
|
|
317
|
-
* @param updateUser Process user object to compute attributes like permissions, avatar URL, etc.
|
|
318
340
|
*/
|
|
319
|
-
setup(
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
341
|
+
setup(app: Application): Promise<void>;
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Middleware to load full user data into req.user
|
|
345
|
+
* - SESSION mode: Loads user from session store (req.session[SESSION_KEY])
|
|
346
|
+
* - TOKEN mode: Loads user from Redis using JWT token
|
|
347
|
+
* - Provides request-level caching to avoid redundant lookups
|
|
348
|
+
* - Should be used after authenticate() middleware
|
|
349
|
+
* @returns Returns express Request Handler
|
|
350
|
+
* @example
|
|
351
|
+
* ```javascript
|
|
352
|
+
* app.get('/api/profile',
|
|
353
|
+
* session.authenticate(), // Verifies authentication
|
|
354
|
+
* session.requireUser(), // Loads user data into req.user
|
|
355
|
+
* (req, res) => {
|
|
356
|
+
* res.json({ user: req.user }); // User data available here
|
|
357
|
+
* }
|
|
358
|
+
* );
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
requireUser(): RequestHandler;
|
|
323
362
|
|
|
324
363
|
/**
|
|
325
364
|
* Resource protection middleware based on configured SESSION_MODE
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
365
|
+
* - SESSION mode: Verifies user exists in session store and is authorized (checks req.session data)
|
|
366
|
+
* - TOKEN mode: Validates JWT token from Authorization header (lightweight validation)
|
|
367
|
+
*
|
|
368
|
+
* Note: This method verifies authentication only and does NOT populate req.user.
|
|
369
|
+
* Use requireUser() after this middleware to load user data into req.user.
|
|
370
|
+
*
|
|
371
|
+
* @param errorRedirectUrl Redirect URL on authentication failure (default: '')
|
|
329
372
|
* @returns Returns express Request Handler
|
|
373
|
+
* @example
|
|
374
|
+
* ```javascript
|
|
375
|
+
* // Option 1: Just verify authentication (user data remains in req.session or token)
|
|
376
|
+
* app.get('/api/check',
|
|
377
|
+
* session.authenticate(),
|
|
378
|
+
* (req, res) => {
|
|
379
|
+
* res.json({ authenticated: true });
|
|
380
|
+
* }
|
|
381
|
+
* );
|
|
382
|
+
*
|
|
383
|
+
* // Option 2: Verify authentication AND populate req.user (recommended for most use cases)
|
|
384
|
+
* app.get('/api/profile',
|
|
385
|
+
* session.authenticate(), // Verifies session/token validity
|
|
386
|
+
* session.requireUser(), // Loads user data into req.user
|
|
387
|
+
* (req, res) => {
|
|
388
|
+
* res.json({ user: req.user }); // User data now available
|
|
389
|
+
* }
|
|
390
|
+
* );
|
|
391
|
+
* ```
|
|
330
392
|
*/
|
|
331
|
-
authenticate(
|
|
393
|
+
authenticate(errorRedirectUrl?: string): RequestHandler;
|
|
332
394
|
|
|
333
395
|
/**
|
|
334
396
|
* Resource protection by token (explicit token verification)
|
|
335
397
|
* Requires Authorization: Bearer {token} header
|
|
336
|
-
* @param
|
|
337
|
-
* @param redirectUrl Redirect URL (default: '')
|
|
398
|
+
* @param errorRedirectUrl Redirect URL (default: '')
|
|
338
399
|
* @returns Returns express Request Handler
|
|
339
400
|
*/
|
|
340
|
-
verifyToken(
|
|
401
|
+
verifyToken(errorRedirectUrl?: string): RequestHandler;
|
|
341
402
|
|
|
342
403
|
/**
|
|
343
404
|
* Resource protection by session (explicit session verification)
|
|
344
|
-
* @param
|
|
345
|
-
* @param redirectUrl Redirect URL (default: '')
|
|
405
|
+
* @param errorRedirectUrl Redirect URL (default: '')
|
|
346
406
|
* @returns Returns express Request Handler
|
|
347
407
|
*/
|
|
348
|
-
verifySession(
|
|
408
|
+
verifySession(errorRedirectUrl?: string): RequestHandler;
|
|
349
409
|
|
|
350
410
|
/**
|
|
351
411
|
* SSO callback for successful login
|