@igxjs/node-components 1.0.15 → 1.0.17
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/components/assets/template.html +7 -2
- package/components/http-handlers.js +4 -3
- package/components/jwt.js +12 -12
- package/components/logger.js +16 -16
- package/components/redis.js +1 -1
- package/components/router.js +5 -5
- package/components/session.js +162 -115
- package/index.d.ts +9 -2
- package/index.js +1 -1
- package/package.json +11 -19
|
@@ -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
|
}
|
|
@@ -161,12 +161,13 @@ export const httpHelper = {
|
|
|
161
161
|
* @returns {string} Formatted string
|
|
162
162
|
*/
|
|
163
163
|
format (str, ...args) {
|
|
164
|
+
let result = str;
|
|
164
165
|
const matched = str.match(/{\d}/ig);
|
|
165
166
|
matched.forEach((element, index) => {
|
|
166
167
|
if(args.length > index)
|
|
167
|
-
|
|
168
|
+
result = result.replace(element, args[index]);
|
|
168
169
|
});
|
|
169
|
-
return
|
|
170
|
+
return result;
|
|
170
171
|
},
|
|
171
172
|
|
|
172
173
|
/**
|
|
@@ -209,4 +210,4 @@ export const httpHelper = {
|
|
|
209
210
|
*/
|
|
210
211
|
export const httpError = (code, message, error, data) => {
|
|
211
212
|
return new CustomError(code, message, error, data);
|
|
212
|
-
};
|
|
213
|
+
};
|
package/components/jwt.js
CHANGED
|
@@ -9,31 +9,31 @@ import { jwtDecrypt, EncryptJWT } from 'jose';
|
|
|
9
9
|
export class JwtManager {
|
|
10
10
|
/** @type {string} JWE algorithm */
|
|
11
11
|
algorithm;
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
/** @type {string} Encryption method */
|
|
14
14
|
encryption;
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
/** @type {number} Token expiration time */
|
|
17
17
|
expirationTime;
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
/** @type {number} Clock tolerance in seconds */
|
|
20
20
|
clockTolerance;
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
/** @type {string} Hash algorithm for secret derivation */
|
|
23
23
|
secretHashAlgorithm;
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
/** @type {string|null} Optional JWT issuer claim */
|
|
26
26
|
issuer;
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
/** @type {string|null} Optional JWT audience claim */
|
|
29
29
|
audience;
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
/** @type {string|null} Optional JWT subject claim */
|
|
32
32
|
subject;
|
|
33
33
|
/**
|
|
34
34
|
* Create a new JwtManager instance with configurable defaults
|
|
35
35
|
* Constructor options use UPPERCASE naming convention with JWT_ prefix (e.g., JWT_ALGORITHM).
|
|
36
|
-
*
|
|
36
|
+
*
|
|
37
37
|
* @typedef {Object} JwtManagerOptions JwtManager configuration options
|
|
38
38
|
* @property {string} [JWT_ALGORITHM='dir'] JWE algorithm (default: 'dir')
|
|
39
39
|
* @property {string} [JWT_ENCRYPTION='A256GCM'] Encryption method (default: 'A256GCM')
|
|
@@ -59,7 +59,7 @@ export class JwtManager {
|
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* Encrypt method options (camelCase naming convention, uses instance defaults when not provided)
|
|
62
|
-
*
|
|
62
|
+
*
|
|
63
63
|
* @typedef {Object} JwtEncryptOptions Encryption method options
|
|
64
64
|
* @property {string} [algorithm='dir'] JWE algorithm (overrides instance JWT_ALGORITHM)
|
|
65
65
|
* @property {string} [encryption='A256GCM'] Encryption method (overrides instance JWT_ENCRYPTION)
|
|
@@ -71,7 +71,7 @@ export class JwtManager {
|
|
|
71
71
|
*/
|
|
72
72
|
/**
|
|
73
73
|
* Generate JWT token for user session
|
|
74
|
-
*
|
|
74
|
+
*
|
|
75
75
|
* @param {import('jose').JWTPayload} data User data payload
|
|
76
76
|
* @param {string} secret Secret key or password for encryption
|
|
77
77
|
* @param {JwtEncryptOptions} [options] Per-call configuration overrides (camelCase naming convention)
|
|
@@ -113,7 +113,7 @@ export class JwtManager {
|
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
115
|
* Decrypt method options (camelCase naming convention, uses instance defaults when not provided)
|
|
116
|
-
*
|
|
116
|
+
*
|
|
117
117
|
* @typedef {Object} JwtDecryptOptions Decryption method options
|
|
118
118
|
* @property {number} [clockTolerance=30] Clock tolerance in seconds (overrides instance JWT_CLOCK_TOLERANCE)
|
|
119
119
|
* @property {string} [secretHashAlgorithm='SHA-256'] Hash algorithm for secret derivation (overrides instance JWT_SECRET_HASH_ALGORITHM)
|
|
@@ -123,7 +123,7 @@ export class JwtManager {
|
|
|
123
123
|
**/
|
|
124
124
|
/**
|
|
125
125
|
* Decrypt JWT
|
|
126
|
-
*
|
|
126
|
+
*
|
|
127
127
|
* @param {string} token JWT token to decrypt
|
|
128
128
|
* @param {string} secret Secret key or password for decryption
|
|
129
129
|
* @param {JwtDecryptOptions} [options] Per-call configuration overrides (camelCase naming convention)
|
package/components/logger.js
CHANGED
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
* Logger utility for node-components
|
|
3
3
|
* Provides configurable logging with enable/disable functionality
|
|
4
4
|
* Uses singleton pattern to manage logger instances per component
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* @example
|
|
7
7
|
* import { Logger } from './logger.js';
|
|
8
|
-
*
|
|
8
|
+
*
|
|
9
9
|
* // Recommended: Get logger instance (singleton pattern)
|
|
10
10
|
* const logger = Logger.getInstance('ComponentName');
|
|
11
|
-
*
|
|
11
|
+
*
|
|
12
12
|
* // With explicit enable/disable
|
|
13
13
|
* const logger = Logger.getInstance('ComponentName', true); // Always enabled
|
|
14
14
|
* const logger = Logger.getInstance('ComponentName', false); // Always disabled
|
|
15
|
-
*
|
|
15
|
+
*
|
|
16
16
|
* // Backward compatibility: Constructor still works
|
|
17
17
|
* const logger = new Logger('ComponentName');
|
|
18
|
-
*
|
|
18
|
+
*
|
|
19
19
|
* // Use logger
|
|
20
20
|
* logger.info('Operation completed');
|
|
21
21
|
* logger.error('Error occurred', error);
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
export class Logger {
|
|
24
24
|
/** @type {Map<string, Logger>} */
|
|
25
25
|
static #instances = new Map();
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
/** @type {boolean} - Global flag to enable/disable colors */
|
|
28
28
|
static #colorsEnabled = true;
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
/** ANSI color codes for different log levels */
|
|
31
31
|
static #colors = {
|
|
32
32
|
reset: '\x1b[0m',
|
|
@@ -35,7 +35,7 @@ export class Logger {
|
|
|
35
35
|
yellow: '\x1b[33m', // warn - yellow
|
|
36
36
|
red: '\x1b[31m', // error - red
|
|
37
37
|
};
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
/** @type {boolean} */
|
|
40
40
|
#enabled;
|
|
41
41
|
/** @type {string} */
|
|
@@ -51,11 +51,11 @@ export class Logger {
|
|
|
51
51
|
*/
|
|
52
52
|
static getInstance(componentName, enableLogging) {
|
|
53
53
|
const key = `${componentName}:${enableLogging ?? 'default'}`;
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
if (!Logger.#instances.has(key)) {
|
|
56
56
|
Logger.#instances.set(key, new Logger(componentName, enableLogging));
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
return Logger.#instances.get(key);
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -90,20 +90,20 @@ export class Logger {
|
|
|
90
90
|
// Otherwise, default to enabled in development, disabled in production
|
|
91
91
|
this.#enabled = enableLogging ?? (process.env.NODE_ENV !== 'production');
|
|
92
92
|
this.#prefix = `[${componentName}]`;
|
|
93
|
-
|
|
93
|
+
|
|
94
94
|
// Determine if colors should be used:
|
|
95
95
|
// - Colors are globally enabled
|
|
96
96
|
// - Output is a terminal (TTY)
|
|
97
97
|
// - NO_COLOR environment variable is not set
|
|
98
|
-
this.#useColors = Logger.#colorsEnabled &&
|
|
99
|
-
process.stdout.isTTY &&
|
|
98
|
+
this.#useColors = Logger.#colorsEnabled &&
|
|
99
|
+
process.stdout.isTTY &&
|
|
100
100
|
!process.env.NO_COLOR;
|
|
101
|
-
|
|
101
|
+
|
|
102
102
|
// Assign methods based on enabled flag to eliminate runtime checks
|
|
103
103
|
// This improves performance by avoiding conditional checks on every log call
|
|
104
104
|
if (this.#enabled) {
|
|
105
105
|
const colors = Logger.#colors;
|
|
106
|
-
|
|
106
|
+
|
|
107
107
|
if (this.#useColors) {
|
|
108
108
|
// Logging enabled with colors: colorize the prefix
|
|
109
109
|
this.debug = (...args) => console.debug(colors.dim + this.#prefix + colors.reset, ...args);
|
|
@@ -128,4 +128,4 @@ export class Logger {
|
|
|
128
128
|
this.log = () => {};
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
-
}
|
|
131
|
+
}
|
package/components/redis.js
CHANGED
package/components/router.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FlexRouter for expressjs
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* @example
|
|
5
5
|
* import { Router } from 'express';
|
|
6
6
|
* import { FlexRouter } from '../models/router.js';
|
|
7
7
|
* import { authenticate } from '../middlewares/common.js';
|
|
8
|
-
*
|
|
8
|
+
*
|
|
9
9
|
* export const publicRouter = Router();
|
|
10
10
|
* export const privateRouter = Router();
|
|
11
|
-
*
|
|
11
|
+
*
|
|
12
12
|
* publicRouter.get('/health', (req, res) => {
|
|
13
13
|
* res.send('OK');
|
|
14
14
|
* });
|
|
15
|
-
*
|
|
15
|
+
*
|
|
16
16
|
* privateRouter.get('/', (req, res) => {
|
|
17
17
|
* res.send('Hello World!');
|
|
18
18
|
* });
|
|
19
|
-
*
|
|
19
|
+
*
|
|
20
20
|
* export const routers = [
|
|
21
21
|
* new FlexRouter('/api/v1/protected', privateRouter, [authenticate]),
|
|
22
22
|
* new FlexRouter('/api/v1/public', healthRouter),
|
package/components/session.js
CHANGED
|
@@ -28,15 +28,15 @@ export const SessionMode = {
|
|
|
28
28
|
* Session configuration options
|
|
29
29
|
*/
|
|
30
30
|
export class SessionConfig {
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
32
|
* @type {string} Authentication mode for protected routes
|
|
33
33
|
* - Supported values: SessionMode.SESSION | SessionMode.TOKEN
|
|
34
34
|
* @default SessionMode.SESSION
|
|
35
35
|
*/
|
|
36
36
|
SESSION_MODE;
|
|
37
|
-
/**
|
|
37
|
+
/**
|
|
38
38
|
* @type {string} Identity Provider microservice endpoint URL
|
|
39
|
-
*
|
|
39
|
+
*
|
|
40
40
|
* This is a fully customized, independent microservice that provides SSO authentication.
|
|
41
41
|
* The endpoint serves multiple applications and provides the following APIs:
|
|
42
42
|
* - GET /auth/providers - List supported identity providers
|
|
@@ -44,7 +44,7 @@ export class SessionConfig {
|
|
|
44
44
|
* - POST /auth/verify - Verify JWT token validity
|
|
45
45
|
* - GET /auth/callback/:idp - Validate authentication and return user data
|
|
46
46
|
* - POST /auth/refresh - Refresh access tokens
|
|
47
|
-
*
|
|
47
|
+
*
|
|
48
48
|
* @example
|
|
49
49
|
* SSO_ENDPOINT_URL: 'https://idp.example.com/open/api/v1'
|
|
50
50
|
*/
|
|
@@ -67,19 +67,19 @@ export class SessionConfig {
|
|
|
67
67
|
SESSION_COOKIE_PATH;
|
|
68
68
|
/** @type {string} Session secret */
|
|
69
69
|
SESSION_SECRET;
|
|
70
|
-
/**
|
|
71
|
-
* @type {string}
|
|
70
|
+
/**
|
|
71
|
+
* @type {string}
|
|
72
72
|
* @default 'ibmid:'
|
|
73
73
|
*/
|
|
74
74
|
SESSION_PREFIX;
|
|
75
|
-
/**
|
|
75
|
+
/**
|
|
76
76
|
* @type {string} Session key
|
|
77
77
|
* - In the `SessionMode.SESSION` mode, this is the key used to store the user in the session.
|
|
78
78
|
* - In the `SessionMode.TOKEN` mode, this is the key of localStorage where the user is stored.
|
|
79
79
|
* @default 'session_token'
|
|
80
80
|
*/
|
|
81
81
|
SESSION_KEY;
|
|
82
|
-
/**
|
|
82
|
+
/**
|
|
83
83
|
* @type {string} Session expiry key
|
|
84
84
|
* - In the `SessionMode.TOKEN` mode, this is the key of localStorage where the session expiry timestamp is stored.
|
|
85
85
|
* @default 'session_expires_at'
|
|
@@ -141,26 +141,33 @@ export class SessionManager {
|
|
|
141
141
|
#htmlTemplate = null;
|
|
142
142
|
|
|
143
143
|
/**
|
|
144
|
-
*
|
|
145
|
-
* @param {SessionConfig} config
|
|
146
|
-
* @throws {TypeError}
|
|
147
|
-
* @throws {Error} If required configuration fields are missing
|
|
144
|
+
* Validate config is an object
|
|
145
|
+
* @param {SessionConfig} config
|
|
146
|
+
* @throws {TypeError}
|
|
148
147
|
*/
|
|
149
|
-
|
|
150
|
-
// Validate config is an object
|
|
148
|
+
#validateConfigType(config) {
|
|
151
149
|
if (!config || typeof config !== 'object') {
|
|
152
150
|
throw new TypeError('SessionManager configuration must be an object');
|
|
153
151
|
}
|
|
152
|
+
}
|
|
154
153
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Validate required configuration fields
|
|
156
|
+
* @param {SessionConfig} config
|
|
157
|
+
* @throws {Error}
|
|
158
|
+
*/
|
|
159
|
+
#validateRequiredFields(config) {
|
|
159
160
|
if (!config.SESSION_SECRET) {
|
|
160
161
|
throw new Error('SESSION_SECRET is required for SessionManager');
|
|
161
162
|
}
|
|
163
|
+
}
|
|
162
164
|
|
|
163
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Validate SSO configuration
|
|
167
|
+
* @param {SessionConfig} config
|
|
168
|
+
* @throws {Error}
|
|
169
|
+
*/
|
|
170
|
+
#validateSsoConfig(config) {
|
|
164
171
|
if (config.SSO_ENDPOINT_URL) {
|
|
165
172
|
if (!config.SSO_CLIENT_ID) {
|
|
166
173
|
throw new Error('SSO_CLIENT_ID is required when SSO_ENDPOINT_URL is provided');
|
|
@@ -169,9 +176,15 @@ export class SessionManager {
|
|
|
169
176
|
throw new Error('SSO_CLIENT_SECRET is required when SSO_ENDPOINT_URL is provided');
|
|
170
177
|
}
|
|
171
178
|
}
|
|
179
|
+
}
|
|
172
180
|
|
|
173
|
-
|
|
174
|
-
|
|
181
|
+
/**
|
|
182
|
+
* Validate TOKEN mode requirements
|
|
183
|
+
* @param {SessionConfig} config
|
|
184
|
+
* @throws {Error}
|
|
185
|
+
*/
|
|
186
|
+
#validateTokenMode(config) {
|
|
187
|
+
if (config.SESSION_MODE === SessionMode.TOKEN) {
|
|
175
188
|
if (!config.SSO_SUCCESS_URL) {
|
|
176
189
|
throw new Error('SSO_SUCCESS_URL is required for TOKEN authentication mode');
|
|
177
190
|
}
|
|
@@ -179,11 +192,16 @@ export class SessionManager {
|
|
|
179
192
|
throw new Error('SSO_FAILURE_URL is required for TOKEN authentication mode');
|
|
180
193
|
}
|
|
181
194
|
}
|
|
195
|
+
}
|
|
182
196
|
|
|
183
|
-
|
|
184
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Build configuration object
|
|
199
|
+
* @param {SessionConfig} config
|
|
200
|
+
* @returns {object}
|
|
201
|
+
*/
|
|
202
|
+
#buildConfig(config) {
|
|
203
|
+
return {
|
|
185
204
|
SESSION_MODE: config.SESSION_MODE || SessionMode.SESSION,
|
|
186
|
-
// Session - SESSION_AGE is now in seconds (default: 64800 = 18 hours)
|
|
187
205
|
SESSION_AGE: config.SESSION_AGE || 64800,
|
|
188
206
|
SESSION_COOKIE_PATH: config.SESSION_COOKIE_PATH || '/',
|
|
189
207
|
SESSION_SECRET: config.SESSION_SECRET,
|
|
@@ -191,18 +209,13 @@ export class SessionManager {
|
|
|
191
209
|
SESSION_KEY: config.SESSION_KEY || 'session_token',
|
|
192
210
|
SESSION_EXPIRY_KEY: config.SESSION_EXPIRY_KEY || 'session_expires_at',
|
|
193
211
|
TOKEN_STORAGE_TEMPLATE_PATH: config.TOKEN_STORAGE_TEMPLATE_PATH,
|
|
194
|
-
|
|
195
|
-
// Identity Provider
|
|
196
212
|
SSO_ENDPOINT_URL: config.SSO_ENDPOINT_URL,
|
|
197
213
|
SSO_CLIENT_ID: config.SSO_CLIENT_ID,
|
|
198
214
|
SSO_CLIENT_SECRET: config.SSO_CLIENT_SECRET,
|
|
199
215
|
SSO_SUCCESS_URL: config.SSO_SUCCESS_URL,
|
|
200
216
|
SSO_FAILURE_URL: config.SSO_FAILURE_URL,
|
|
201
|
-
// Redis
|
|
202
217
|
REDIS_URL: config.REDIS_URL,
|
|
203
218
|
REDIS_CERT_PATH: config.REDIS_CERT_PATH,
|
|
204
|
-
|
|
205
|
-
// JWT Manager
|
|
206
219
|
JWT_ALGORITHM: config.JWT_ALGORITHM || 'dir',
|
|
207
220
|
JWT_ENCRYPTION: config.JWT_ENCRYPTION || 'A256GCM',
|
|
208
221
|
JWT_CLOCK_TOLERANCE: config.JWT_CLOCK_TOLERANCE ?? 30,
|
|
@@ -213,6 +226,20 @@ export class SessionManager {
|
|
|
213
226
|
};
|
|
214
227
|
}
|
|
215
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Create a new session manager
|
|
231
|
+
* @param {SessionConfig} config Session configuration
|
|
232
|
+
* @throws {TypeError} If config is not an object
|
|
233
|
+
* @throws {Error} If required configuration fields are missing
|
|
234
|
+
*/
|
|
235
|
+
constructor(config) {
|
|
236
|
+
this.#validateConfigType(config);
|
|
237
|
+
this.#validateRequiredFields(config);
|
|
238
|
+
this.#validateSsoConfig(config);
|
|
239
|
+
this.#validateTokenMode(config);
|
|
240
|
+
this.#config = this.#buildConfig(config);
|
|
241
|
+
}
|
|
242
|
+
|
|
216
243
|
/**
|
|
217
244
|
* Check if the email has a session refresh lock
|
|
218
245
|
* @param {string} email Email address
|
|
@@ -272,7 +299,7 @@ export class SessionManager {
|
|
|
272
299
|
* @private
|
|
273
300
|
*/
|
|
274
301
|
#getTokenRedisKey(email, tid) {
|
|
275
|
-
return `${this.#config.SESSION_KEY}
|
|
302
|
+
return `${this.#config.SESSION_KEY}:${email}:${tid}`;
|
|
276
303
|
}
|
|
277
304
|
|
|
278
305
|
/**
|
|
@@ -282,7 +309,7 @@ export class SessionManager {
|
|
|
282
309
|
* @private
|
|
283
310
|
*/
|
|
284
311
|
#getTokenRedisPattern(email) {
|
|
285
|
-
return `${this.#config.SESSION_KEY}
|
|
312
|
+
return `${this.#config.SESSION_KEY}:${email}:*`;
|
|
286
313
|
}
|
|
287
314
|
|
|
288
315
|
/**
|
|
@@ -311,12 +338,12 @@ export class SessionManager {
|
|
|
311
338
|
});
|
|
312
339
|
app.set('trust proxy', 1);
|
|
313
340
|
const isOK = await this.#redisManager.connect(this.#config.REDIS_URL, this.#config.REDIS_CERT_PATH);
|
|
314
|
-
if (this
|
|
341
|
+
if (this.getSessionMode() === SessionMode.SESSION) {
|
|
315
342
|
app.use(this.#sessionHandler(isOK));
|
|
316
343
|
}
|
|
317
344
|
|
|
318
345
|
// Cache HTML template for TOKEN mode
|
|
319
|
-
if (this
|
|
346
|
+
if (this.getSessionMode() === SessionMode.TOKEN) {
|
|
320
347
|
const templatePath = this.#config.TOKEN_STORAGE_TEMPLATE_PATH ||
|
|
321
348
|
path.resolve(__dirname, 'assets', 'template.html');
|
|
322
349
|
this.#htmlTemplate = fs.readFileSync(templatePath, 'utf8');
|
|
@@ -324,27 +351,37 @@ export class SessionManager {
|
|
|
324
351
|
}
|
|
325
352
|
}
|
|
326
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Generate lightweight JWT token
|
|
356
|
+
* @param {string} email User email
|
|
357
|
+
* @param {string} tokenId Token ID
|
|
358
|
+
* @param {number} expirationTime Expiration time in seconds
|
|
359
|
+
* @returns {Promise<string>} Returns the generated JWT token
|
|
360
|
+
* @private
|
|
361
|
+
*/
|
|
362
|
+
async #getLightweightToken(email, tokenId, expirationTime) {
|
|
363
|
+
return await this.#jwtManager.encrypt({ email, tid: tokenId }, this.#config.SSO_CLIENT_SECRET, { expirationTime });
|
|
364
|
+
}
|
|
365
|
+
|
|
327
366
|
/**
|
|
328
367
|
* Generate and store JWT token in Redis
|
|
329
368
|
* - JWT payload contains only { email, tid } for minimal size
|
|
330
369
|
* - Full user data is stored in Redis as single source of truth
|
|
331
|
-
* @param {
|
|
370
|
+
* @param {string} tid Token ID
|
|
371
|
+
* @param {Record<string, any> & { email: string, tid: string }} user User object with email and attributes
|
|
332
372
|
* @returns {Promise<string>} Returns the generated JWT token
|
|
333
373
|
* @throws {Error} If JWT encryption fails
|
|
334
374
|
* @throws {Error} If Redis storage fails
|
|
335
375
|
* @private
|
|
336
376
|
* @example
|
|
337
|
-
* const token = await this.#generateAndStoreToken({
|
|
377
|
+
* const token = await this.#generateAndStoreToken('tid', {
|
|
338
378
|
* email: 'user@example.com',
|
|
339
379
|
* attributes: { /* user data * / }
|
|
340
380
|
* });
|
|
341
381
|
*/
|
|
342
|
-
async #generateAndStoreToken(user) {
|
|
343
|
-
//
|
|
344
|
-
const
|
|
345
|
-
// Create JWT token with only email and tid (minimal payload)
|
|
346
|
-
const payload = { email: user.email, tid };
|
|
347
|
-
const token = await this.#jwtManager.encrypt(payload, this.#config.SESSION_SECRET, { expirationTime: this.#config.SESSION_AGE });
|
|
382
|
+
async #generateAndStoreToken(tid, user) {
|
|
383
|
+
// Create JWT token with only email, tid and idp (minimal payload)
|
|
384
|
+
const token = await this.#getLightweightToken(user.email, tid, this.#config.SESSION_AGE);
|
|
348
385
|
|
|
349
386
|
// Store user data in Redis with TTL
|
|
350
387
|
const redisKey = this.#getTokenRedisKey(user.email, tid);
|
|
@@ -357,28 +394,28 @@ export class SessionManager {
|
|
|
357
394
|
/**
|
|
358
395
|
* Extract and validate user data from Authorization header (TOKEN mode only)
|
|
359
396
|
* @param {string} authHeader Authorization header in format "Bearer {token}"
|
|
360
|
-
* @param {boolean} [
|
|
361
|
-
* - true: Returns { tid, user } with full user data
|
|
397
|
+
* @param {boolean} [includeUserData=true] Whether to include full user data in response
|
|
398
|
+
* - true: Returns { tid, user } with full user data (default)
|
|
362
399
|
* - false: Returns JWT payload only (lightweight validation)
|
|
363
|
-
* @returns {Promise<{ tid: string
|
|
364
|
-
* - When
|
|
365
|
-
* - When
|
|
400
|
+
* @returns {Promise<{ tid: string, user: { email: string, attributes: { expires_at: number, sub: string } } } & object>}
|
|
401
|
+
* - When includeUserData=true: { tid: string, user: object }
|
|
402
|
+
* - When includeUserData=false: JWT payload object
|
|
366
403
|
* @throws {CustomError} UNAUTHORIZED (401) if:
|
|
367
404
|
* - Authorization header is missing or invalid format
|
|
368
405
|
* - Token decryption fails
|
|
369
406
|
* - Token payload is invalid (missing email/tid)
|
|
370
|
-
* - Token not found in Redis (when
|
|
407
|
+
* - Token not found in Redis (when includeUserData=true)
|
|
371
408
|
* @private
|
|
372
409
|
*/
|
|
373
|
-
async #getUserFromToken(authHeader,
|
|
410
|
+
async #getUserFromToken(authHeader, includeUserData = true) {
|
|
374
411
|
if (!authHeader?.startsWith('Bearer ')) {
|
|
375
412
|
throw new CustomError(httpCodes.UNAUTHORIZED, 'Missing or invalid authorization header');
|
|
376
413
|
}
|
|
377
414
|
const token = authHeader.substring(7); // Remove 'Bearer ' prefix
|
|
378
|
-
|
|
379
|
-
const { payload } = await this.#jwtManager.decrypt(token, this.#config.
|
|
415
|
+
/** @type {{ payload: { email: string, tid: string } & import('jose').JWTPayload }} */
|
|
416
|
+
const { payload } = await this.#jwtManager.decrypt(token, this.#config.SSO_CLIENT_SECRET);
|
|
380
417
|
|
|
381
|
-
if (
|
|
418
|
+
if (includeUserData) {
|
|
382
419
|
/** @type {{ email: string, tid: string }} Extract email and token ID */
|
|
383
420
|
const { email, tid } = payload;
|
|
384
421
|
if (!email || !tid) {
|
|
@@ -392,14 +429,15 @@ export class SessionManager {
|
|
|
392
429
|
if (!userData) {
|
|
393
430
|
throw new CustomError(httpCodes.UNAUTHORIZED, 'Token not found or expired');
|
|
394
431
|
}
|
|
395
|
-
return { tid, user: JSON.parse(userData) };
|
|
432
|
+
return { tid, user: { ...JSON.parse(userData) } };
|
|
396
433
|
}
|
|
397
|
-
return payload;
|
|
434
|
+
return { tid: payload.tid, user: { email: payload.email, attributes: { sub: payload.sub, expires_at: payload.exp ? payload.exp * 1000 : 0 } } };
|
|
398
435
|
}
|
|
399
436
|
|
|
400
437
|
/**
|
|
401
438
|
* Get authenticated user data (works for both SESSION and TOKEN modes)
|
|
402
439
|
* @param {import('@types/express').Request} req Express request object
|
|
440
|
+
* @param {boolean} [includeUserData=false] Whether to include full user data in response
|
|
403
441
|
* @returns {Promise<object>} Full user data object
|
|
404
442
|
* @throws {CustomError} If user is not authenticated
|
|
405
443
|
* @public
|
|
@@ -415,9 +453,9 @@ export class SessionManager {
|
|
|
415
453
|
* }
|
|
416
454
|
* });
|
|
417
455
|
*/
|
|
418
|
-
async getUser(req) {
|
|
419
|
-
if (this
|
|
420
|
-
const { user } = await this.#getUserFromToken(req.headers.authorization,
|
|
456
|
+
async getUser(req, includeUserData = false) {
|
|
457
|
+
if (this.getSessionMode() === SessionMode.TOKEN) {
|
|
458
|
+
const { user } = await this.#getUserFromToken(req.headers.authorization, includeUserData);
|
|
421
459
|
return user;
|
|
422
460
|
}
|
|
423
461
|
// Session mode
|
|
@@ -451,7 +489,7 @@ export class SessionManager {
|
|
|
451
489
|
return next(new CustomError(httpCodes.UNAUTHORIZED, 'Token expired'));
|
|
452
490
|
}
|
|
453
491
|
|
|
454
|
-
return next(error instanceof CustomError ? error :
|
|
492
|
+
return next(error instanceof CustomError ? error :
|
|
455
493
|
new CustomError(httpCodes.UNAUTHORIZED, 'Token verification failed'));
|
|
456
494
|
}
|
|
457
495
|
}
|
|
@@ -484,17 +522,16 @@ export class SessionManager {
|
|
|
484
522
|
* @param {import('@types/express').Request} req Request with Authorization header
|
|
485
523
|
* @param {import('@types/express').Response} res Response object
|
|
486
524
|
* @param {import('@types/express').NextFunction} next Next middleware function
|
|
487
|
-
* @param {(user: object) => object} initUser Function to initialize/transform user object
|
|
525
|
+
* @param {(user: object) => object & { email: string }} initUser Function to initialize/transform user object
|
|
488
526
|
* @param {string} idpRefreshUrl Identity provider refresh endpoint URL
|
|
489
527
|
* @throws {CustomError} If refresh lock is active or SSO refresh fails
|
|
490
528
|
* @private
|
|
491
529
|
* @example
|
|
492
530
|
* // Response format:
|
|
493
|
-
* // { jwt: "new_jwt", user: {...}
|
|
531
|
+
* // { jwt: "new_jwt", user: {...} }
|
|
494
532
|
*/
|
|
495
533
|
async #refreshToken(req, res, next, initUser, idpRefreshUrl) {
|
|
496
534
|
try {
|
|
497
|
-
/** @type {{ tid: string, user: { email: string, attributes: { idp: string, refresh_token: string }? } }} */
|
|
498
535
|
const { tid, user } = await this.#getUserFromToken(req.headers.authorization, true);
|
|
499
536
|
|
|
500
537
|
// Check refresh lock
|
|
@@ -505,12 +542,11 @@ export class SessionManager {
|
|
|
505
542
|
|
|
506
543
|
// Call SSO refresh endpoint
|
|
507
544
|
const response = await this.#idpRequest.post(idpRefreshUrl, {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
}
|
|
545
|
+
idp: user?.attributes?.idp,
|
|
546
|
+
refresh_token: user?.attributes?.refresh_token
|
|
547
|
+
}, {
|
|
548
|
+
headers: {
|
|
549
|
+
Authorization: req.headers.authorization
|
|
514
550
|
}
|
|
515
551
|
});
|
|
516
552
|
|
|
@@ -530,16 +566,12 @@ export class SessionManager {
|
|
|
530
566
|
const newUser = initUser(newPayload.user);
|
|
531
567
|
|
|
532
568
|
// Generate new token
|
|
533
|
-
const newToken = await this.#generateAndStoreToken(newUser);
|
|
534
|
-
|
|
535
|
-
// Remove old token from Redis
|
|
536
|
-
const oldRedisKey = this.#getTokenRedisKey(user.email, tid);
|
|
537
|
-
await this.#redisManager.getClient().del(oldRedisKey);
|
|
569
|
+
const newToken = await this.#generateAndStoreToken(tid, newUser);
|
|
538
570
|
|
|
539
571
|
this.#logger.debug('### TOKEN REFRESHED SUCCESSFULLY ###');
|
|
540
572
|
|
|
541
573
|
// Return new token
|
|
542
|
-
return res.json({ jwt: newToken, user: newUser
|
|
574
|
+
return res.json({ jwt: newToken, user: newUser });
|
|
543
575
|
} catch (error) {
|
|
544
576
|
return next(httpHelper.handleAxiosError(error));
|
|
545
577
|
}
|
|
@@ -557,23 +589,30 @@ export class SessionManager {
|
|
|
557
589
|
async #refreshSession(req, res, next, initUser, idpRefreshUrl) {
|
|
558
590
|
try {
|
|
559
591
|
const { email, attributes } = req.user || { email: '', attributes: {} };
|
|
592
|
+
// Check refresh lock
|
|
560
593
|
if (this.hasLock(email)) {
|
|
561
|
-
throw new CustomError(httpCodes.CONFLICT, '
|
|
594
|
+
throw new CustomError(httpCodes.CONFLICT, 'Session refresh is locked');
|
|
562
595
|
}
|
|
563
596
|
this.lock(email);
|
|
597
|
+
|
|
598
|
+
/** @type {string} */
|
|
599
|
+
const token = await this.#getLightweightToken(email, req.sessionID, req.user.attributes.expires_at);
|
|
600
|
+
|
|
601
|
+
// Call SSO refresh endpoint
|
|
564
602
|
const response = await this.#idpRequest.post(idpRefreshUrl, {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
}
|
|
603
|
+
idp: attributes?.idp,
|
|
604
|
+
refresh_token: attributes?.refresh_token,
|
|
605
|
+
}, {
|
|
606
|
+
headers: {
|
|
607
|
+
Authorization: `Bearer ${token}`
|
|
571
608
|
}
|
|
572
609
|
});
|
|
573
610
|
if (response.status === httpCodes.OK) {
|
|
611
|
+
/** @type {{ jwt: string }} */
|
|
574
612
|
const { jwt } = response.data;
|
|
575
|
-
const payload = await this.#
|
|
576
|
-
|
|
613
|
+
const { payload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
|
|
614
|
+
const result = await this.#saveSession(req, payload, initUser);
|
|
615
|
+
return res.json(result);
|
|
577
616
|
}
|
|
578
617
|
throw new CustomError(response.status, response.statusText);
|
|
579
618
|
} catch (error) {
|
|
@@ -610,7 +649,7 @@ export class SessionManager {
|
|
|
610
649
|
if (isRedirect) {
|
|
611
650
|
return res.redirect(this.#config.SSO_SUCCESS_URL);
|
|
612
651
|
}
|
|
613
|
-
return res.json({
|
|
652
|
+
return res.json({
|
|
614
653
|
message: 'All tokens logged out successfully',
|
|
615
654
|
tokensRemoved: keys.length,
|
|
616
655
|
redirect_url: this.#config.SSO_SUCCESS_URL
|
|
@@ -620,7 +659,7 @@ export class SessionManager {
|
|
|
620
659
|
if (isRedirect) {
|
|
621
660
|
return res.redirect(this.#config.SSO_FAILURE_URL);
|
|
622
661
|
}
|
|
623
|
-
return res.status(httpCodes.SYSTEM_FAILURE).json({
|
|
662
|
+
return res.status(httpCodes.SYSTEM_FAILURE).json({
|
|
624
663
|
error: 'Logout all failed',
|
|
625
664
|
redirect_url: this.#config.SSO_FAILURE_URL
|
|
626
665
|
});
|
|
@@ -643,8 +682,8 @@ export class SessionManager {
|
|
|
643
682
|
|
|
644
683
|
try {
|
|
645
684
|
// Extract Token ID and email from current token
|
|
646
|
-
const
|
|
647
|
-
const { email
|
|
685
|
+
const { tid, user } = await this.#getUserFromToken(req.headers.authorization, false);
|
|
686
|
+
const { email } = user;
|
|
648
687
|
|
|
649
688
|
if (!email || !tid) {
|
|
650
689
|
throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid token payload');
|
|
@@ -659,7 +698,7 @@ export class SessionManager {
|
|
|
659
698
|
if (isRedirect) {
|
|
660
699
|
return res.redirect(this.#config.SSO_SUCCESS_URL);
|
|
661
700
|
}
|
|
662
|
-
return res.json({
|
|
701
|
+
return res.json({
|
|
663
702
|
message: 'Logout successful',
|
|
664
703
|
redirect_url: this.#config.SSO_SUCCESS_URL
|
|
665
704
|
});
|
|
@@ -669,7 +708,7 @@ export class SessionManager {
|
|
|
669
708
|
if (isRedirect) {
|
|
670
709
|
return res.redirect(this.#config.SSO_FAILURE_URL);
|
|
671
710
|
}
|
|
672
|
-
return res.status(httpCodes.SYSTEM_FAILURE).json({
|
|
711
|
+
return res.status(httpCodes.SYSTEM_FAILURE).json({
|
|
673
712
|
error: 'Logout failed',
|
|
674
713
|
redirect_url: this.#config.SSO_FAILURE_URL
|
|
675
714
|
});
|
|
@@ -749,9 +788,9 @@ export class SessionManager {
|
|
|
749
788
|
* @returns {import('@types/express').RequestHandler} Returns express Request Handler
|
|
750
789
|
*/
|
|
751
790
|
requireUser = () => {
|
|
752
|
-
return async (req,
|
|
791
|
+
return async (req, _res, next) => {
|
|
753
792
|
try {
|
|
754
|
-
req.user = await this.getUser(req);
|
|
793
|
+
req.user = await this.getUser(req, true);
|
|
755
794
|
return next();
|
|
756
795
|
}
|
|
757
796
|
catch (error) {
|
|
@@ -764,9 +803,9 @@ export class SessionManager {
|
|
|
764
803
|
* Resource protection based on configured SESSION_MODE
|
|
765
804
|
* - SESSION mode: Verifies user exists in session store and is authorized (checks req.session data)
|
|
766
805
|
* - TOKEN mode: Validates JWT token from Authorization header (lightweight validation)
|
|
767
|
-
*
|
|
806
|
+
*
|
|
768
807
|
* Note: This method verifies authentication only. Use requireUser() after this to populate req.user.
|
|
769
|
-
*
|
|
808
|
+
*
|
|
770
809
|
* @param {string} [errorRedirectUrl=''] Redirect URL on authentication failure
|
|
771
810
|
* @returns {import('@types/express').RequestHandler} Returns express Request Handler
|
|
772
811
|
* @example
|
|
@@ -774,9 +813,9 @@ export class SessionManager {
|
|
|
774
813
|
* app.get('/api/check', session.authenticate(), (req, res) => {
|
|
775
814
|
* res.json({ authenticated: true });
|
|
776
815
|
* });
|
|
777
|
-
*
|
|
816
|
+
*
|
|
778
817
|
* // Option 2: Verify authentication AND load user data into req.user
|
|
779
|
-
* app.get('/api/profile',
|
|
818
|
+
* app.get('/api/profile',
|
|
780
819
|
* session.authenticate(), // Verifies session/token
|
|
781
820
|
* session.requireUser(), // Loads user data into req.user
|
|
782
821
|
* (req, res) => {
|
|
@@ -786,7 +825,7 @@ export class SessionManager {
|
|
|
786
825
|
*/
|
|
787
826
|
authenticate(errorRedirectUrl = '') {
|
|
788
827
|
return async (req, res, next) => {
|
|
789
|
-
const mode = this
|
|
828
|
+
const mode = this.getSessionMode() || SessionMode.SESSION;
|
|
790
829
|
if (mode === SessionMode.TOKEN) {
|
|
791
830
|
return this.#verifyToken(req, res, next, errorRedirectUrl);
|
|
792
831
|
}
|
|
@@ -819,13 +858,11 @@ export class SessionManager {
|
|
|
819
858
|
/**
|
|
820
859
|
* Save session
|
|
821
860
|
* @param {import('@types/express').Request} request Request object
|
|
822
|
-
* @param {
|
|
861
|
+
* @param {import('jose').JWTPayload} payload JWT
|
|
823
862
|
* @param {(user: object) => object} initUser Redirect URL
|
|
824
863
|
* @returns {Promise<{ user: import('../models/types/user').UserModel, redirect_url: string }>} Promise
|
|
825
864
|
*/
|
|
826
|
-
#saveSession = async (request,
|
|
827
|
-
/** @type {{ payload: { user: import('../models/types/user').UserModel, redirect_url: string } }} */
|
|
828
|
-
const { payload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
|
|
865
|
+
#saveSession = async (request, payload, initUser) => {
|
|
829
866
|
if (payload?.user) {
|
|
830
867
|
this.#logger.debug('### CALLBACK USER ###');
|
|
831
868
|
request.session[this.#getSessionKey()] = initUser(payload.user);
|
|
@@ -842,7 +879,7 @@ export class SessionManager {
|
|
|
842
879
|
}
|
|
843
880
|
throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload');
|
|
844
881
|
};
|
|
845
|
-
|
|
882
|
+
|
|
846
883
|
/**
|
|
847
884
|
* Render HTML template for token storage
|
|
848
885
|
* @param {string} token JWT token
|
|
@@ -860,7 +897,7 @@ export class SessionManager {
|
|
|
860
897
|
.replaceAll('{{SSO_SUCCESS_URL}}', sucessRedirectUrl)
|
|
861
898
|
.replaceAll('{{SSO_FAILURE_URL}}', this.#config.SSO_FAILURE_URL);
|
|
862
899
|
}
|
|
863
|
-
|
|
900
|
+
|
|
864
901
|
/**
|
|
865
902
|
* SSO callback for successful login
|
|
866
903
|
* @param {(user: object) => object} initUser Initialize user object function
|
|
@@ -872,30 +909,32 @@ export class SessionManager {
|
|
|
872
909
|
if (!jwt) {
|
|
873
910
|
return next(new CustomError(httpCodes.BAD_REQUEST, 'Missing `jwt` in query parameters'));
|
|
874
911
|
}
|
|
875
|
-
|
|
912
|
+
|
|
876
913
|
try {
|
|
877
914
|
// Decrypt JWT from Identity Adapter
|
|
878
915
|
const { payload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
|
|
879
|
-
|
|
916
|
+
|
|
880
917
|
if (!payload?.user) {
|
|
881
918
|
throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload');
|
|
882
919
|
}
|
|
883
|
-
|
|
884
|
-
/** @type {import('../index.js').SessionUser} */
|
|
885
|
-
const user = initUser(payload.user);
|
|
920
|
+
|
|
886
921
|
/** @type {string} */
|
|
887
922
|
const callbackRedirectUrl = payload.redirect_url || this.#config.SSO_SUCCESS_URL;
|
|
888
|
-
|
|
923
|
+
|
|
889
924
|
// Token mode: Generate token and return HTML page
|
|
890
|
-
if (this
|
|
891
|
-
|
|
925
|
+
if (this.getSessionMode() === SessionMode.TOKEN) {
|
|
926
|
+
/** @type {import('../index.js').SessionUser} */
|
|
927
|
+
const user = initUser(payload.user);
|
|
928
|
+
// Generate unique token ID for this device/session
|
|
929
|
+
const tid = crypto.randomUUID();
|
|
930
|
+
const token = await this.#generateAndStoreToken(tid, user);
|
|
892
931
|
this.#logger.debug('### CALLBACK TOKEN GENERATED ###');
|
|
893
932
|
const html = this.#renderTokenStorageHtml(token, user.attributes.expires_at, callbackRedirectUrl);
|
|
894
933
|
return res.send(html);
|
|
895
934
|
}
|
|
896
935
|
|
|
897
936
|
// Session mode: Save to session and redirect
|
|
898
|
-
await this.#saveSession(req,
|
|
937
|
+
await this.#saveSession(req, payload, initUser);
|
|
899
938
|
return res.redirect(callbackRedirectUrl);
|
|
900
939
|
}
|
|
901
940
|
catch (error) {
|
|
@@ -939,11 +978,12 @@ export class SessionManager {
|
|
|
939
978
|
refresh(initUser) {
|
|
940
979
|
const idpRefreshUrl = '/auth/refresh'.concat('?client_id=').concat(this.#config.SSO_CLIENT_ID);
|
|
941
980
|
return async (req, res, next) => {
|
|
942
|
-
const mode = this
|
|
981
|
+
const mode = this.getSessionMode() || SessionMode.SESSION;
|
|
943
982
|
|
|
944
983
|
if (mode === SessionMode.TOKEN) {
|
|
945
984
|
return this.#refreshToken(req, res, next, initUser, idpRefreshUrl);
|
|
946
|
-
}
|
|
985
|
+
}
|
|
986
|
+
else {
|
|
947
987
|
return this.#refreshSession(req, res, next, initUser, idpRefreshUrl);
|
|
948
988
|
}
|
|
949
989
|
};
|
|
@@ -958,8 +998,8 @@ export class SessionManager {
|
|
|
958
998
|
const { redirect = false, all = false } = req.query;
|
|
959
999
|
const isRedirect = (redirect === 'true' || redirect === true);
|
|
960
1000
|
const logoutAll = (all === 'true' || all === true);
|
|
961
|
-
const mode = this
|
|
962
|
-
|
|
1001
|
+
const mode = this.getSessionMode() || SessionMode.SESSION;
|
|
1002
|
+
|
|
963
1003
|
if (mode === SessionMode.TOKEN) {
|
|
964
1004
|
return this.#logoutToken(req, res, isRedirect, logoutAll);
|
|
965
1005
|
}
|
|
@@ -973,8 +1013,8 @@ export class SessionManager {
|
|
|
973
1013
|
if (isRedirect) {
|
|
974
1014
|
return res.redirect(this.#config.SSO_FAILURE_URL);
|
|
975
1015
|
}
|
|
976
|
-
return res.status(httpCodes.AUTHORIZATION_FAILED).send({
|
|
977
|
-
redirect_url: this.#config.SSO_FAILURE_URL
|
|
1016
|
+
return res.status(httpCodes.AUTHORIZATION_FAILED).send({
|
|
1017
|
+
redirect_url: this.#config.SSO_FAILURE_URL
|
|
978
1018
|
});
|
|
979
1019
|
}
|
|
980
1020
|
if (isRedirect) {
|
|
@@ -985,4 +1025,11 @@ export class SessionManager {
|
|
|
985
1025
|
};
|
|
986
1026
|
}
|
|
987
1027
|
|
|
1028
|
+
/**
|
|
1029
|
+
* Get session mode
|
|
1030
|
+
* @returns {string} Session mode
|
|
1031
|
+
*/
|
|
1032
|
+
getSessionMode() {
|
|
1033
|
+
return this.#config.SESSION_MODE;
|
|
1034
|
+
}
|
|
988
1035
|
}
|
package/index.d.ts
CHANGED
|
@@ -316,6 +316,7 @@ export class SessionManager {
|
|
|
316
316
|
/**
|
|
317
317
|
* Get authenticated user data (works for both SESSION and TOKEN modes)
|
|
318
318
|
* @param req Express request object
|
|
319
|
+
* @param includeUserData Include user data in the response (default: false)
|
|
319
320
|
* @returns Promise resolving to full user data object
|
|
320
321
|
* @throws CustomError If user is not authenticated
|
|
321
322
|
* @example
|
|
@@ -323,7 +324,7 @@ export class SessionManager {
|
|
|
323
324
|
* // Use in custom middleware
|
|
324
325
|
* app.use(async (req, res, next) => {
|
|
325
326
|
* try {
|
|
326
|
-
* const user = await sessionManager.getUser(req);
|
|
327
|
+
* const user = await sessionManager.getUser(req, true);
|
|
327
328
|
* req.customUser = user;
|
|
328
329
|
* next();
|
|
329
330
|
* } catch (error) {
|
|
@@ -332,7 +333,7 @@ export class SessionManager {
|
|
|
332
333
|
* });
|
|
333
334
|
* ```
|
|
334
335
|
*/
|
|
335
|
-
getUser(req: Request): Promise<SessionUser>;
|
|
336
|
+
getUser(req: Request, includeUserData: boolean?): Promise<SessionUser>;
|
|
336
337
|
|
|
337
338
|
/**
|
|
338
339
|
* Initialize the session configurations and middleware
|
|
@@ -439,6 +440,12 @@ export class SessionManager {
|
|
|
439
440
|
* @returns Returns express Request Handler
|
|
440
441
|
*/
|
|
441
442
|
logout(): RequestHandler;
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Get the current session mode
|
|
446
|
+
* @returns Returns 'session' or 'token' based on configuration
|
|
447
|
+
*/
|
|
448
|
+
getSessionMode(): string;
|
|
442
449
|
}
|
|
443
450
|
|
|
444
451
|
// Custom Error class
|
package/index.js
CHANGED
|
@@ -3,4 +3,4 @@ export { httpCodes, httpMessages, httpErrorHandler, httpNotFoundHandler, CustomE
|
|
|
3
3
|
export { RedisManager } from './components/redis.js';
|
|
4
4
|
export { FlexRouter } from './components/router.js';
|
|
5
5
|
export { JwtManager } from './components/jwt.js';
|
|
6
|
-
export { Logger } from './components/logger.js';
|
|
6
|
+
export { Logger } from './components/logger.js';
|
package/package.json
CHANGED
|
@@ -1,32 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@igxjs/node-components",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
4
4
|
"description": "Node components for igxjs",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "mocha tests/**/*.test.js --timeout 5000"
|
|
8
|
+
"test": "mocha tests/**/*.test.js --timeout 5000",
|
|
9
|
+
"lint": "eslint .",
|
|
10
|
+
"lint:fix": "eslint . --fix"
|
|
9
11
|
},
|
|
10
12
|
"repository": {
|
|
11
13
|
"type": "git",
|
|
12
14
|
"url": "git+https://github.com/igxjs/node-components.git"
|
|
13
15
|
},
|
|
14
16
|
"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"
|
|
17
|
+
"igxjs"
|
|
30
18
|
],
|
|
31
19
|
"author": "Michael",
|
|
32
20
|
"license": "Apache-2.0",
|
|
@@ -43,11 +31,14 @@
|
|
|
43
31
|
"axios": "^1.13.6",
|
|
44
32
|
"connect-redis": "^9.0.0",
|
|
45
33
|
"express-session": "^1.19.0",
|
|
46
|
-
"jose": "^6.2.
|
|
34
|
+
"jose": "^6.2.2",
|
|
47
35
|
"memorystore": "^1.6.7"
|
|
48
36
|
},
|
|
49
37
|
"devDependencies": {
|
|
38
|
+
"@eslint/js": "^10.0.1",
|
|
50
39
|
"chai": "^6.2.2",
|
|
40
|
+
"eslint": "^10.1.0",
|
|
41
|
+
"globals": "^17.4.0",
|
|
51
42
|
"mocha": "^12.0.0-beta-10",
|
|
52
43
|
"sinon": "^21.0.3",
|
|
53
44
|
"supertest": "^7.0.0"
|
|
@@ -68,7 +59,8 @@
|
|
|
68
59
|
"README.md"
|
|
69
60
|
],
|
|
70
61
|
"publishConfig": {
|
|
71
|
-
"access": "public"
|
|
62
|
+
"access": "public",
|
|
63
|
+
"registry": "https://npm.pkg.github.com"
|
|
72
64
|
},
|
|
73
65
|
"types": "./index.d.ts"
|
|
74
66
|
}
|