@carecard/jwt-read 3.1.16 → 3.1.18
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/.agents/skills/carecard-workspace-standards/SKILL.md +5 -5
- package/.agents/skills/github-pr-create-update/SKILL.md +63 -84
- package/.agents/skills/github-pr-merge-cleanup/SKILL.md +103 -79
- package/.agents/skills/pkg-jwt-read-jwt-middleware-library/SKILL.md +15 -5
- package/.agents/skills/software-design-patterns-and-clean-code/SKILL.md +1 -1
- package/index.d.ts +123 -9
- package/index.js +4 -0
- package/lib/jwtLib.js +256 -34
- package/package.json +3 -3
- package/readme.md +41 -0
package/lib/jwtLib.js
CHANGED
|
@@ -3,6 +3,9 @@ const { jwtVerifySignedToken, jwtGetHeaderPayload } = require('@carecard/auth-ut
|
|
|
3
3
|
|
|
4
4
|
const { throwLoginRequiredError, throwNotAuthorizedError } = require('@carecard/common-util');
|
|
5
5
|
|
|
6
|
+
const DEFAULT_USER_AUTHORIZATION_HEADER_NAME = 'X-Authorization-Context';
|
|
7
|
+
const DEFAULT_USER_AUTHORIZATION_MAX_TOKEN_LENGTH = 2048;
|
|
8
|
+
|
|
6
9
|
function jwtClientId(req) {
|
|
7
10
|
const jwtObj = req?.jwt || this;
|
|
8
11
|
return jwtObj?.payload?.sub;
|
|
@@ -70,13 +73,15 @@ function jwtAgeInSeconds(req) {
|
|
|
70
73
|
}
|
|
71
74
|
}
|
|
72
75
|
|
|
73
|
-
|
|
76
|
+
// Pattern: Decorator - preserves JWT extraction while optionally adding a scoped authorization context.
|
|
77
|
+
function validateAndExtractJwtObject(req, publicKey, customErrorFunction, options) {
|
|
74
78
|
const jwtString = _validateJwt(req, customErrorFunction);
|
|
75
79
|
|
|
76
80
|
const isJwtSignatureValid = jwtVerifySignedToken(jwtString, publicKey);
|
|
77
81
|
|
|
78
82
|
if (jwtString && isJwtSignatureValid) {
|
|
79
83
|
_extractJwtObject(req, jwtString, customErrorFunction);
|
|
84
|
+
validateAndExtractOptionalUserAuthorizationObject(req, options, customErrorFunction);
|
|
80
85
|
} else {
|
|
81
86
|
req['jwt'] = null;
|
|
82
87
|
throwError(customErrorFunction);
|
|
@@ -85,13 +90,15 @@ function validateAndExtractJwtObject(req, publicKey, customErrorFunction) {
|
|
|
85
90
|
return req;
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
|
|
93
|
+
// Pattern: Decorator - keeps custom-header JWT behavior and optionally reads user authorization context.
|
|
94
|
+
function validateAndExtractWebToken(req, publicKey, headerName, customErrorFunction, options) {
|
|
89
95
|
const webTokenString = _validateWebToken(req, headerName, customErrorFunction);
|
|
90
96
|
|
|
91
97
|
const isJwtSignatureValid = jwtVerifySignedToken(webTokenString, publicKey);
|
|
92
98
|
|
|
93
99
|
if (webTokenString && isJwtSignatureValid) {
|
|
94
100
|
_extractJwtObject(req, webTokenString, customErrorFunction);
|
|
101
|
+
validateAndExtractOptionalUserAuthorizationObject(req, options, customErrorFunction);
|
|
95
102
|
} else {
|
|
96
103
|
req['jwt'] = null;
|
|
97
104
|
throwError(customErrorFunction);
|
|
@@ -100,49 +107,68 @@ function validateAndExtractWebToken(req, publicKey, headerName, customErrorFunct
|
|
|
100
107
|
return req;
|
|
101
108
|
}
|
|
102
109
|
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
// Pattern: Decorator - extends no-throw JWT extraction without changing req.jwt failure semantics.
|
|
111
|
+
function validateAndExtractJwtObjectNoThrow(req, publicKey, options) {
|
|
112
|
+
_validateAndExtractGenericNoThrow(req, publicKey, _validateJwtNoThrow, _extractJwtObjectNoThrow, 'jwt');
|
|
113
|
+
validateAndExtractOptionalUserAuthorizationObjectNoThrow(req, options);
|
|
114
|
+
return req;
|
|
105
115
|
}
|
|
106
116
|
|
|
107
|
-
|
|
117
|
+
// Pattern: Decorator - keeps service-JWT checks distinct from optional user authorization context.
|
|
118
|
+
function validateAndExtractServiceJwtObject(req, publicKey, expectedIssuer, expectedAudience, customErrorFunction, options) {
|
|
108
119
|
const jwtString = _validateJwt(req, customErrorFunction);
|
|
109
120
|
const isJwtSignatureValid = jwtVerifySignedToken(jwtString, publicKey);
|
|
110
121
|
|
|
111
122
|
if (jwtString && isJwtSignatureValid) {
|
|
112
123
|
_extractJwtObject(req, jwtString, customErrorFunction);
|
|
113
|
-
} else
|
|
114
|
-
|
|
124
|
+
} else {
|
|
125
|
+
/* istanbul ignore else */
|
|
126
|
+
if (req) {
|
|
127
|
+
req.jwt = null;
|
|
128
|
+
}
|
|
115
129
|
}
|
|
116
130
|
|
|
117
131
|
if (!req?.jwt || !_isServiceJwtFor(req.jwt.payload, expectedIssuer, expectedAudience)) {
|
|
132
|
+
/* istanbul ignore else */
|
|
118
133
|
if (req) req.jwt = null;
|
|
119
134
|
throwError(customErrorFunction);
|
|
120
135
|
}
|
|
121
136
|
|
|
137
|
+
validateAndExtractOptionalUserAuthorizationObject(req, options, customErrorFunction);
|
|
122
138
|
return req;
|
|
123
139
|
}
|
|
124
140
|
|
|
125
|
-
|
|
126
|
-
|
|
141
|
+
// Pattern: Decorator - normalizes primary auth first, then applies optional scoped authorization.
|
|
142
|
+
async function validateAndExtractJwtOrServerAuthObject(req, publicKey, serverAuthIntrospector, customErrorFunction, options) {
|
|
143
|
+
if (!tryValidateAndExtractJwtObject(req, publicKey)) {
|
|
144
|
+
const serverAuthToken = _validateServerAuthToken(req, customErrorFunction);
|
|
145
|
+
const claims = await introspectServerAuthToken(serverAuthIntrospector, serverAuthToken, req, customErrorFunction);
|
|
146
|
+
attachServerAuthClaims(req, claims, customErrorFunction);
|
|
147
|
+
}
|
|
127
148
|
|
|
128
|
-
|
|
129
|
-
const claims = await introspectServerAuthToken(serverAuthIntrospector, serverAuthToken, req, customErrorFunction);
|
|
130
|
-
attachServerAuthClaims(req, claims, customErrorFunction);
|
|
149
|
+
validateAndExtractOptionalUserAuthorizationObject(req, options, customErrorFunction);
|
|
131
150
|
return req;
|
|
132
151
|
}
|
|
133
152
|
|
|
134
|
-
|
|
135
|
-
|
|
153
|
+
// Pattern: Decorator - extends custom-header no-throw extraction with optional user authorization context.
|
|
154
|
+
function validateAndExtractWebTokenObjectNoThrow(req, publicKey, headerName, options) {
|
|
155
|
+
_validateAndExtractGenericNoThrow(req, publicKey, r => _validateWebTokenNoThrow(r, headerName), _extractJwtObjectNoThrow, 'jwt');
|
|
156
|
+
validateAndExtractOptionalUserAuthorizationObjectNoThrow(req, options);
|
|
157
|
+
return req;
|
|
136
158
|
}
|
|
137
159
|
|
|
138
|
-
|
|
139
|
-
|
|
160
|
+
// Pattern: Decorator - keeps visitor token extraction independent from optional user authorization context.
|
|
161
|
+
function validateAndExtractVisitorObjectNoThrow(req, publicKey, options) {
|
|
162
|
+
_validateAndExtractGenericNoThrow(req, publicKey, _validateVisitorNoThrow, _extractVisitorObjectNoThrow, 'visitor');
|
|
163
|
+
validateAndExtractOptionalUserAuthorizationObjectNoThrow(req, options);
|
|
164
|
+
return req;
|
|
140
165
|
}
|
|
141
166
|
|
|
142
|
-
|
|
167
|
+
// Pattern: Middleware - composes JWT verification, optional user authorization, and role checks.
|
|
168
|
+
function verifyJwtAndRole(role, publicKey, customErrorFunction, options) {
|
|
143
169
|
return function (req, res, next) {
|
|
144
170
|
try {
|
|
145
|
-
validateAndExtractJwtObject(req, publicKey, customErrorFunction);
|
|
171
|
+
validateAndExtractJwtObject(req, publicKey, customErrorFunction, options);
|
|
146
172
|
const isRoleExist = doesJwtUserHasRole(req, role);
|
|
147
173
|
_isLoginRequired(isRoleExist, customErrorFunction);
|
|
148
174
|
next();
|
|
@@ -152,10 +178,11 @@ function verifyJwtAndRole(role, publicKey, customErrorFunction) {
|
|
|
152
178
|
};
|
|
153
179
|
}
|
|
154
180
|
|
|
155
|
-
|
|
181
|
+
// Pattern: Middleware - verifies the primary JWT and optionally attaches user authorization context.
|
|
182
|
+
function verifyJwt(publicKey, customErrorFunction, options) {
|
|
156
183
|
return function (req, res, next) {
|
|
157
184
|
try {
|
|
158
|
-
validateAndExtractJwtObject(req, publicKey, customErrorFunction);
|
|
185
|
+
validateAndExtractJwtObject(req, publicKey, customErrorFunction, options);
|
|
159
186
|
next();
|
|
160
187
|
} catch (err) {
|
|
161
188
|
next(err);
|
|
@@ -163,10 +190,11 @@ function verifyJwt(publicKey, customErrorFunction) {
|
|
|
163
190
|
};
|
|
164
191
|
}
|
|
165
192
|
|
|
166
|
-
|
|
193
|
+
// Pattern: Middleware - verifies service identity and optionally carries user authorization context.
|
|
194
|
+
function verifyServiceJwt(publicKey, expectedIssuer, expectedAudience, customErrorFunction, options) {
|
|
167
195
|
return function (req, res, next) {
|
|
168
196
|
try {
|
|
169
|
-
validateAndExtractServiceJwtObject(req, publicKey, expectedIssuer, expectedAudience, customErrorFunction);
|
|
197
|
+
validateAndExtractServiceJwtObject(req, publicKey, expectedIssuer, expectedAudience, customErrorFunction, options);
|
|
170
198
|
next();
|
|
171
199
|
} catch (err) {
|
|
172
200
|
next(err);
|
|
@@ -174,10 +202,11 @@ function verifyServiceJwt(publicKey, expectedIssuer, expectedAudience, customErr
|
|
|
174
202
|
};
|
|
175
203
|
}
|
|
176
204
|
|
|
177
|
-
|
|
205
|
+
// Pattern: Middleware - accepts JWT or server-auth and then optionally verifies scoped authorization.
|
|
206
|
+
function verifyJwtOrServerAuth(publicKey, serverAuthIntrospector, customErrorFunction, options) {
|
|
178
207
|
return async function (req, res, next) {
|
|
179
208
|
try {
|
|
180
|
-
await validateAndExtractJwtOrServerAuthObject(req, publicKey, serverAuthIntrospector, customErrorFunction);
|
|
209
|
+
await validateAndExtractJwtOrServerAuthObject(req, publicKey, serverAuthIntrospector, customErrorFunction, options);
|
|
181
210
|
next();
|
|
182
211
|
} catch (err) {
|
|
183
212
|
next(err);
|
|
@@ -185,10 +214,11 @@ function verifyJwtOrServerAuth(publicKey, serverAuthIntrospector, customErrorFun
|
|
|
185
214
|
};
|
|
186
215
|
}
|
|
187
216
|
|
|
188
|
-
|
|
217
|
+
// Pattern: Middleware - composes flexible auth, optional user authorization, and role checks.
|
|
218
|
+
function verifyJwtOrServerAuthAndHasRole(role, publicKey, serverAuthIntrospector, customErrorFunction, options) {
|
|
189
219
|
return async function (req, res, next) {
|
|
190
220
|
try {
|
|
191
|
-
await validateAndExtractJwtOrServerAuthObject(req, publicKey, serverAuthIntrospector, customErrorFunction);
|
|
221
|
+
await validateAndExtractJwtOrServerAuthObject(req, publicKey, serverAuthIntrospector, customErrorFunction, options);
|
|
192
222
|
const isRoleExist = doesJwtUserHasRole(req, role);
|
|
193
223
|
_isLoginRequired(isRoleExist, customErrorFunction);
|
|
194
224
|
next();
|
|
@@ -198,10 +228,23 @@ function verifyJwtOrServerAuthAndHasRole(role, publicKey, serverAuthIntrospector
|
|
|
198
228
|
};
|
|
199
229
|
}
|
|
200
230
|
|
|
201
|
-
|
|
231
|
+
// Pattern: Middleware - verifies a custom-header JWT and optionally scoped user authorization.
|
|
232
|
+
function verifyWebToken(publicKey, headerName, customErrorFunction, options) {
|
|
233
|
+
return function (req, res, next) {
|
|
234
|
+
try {
|
|
235
|
+
validateAndExtractWebToken(req, publicKey, headerName, customErrorFunction, options);
|
|
236
|
+
next();
|
|
237
|
+
} catch (err) {
|
|
238
|
+
next(err);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Pattern: Middleware - preserves no-throw JWT behavior while clearing invalid optional context.
|
|
244
|
+
function verifyJwtNoThrow(publicKey, options) {
|
|
202
245
|
return function (req, res, next) {
|
|
203
246
|
try {
|
|
204
|
-
|
|
247
|
+
validateAndExtractJwtObjectNoThrow(req, publicKey, options);
|
|
205
248
|
next();
|
|
206
249
|
} catch (err) {
|
|
207
250
|
next(err);
|
|
@@ -209,10 +252,11 @@ function verifyWebToken(publicKey, headerName, customErrorFunction) {
|
|
|
209
252
|
};
|
|
210
253
|
}
|
|
211
254
|
|
|
212
|
-
|
|
255
|
+
// Pattern: Middleware - preserves custom-header no-throw behavior with optional context extraction.
|
|
256
|
+
function verifyWebTokenNoThrow(publicKey, headerName, options) {
|
|
213
257
|
return function (req, res, next) {
|
|
214
258
|
try {
|
|
215
|
-
|
|
259
|
+
validateAndExtractWebTokenObjectNoThrow(req, publicKey, headerName, options);
|
|
216
260
|
next();
|
|
217
261
|
} catch (err) {
|
|
218
262
|
next(err);
|
|
@@ -220,10 +264,11 @@ function verifyJwtNoThrow(publicKey) {
|
|
|
220
264
|
};
|
|
221
265
|
}
|
|
222
266
|
|
|
223
|
-
|
|
267
|
+
// Pattern: Middleware - keeps visitor extraction no-throw and independently reads optional context.
|
|
268
|
+
function verifyVisitorNoThrow(publicKey, options) {
|
|
224
269
|
return function (req, res, next) {
|
|
225
270
|
try {
|
|
226
|
-
|
|
271
|
+
validateAndExtractVisitorObjectNoThrow(req, publicKey, options);
|
|
227
272
|
next();
|
|
228
273
|
} catch (err) {
|
|
229
274
|
next(err);
|
|
@@ -231,10 +276,11 @@ function verifyWebTokenNoThrow(publicKey, headerName) {
|
|
|
231
276
|
};
|
|
232
277
|
}
|
|
233
278
|
|
|
234
|
-
|
|
279
|
+
// Pattern: Middleware - verifies only the scoped user authorization token.
|
|
280
|
+
function verifyUserAuthorization(publicKey, customErrorFunction, options) {
|
|
235
281
|
return function (req, res, next) {
|
|
236
282
|
try {
|
|
237
|
-
|
|
283
|
+
validateAndExtractUserAuthorizationObject(req, publicKey, customErrorFunction, options);
|
|
238
284
|
next();
|
|
239
285
|
} catch (err) {
|
|
240
286
|
next(err);
|
|
@@ -242,6 +288,88 @@ function verifyVisitorNoThrow(publicKey) {
|
|
|
242
288
|
};
|
|
243
289
|
}
|
|
244
290
|
|
|
291
|
+
// Pattern: Middleware - reads scoped user authorization without throwing for invalid tokens.
|
|
292
|
+
function verifyUserAuthorizationNoThrow(publicKey, options) {
|
|
293
|
+
return function (req, res, next) {
|
|
294
|
+
try {
|
|
295
|
+
validateAndExtractUserAuthorizationObjectNoThrow(req, publicKey, options);
|
|
296
|
+
next();
|
|
297
|
+
} catch (err) {
|
|
298
|
+
next(err);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Pattern: Single Responsibility - validates and attaches only the scoped user authorization token.
|
|
304
|
+
function validateAndExtractUserAuthorizationObject(req, publicKey, customErrorFunction, options) {
|
|
305
|
+
const config = createUserAuthorizationConfig(publicKey, options);
|
|
306
|
+
const header = readUserAuthorizationHeader(req, config);
|
|
307
|
+
|
|
308
|
+
if (header.present && isUserAuthorizationTokenAllowed(header.token, config)) {
|
|
309
|
+
_extractUserAuthorizationObjectNoThrow(req, header.token);
|
|
310
|
+
} else {
|
|
311
|
+
if (req) req.userAuthorization = null;
|
|
312
|
+
throwError(customErrorFunction);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return req;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Pattern: Single Responsibility - clears invalid scoped authorization without changing primary auth state.
|
|
319
|
+
function validateAndExtractUserAuthorizationObjectNoThrow(req, publicKey, options) {
|
|
320
|
+
const config = createUserAuthorizationConfig(publicKey, options);
|
|
321
|
+
const header = readUserAuthorizationHeader(req, config);
|
|
322
|
+
|
|
323
|
+
if (header.present && isUserAuthorizationTokenAllowed(header.token, config)) {
|
|
324
|
+
_extractUserAuthorizationObjectNoThrow(req, header.token);
|
|
325
|
+
} else if (req) {
|
|
326
|
+
req.userAuthorization = null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return req;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Pattern: Guard Clause - skips optional context handling unless a caller explicitly configured it.
|
|
333
|
+
function validateAndExtractOptionalUserAuthorizationObject(req, options, customErrorFunction) {
|
|
334
|
+
const config = createOptionalUserAuthorizationConfig(options);
|
|
335
|
+
if (!config) return req;
|
|
336
|
+
|
|
337
|
+
const header = readUserAuthorizationHeader(req, config);
|
|
338
|
+
if (!header.present) {
|
|
339
|
+
/* istanbul ignore else */
|
|
340
|
+
if (req) req.userAuthorization = null;
|
|
341
|
+
return req;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (isUserAuthorizationTokenAllowed(header.token, config)) {
|
|
345
|
+
_extractUserAuthorizationObjectNoThrow(req, header.token);
|
|
346
|
+
} else {
|
|
347
|
+
/* istanbul ignore else */
|
|
348
|
+
if (req) req.userAuthorization = null;
|
|
349
|
+
throwError(customErrorFunction);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return req;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Pattern: Guard Clause - optional no-throw context extraction never changes primary auth results.
|
|
356
|
+
function validateAndExtractOptionalUserAuthorizationObjectNoThrow(req, options) {
|
|
357
|
+
const config = createOptionalUserAuthorizationConfig(options);
|
|
358
|
+
if (!config) return req;
|
|
359
|
+
|
|
360
|
+
const header = readUserAuthorizationHeader(req, config);
|
|
361
|
+
if (header.present && isUserAuthorizationTokenAllowed(header.token, config)) {
|
|
362
|
+
_extractUserAuthorizationObjectNoThrow(req, header.token);
|
|
363
|
+
} else {
|
|
364
|
+
/* istanbul ignore else */
|
|
365
|
+
if (req) {
|
|
366
|
+
req.userAuthorization = null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return req;
|
|
371
|
+
}
|
|
372
|
+
|
|
245
373
|
function throwUsedTokenError() {
|
|
246
374
|
throw new Error('Used_Token');
|
|
247
375
|
}
|
|
@@ -261,6 +389,7 @@ function tryValidateAndExtractJwtObject(req, publicKey) {
|
|
|
261
389
|
|
|
262
390
|
const isJwtSignatureValid = jwtVerifySignedToken(jwtString, publicKey);
|
|
263
391
|
if (!isJwtSignatureValid) {
|
|
392
|
+
/* istanbul ignore else */
|
|
264
393
|
if (req) req.jwt = null;
|
|
265
394
|
return false;
|
|
266
395
|
}
|
|
@@ -371,6 +500,91 @@ function isJwtPayloadNotYetValid(payload) {
|
|
|
371
500
|
return Math.floor(Date.now() / 1000) < nbf;
|
|
372
501
|
}
|
|
373
502
|
|
|
503
|
+
// Pattern: Factory - normalizes direct user authorization options into one config shape.
|
|
504
|
+
function createUserAuthorizationConfig(publicKey, options) {
|
|
505
|
+
return normalizeUserAuthorizationConfig({
|
|
506
|
+
...(options || {}),
|
|
507
|
+
publicKey,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Pattern: Factory - detects optional user authorization configuration without affecting legacy callers.
|
|
512
|
+
function createOptionalUserAuthorizationConfig(options) {
|
|
513
|
+
if (!options || !Object.prototype.hasOwnProperty.call(options, 'userAuthorization')) return null;
|
|
514
|
+
return normalizeUserAuthorizationConfig(options.userAuthorization || {});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Pattern: Pure Function - centralizes defaults for scoped authorization header verification.
|
|
518
|
+
function normalizeUserAuthorizationConfig(options) {
|
|
519
|
+
return {
|
|
520
|
+
publicKey: options?.publicKey,
|
|
521
|
+
headerName: options?.headerName || DEFAULT_USER_AUTHORIZATION_HEADER_NAME,
|
|
522
|
+
maxTokenLength: normalizePositiveInteger(options?.maxTokenLength, DEFAULT_USER_AUTHORIZATION_MAX_TOKEN_LENGTH),
|
|
523
|
+
expectedType: options?.expectedType,
|
|
524
|
+
expectedIssuer: options?.expectedIssuer,
|
|
525
|
+
expectedAudience: options?.expectedAudience,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Pattern: Pure Function - keeps numeric option validation deterministic and local.
|
|
530
|
+
function normalizePositiveInteger(value, defaultValue) {
|
|
531
|
+
const normalized = Number(value);
|
|
532
|
+
return Number.isInteger(normalized) && normalized > 0 ? normalized : defaultValue;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Pattern: Single Responsibility - reads only the configured header and never logs token material.
|
|
536
|
+
function readUserAuthorizationHeader(req, config) {
|
|
537
|
+
const rawToken = getRequestHeader(req, config.headerName);
|
|
538
|
+
const present = rawToken !== undefined && rawToken !== null;
|
|
539
|
+
if (!present || typeof rawToken !== 'string') return { present, token: null };
|
|
540
|
+
|
|
541
|
+
const token = rawToken.trim();
|
|
542
|
+
if (!token || token.length > config.maxTokenLength || !isJwtString(token)) return { present: true, token: null };
|
|
543
|
+
return { present: true, token };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Pattern: Adapter - supports Express case-insensitive headers and simple test doubles.
|
|
547
|
+
function getRequestHeader(req, headerName) {
|
|
548
|
+
const value = req?.get?.(headerName);
|
|
549
|
+
if (value !== undefined && value !== null) return value;
|
|
550
|
+
|
|
551
|
+
const lowerCaseHeaderName = headerName.toLowerCase();
|
|
552
|
+
if (lowerCaseHeaderName === headerName) return value;
|
|
553
|
+
return req?.get?.(lowerCaseHeaderName);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Pattern: Single Responsibility - verifies signature and registered JWT time claims for user authorization.
|
|
557
|
+
function isUserAuthorizationTokenAllowed(token, config) {
|
|
558
|
+
if (!token || !config.publicKey || !jwtVerifySignedToken(token, config.publicKey)) return false;
|
|
559
|
+
|
|
560
|
+
const payload = readJwtPayloadNoThrow(token);
|
|
561
|
+
if (!payload) return false;
|
|
562
|
+
if (isJwtPayloadIssuedInFuture(payload)) return false;
|
|
563
|
+
if (isJwtPayloadExpired(payload)) return false;
|
|
564
|
+
if (isJwtPayloadNotYetValid(payload)) return false;
|
|
565
|
+
if (config.expectedType && payload.typ !== config.expectedType) return false;
|
|
566
|
+
if (config.expectedIssuer && payload.iss !== config.expectedIssuer) return false;
|
|
567
|
+
if (!expectedAudienceMatches(payload.aud, config.expectedAudience)) return false;
|
|
568
|
+
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Pattern: Pure Function - decodes payload defensively after signature and shape checks.
|
|
573
|
+
function readJwtPayloadNoThrow(token) {
|
|
574
|
+
try {
|
|
575
|
+
return jwtGetHeaderPayload(token)?.payload || null;
|
|
576
|
+
} catch {
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Pattern: Pure Function - supports either one expected audience or a small allowed set.
|
|
582
|
+
function expectedAudienceMatches(actualAudience, expectedAudience) {
|
|
583
|
+
if (expectedAudience === undefined || expectedAudience === null || expectedAudience === '') return true;
|
|
584
|
+
if (Array.isArray(expectedAudience)) return expectedAudience.some(audience => payloadAudienceMatches(actualAudience, audience));
|
|
585
|
+
return payloadAudienceMatches(actualAudience, expectedAudience);
|
|
586
|
+
}
|
|
587
|
+
|
|
374
588
|
function _validateGenericNoThrow(req, headerName, extractor) {
|
|
375
589
|
let jwtRaw = req.get(headerName);
|
|
376
590
|
if (!jwtRaw) return null;
|
|
@@ -429,6 +643,10 @@ function _extractVisitorObjectNoThrow(req, jwt) {
|
|
|
429
643
|
_extractGenericObjectNoThrow(req, jwt, _attachVisitorMethods, 'visitor');
|
|
430
644
|
}
|
|
431
645
|
|
|
646
|
+
function _extractUserAuthorizationObjectNoThrow(req, jwt) {
|
|
647
|
+
_extractGenericObjectNoThrow(req, jwt, () => {}, 'userAuthorization');
|
|
648
|
+
}
|
|
649
|
+
|
|
432
650
|
function _attachJwtMethods(jwtObj) {
|
|
433
651
|
if (jwtObj) {
|
|
434
652
|
Object.defineProperties(jwtObj, {
|
|
@@ -540,6 +758,8 @@ module.exports = {
|
|
|
540
758
|
_isJwtSignatureValidNoThrow,
|
|
541
759
|
_extractJwtObject,
|
|
542
760
|
validateAndExtractJwtObject,
|
|
761
|
+
validateAndExtractUserAuthorizationObject,
|
|
762
|
+
validateAndExtractUserAuthorizationObjectNoThrow,
|
|
543
763
|
validateAndExtractServiceJwtObject,
|
|
544
764
|
validateAndExtractJwtOrServerAuthObject,
|
|
545
765
|
validateAndExtractWebToken,
|
|
@@ -553,6 +773,8 @@ module.exports = {
|
|
|
553
773
|
verifyJwtOrServerAuth,
|
|
554
774
|
verifyJwtOrServerAuthAndHasRole,
|
|
555
775
|
verifyJwt,
|
|
776
|
+
verifyUserAuthorization,
|
|
777
|
+
verifyUserAuthorizationNoThrow,
|
|
556
778
|
verifyWebTokenNoThrow,
|
|
557
779
|
verifyWebToken,
|
|
558
780
|
verifyJwtNoThrow,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carecard/jwt-read",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.18",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/CareCard-ca/pkg-jwt-read.git"
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"typescript": "6.0.3"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
+
"@carecard/auth-util": "3.1.16",
|
|
44
45
|
"@carecard/common-util": "3.1.15",
|
|
45
|
-
"@carecard/
|
|
46
|
-
"@carecard/validate": "3.1.24"
|
|
46
|
+
"@carecard/validate": "3.1.27"
|
|
47
47
|
}
|
|
48
48
|
}
|
package/readme.md
CHANGED
|
@@ -18,6 +18,9 @@ introspected by `ms-auth`.
|
|
|
18
18
|
- **Service JWTs**: Helpers for verifying and extracting microservice-to-microservice JWTs with standard `iss`, `sub`, `aud`, `iat`, and `exp` claims.
|
|
19
19
|
- **JWT or Server Auth**: Middleware helpers that verify normal JWTs locally and
|
|
20
20
|
call a service-provided introspector for opaque server-auth tokens.
|
|
21
|
+
- **Scoped User Authorization**: Optional `X-Authorization-Context`
|
|
22
|
+
verification attaches compact scoped authorization claims to
|
|
23
|
+
`req.userAuthorization` without replacing `req.jwt`.
|
|
21
24
|
|
|
22
25
|
## Installation
|
|
23
26
|
|
|
@@ -133,6 +136,44 @@ those claims onto `req.jwt.payload` with `authMode: "server-auth"` and
|
|
|
133
136
|
`auth_mode: "server-auth"` so services can keep their existing JWT-backed
|
|
134
137
|
database context and role checks.
|
|
135
138
|
|
|
139
|
+
### Scoped User Authorization Context
|
|
140
|
+
|
|
141
|
+
Use `jwtVerifyUserAuthorization` when a route needs to verify only the compact
|
|
142
|
+
authorization-context token carried in `X-Authorization-Context`. The token is
|
|
143
|
+
read as a raw JWT header value, not as `Bearer <token>`.
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
const { jwtVerifyUserAuthorization } = require('@carecard/jwt-read');
|
|
147
|
+
|
|
148
|
+
app.use(
|
|
149
|
+
jwtVerifyUserAuthorization(institutionsPublicKey, throwNotAuthorizedError, {
|
|
150
|
+
expectedType: 'carecard.authorization-context.scoped.v1',
|
|
151
|
+
expectedIssuer: 'ms-institutions',
|
|
152
|
+
expectedAudience: 'ms-documents',
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Existing JWT verification helpers can also read the header by passing an
|
|
158
|
+
optional trailing options object. This preserves current `req.jwt` behavior and
|
|
159
|
+
adds decoded scoped claims to `req.userAuthorization`.
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
const verifyUser = jwtVerifyOrServerAuth(msAuthPublicKey, token => introspectServerAuthTokenWithMsAuth(token), throwNotAuthorizedError, {
|
|
163
|
+
userAuthorization: {
|
|
164
|
+
publicKey: institutionsPublicKey,
|
|
165
|
+
expectedType: 'carecard.authorization-context.scoped.v1',
|
|
166
|
+
expectedIssuer: 'ms-institutions',
|
|
167
|
+
expectedAudience: 'ms-documents',
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
When the optional reader is configured, a missing `X-Authorization-Context`
|
|
173
|
+
leaves `req.userAuthorization` as `null`. If the header is present but invalid,
|
|
174
|
+
throwing middleware fails closed. No-throw middleware clears
|
|
175
|
+
`req.userAuthorization` and continues.
|
|
176
|
+
|
|
136
177
|
## Testing
|
|
137
178
|
|
|
138
179
|
Run tests using:
|