@igxjs/node-components 1.0.10 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -4
- package/components/session.js +538 -86
- package/index.d.ts +52 -6
- package/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ npm install @igxjs/node-components
|
|
|
12
12
|
|
|
13
13
|
| Component | Description | Documentation |
|
|
14
14
|
|-----------|-------------|---------------|
|
|
15
|
-
| **SessionManager** | SSO session management with Redis/memory storage | [View docs](./docs/session-manager.md) |
|
|
15
|
+
| **SessionManager** | SSO session management with Redis/memory storage, supporting both session and token-based authentication | [View docs](./docs/session-manager.md) |
|
|
16
16
|
| **FlexRouter** | Flexible routing with context paths and middleware | [View docs](./docs/flex-router.md) |
|
|
17
17
|
| **RedisManager** | Redis connection management with TLS support | [View docs](./docs/redis-manager.md) |
|
|
18
18
|
| **JWT Manager** | Secure JWT encryption/decryption with JWE | [View docs](./docs/jwt-manager.md) |
|
|
@@ -23,9 +23,9 @@ npm install @igxjs/node-components
|
|
|
23
23
|
### SessionManager
|
|
24
24
|
|
|
25
25
|
```javascript
|
|
26
|
-
import { SessionManager } from '@igxjs/node-components';
|
|
26
|
+
import { SessionManager, SessionMode } from '@igxjs/node-components';
|
|
27
27
|
|
|
28
|
-
// Create singleton instance
|
|
28
|
+
// Create singleton instance with SESSION authentication (default)
|
|
29
29
|
export const session = new SessionManager({
|
|
30
30
|
SSO_ENDPOINT_URL: process.env.SSO_ENDPOINT_URL,
|
|
31
31
|
SSO_CLIENT_ID: process.env.SSO_CLIENT_ID,
|
|
@@ -34,6 +34,16 @@ export const session = new SessionManager({
|
|
|
34
34
|
REDIS_URL: process.env.REDIS_URL
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
// Create singleton instance with TOKEN authentication
|
|
38
|
+
export const tokenSession = new SessionManager({
|
|
39
|
+
SESSION_MODE: SessionMode.TOKEN, // Use token-based authentication
|
|
40
|
+
SSO_ENDPOINT_URL: process.env.SSO_ENDPOINT_URL,
|
|
41
|
+
SSO_CLIENT_ID: process.env.SSO_CLIENT_ID,
|
|
42
|
+
SSO_CLIENT_SECRET: process.env.SSO_CLIENT_SECRET,
|
|
43
|
+
SESSION_SECRET: process.env.SESSION_SECRET,
|
|
44
|
+
REDIS_URL: process.env.REDIS_URL,
|
|
45
|
+
});
|
|
46
|
+
|
|
37
47
|
// Setup in your app
|
|
38
48
|
await session.setup(app, (user) => ({ ...user, displayName: user.email }));
|
|
39
49
|
|
|
@@ -105,9 +115,93 @@ app.use(httpErrorHandler);
|
|
|
105
115
|
|
|
106
116
|
[📖 Full HTTP Handlers Documentation](./docs/http-handlers.md)
|
|
107
117
|
|
|
118
|
+
## SessionManager Authentication Modes
|
|
119
|
+
|
|
120
|
+
The `SessionManager` supports two authentication modes:
|
|
121
|
+
|
|
122
|
+
### SESSION Mode (Default)
|
|
123
|
+
|
|
124
|
+
Uses traditional server-side session cookies. When a user authenticates via SSO, their session is stored in Redis or memory storage. The client sends the session cookie with each request to prove authentication.
|
|
125
|
+
|
|
126
|
+
**Configuration:**
|
|
127
|
+
- `SESSION_MODE`: `SessionMode.SESSION` (default) - Uses session-based authentication
|
|
128
|
+
- `SESSION_AGE`: Session timeout in milliseconds (default: 64800000)
|
|
129
|
+
- `REDIS_URL`: Redis connection string for session storage
|
|
130
|
+
|
|
131
|
+
**Auth Methods:**
|
|
132
|
+
- `session.authenticate()` - Protect routes with SSO session verification
|
|
133
|
+
- `session.verifySession(isDebugging, redirectUrl)` - Explicit session verification method
|
|
134
|
+
- `session.logout(redirect?, all?)` - Logout current session (or logout all for token mode)
|
|
135
|
+
|
|
136
|
+
### TOKEN Mode
|
|
137
|
+
|
|
138
|
+
Uses JWT bearer tokens instead of session cookies. When a user authenticates via SSO, a JWT token is generated and stored in Redis. The client includes the token in the Authorization header (`Bearer {token}`) with each request.
|
|
139
|
+
|
|
140
|
+
**Configuration:**
|
|
141
|
+
- `SESSION_MODE`: `SessionMode.TOKEN` - Uses token-based authentication
|
|
142
|
+
- `SSO_SUCCESS_URL`: Redirect URL after successful SSO login
|
|
143
|
+
- `SSO_FAILURE_URL`: Redirect URL after failed SSO login
|
|
144
|
+
- `JWT_ALGORITHM`: JWT algorithm (default: `'dir'`)
|
|
145
|
+
- `JWT_ENCRYPTION`: Encryption algorithm (default: `'A256GCM'`)
|
|
146
|
+
- `JWT_EXPIRATION_TIME`: Token expiration time (default: `'10m'`)
|
|
147
|
+
- `JWT_CLOCK_TOLERANCE`: Clock skew tolerance in seconds (default: 30)
|
|
148
|
+
|
|
149
|
+
**Auth Methods:**
|
|
150
|
+
- `session.verifyToken(isDebugging, redirectUrl)` - Protect routes with token verification
|
|
151
|
+
- `session.callback(initUser)` - SSO callback handler for token generation
|
|
152
|
+
- `session.refresh(initUser)` - Refresh user authentication based on auth mode
|
|
153
|
+
- `session.logout(redirect?, all?)` - Logout current or all tokens
|
|
154
|
+
|
|
155
|
+
**Token Storage (Client-Side):**
|
|
156
|
+
|
|
157
|
+
When using token-based authentication, the client-side HTML page stores the token in `localStorage`:
|
|
158
|
+
|
|
159
|
+
```html
|
|
160
|
+
<script>
|
|
161
|
+
// Store auth data in localStorage
|
|
162
|
+
localStorage.setItem('authToken', ${JSON.stringify(token)});
|
|
163
|
+
localStorage.setItem('tokenExpiry', ${Date.now() + sessionAge});
|
|
164
|
+
localStorage.setItem('user', ${JSON.stringify({
|
|
165
|
+
email: user.email,
|
|
166
|
+
name: user.name,
|
|
167
|
+
})});
|
|
168
|
+
|
|
169
|
+
// Redirect to original destination
|
|
170
|
+
window.location.replace(redirectUrl);
|
|
171
|
+
</script>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## SessionManager Configuration Options
|
|
175
|
+
|
|
176
|
+
| Option | Type | Default | Description |
|
|
177
|
+
|--------|------|---------|-------------|
|
|
178
|
+
| `SSO_ENDPOINT_URL` | string | - | Identity provider endpoint URL |
|
|
179
|
+
| `SSO_CLIENT_ID` | string | - | SSO client ID |
|
|
180
|
+
| `SSO_CLIENT_SECRET` | string | - | SSO client secret |
|
|
181
|
+
| `SSO_SUCCESS_URL` | string | - | Redirect URL after successful login (token mode) |
|
|
182
|
+
| `SSO_FAILURE_URL` | string | - | Redirect URL after failed login (token mode) |
|
|
183
|
+
| `SESSION_MODE` | string | `SessionMode.SESSION` | Authentication mode: `SessionMode.SESSION` or `SessionMode.TOKEN` |
|
|
184
|
+
| `SESSION_AGE` | number | 64800000 | Session timeout in milliseconds |
|
|
185
|
+
| `SESSION_COOKIE_PATH` | string | `'/'` | Session cookie path |
|
|
186
|
+
| `SESSION_SECRET` | string | - | Session/JWT secret key |
|
|
187
|
+
| `SESSION_PREFIX` | string | `'ibmid:'` | Redis session/key prefix |
|
|
188
|
+
| `REDIS_URL` | string | - | Redis connection URL (optional) |
|
|
189
|
+
| `REDIS_CERT_PATH` | string | - | Path to Redis TLS certificate |
|
|
190
|
+
| `JWT_ALGORITHM` | string | `'dir'` | JWT signing algorithm |
|
|
191
|
+
| `JWT_ENCRYPTION` | string | `'A256GCM'` | JWE encryption algorithm |
|
|
192
|
+
| `JWT_EXPIRATION_TIME` | string | `'10m'` | Token expiration duration |
|
|
193
|
+
| `JWT_CLOCK_TOLERANCE` | number | 30 | Clock skew tolerance in seconds |
|
|
194
|
+
| `JWT_SECRET_HASH_ALGORITHM` | string | `'SHA-256'` | Algorithm for hashing secrets |
|
|
195
|
+
| `JWT_ISSUER` | string | - | JWT issuer identifier |
|
|
196
|
+
| `JWT_AUDIENCE` | string | - | JWT audience identifier |
|
|
197
|
+
| `JWT_SUBJECT` | string | - | JWT subject identifier |
|
|
198
|
+
|
|
108
199
|
## Features
|
|
109
200
|
|
|
110
201
|
- ✅ **SSO Integration** - Full SSO support with Redis or memory storage
|
|
202
|
+
- ✅ **Dual Authentication Modes** - SESSION (cookies) or TOKEN (Bearer tokens)
|
|
203
|
+
- ✅ **Token Refresh** - Automatic token refresh via SSO endpoints
|
|
204
|
+
- ✅ **Session Refresh Locks** - Prevent concurrent token/session refresh attacks
|
|
111
205
|
- ✅ **JWT Security** - Encrypted JWT tokens using JWE (jose library)
|
|
112
206
|
- ✅ **Flexible Routing** - Easy mounting with context paths and middleware
|
|
113
207
|
- ✅ **Redis Support** - TLS/SSL and automatic reconnection
|
|
@@ -148,4 +242,4 @@ import type {
|
|
|
148
242
|
|
|
149
243
|
## License
|
|
150
244
|
|
|
151
|
-
[Apache 2.0](LICENSE)
|
|
245
|
+
[Apache 2.0](LICENSE)
|
package/components/session.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
1
2
|
import axios from 'axios';
|
|
2
3
|
import session from 'express-session';
|
|
3
4
|
import memStore from 'memorystore';
|
|
@@ -7,11 +8,25 @@ import { CustomError, httpCodes, httpHelper, httpMessages } from './http-handler
|
|
|
7
8
|
import { JwtManager } from './jwt.js';
|
|
8
9
|
import { RedisManager } from './redis.js';
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Session authentication mode constants
|
|
13
|
+
*/
|
|
14
|
+
export const SessionMode = {
|
|
15
|
+
SESSION: 'session',
|
|
16
|
+
TOKEN: 'token'
|
|
17
|
+
};
|
|
18
|
+
|
|
10
19
|
/**
|
|
11
20
|
* Session configuration options
|
|
12
|
-
* Uses strict UPPERCASE naming convention for all property names
|
|
13
21
|
*/
|
|
14
22
|
export class SessionConfig {
|
|
23
|
+
/**
|
|
24
|
+
* @type {string}
|
|
25
|
+
* Authentication mode for protected routes
|
|
26
|
+
* Supported values: SessionMode.SESSION | SessionMode.TOKEN
|
|
27
|
+
* @default SessionMode.SESSION
|
|
28
|
+
*/
|
|
29
|
+
SESSION_MODE;
|
|
15
30
|
/** @type {string} */
|
|
16
31
|
SSO_ENDPOINT_URL;
|
|
17
32
|
/** @type {string} */
|
|
@@ -73,6 +88,8 @@ export class SessionManager {
|
|
|
73
88
|
*/
|
|
74
89
|
constructor(config) {
|
|
75
90
|
this.#config = {
|
|
91
|
+
// Session Mode
|
|
92
|
+
SESSION_MODE: config.SESSION_MODE || SessionMode.SESSION,
|
|
76
93
|
// Session
|
|
77
94
|
SESSION_AGE: config.SESSION_AGE || 64800000,
|
|
78
95
|
SESSION_COOKIE_PATH: config.SESSION_COOKIE_PATH || '/',
|
|
@@ -141,6 +158,27 @@ export class SessionManager {
|
|
|
141
158
|
return 'user';
|
|
142
159
|
}
|
|
143
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Get Redis key for token storage
|
|
163
|
+
* @param {string} email User email
|
|
164
|
+
* @param {string} tokenId Token ID
|
|
165
|
+
* @returns {string} Returns the Redis key for token storage
|
|
166
|
+
* @private
|
|
167
|
+
*/
|
|
168
|
+
#getTokenRedisKey(email, tokenId) {
|
|
169
|
+
return `${this.#config.SESSION_PREFIX}token:${email}:${tokenId}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get Redis key pattern for all user tokens
|
|
174
|
+
* @param {string} email User email
|
|
175
|
+
* @returns {string} Returns the Redis key pattern for all user tokens
|
|
176
|
+
* @private
|
|
177
|
+
*/
|
|
178
|
+
#getTokenRedisPattern(email) {
|
|
179
|
+
return `${this.#config.SESSION_PREFIX}token:${email}:*`;
|
|
180
|
+
}
|
|
181
|
+
|
|
144
182
|
/**
|
|
145
183
|
* Get RedisManager instance
|
|
146
184
|
* @returns {import('./redis.js').RedisManager} Returns the RedisManager instance
|
|
@@ -149,6 +187,368 @@ export class SessionManager {
|
|
|
149
187
|
return this.#redisManager;
|
|
150
188
|
}
|
|
151
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Generate and store JWT token in Redis
|
|
192
|
+
* @param {object} user User object
|
|
193
|
+
* @returns {Promise<string>} Returns the generated JWT token
|
|
194
|
+
* @private
|
|
195
|
+
*/
|
|
196
|
+
async #generateAndStoreToken(user) {
|
|
197
|
+
// Generate unique token ID for this device/session
|
|
198
|
+
const tokenId = crypto.randomUUID();
|
|
199
|
+
|
|
200
|
+
// Create JWT token with email and tokenId
|
|
201
|
+
const token = await this.#jwtManager.encrypt(
|
|
202
|
+
{
|
|
203
|
+
email: user.email,
|
|
204
|
+
tokenId
|
|
205
|
+
},
|
|
206
|
+
this.#config.SESSION_SECRET,
|
|
207
|
+
{ expirationTime: this.#config.JWT_EXPIRATION_TIME }
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// Store user data in Redis with TTL
|
|
211
|
+
const redisKey = this.#getTokenRedisKey(user.email, tokenId);
|
|
212
|
+
const ttlSeconds = Math.floor(this.#config.SESSION_AGE / 1000);
|
|
213
|
+
|
|
214
|
+
await this.#redisManager.getClient().setEx(
|
|
215
|
+
redisKey,
|
|
216
|
+
ttlSeconds,
|
|
217
|
+
JSON.stringify(user)
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
console.debug(`### TOKEN GENERATED: ${user.email} ###`);
|
|
221
|
+
|
|
222
|
+
return token;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Verify token authentication
|
|
227
|
+
* @param {import('@types/express').Request} req Request
|
|
228
|
+
* @param {import('@types/express').Response} res Response
|
|
229
|
+
* @param {import('@types/express').NextFunction} next Next function
|
|
230
|
+
* @param {boolean} isDebugging Debugging flag
|
|
231
|
+
* @param {string} redirectUrl Redirect URL
|
|
232
|
+
* @private
|
|
233
|
+
*/
|
|
234
|
+
async #verifyToken(req, res, next, isDebugging, redirectUrl) {
|
|
235
|
+
try {
|
|
236
|
+
// Extract token from Authorization header
|
|
237
|
+
const authHeader = req.headers.authorization;
|
|
238
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
239
|
+
throw new CustomError(httpCodes.UNAUTHORIZED, 'Missing or invalid authorization header');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const token = authHeader.substring(7); // Remove 'Bearer ' prefix
|
|
243
|
+
|
|
244
|
+
// Decrypt JWT token
|
|
245
|
+
const { payload } = await this.#jwtManager.decrypt(
|
|
246
|
+
token,
|
|
247
|
+
this.#config.SESSION_SECRET
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// Extract email and tokenId
|
|
251
|
+
const { email, tokenId } = payload;
|
|
252
|
+
if (!email || !tokenId) {
|
|
253
|
+
throw new CustomError(httpCodes.UNAUTHORIZED, 'Invalid token payload');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Lookup user in Redis
|
|
257
|
+
const redisKey = this.#getTokenRedisKey(email, tokenId);
|
|
258
|
+
const userData = await this.#redisManager.getClient().get(redisKey);
|
|
259
|
+
|
|
260
|
+
if (!userData) {
|
|
261
|
+
throw new CustomError(httpCodes.UNAUTHORIZED, 'Token not found or expired');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Parse and attach user to request
|
|
265
|
+
req.user = JSON.parse(userData);
|
|
266
|
+
res.locals.user = req.user;
|
|
267
|
+
|
|
268
|
+
// Validate authorization
|
|
269
|
+
const { authorized = isDebugging } = req.user ?? { authorized: isDebugging };
|
|
270
|
+
if (!authorized && !isDebugging) {
|
|
271
|
+
throw new CustomError(httpCodes.FORBIDDEN, 'User is not authorized');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return next();
|
|
275
|
+
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (isDebugging) {
|
|
278
|
+
console.warn('### TOKEN VERIFICATION FAILED (debugging mode) ###', error.message);
|
|
279
|
+
return next();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (redirectUrl) {
|
|
283
|
+
return res.redirect(redirectUrl);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Handle specific JWT errors
|
|
287
|
+
if (error.code === 'ERR_JWT_EXPIRED') {
|
|
288
|
+
return next(new CustomError(httpCodes.UNAUTHORIZED, 'Token expired'));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return next(error instanceof CustomError ? error :
|
|
292
|
+
new CustomError(httpCodes.UNAUTHORIZED, 'Token verification failed'));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Verify session authentication
|
|
298
|
+
* @param {import('@types/express').Request} req Request
|
|
299
|
+
* @param {import('@types/express').Response} res Response
|
|
300
|
+
* @param {import('@types/express').NextFunction} next Next function
|
|
301
|
+
* @param {boolean} isDebugging Debugging flag
|
|
302
|
+
* @param {string} redirectUrl Redirect URL
|
|
303
|
+
* @private
|
|
304
|
+
*/
|
|
305
|
+
async #verifySession(req, res, next, isDebugging, redirectUrl) {
|
|
306
|
+
const { authorized = isDebugging } = req.user ?? { authorized: isDebugging };
|
|
307
|
+
if (authorized) {
|
|
308
|
+
return next();
|
|
309
|
+
}
|
|
310
|
+
if (redirectUrl) {
|
|
311
|
+
return res.redirect(redirectUrl);
|
|
312
|
+
}
|
|
313
|
+
return next(new CustomError(httpCodes.UNAUTHORIZED, httpMessages.UNAUTHORIZED));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Refresh token authentication
|
|
318
|
+
* @param {import('@types/express').Request} req Request
|
|
319
|
+
* @param {import('@types/express').Response} res Response
|
|
320
|
+
* @param {import('@types/express').NextFunction} next Next function
|
|
321
|
+
* @param {(user: object) => object} initUser Initialize user function
|
|
322
|
+
* @param {string} idpUrl Identity provider URL
|
|
323
|
+
* @private
|
|
324
|
+
*/
|
|
325
|
+
async #refreshToken(req, res, next, initUser, idpUrl) {
|
|
326
|
+
try {
|
|
327
|
+
// Get current user from verifyToken middleware
|
|
328
|
+
const { email, attributes } = req.user || {};
|
|
329
|
+
|
|
330
|
+
if (!email) {
|
|
331
|
+
throw new CustomError(httpCodes.UNAUTHORIZED, 'User not authenticated');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Extract tokenId from current token
|
|
335
|
+
const authHeader = req.headers.authorization;
|
|
336
|
+
const token = authHeader?.substring(7);
|
|
337
|
+
const { payload } = await this.#jwtManager.decrypt(token, this.#config.SESSION_SECRET);
|
|
338
|
+
const oldTokenId = payload.tokenId;
|
|
339
|
+
|
|
340
|
+
// Check refresh lock
|
|
341
|
+
if (this.hasLock(email)) {
|
|
342
|
+
throw new CustomError(httpCodes.CONFLICT, 'Token refresh is locked');
|
|
343
|
+
}
|
|
344
|
+
this.lock(email);
|
|
345
|
+
|
|
346
|
+
// Call SSO refresh endpoint
|
|
347
|
+
const response = await this.#idpRequest.post(idpUrl, {
|
|
348
|
+
user: {
|
|
349
|
+
email,
|
|
350
|
+
attributes: {
|
|
351
|
+
idp: attributes?.idp,
|
|
352
|
+
refresh_token: attributes?.refresh_token
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
if (response.status !== httpCodes.OK) {
|
|
358
|
+
throw new CustomError(response.status, response.statusText);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Decrypt new user data from SSO
|
|
362
|
+
const { jwt } = response.data;
|
|
363
|
+
const { payload: newPayload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
|
|
364
|
+
|
|
365
|
+
if (!newPayload?.user) {
|
|
366
|
+
throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload from SSO');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Initialize user with new data
|
|
370
|
+
const user = initUser(newPayload.user);
|
|
371
|
+
|
|
372
|
+
// Generate new token
|
|
373
|
+
const newToken = await this.#generateAndStoreToken(user);
|
|
374
|
+
|
|
375
|
+
// Remove old token from Redis
|
|
376
|
+
const oldRedisKey = this.#getTokenRedisKey(email, oldTokenId);
|
|
377
|
+
await this.#redisManager.getClient().del(oldRedisKey);
|
|
378
|
+
|
|
379
|
+
console.debug('### TOKEN REFRESHED SUCCESSFULLY ###');
|
|
380
|
+
|
|
381
|
+
// Return new token
|
|
382
|
+
return res.json({
|
|
383
|
+
token: newToken,
|
|
384
|
+
user,
|
|
385
|
+
expiresIn: Math.floor(this.#config.SESSION_AGE / 1000),
|
|
386
|
+
tokenType: 'Bearer'
|
|
387
|
+
});
|
|
388
|
+
} catch (error) {
|
|
389
|
+
return next(httpHelper.handleAxiosError(error));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Refresh session authentication
|
|
395
|
+
* @param {import('@types/express').Request} req Request
|
|
396
|
+
* @param {import('@types/express').Response} res Response
|
|
397
|
+
* @param {import('@types/express').NextFunction} next Next function
|
|
398
|
+
* @param {(user: object) => object} initUser Initialize user function
|
|
399
|
+
* @param {string} idpUrl Identity provider URL
|
|
400
|
+
* @private
|
|
401
|
+
*/
|
|
402
|
+
async #refreshSession(req, res, next, initUser, idpUrl) {
|
|
403
|
+
try {
|
|
404
|
+
const { email, attributes } = req.user || { email: '', attributes: {} };
|
|
405
|
+
if (this.hasLock(email)) {
|
|
406
|
+
throw new CustomError(httpCodes.CONFLICT, 'User refresh is locked');
|
|
407
|
+
}
|
|
408
|
+
this.lock(email);
|
|
409
|
+
const response = await this.#idpRequest.post(idpUrl, {
|
|
410
|
+
user: {
|
|
411
|
+
email,
|
|
412
|
+
attributes: {
|
|
413
|
+
idp: attributes?.idp,
|
|
414
|
+
refresh_token: attributes?.refresh_token
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
if (response.status === httpCodes.OK) {
|
|
419
|
+
const { jwt } = response.data;
|
|
420
|
+
const payload = await this.#saveSession(req, jwt, initUser);
|
|
421
|
+
return res.json(payload);
|
|
422
|
+
}
|
|
423
|
+
throw new CustomError(response.status, response.statusText);
|
|
424
|
+
} catch (error) {
|
|
425
|
+
return next(httpHelper.handleAxiosError(error));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Logout all tokens for a user
|
|
431
|
+
* @param {import('@types/express').Request} req Request
|
|
432
|
+
* @param {import('@types/express').Response} res Response
|
|
433
|
+
* @param {boolean} isRedirect Whether to redirect
|
|
434
|
+
* @private
|
|
435
|
+
*/
|
|
436
|
+
async #logoutAllTokens(req, res, isRedirect) {
|
|
437
|
+
try {
|
|
438
|
+
const { email } = req.user || {};
|
|
439
|
+
|
|
440
|
+
if (!email) {
|
|
441
|
+
throw new CustomError(httpCodes.UNAUTHORIZED, 'User not authenticated');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Find all tokens for this user
|
|
445
|
+
const pattern = this.#getTokenRedisPattern(email);
|
|
446
|
+
const keys = await this.#redisManager.getClient().keys(pattern);
|
|
447
|
+
|
|
448
|
+
// Delete all tokens
|
|
449
|
+
if (keys.length > 0) {
|
|
450
|
+
await this.#redisManager.getClient().del(keys);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
console.info(`### ALL TOKENS LOGGED OUT: ${email} (${keys.length} tokens) ###`);
|
|
454
|
+
|
|
455
|
+
if (isRedirect) {
|
|
456
|
+
return res.redirect(this.#config.SSO_SUCCESS_URL);
|
|
457
|
+
}
|
|
458
|
+
return res.json({
|
|
459
|
+
message: 'All tokens logged out successfully',
|
|
460
|
+
tokensRemoved: keys.length,
|
|
461
|
+
redirect_url: this.#config.SSO_SUCCESS_URL
|
|
462
|
+
});
|
|
463
|
+
} catch (error) {
|
|
464
|
+
console.error('### LOGOUT ALL TOKENS ERROR ###', error);
|
|
465
|
+
if (isRedirect) {
|
|
466
|
+
return res.redirect(this.#config.SSO_FAILURE_URL);
|
|
467
|
+
}
|
|
468
|
+
return res.status(httpCodes.SYSTEM_FAILURE).json({
|
|
469
|
+
error: 'Logout all failed',
|
|
470
|
+
redirect_url: this.#config.SSO_FAILURE_URL
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Logout token authentication
|
|
477
|
+
* @param {import('@types/express').Request} req Request
|
|
478
|
+
* @param {import('@types/express').Response} res Response
|
|
479
|
+
* @param {boolean} isRedirect Whether to redirect
|
|
480
|
+
* @param {boolean} logoutAll Whether to logout all tokens
|
|
481
|
+
* @private
|
|
482
|
+
*/
|
|
483
|
+
async #logoutToken(req, res, isRedirect, logoutAll = false) {
|
|
484
|
+
// If logoutAll is true, delegate to the all tokens logout method
|
|
485
|
+
if (logoutAll) {
|
|
486
|
+
return this.#logoutAllTokens(req, res, isRedirect);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
// Extract tokenId from current token
|
|
491
|
+
const authHeader = req.headers.authorization;
|
|
492
|
+
const token = authHeader?.substring(7);
|
|
493
|
+
|
|
494
|
+
if (!token) {
|
|
495
|
+
throw new CustomError(httpCodes.BAD_REQUEST, 'No token provided');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const { payload } = await this.#jwtManager.decrypt(token, this.#config.SESSION_SECRET);
|
|
499
|
+
const { email, tokenId } = payload;
|
|
500
|
+
|
|
501
|
+
// Remove token from Redis
|
|
502
|
+
const redisKey = this.#getTokenRedisKey(email, tokenId);
|
|
503
|
+
await this.#redisManager.getClient().del(redisKey);
|
|
504
|
+
|
|
505
|
+
console.info('### TOKEN LOGOUT SUCCESSFULLY ###');
|
|
506
|
+
|
|
507
|
+
if (isRedirect) {
|
|
508
|
+
return res.redirect(this.#config.SSO_SUCCESS_URL);
|
|
509
|
+
}
|
|
510
|
+
return res.json({
|
|
511
|
+
message: 'Logout successful',
|
|
512
|
+
redirect_url: this.#config.SSO_SUCCESS_URL
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
} catch (error) {
|
|
516
|
+
console.error('### TOKEN LOGOUT ERROR ###', error);
|
|
517
|
+
if (isRedirect) {
|
|
518
|
+
return res.redirect(this.#config.SSO_FAILURE_URL);
|
|
519
|
+
}
|
|
520
|
+
return res.status(httpCodes.SYSTEM_FAILURE).json({
|
|
521
|
+
error: 'Logout failed',
|
|
522
|
+
redirect_url: this.#config.SSO_FAILURE_URL
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Logout session authentication
|
|
529
|
+
* @param {import('@types/express').Request} req Request
|
|
530
|
+
* @param {import('@types/express').Response} res Response
|
|
531
|
+
* @param {Function} callback Callback function
|
|
532
|
+
* @private
|
|
533
|
+
*/
|
|
534
|
+
#logoutSession(req, res, callback) {
|
|
535
|
+
try {
|
|
536
|
+
res.clearCookie('connect.sid');
|
|
537
|
+
} catch (error) {
|
|
538
|
+
console.error('### CLEAR COOKIE ERROR ###');
|
|
539
|
+
console.error(error);
|
|
540
|
+
}
|
|
541
|
+
return req.session.destroy((sessionError) => {
|
|
542
|
+
if (sessionError) {
|
|
543
|
+
console.error('### SESSION DESTROY CALLBACK ERROR ###');
|
|
544
|
+
console.error(sessionError);
|
|
545
|
+
return callback(sessionError);
|
|
546
|
+
}
|
|
547
|
+
console.info('### SESSION LOGOUT SUCCESSFULLY ###');
|
|
548
|
+
return callback(null);
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
152
552
|
/**
|
|
153
553
|
* Setup the session/user handlers with configurations
|
|
154
554
|
* @param {import('@types/express').Application} app Express application
|
|
@@ -225,24 +625,44 @@ export class SessionManager {
|
|
|
225
625
|
}
|
|
226
626
|
|
|
227
627
|
/**
|
|
228
|
-
* Resource protection
|
|
628
|
+
* Resource protection based on configured SESSION_MODE
|
|
229
629
|
* @param {boolean} [isDebugging=false] Debugging flag
|
|
230
|
-
* @param {
|
|
630
|
+
* @param {string} [redirectUrl=''] Redirect URL
|
|
231
631
|
* @returns {import('@types/express').RequestHandler} Returns express Request Handler
|
|
232
632
|
*/
|
|
233
|
-
authenticate
|
|
633
|
+
authenticate(isDebugging = false, redirectUrl = '') {
|
|
234
634
|
return async (req, res, next) => {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return next();
|
|
635
|
+
const mode = this.#config.SESSION_MODE || SessionMode.SESSION;
|
|
636
|
+
if (mode === SessionMode.TOKEN) {
|
|
637
|
+
return this.#verifyToken(req, res, next, isDebugging, redirectUrl);
|
|
239
638
|
}
|
|
240
|
-
|
|
241
|
-
return res.redirect(redirectUrl);
|
|
242
|
-
}
|
|
243
|
-
return next(new CustomError(httpCodes.UNAUTHORIZED, httpMessages.UNAUTHORIZED));
|
|
639
|
+
return this.#verifySession(req, res, next, isDebugging, redirectUrl);
|
|
244
640
|
};
|
|
245
|
-
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Resource protection by token (explicit token verification)
|
|
645
|
+
* @param {boolean} [isDebugging=false] Debugging flag
|
|
646
|
+
* @param {string} [redirectUrl=''] Redirect URL
|
|
647
|
+
* @returns {import('@types/express').RequestHandler} Returns express Request Handler
|
|
648
|
+
*/
|
|
649
|
+
verifyToken(isDebugging = false, redirectUrl = '') {
|
|
650
|
+
return async (req, res, next) => {
|
|
651
|
+
return this.#verifyToken(req, res, next, isDebugging, redirectUrl);
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Resource protection by session (explicit session verification)
|
|
657
|
+
* @param {boolean} [isDebugging=false] Debugging flag
|
|
658
|
+
* @param {string} [redirectUrl=''] Redirect URL
|
|
659
|
+
* @returns {import('@types/express').RequestHandler} Returns express Request Handler
|
|
660
|
+
*/
|
|
661
|
+
verifySession(isDebugging = false, redirectUrl = '') {
|
|
662
|
+
return async (req, res, next) => {
|
|
663
|
+
return this.#verifySession(req, res, next, isDebugging, redirectUrl);
|
|
664
|
+
};
|
|
665
|
+
}
|
|
246
666
|
|
|
247
667
|
/**
|
|
248
668
|
* Save session
|
|
@@ -279,16 +699,77 @@ export class SessionManager {
|
|
|
279
699
|
callback(initUser) {
|
|
280
700
|
return async (req, res, next) => {
|
|
281
701
|
const { jwt = '' } = req.query;
|
|
282
|
-
if(!jwt) {
|
|
702
|
+
if (!jwt) {
|
|
283
703
|
return next(new CustomError(httpCodes.BAD_REQUEST, 'Missing `jwt` in query parameters'));
|
|
284
704
|
}
|
|
705
|
+
|
|
285
706
|
try {
|
|
286
|
-
|
|
287
|
-
|
|
707
|
+
// Decrypt JWT from Identity Adapter
|
|
708
|
+
const { payload } = await this.#jwtManager.decrypt(jwt, this.#config.SSO_CLIENT_SECRET);
|
|
709
|
+
|
|
710
|
+
if (!payload?.user) {
|
|
711
|
+
throw new CustomError(httpCodes.BAD_REQUEST, 'Invalid JWT payload');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const user = initUser(payload.user);
|
|
715
|
+
const redirectUrl = payload.redirect_url || this.#config.SSO_SUCCESS_URL;
|
|
716
|
+
|
|
717
|
+
// Check SESSION_MODE to determine response type
|
|
718
|
+
if (this.#config.SESSION_MODE === SessionMode.TOKEN) {
|
|
719
|
+
// Token-based: Generate token and return HTML page that stores it
|
|
720
|
+
const token = await this.#generateAndStoreToken(user);
|
|
721
|
+
|
|
722
|
+
console.debug('### CALLBACK TOKEN GENERATED ###');
|
|
723
|
+
|
|
724
|
+
// Return HTML page that stores token in localStorage and redirects
|
|
725
|
+
return res.send(`
|
|
726
|
+
<!DOCTYPE html>
|
|
727
|
+
<html>
|
|
728
|
+
<head>
|
|
729
|
+
<meta charset="UTF-8">
|
|
730
|
+
<title>Authentication Complete</title>
|
|
731
|
+
<script>
|
|
732
|
+
(function() {
|
|
733
|
+
try {
|
|
734
|
+
// Store auth data in localStorage
|
|
735
|
+
localStorage.setItem('authToken', ${JSON.stringify(token)});
|
|
736
|
+
localStorage.setItem('tokenExpiry', ${Date.now() + this.#config.SESSION_AGE});
|
|
737
|
+
localStorage.setItem('user', ${JSON.stringify({
|
|
738
|
+
email: user.email,
|
|
739
|
+
name: user.name,
|
|
740
|
+
})});
|
|
741
|
+
|
|
742
|
+
// Redirect to original destination
|
|
743
|
+
window.location.replace(${JSON.stringify(redirectUrl)});
|
|
744
|
+
} catch (error) {
|
|
745
|
+
console.error('Failed to store authentication:', error);
|
|
746
|
+
document.getElementById('error').style.display = 'block';
|
|
747
|
+
}
|
|
748
|
+
})();
|
|
749
|
+
</script>
|
|
750
|
+
<style>
|
|
751
|
+
body { font-family: system-ui, sans-serif; text-align: center; padding: 50px; }
|
|
752
|
+
#error { display: none; color: #d32f2f; margin-top: 20px; }
|
|
753
|
+
</style>
|
|
754
|
+
</head>
|
|
755
|
+
<body>
|
|
756
|
+
<p>Completing authentication...</p>
|
|
757
|
+
<div id="error">
|
|
758
|
+
<p>Authentication failed. Please try again.</p>
|
|
759
|
+
<a href="${this.#config.SSO_FAILURE_URL}">Return to login</a>
|
|
760
|
+
</div>
|
|
761
|
+
</body>
|
|
762
|
+
</html>
|
|
763
|
+
`);
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
// Session-based: Save to session and redirect
|
|
767
|
+
await this.#saveSession(req, jwt, initUser);
|
|
768
|
+
return res.redirect(redirectUrl);
|
|
769
|
+
}
|
|
288
770
|
}
|
|
289
771
|
catch (error) {
|
|
290
|
-
console.error('###
|
|
291
|
-
console.error(error);
|
|
772
|
+
console.error('### CALLBACK ERROR ###', error);
|
|
292
773
|
return res.redirect(this.#config.SSO_FAILURE_URL.concat('?message=').concat(encodeURIComponent(error.message)));
|
|
293
774
|
}
|
|
294
775
|
};
|
|
@@ -315,86 +796,57 @@ export class SessionManager {
|
|
|
315
796
|
}
|
|
316
797
|
|
|
317
798
|
/**
|
|
318
|
-
*
|
|
319
|
-
* @returns {import('@types/express').RequestHandler} Returns express Request Handler
|
|
320
|
-
*/
|
|
321
|
-
logout() {
|
|
322
|
-
return (req, res) => {
|
|
323
|
-
const { redirect = false } = req.query;
|
|
324
|
-
const isRedirect = (redirect === 'true' || redirect === true);
|
|
325
|
-
return this.#logout(req, res, (error => {
|
|
326
|
-
if (error) {
|
|
327
|
-
console.error('### LOGOUT CALLBACK ERROR ###');
|
|
328
|
-
console.error(error);
|
|
329
|
-
if (isRedirect)
|
|
330
|
-
return res.redirect(this.#config.SSO_FAILURE_URL);
|
|
331
|
-
return res.status(httpCodes.AUTHORIZATION_FAILED).send({ redirect_url: this.#config.SSO_FAILURE_URL });
|
|
332
|
-
}
|
|
333
|
-
if (isRedirect)
|
|
334
|
-
return res.redirect(this.#config.SSO_SUCCESS_URL);
|
|
335
|
-
return res.send({ redirect_url: this.#config.SSO_SUCCESS_URL });
|
|
336
|
-
}));
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Refresh user session
|
|
799
|
+
* Refresh user authentication based on configured SESSION_MODE
|
|
342
800
|
* @param {(user: object) => object} initUser Initialize user object function
|
|
343
801
|
* @returns {import('@types/express').RequestHandler} Returns express Request Handler
|
|
344
802
|
*/
|
|
345
803
|
refresh(initUser) {
|
|
346
804
|
const idpUrl = '/auth/refresh'.concat('?client_id=').concat(this.#config.SSO_CLIENT_ID);
|
|
347
805
|
return async (req, res, next) => {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
this
|
|
354
|
-
const response = await this.#idpRequest.post(idpUrl, {
|
|
355
|
-
user: {
|
|
356
|
-
email,
|
|
357
|
-
attributes: {
|
|
358
|
-
idp: attributes?.idp,
|
|
359
|
-
refresh_token: attributes?.refresh_token
|
|
360
|
-
},
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
if(response.status === httpCodes.OK) {
|
|
364
|
-
/** @type {{ jwt: string }} */
|
|
365
|
-
const { jwt } = response.data;
|
|
366
|
-
const payload = await this.#saveSession(req, jwt, initUser);
|
|
367
|
-
return res.json(payload);
|
|
368
|
-
}
|
|
369
|
-
throw new CustomError(response.status, response.statusText);
|
|
370
|
-
}
|
|
371
|
-
catch(error) {
|
|
372
|
-
return next(httpHelper.handleAxiosError(error));
|
|
806
|
+
const mode = this.#config.SESSION_MODE || SessionMode.SESSION;
|
|
807
|
+
|
|
808
|
+
if (mode === SessionMode.TOKEN) {
|
|
809
|
+
return this.#refreshToken(req, res, next, initUser, idpUrl);
|
|
810
|
+
} else {
|
|
811
|
+
return this.#refreshSession(req, res, next, initUser, idpUrl);
|
|
373
812
|
}
|
|
374
813
|
};
|
|
375
814
|
}
|
|
376
815
|
|
|
377
816
|
/**
|
|
378
|
-
*
|
|
379
|
-
* @
|
|
380
|
-
* @param {import('@types/express').Response} res Response
|
|
381
|
-
* @param {(error: Error)} callback Callback
|
|
817
|
+
* Application logout based on configured SESSION_MODE (NOT SSO)
|
|
818
|
+
* @returns {import('@types/express').RequestHandler} Returns express Request Handler
|
|
382
819
|
*/
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
console.error('### SESSION DESTROY CALLBACK ERROR ###');
|
|
393
|
-
console.error(sessionError);
|
|
394
|
-
return callback(sessionError);
|
|
820
|
+
logout() {
|
|
821
|
+
return async (req, res) => {
|
|
822
|
+
const { redirect = false, all = false } = req.query;
|
|
823
|
+
const isRedirect = (redirect === 'true' || redirect === true);
|
|
824
|
+
const logoutAll = (all === 'true' || all === true);
|
|
825
|
+
const mode = this.#config.SESSION_MODE || SessionMode.SESSION;
|
|
826
|
+
|
|
827
|
+
if (mode === SessionMode.TOKEN) {
|
|
828
|
+
return this.#logoutToken(req, res, isRedirect, logoutAll);
|
|
395
829
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
830
|
+
|
|
831
|
+
// Note: 'all' parameter is only applicable for token-based authentication
|
|
832
|
+
// Session-based authentication is already single-instance per cookie
|
|
833
|
+
return this.#logoutSession(req, res, (error) => {
|
|
834
|
+
if (error) {
|
|
835
|
+
console.error('### LOGOUT CALLBACK ERROR ###');
|
|
836
|
+
console.error(error);
|
|
837
|
+
if (isRedirect) {
|
|
838
|
+
return res.redirect(this.#config.SSO_FAILURE_URL);
|
|
839
|
+
}
|
|
840
|
+
return res.status(httpCodes.AUTHORIZATION_FAILED).send({
|
|
841
|
+
redirect_url: this.#config.SSO_FAILURE_URL
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
if (isRedirect) {
|
|
845
|
+
return res.redirect(this.#config.SSO_SUCCESS_URL);
|
|
846
|
+
}
|
|
847
|
+
return res.send({ redirect_url: this.#config.SSO_SUCCESS_URL });
|
|
848
|
+
});
|
|
849
|
+
};
|
|
399
850
|
}
|
|
851
|
+
|
|
400
852
|
}
|
package/index.d.ts
CHANGED
|
@@ -5,6 +5,14 @@ import { EncryptJWT, JWTDecryptResult, JWTPayload } from 'jose';
|
|
|
5
5
|
import { RedisClientType } from '@redis/client';
|
|
6
6
|
import { Application, RequestHandler, Request, Response, NextFunction, Router } from 'express';
|
|
7
7
|
|
|
8
|
+
export { JWTPayload } from 'jose';
|
|
9
|
+
|
|
10
|
+
// Session Mode constants
|
|
11
|
+
export const SessionMode: {
|
|
12
|
+
SESSION: string;
|
|
13
|
+
TOKEN: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
8
16
|
// Session Configuration - uses strict UPPERCASE naming convention for all property names
|
|
9
17
|
export interface SessionConfig {
|
|
10
18
|
/** Identity Provider */
|
|
@@ -14,6 +22,9 @@ export interface SessionConfig {
|
|
|
14
22
|
SSO_SUCCESS_URL?: string;
|
|
15
23
|
SSO_FAILURE_URL?: string;
|
|
16
24
|
|
|
25
|
+
/** Authentication mode: 'session' or 'token' (default: 'session') */
|
|
26
|
+
SESSION_MODE?: string;
|
|
27
|
+
|
|
17
28
|
SESSION_AGE?: number;
|
|
18
29
|
SESSION_COOKIE_PATH?: string;
|
|
19
30
|
SESSION_SECRET?: string;
|
|
@@ -21,6 +32,15 @@ export interface SessionConfig {
|
|
|
21
32
|
|
|
22
33
|
REDIS_URL?: string;
|
|
23
34
|
REDIS_CERT_PATH?: string;
|
|
35
|
+
|
|
36
|
+
JWT_ALGORITHM?: string;
|
|
37
|
+
JWT_ENCRYPTION?: string;
|
|
38
|
+
JWT_EXPIRATION_TIME?: string;
|
|
39
|
+
JWT_CLOCK_TOLERANCE?: number;
|
|
40
|
+
JWT_SECRET_HASH_ALGORITHM?: string;
|
|
41
|
+
JWT_ISSUER?: string;
|
|
42
|
+
JWT_AUDIENCE?: string;
|
|
43
|
+
JWT_SUBJECT?: string;
|
|
24
44
|
}
|
|
25
45
|
|
|
26
46
|
export interface SessionUserAttributes {
|
|
@@ -98,15 +118,35 @@ export class SessionManager {
|
|
|
98
118
|
): Promise<void>;
|
|
99
119
|
|
|
100
120
|
/**
|
|
101
|
-
* Resource protection middleware
|
|
121
|
+
* Resource protection middleware based on configured SESSION_MODE
|
|
122
|
+
* Uses verifySession() for SESSION mode and verifyToken() for TOKEN mode
|
|
102
123
|
* @param isDebugging Debugging flag (default: false)
|
|
103
124
|
* @param redirectUrl Redirect URL (default: '')
|
|
104
125
|
* @returns Returns express Request Handler
|
|
105
126
|
*/
|
|
106
127
|
authenticate(isDebugging?: boolean, redirectUrl?: string): RequestHandler;
|
|
107
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Resource protection by token (explicit token verification)
|
|
131
|
+
* Requires Authorization: Bearer {token} header
|
|
132
|
+
* @param isDebugging Debugging flag (default: false)
|
|
133
|
+
* @param redirectUrl Redirect URL (default: '')
|
|
134
|
+
* @returns Returns express Request Handler
|
|
135
|
+
*/
|
|
136
|
+
verifyToken(isDebugging?: boolean, redirectUrl?: string): RequestHandler;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Resource protection by session (explicit session verification)
|
|
140
|
+
* @param isDebugging Debugging flag (default: false)
|
|
141
|
+
* @param redirectUrl Redirect URL (default: '')
|
|
142
|
+
* @returns Returns express Request Handler
|
|
143
|
+
*/
|
|
144
|
+
verifySession(isDebugging?: boolean, redirectUrl?: string): RequestHandler;
|
|
145
|
+
|
|
108
146
|
/**
|
|
109
147
|
* SSO callback for successful login
|
|
148
|
+
* SESSION mode: Saves session and redirects
|
|
149
|
+
* TOKEN mode: Generates JWT token, returns HTML page with localStorage script
|
|
110
150
|
* @param initUser Initialize user object function
|
|
111
151
|
* @returns Returns express Request Handler
|
|
112
152
|
*/
|
|
@@ -119,17 +159,22 @@ export class SessionManager {
|
|
|
119
159
|
identityProviders(): RequestHandler;
|
|
120
160
|
|
|
121
161
|
/**
|
|
122
|
-
*
|
|
162
|
+
* Refresh user authentication based on configured SESSION_MODE
|
|
163
|
+
* SESSION mode: Refreshes session data
|
|
164
|
+
* TOKEN mode: Generates new token, invalidates old token
|
|
165
|
+
* @param initUser Initialize user object function
|
|
123
166
|
* @returns Returns express Request Handler
|
|
124
167
|
*/
|
|
125
|
-
|
|
168
|
+
refresh(initUser: (user: SessionUser) => SessionUser): RequestHandler;
|
|
126
169
|
|
|
127
170
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
171
|
+
* Application logout based on configured SESSION_MODE (NOT SSO)
|
|
172
|
+
* SESSION mode: Destroys session and clears cookie
|
|
173
|
+
* TOKEN mode: Invalidates current token or all tokens (with ?all=true query param)
|
|
174
|
+
* Query params: redirect=true (redirect to success/failure URL), all=true (logout all tokens - TOKEN mode only)
|
|
130
175
|
* @returns Returns express Request Handler
|
|
131
176
|
*/
|
|
132
|
-
|
|
177
|
+
logout(): RequestHandler;
|
|
133
178
|
}
|
|
134
179
|
|
|
135
180
|
// Custom Error class
|
|
@@ -273,6 +318,7 @@ export interface JwtDecryptOptions {
|
|
|
273
318
|
}
|
|
274
319
|
|
|
275
320
|
export type JwtDecryptResult = JWTDecryptResult<EncryptJWT>;
|
|
321
|
+
|
|
276
322
|
// JwtManager class for JWT encryption and decryption
|
|
277
323
|
export class JwtManager {
|
|
278
324
|
algorithm: string;
|
package/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { SessionConfig, SessionManager } from './components/session.js';
|
|
1
|
+
export { SessionConfig, SessionManager, SessionMode } from './components/session.js';
|
|
2
2
|
export { httpCodes, httpMessages, httpErrorHandler, httpNotFoundHandler, CustomError, httpHelper, httpError } from './components/http-handlers.js';
|
|
3
3
|
export { RedisManager } from './components/redis.js';
|
|
4
4
|
export { FlexRouter } from './components/router.js';
|