w3home-utils 1.0.0 → 1.1.0
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 +108 -4
- package/package.json +8 -3
- package/w3home-utils/activityLogger.js +19 -7
- package/w3home-utils/authUtils.cognito.js +42 -0
- package/w3home-utils/authUtils.js +79 -27
- package/w3home-utils/authUtils.w3.js +62 -0
- package/w3home-utils/cacheUtils.js +101 -0
- package/w3home-utils/index.js +15 -2
- package/w3home-utils/mappingUtils.js +79 -0
- package/w3home-utils/test/activityLogger.test.js +262 -0
- package/w3home-utils/test/authUtils.dualAuth.test.js +306 -0
- package/w3home-utils/test/authUtils.test.js +223 -0
package/README.md
CHANGED
|
@@ -14,10 +14,21 @@ npm install w3home-utils
|
|
|
14
14
|
const {
|
|
15
15
|
// Authentication
|
|
16
16
|
getIndetifiers,
|
|
17
|
+
getIndetifiersCognito,
|
|
18
|
+
verifyW3JWT,
|
|
17
19
|
getUser,
|
|
18
20
|
getUserType,
|
|
19
21
|
decodeIdToken,
|
|
20
|
-
|
|
22
|
+
clearUserCache,
|
|
23
|
+
|
|
24
|
+
// W3 User Mapping
|
|
25
|
+
resolveW3UserToHomepayUser,
|
|
26
|
+
clearMappingCache,
|
|
27
|
+
|
|
28
|
+
// Redis/JWKS Cache (advanced)
|
|
29
|
+
getRedisClient,
|
|
30
|
+
getCachedJWKS,
|
|
31
|
+
|
|
21
32
|
// Authorization
|
|
22
33
|
authorize,
|
|
23
34
|
withAuthorization,
|
|
@@ -25,12 +36,12 @@ const {
|
|
|
25
36
|
authorizeBackofficeProject,
|
|
26
37
|
ROLES,
|
|
27
38
|
UserType,
|
|
28
|
-
|
|
39
|
+
|
|
29
40
|
// Activity Logging
|
|
30
41
|
logActivity,
|
|
31
42
|
logPostActivity,
|
|
32
43
|
withActivityLogging,
|
|
33
|
-
|
|
44
|
+
|
|
34
45
|
// Common
|
|
35
46
|
corsHeaders
|
|
36
47
|
} = require('w3home-utils');
|
|
@@ -38,13 +49,22 @@ const {
|
|
|
38
49
|
|
|
39
50
|
## Authentication
|
|
40
51
|
|
|
52
|
+
### Dual-Auth Support (W3 Platform + Cognito)
|
|
53
|
+
|
|
54
|
+
**w3home-utils v1.1.0** supports dual authentication via the `AUTH_MODE` environment variable:
|
|
55
|
+
|
|
56
|
+
- `cognito` (default): Legacy Cognito JWT decode
|
|
57
|
+
- `w3`: W3 Platform JWT validation with user mapping
|
|
58
|
+
- `dual`: Try W3 first, fall back to Cognito on failure
|
|
59
|
+
|
|
41
60
|
### Get User Identifiers from Request Headers
|
|
42
61
|
|
|
43
62
|
```javascript
|
|
44
63
|
const { getIndetifiers } = require('w3home-utils');
|
|
45
64
|
|
|
46
65
|
const handler = async (event) => {
|
|
47
|
-
|
|
66
|
+
// Returns: { userId: string|null, w3Sub: string|null, authType?: 'w3'|'cognito' }
|
|
67
|
+
const { userId, w3Sub, authType } = await getIndetifiers(event.headers);
|
|
48
68
|
if (!userId) {
|
|
49
69
|
return { statusCode: 401, body: JSON.stringify({ error: 'Unauthorized' }) };
|
|
50
70
|
}
|
|
@@ -52,6 +72,11 @@ const handler = async (event) => {
|
|
|
52
72
|
};
|
|
53
73
|
```
|
|
54
74
|
|
|
75
|
+
**Return shape:**
|
|
76
|
+
- `userId`: Homepay user ID (always present for authenticated users)
|
|
77
|
+
- `w3Sub`: W3 platform user ID (present only for W3-authenticated users)
|
|
78
|
+
- `authType`: `'w3'` or `'cognito'` (indicates which auth backend was used)
|
|
79
|
+
|
|
55
80
|
### Get User Details
|
|
56
81
|
|
|
57
82
|
```javascript
|
|
@@ -171,6 +196,8 @@ console.log(UserType.BACKOFFICE_USER); // 'BACKOFFICE_USER'
|
|
|
171
196
|
|
|
172
197
|
## Environment Variables
|
|
173
198
|
|
|
199
|
+
### Core Configuration
|
|
200
|
+
|
|
174
201
|
| Variable | Default | Description |
|
|
175
202
|
|----------|---------|-------------|
|
|
176
203
|
| `USERS_TABLE` | `w3HomeUsers` | DynamoDB table for users |
|
|
@@ -178,6 +205,36 @@ console.log(UserType.BACKOFFICE_USER); // 'BACKOFFICE_USER'
|
|
|
178
205
|
| `CONFIG_TABLE` | `w3home-config` | DynamoDB table for config |
|
|
179
206
|
| `STAGE` | `dev` | Environment stage |
|
|
180
207
|
|
|
208
|
+
### Authentication Mode (v1.1.0+)
|
|
209
|
+
|
|
210
|
+
| Variable | Values | Description |
|
|
211
|
+
|----------|--------|-------------|
|
|
212
|
+
| `AUTH_MODE` | `cognito` (default), `w3`, `dual` | Authentication backend selector |
|
|
213
|
+
|
|
214
|
+
**Auth modes:**
|
|
215
|
+
- `cognito`: Legacy Cognito JWT decode (backward compatible)
|
|
216
|
+
- `w3`: W3 Platform JWT validation + user mapping lookup
|
|
217
|
+
- `dual`: Try W3 first, fall back to Cognito on failure (recommended for migration)
|
|
218
|
+
|
|
219
|
+
### W3 Platform Configuration (required when `AUTH_MODE=w3` or `dual`)
|
|
220
|
+
|
|
221
|
+
| Variable | Required | Default | Description |
|
|
222
|
+
|----------|----------|---------|-------------|
|
|
223
|
+
| `W3_JWKS_URL` | Yes | - | W3 platform JWKS endpoint URL |
|
|
224
|
+
| `W3_ISSUER` | Yes | - | W3 platform issuer (iss claim) |
|
|
225
|
+
| `W3_USER_MAPPING_TABLE` | No | `w3UserMapping` | DynamoDB table for w3UserId → homepayUserId mapping |
|
|
226
|
+
|
|
227
|
+
### Redis Configuration (required for W3 JWKS caching)
|
|
228
|
+
|
|
229
|
+
| Variable | Required | Default | Description |
|
|
230
|
+
|----------|----------|---------|-------------|
|
|
231
|
+
| `REDIS_HOST` | Yes | - | Redis host for JWKS cache |
|
|
232
|
+
| `REDIS_PORT` | No | `6379` | Redis port |
|
|
233
|
+
| `REDIS_PASSWORD` | Yes* | - | Redis password (*required for production) |
|
|
234
|
+
| `REDIS_TLS` | No | `false` | Enable TLS for Redis connection |
|
|
235
|
+
|
|
236
|
+
**JWKS cache TTL:** 10 minutes (reduces load on W3 platform JWKS endpoint)
|
|
237
|
+
|
|
181
238
|
## Peer Dependencies
|
|
182
239
|
|
|
183
240
|
This package requires `aws-sdk` as a peer dependency. In Lambda, this is already available. For local development:
|
|
@@ -186,6 +243,53 @@ This package requires `aws-sdk` as a peer dependency. In Lambda, this is already
|
|
|
186
243
|
npm install aws-sdk --save-dev
|
|
187
244
|
```
|
|
188
245
|
|
|
246
|
+
## Migration Guide
|
|
247
|
+
|
|
248
|
+
### Migrating to W3 Platform Authentication (v1.1.0)
|
|
249
|
+
|
|
250
|
+
**Step 1: Update w3home-utils**
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
npm update w3home-utils
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Step 2: Enable dual-auth mode**
|
|
257
|
+
|
|
258
|
+
Add to your Lambda environment variables:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
AUTH_MODE=dual
|
|
262
|
+
W3_JWKS_URL=https://api.w3mcp.ai/.well-known/jwks.json
|
|
263
|
+
W3_ISSUER=https://api.w3mcp.ai
|
|
264
|
+
W3_USER_MAPPING_TABLE=w3UserMapping # Optional, defaults to this
|
|
265
|
+
REDIS_HOST=your-redis-host
|
|
266
|
+
REDIS_PORT=6379
|
|
267
|
+
REDIS_PASSWORD=your-redis-password
|
|
268
|
+
REDIS_TLS=true # For production
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Step 3: No code changes needed!**
|
|
272
|
+
|
|
273
|
+
`getIndetifiers()` now returns `{ userId, w3Sub, authType }`. The `userId` field works exactly as before.
|
|
274
|
+
|
|
275
|
+
**Optional:** Use `w3Sub` for enhanced audit trails:
|
|
276
|
+
|
|
277
|
+
```javascript
|
|
278
|
+
const { userId, w3Sub, authType } = await getIndetifiers(event.headers);
|
|
279
|
+
logActivity({
|
|
280
|
+
event,
|
|
281
|
+
userId,
|
|
282
|
+
w3Sub, // Now captured in activity logs
|
|
283
|
+
action: 'READ',
|
|
284
|
+
resource: 'projects',
|
|
285
|
+
statusCode: 200
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Rollback:** Set `AUTH_MODE=cognito` to instantly revert to Cognito-only auth.
|
|
290
|
+
|
|
291
|
+
**Cutover:** Once all users migrated to W3 platform, set `AUTH_MODE=w3` for W3-only validation (no Cognito fallback).
|
|
292
|
+
|
|
189
293
|
## License
|
|
190
294
|
|
|
191
295
|
UNLICENSED - HomePay Internal Use Only
|
package/package.json
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "w3home-utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "W3Home Utilities - Authorization, Activity Logging, Auth Utilities",
|
|
5
5
|
"main": "w3home-utils/index.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"w3home-utils/**/*.js"
|
|
8
8
|
],
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "
|
|
10
|
+
"test": "jest --coverage --passWithNoTests",
|
|
11
|
+
"test:watch": "jest --watch",
|
|
11
12
|
"prepublishOnly": "npm test",
|
|
12
13
|
"seed:dev": "node seed-tables.js dev",
|
|
13
14
|
"seed:prd": "node seed-tables.js prd"
|
|
14
15
|
},
|
|
15
16
|
"dependencies": {
|
|
17
|
+
"ioredis": "^5.10.0",
|
|
18
|
+
"jose": "^5.10.0",
|
|
16
19
|
"jsonwebtoken": "^9.0.2"
|
|
17
20
|
},
|
|
18
21
|
"peerDependencies": {
|
|
19
22
|
"aws-sdk": "^2.1000.0"
|
|
20
23
|
},
|
|
21
24
|
"devDependencies": {
|
|
22
|
-
"aws-sdk": "^2.1692.0"
|
|
25
|
+
"aws-sdk": "^2.1692.0",
|
|
26
|
+
"aws-sdk-mock": "^6.2.2",
|
|
27
|
+
"jest": "^29.7.0"
|
|
23
28
|
},
|
|
24
29
|
"keywords": [
|
|
25
30
|
"authorization",
|
|
@@ -39,14 +39,16 @@ function extractRequestContext(event) {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function logActivity({ event, userId, action, resource, statusCode, metadata = {}, context }) {
|
|
42
|
+
function logActivity({ event, userId, w3Sub, action, resource, statusCode, metadata = {}, context }) {
|
|
43
43
|
const identifiers = extractIdentifiers(event, userId);
|
|
44
44
|
const requestContext = extractRequestContext(event);
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
const activityLog = {
|
|
47
47
|
logType: LOG_PREFIX,
|
|
48
48
|
timestamp: new Date().toISOString(),
|
|
49
49
|
userId: identifiers.userId,
|
|
50
|
+
w3Sub: w3Sub || null,
|
|
51
|
+
authMode: process.env.AUTH_MODE || 'cognito',
|
|
50
52
|
projectId: identifiers.projectId,
|
|
51
53
|
apartmentId: identifiers.apartmentId,
|
|
52
54
|
action,
|
|
@@ -78,13 +80,23 @@ function withActivityLogging(handler, config = {}) {
|
|
|
78
80
|
return async (event, context) => {
|
|
79
81
|
const resource = config.resource || 'unknown';
|
|
80
82
|
let userId = null;
|
|
83
|
+
let w3Sub = null;
|
|
81
84
|
let action = 'CREATE';
|
|
82
85
|
let statusCode = null;
|
|
83
86
|
let metadata = {};
|
|
84
87
|
|
|
85
88
|
try {
|
|
86
|
-
if (config.extractUserId)
|
|
87
|
-
|
|
89
|
+
if (config.extractUserId) {
|
|
90
|
+
const userIdResult = await config.extractUserId(event);
|
|
91
|
+
// Support both string and object return values
|
|
92
|
+
if (typeof userIdResult === 'object' && userIdResult !== null) {
|
|
93
|
+
userId = userIdResult.userId || null;
|
|
94
|
+
w3Sub = userIdResult.w3Sub || null;
|
|
95
|
+
} else {
|
|
96
|
+
userId = userIdResult;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
88
100
|
if (config.extractAction) {
|
|
89
101
|
action = await config.extractAction(event);
|
|
90
102
|
} else {
|
|
@@ -95,15 +107,15 @@ function withActivityLogging(handler, config = {}) {
|
|
|
95
107
|
|
|
96
108
|
const result = await handler(event, context);
|
|
97
109
|
statusCode = result.statusCode || 200;
|
|
98
|
-
|
|
110
|
+
|
|
99
111
|
if (config.extractMetadata) metadata = config.extractMetadata(result, event);
|
|
100
|
-
logActivity({ event, userId, action, resource, statusCode, metadata, context });
|
|
112
|
+
logActivity({ event, userId, w3Sub, action, resource, statusCode, metadata, context });
|
|
101
113
|
|
|
102
114
|
return result;
|
|
103
115
|
} catch (error) {
|
|
104
116
|
statusCode = 500;
|
|
105
117
|
metadata = { error: error.message };
|
|
106
|
-
logActivity({ event, userId, action, resource, statusCode, metadata, context });
|
|
118
|
+
logActivity({ event, userId, w3Sub, action, resource, statusCode, metadata, context });
|
|
107
119
|
throw error;
|
|
108
120
|
}
|
|
109
121
|
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cognito Authentication Utilities
|
|
3
|
+
*
|
|
4
|
+
* Extracted from authUtils.js for dual-auth support.
|
|
5
|
+
* Handles legacy Cognito JWT decode flow (no verification, just decode).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const jwt = require('jsonwebtoken');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Decode Cognito JWT token (no verification - backward compatible)
|
|
12
|
+
* @param {string} idToken - Cognito JWT token
|
|
13
|
+
* @returns {string|null} - Cognito sub claim or null
|
|
14
|
+
*/
|
|
15
|
+
function decodeIdToken(idToken) {
|
|
16
|
+
try {
|
|
17
|
+
const decoded = jwt.decode(idToken);
|
|
18
|
+
return decoded?.sub || null;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error('Error decoding Cognito token:', error.message);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get identifiers using Cognito flow (legacy)
|
|
27
|
+
* @param {string} token - JWT token
|
|
28
|
+
* @returns {Promise<{userId: string|null, w3Sub: null, authType: 'cognito'}>}
|
|
29
|
+
*/
|
|
30
|
+
async function getIndetifiersCognito(token) {
|
|
31
|
+
const sub = decodeIdToken(token);
|
|
32
|
+
return {
|
|
33
|
+
userId: sub,
|
|
34
|
+
w3Sub: null,
|
|
35
|
+
authType: 'cognito'
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
getIndetifiersCognito,
|
|
41
|
+
decodeIdToken
|
|
42
|
+
};
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Authentication Utilities
|
|
3
|
+
*
|
|
4
|
+
* Supports dual-auth mode: Cognito (legacy) and W3 Platform JWT
|
|
5
|
+
* Routing controlled via AUTH_MODE environment variable:
|
|
6
|
+
* - 'cognito' (default): Legacy Cognito JWT decode
|
|
7
|
+
* - 'w3': W3 Platform JWT validation + user mapping
|
|
8
|
+
* - 'dual': Try W3 first, fall back to Cognito on failure
|
|
3
9
|
*/
|
|
4
10
|
|
|
5
|
-
const jwt = require('jsonwebtoken');
|
|
6
11
|
const AWS = require('aws-sdk');
|
|
12
|
+
const { getIndetifiersCognito, decodeIdToken } = require('./authUtils.cognito');
|
|
13
|
+
const { verifyW3JWT } = require('./authUtils.w3');
|
|
14
|
+
const { resolveW3UserToHomepayUser } = require('./mappingUtils');
|
|
7
15
|
|
|
8
16
|
const USERS_TABLE = process.env.USERS_TABLE || 'w3HomeUsers';
|
|
9
17
|
const dynamodb = new AWS.DynamoDB.DocumentClient({ region: 'eu-west-1' });
|
|
@@ -11,40 +19,84 @@ const dynamodb = new AWS.DynamoDB.DocumentClient({ region: 'eu-west-1' });
|
|
|
11
19
|
const USER_CACHE_TTL = 5 * 60 * 1000;
|
|
12
20
|
const userCache = new Map();
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
}
|
|
22
|
+
/**
|
|
23
|
+
* Extract token from various header formats
|
|
24
|
+
* @param {Object} headers - Request headers
|
|
25
|
+
* @returns {string|null} - Extracted token or null
|
|
26
|
+
*/
|
|
27
|
+
function extractToken(headers) {
|
|
28
|
+
let token = '';
|
|
23
29
|
|
|
24
|
-
async function getIndetifiers(headers) {
|
|
25
|
-
let authorizationToken = '';
|
|
26
|
-
|
|
27
30
|
if (headers['Authorization']) {
|
|
28
31
|
const parts = headers['Authorization'].split(' ');
|
|
29
|
-
|
|
32
|
+
token = parts.length > 1 ? parts[1] : parts[0];
|
|
30
33
|
}
|
|
31
|
-
if (!
|
|
32
|
-
|
|
34
|
+
if (!token && headers['Token']) {
|
|
35
|
+
token = headers['Token'];
|
|
33
36
|
}
|
|
34
|
-
if (!
|
|
37
|
+
if (!token && headers['authorization']) {
|
|
35
38
|
const parts = headers['authorization'].split(' ');
|
|
36
|
-
|
|
39
|
+
token = parts.length > 1 ? parts[1] : parts[0];
|
|
37
40
|
}
|
|
38
|
-
if (!
|
|
39
|
-
|
|
41
|
+
if (!token && headers['token']) {
|
|
42
|
+
token = headers['token'];
|
|
40
43
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
|
|
45
|
+
return token || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get identifiers with W3 Platform JWT validation
|
|
50
|
+
* @param {string} token - JWT token
|
|
51
|
+
* @returns {Promise<{userId: string, w3Sub: string, authType: 'w3'}>}
|
|
52
|
+
*/
|
|
53
|
+
async function getIndetifiersW3(token) {
|
|
54
|
+
const payload = await verifyW3JWT(token);
|
|
55
|
+
const homepayUserId = await resolveW3UserToHomepayUser(payload.sub);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
userId: homepayUserId,
|
|
59
|
+
w3Sub: payload.sub,
|
|
60
|
+
authType: 'w3'
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get identifiers with dual-auth routing
|
|
66
|
+
* @param {Object} headers - Request headers
|
|
67
|
+
* @returns {Promise<{userId: string|null, w3Sub: string|null, authType?: string}>}
|
|
68
|
+
*/
|
|
69
|
+
async function getIndetifiers(headers) {
|
|
70
|
+
const token = extractToken(headers);
|
|
71
|
+
|
|
72
|
+
if (!token) {
|
|
73
|
+
return { userId: null, w3Sub: null };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const authMode = process.env.AUTH_MODE || 'cognito';
|
|
77
|
+
|
|
78
|
+
switch (authMode) {
|
|
79
|
+
case 'cognito':
|
|
80
|
+
return await getIndetifiersCognito(token);
|
|
81
|
+
|
|
82
|
+
case 'w3':
|
|
83
|
+
return await getIndetifiersW3(token);
|
|
84
|
+
|
|
85
|
+
case 'dual':
|
|
86
|
+
// Try W3 first, fall back to Cognito on failure
|
|
87
|
+
try {
|
|
88
|
+
return await getIndetifiersW3(token);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.warn(
|
|
91
|
+
'Dual auth: W3 validation failed, falling back to Cognito.',
|
|
92
|
+
`Error: ${error.message}`
|
|
93
|
+
);
|
|
94
|
+
return await getIndetifiersCognito(token);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
default:
|
|
98
|
+
throw new Error(`Invalid AUTH_MODE: ${authMode}`);
|
|
45
99
|
}
|
|
46
|
-
|
|
47
|
-
return { userId: null };
|
|
48
100
|
}
|
|
49
101
|
|
|
50
102
|
async function getUser(userId, options = {}, noCredentials = false) {
|
|
@@ -106,7 +158,7 @@ function getUserInstitutionIds(user) {
|
|
|
106
158
|
}
|
|
107
159
|
|
|
108
160
|
module.exports = {
|
|
109
|
-
decodeIdToken,
|
|
161
|
+
decodeIdToken, // Re-exported from authUtils.cognito for backward compatibility
|
|
110
162
|
getIndetifiers,
|
|
111
163
|
getUser,
|
|
112
164
|
getUserType,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W3 Platform JWT Authentication Utilities
|
|
3
|
+
*
|
|
4
|
+
* Validates W3 platform JWTs using jose library with RS256 signature verification.
|
|
5
|
+
* Uses Redis-cached JWKS to reduce remote fetches.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { createLocalJWKSet, jwtVerify } = require('jose');
|
|
9
|
+
const { getCachedJWKS } = require('./cacheUtils');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Verify W3 platform JWT
|
|
13
|
+
*
|
|
14
|
+
* @param {string} token - JWT token to verify
|
|
15
|
+
* @returns {Promise<Object>} Decoded payload with { sub, iss, exp, iat, org, fullPayload }
|
|
16
|
+
* @throws {Error} If token is invalid, expired, or signature verification fails
|
|
17
|
+
*/
|
|
18
|
+
async function verifyW3JWT(token) {
|
|
19
|
+
const issuer = process.env.W3_ISSUER;
|
|
20
|
+
|
|
21
|
+
if (!issuer) {
|
|
22
|
+
throw new Error('W3_ISSUER environment variable not set');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Get JWKS (from Redis cache or fetch)
|
|
27
|
+
const jwks = await getCachedJWKS();
|
|
28
|
+
|
|
29
|
+
// Create local JWK Set for signature verification
|
|
30
|
+
const JWKS = createLocalJWKSet(jwks);
|
|
31
|
+
|
|
32
|
+
// Verify JWT with RS256 algorithm, issuer validation, and 5-minute clock tolerance
|
|
33
|
+
const { payload } = await jwtVerify(token, JWKS, {
|
|
34
|
+
issuer,
|
|
35
|
+
clockTolerance: 300, // 5 minutes (300 seconds)
|
|
36
|
+
algorithms: ['RS256']
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Return extracted claims plus full payload
|
|
40
|
+
return {
|
|
41
|
+
sub: payload.sub,
|
|
42
|
+
iss: payload.iss,
|
|
43
|
+
exp: payload.exp,
|
|
44
|
+
iat: payload.iat,
|
|
45
|
+
org: payload.org,
|
|
46
|
+
fullPayload: payload
|
|
47
|
+
};
|
|
48
|
+
} catch (error) {
|
|
49
|
+
// Log error details for debugging
|
|
50
|
+
console.error('W3 JWT verification failed:', {
|
|
51
|
+
code: error.code,
|
|
52
|
+
message: error.message
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Re-throw for caller to handle
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
verifyW3JWT
|
|
62
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Utilities for W3 JWT Authentication
|
|
3
|
+
*
|
|
4
|
+
* Provides Redis singleton client and JWKS caching layer.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const Redis = require('ioredis');
|
|
8
|
+
|
|
9
|
+
// Redis singleton instance
|
|
10
|
+
let redisClient = null;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get Redis client singleton
|
|
14
|
+
*
|
|
15
|
+
* @returns {Redis} Singleton Redis client instance
|
|
16
|
+
*/
|
|
17
|
+
function getRedisClient() {
|
|
18
|
+
if (!redisClient) {
|
|
19
|
+
const redisConfig = {
|
|
20
|
+
host: process.env.REDIS_HOST || process.env.CACHE_URL || 'localhost',
|
|
21
|
+
port: parseInt(process.env.REDIS_PORT || process.env.CACHE_PORT || '6379', 10),
|
|
22
|
+
lazyConnect: true,
|
|
23
|
+
enableOfflineQueue: true,
|
|
24
|
+
maxRetriesPerRequest: 3,
|
|
25
|
+
retryStrategy(times) {
|
|
26
|
+
const delay = Math.min(times * 50, 2000);
|
|
27
|
+
return delay;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Enable TLS if configured
|
|
32
|
+
if (process.env.REDIS_TLS === 'true') {
|
|
33
|
+
redisConfig.tls = {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
redisClient = new Redis(redisConfig);
|
|
37
|
+
|
|
38
|
+
// Event handlers
|
|
39
|
+
redisClient.on('error', (err) => {
|
|
40
|
+
console.error('Redis error:', err.message);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
redisClient.on('reconnecting', () => {
|
|
44
|
+
console.log('Redis reconnecting...');
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return redisClient;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get cached JWKS from Redis or fetch from W3 platform
|
|
53
|
+
*
|
|
54
|
+
* @returns {Promise<Object>} JWKS object with keys array
|
|
55
|
+
* @throws {Error} If fetch fails (does not throw on Redis errors - falls back to fetch)
|
|
56
|
+
*/
|
|
57
|
+
async function getCachedJWKS() {
|
|
58
|
+
const CACHE_KEY = 'w3:jwks:v1';
|
|
59
|
+
const CACHE_TTL = 600; // 10 minutes
|
|
60
|
+
const jwksUrl = process.env.W3_JWKS_URL;
|
|
61
|
+
|
|
62
|
+
if (!jwksUrl) {
|
|
63
|
+
throw new Error('W3_JWKS_URL environment variable not set');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const redis = getRedisClient();
|
|
68
|
+
const cached = await redis.get(CACHE_KEY);
|
|
69
|
+
|
|
70
|
+
if (cached) {
|
|
71
|
+
return JSON.parse(cached);
|
|
72
|
+
}
|
|
73
|
+
} catch (redisError) {
|
|
74
|
+
console.error('Redis cache read failed, falling back to direct fetch:', redisError.message);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Cache miss or Redis error - fetch from URL
|
|
78
|
+
const response = await fetch(jwksUrl);
|
|
79
|
+
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
throw new Error(`Failed to fetch JWKS from ${jwksUrl}: ${response.status} ${response.statusText}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const jwks = await response.json();
|
|
85
|
+
|
|
86
|
+
// Cache in Redis (fire-and-forget - don't wait or throw on error)
|
|
87
|
+
try {
|
|
88
|
+
const redis = getRedisClient();
|
|
89
|
+
await redis.setex(CACHE_KEY, CACHE_TTL, JSON.stringify(jwks));
|
|
90
|
+
} catch (cacheWriteError) {
|
|
91
|
+
console.error('Failed to cache JWKS in Redis:', cacheWriteError.message);
|
|
92
|
+
// Continue - we have the JWKS from fetch
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return jwks;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
getRedisClient,
|
|
100
|
+
getCachedJWKS
|
|
101
|
+
};
|
package/w3home-utils/index.js
CHANGED
|
@@ -12,6 +12,11 @@ const {
|
|
|
12
12
|
getUserInstitutionIds
|
|
13
13
|
} = require('./authUtils');
|
|
14
14
|
|
|
15
|
+
const { verifyW3JWT } = require('./authUtils.w3');
|
|
16
|
+
const { getIndetifiersCognito, decodeIdToken: decodeIdTokenCognito } = require('./authUtils.cognito');
|
|
17
|
+
const { resolveW3UserToHomepayUser, clearMappingCache } = require('./mappingUtils');
|
|
18
|
+
const { getRedisClient, getCachedJWKS } = require('./cacheUtils');
|
|
19
|
+
|
|
15
20
|
const {
|
|
16
21
|
authorize,
|
|
17
22
|
withAuthorization,
|
|
@@ -59,7 +64,7 @@ const corsHeaders = {
|
|
|
59
64
|
};
|
|
60
65
|
|
|
61
66
|
module.exports = {
|
|
62
|
-
// Auth
|
|
67
|
+
// Auth (Legacy Cognito)
|
|
63
68
|
decodeIdToken,
|
|
64
69
|
getIndetifiers,
|
|
65
70
|
getUser,
|
|
@@ -67,7 +72,15 @@ module.exports = {
|
|
|
67
72
|
clearUserCache,
|
|
68
73
|
getUserInstitutionId,
|
|
69
74
|
getUserInstitutionIds,
|
|
70
|
-
|
|
75
|
+
|
|
76
|
+
// W3 Platform Auth (New)
|
|
77
|
+
verifyW3JWT,
|
|
78
|
+
getIndetifiersCognito,
|
|
79
|
+
resolveW3UserToHomepayUser,
|
|
80
|
+
clearMappingCache,
|
|
81
|
+
getRedisClient,
|
|
82
|
+
getCachedJWKS,
|
|
83
|
+
|
|
71
84
|
// Authorization
|
|
72
85
|
authorize,
|
|
73
86
|
withAuthorization,
|