@carecard/jwt-read 3.18.0 → 3.20.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/.agents/skills/carecard-workspace-standards/SKILL.md +49 -20
- package/.agents/skills/github-pr-create-update/SKILL.md +22 -1
- package/.agents/skills/github-pr-merge-cleanup/SKILL.md +22 -1
- package/.agents/skills/pkg-jwt-read-coding-standards-and-best-practices/SKILL.md +42 -7
- package/.agents/skills/pkg-jwt-read-jwt-middleware-library/SKILL.md +39 -12
- package/.agents/skills/pkg-publish/SKILL.md +22 -1
- package/.agents/skills/software-design-patterns-and-clean-code/SKILL.md +23 -3
- package/.prettierrc.js +4 -2
- package/AGENTS.md +19 -0
- package/eslint.config.mjs +52 -0
- package/index.d.ts +5 -1
- package/index.js +2 -1
- package/lib/jwtLib.js +240 -62
- package/lib/jwtRoles.js +6 -2
- package/lint-staged.config.mjs +36 -0
- package/package.json +20 -14
- package/readme.md +22 -1
- package/scripts/canonicalTestCommand.test.mjs +33 -0
- package/scripts/packageTaskRunner.audit.mjs +37 -0
- package/scripts/packageTaskRunner.test.mjs +4 -26
- package/scripts/runPackageTask.mjs +65 -16
- package/scripts/testOrder/randomizeTestOrder.cjs +15 -5
- package/scripts/testOrder/{testOrderPolicy.test.mjs → testOrderPolicy.audit.mjs} +8 -2
- package/scripts/testParallel/{parallelTestPolicy.test.mjs → parallelTestPolicy.audit.mjs} +28 -9
- package/scripts/testParallel/runIndexedMochaTests.cjs +3 -1
- package/scripts/testParallel/runIndexedMochaTests.test.mjs +8 -2
package/lib/jwtLib.js
CHANGED
|
@@ -26,7 +26,12 @@ function doesJwtUserHasRole(req, userRole) {
|
|
|
26
26
|
|
|
27
27
|
const jwtObj = req?.jwt || this;
|
|
28
28
|
|
|
29
|
-
if (
|
|
29
|
+
if (
|
|
30
|
+
!userRole ||
|
|
31
|
+
typeof userRole !== 'string' ||
|
|
32
|
+
!jwtObj?.payload?.roles ||
|
|
33
|
+
!Array.isArray(jwtObj.payload.roles)
|
|
34
|
+
) {
|
|
30
35
|
throwNotAuthorizedError();
|
|
31
36
|
}
|
|
32
37
|
|
|
@@ -40,7 +45,9 @@ function throwError(customErrorFunction) {
|
|
|
40
45
|
|
|
41
46
|
const customError = customErrorFunction();
|
|
42
47
|
|
|
43
|
-
if (customError instanceof Error)
|
|
48
|
+
if (customError instanceof Error) {
|
|
49
|
+
throw customError;
|
|
50
|
+
}
|
|
44
51
|
|
|
45
52
|
throw new Error('Custom authentication error function returned without throwing');
|
|
46
53
|
}
|
|
@@ -90,6 +97,7 @@ function validateAndExtractJwtObject(req, publicKey, customErrorFunction, option
|
|
|
90
97
|
|
|
91
98
|
if (jwtString && isJwtSignatureValid) {
|
|
92
99
|
_extractJwtObject(req, jwtString, customErrorFunction);
|
|
100
|
+
requireActiveExtractedToken(req, 'jwt', customErrorFunction);
|
|
93
101
|
validateAndExtractOptionalUserAuthorizationObject(req, options, customErrorFunction);
|
|
94
102
|
} else {
|
|
95
103
|
req['jwt'] = null;
|
|
@@ -107,6 +115,7 @@ function validateAndExtractWebToken(req, publicKey, headerName, customErrorFunct
|
|
|
107
115
|
|
|
108
116
|
if (webTokenString && isJwtSignatureValid) {
|
|
109
117
|
_extractJwtObject(req, webTokenString, customErrorFunction);
|
|
118
|
+
requireActiveExtractedToken(req, 'jwt', customErrorFunction);
|
|
110
119
|
validateAndExtractOptionalUserAuthorizationObject(req, options, customErrorFunction);
|
|
111
120
|
} else {
|
|
112
121
|
req['jwt'] = null;
|
|
@@ -118,13 +127,26 @@ function validateAndExtractWebToken(req, publicKey, headerName, customErrorFunct
|
|
|
118
127
|
|
|
119
128
|
// Pattern: Decorator - extends no-throw JWT extraction without changing req.jwt failure semantics.
|
|
120
129
|
function validateAndExtractJwtObjectNoThrow(req, publicKey, options) {
|
|
121
|
-
_validateAndExtractGenericNoThrow(
|
|
130
|
+
_validateAndExtractGenericNoThrow(
|
|
131
|
+
req,
|
|
132
|
+
publicKey,
|
|
133
|
+
_validateJwtNoThrow,
|
|
134
|
+
_extractJwtObjectNoThrow,
|
|
135
|
+
'jwt',
|
|
136
|
+
);
|
|
122
137
|
validateAndExtractOptionalUserAuthorizationObjectNoThrow(req, options);
|
|
123
138
|
return req;
|
|
124
139
|
}
|
|
125
140
|
|
|
126
141
|
// Pattern: Decorator - keeps service-JWT checks distinct from optional user authorization context.
|
|
127
|
-
function validateAndExtractServiceJwtObject(
|
|
142
|
+
function validateAndExtractServiceJwtObject(
|
|
143
|
+
req,
|
|
144
|
+
publicKey,
|
|
145
|
+
expectedIssuer,
|
|
146
|
+
expectedAudience,
|
|
147
|
+
customErrorFunction,
|
|
148
|
+
options,
|
|
149
|
+
) {
|
|
128
150
|
const jwtString = _validateJwt(req, customErrorFunction);
|
|
129
151
|
const isJwtSignatureValid = jwtVerifySignedToken(jwtString, publicKey);
|
|
130
152
|
|
|
@@ -144,10 +166,21 @@ function validateAndExtractServiceJwtObject(req, publicKey, expectedIssuer, expe
|
|
|
144
166
|
}
|
|
145
167
|
|
|
146
168
|
// Pattern: Decorator - normalizes primary auth first, then applies optional scoped authorization.
|
|
147
|
-
async function validateAndExtractJwtOrServerAuthObject(
|
|
169
|
+
async function validateAndExtractJwtOrServerAuthObject(
|
|
170
|
+
req,
|
|
171
|
+
publicKey,
|
|
172
|
+
serverAuthIntrospector,
|
|
173
|
+
customErrorFunction,
|
|
174
|
+
options,
|
|
175
|
+
) {
|
|
148
176
|
if (!tryValidateAndExtractJwtObject(req, publicKey)) {
|
|
149
177
|
const serverAuthToken = _validateServerAuthToken(req, customErrorFunction);
|
|
150
|
-
const claims = await introspectServerAuthToken(
|
|
178
|
+
const claims = await introspectServerAuthToken(
|
|
179
|
+
serverAuthIntrospector,
|
|
180
|
+
serverAuthToken,
|
|
181
|
+
req,
|
|
182
|
+
customErrorFunction,
|
|
183
|
+
);
|
|
151
184
|
attachServerAuthClaims(req, claims, customErrorFunction);
|
|
152
185
|
}
|
|
153
186
|
|
|
@@ -157,14 +190,26 @@ async function validateAndExtractJwtOrServerAuthObject(req, publicKey, serverAut
|
|
|
157
190
|
|
|
158
191
|
// Pattern: Decorator - extends custom-header no-throw extraction with optional user authorization context.
|
|
159
192
|
function validateAndExtractWebTokenObjectNoThrow(req, publicKey, headerName, options) {
|
|
160
|
-
_validateAndExtractGenericNoThrow(
|
|
193
|
+
_validateAndExtractGenericNoThrow(
|
|
194
|
+
req,
|
|
195
|
+
publicKey,
|
|
196
|
+
r => _validateWebTokenNoThrow(r, headerName),
|
|
197
|
+
_extractJwtObjectNoThrow,
|
|
198
|
+
'jwt',
|
|
199
|
+
);
|
|
161
200
|
validateAndExtractOptionalUserAuthorizationObjectNoThrow(req, options);
|
|
162
201
|
return req;
|
|
163
202
|
}
|
|
164
203
|
|
|
165
204
|
// Pattern: Decorator - keeps visitor token extraction independent from optional user authorization context.
|
|
166
205
|
function validateAndExtractVisitorObjectNoThrow(req, publicKey, options) {
|
|
167
|
-
_validateAndExtractGenericNoThrow(
|
|
206
|
+
_validateAndExtractGenericNoThrow(
|
|
207
|
+
req,
|
|
208
|
+
publicKey,
|
|
209
|
+
_validateVisitorNoThrow,
|
|
210
|
+
_extractVisitorObjectNoThrow,
|
|
211
|
+
'visitor',
|
|
212
|
+
);
|
|
168
213
|
validateAndExtractOptionalUserAuthorizationObjectNoThrow(req, options);
|
|
169
214
|
return req;
|
|
170
215
|
}
|
|
@@ -196,10 +241,23 @@ function verifyJwt(publicKey, customErrorFunction, options) {
|
|
|
196
241
|
}
|
|
197
242
|
|
|
198
243
|
// Pattern: Middleware - verifies service identity and optionally carries user authorization context.
|
|
199
|
-
function verifyServiceJwt(
|
|
244
|
+
function verifyServiceJwt(
|
|
245
|
+
publicKey,
|
|
246
|
+
expectedIssuer,
|
|
247
|
+
expectedAudience,
|
|
248
|
+
customErrorFunction,
|
|
249
|
+
options,
|
|
250
|
+
) {
|
|
200
251
|
return function (req, res, next) {
|
|
201
252
|
try {
|
|
202
|
-
validateAndExtractServiceJwtObject(
|
|
253
|
+
validateAndExtractServiceJwtObject(
|
|
254
|
+
req,
|
|
255
|
+
publicKey,
|
|
256
|
+
expectedIssuer,
|
|
257
|
+
expectedAudience,
|
|
258
|
+
customErrorFunction,
|
|
259
|
+
options,
|
|
260
|
+
);
|
|
203
261
|
next();
|
|
204
262
|
} catch (err) {
|
|
205
263
|
next(err);
|
|
@@ -211,7 +269,13 @@ function verifyServiceJwt(publicKey, expectedIssuer, expectedAudience, customErr
|
|
|
211
269
|
function verifyJwtOrServerAuth(publicKey, serverAuthIntrospector, customErrorFunction, options) {
|
|
212
270
|
return async function (req, res, next) {
|
|
213
271
|
try {
|
|
214
|
-
await validateAndExtractJwtOrServerAuthObject(
|
|
272
|
+
await validateAndExtractJwtOrServerAuthObject(
|
|
273
|
+
req,
|
|
274
|
+
publicKey,
|
|
275
|
+
serverAuthIntrospector,
|
|
276
|
+
customErrorFunction,
|
|
277
|
+
options,
|
|
278
|
+
);
|
|
215
279
|
next();
|
|
216
280
|
} catch (err) {
|
|
217
281
|
next(err);
|
|
@@ -220,10 +284,22 @@ function verifyJwtOrServerAuth(publicKey, serverAuthIntrospector, customErrorFun
|
|
|
220
284
|
}
|
|
221
285
|
|
|
222
286
|
// Pattern: Middleware - composes flexible auth, optional user authorization, and role checks.
|
|
223
|
-
function verifyJwtOrServerAuthAndHasRole(
|
|
287
|
+
function verifyJwtOrServerAuthAndHasRole(
|
|
288
|
+
role,
|
|
289
|
+
publicKey,
|
|
290
|
+
serverAuthIntrospector,
|
|
291
|
+
customErrorFunction,
|
|
292
|
+
options,
|
|
293
|
+
) {
|
|
224
294
|
return async function (req, res, next) {
|
|
225
295
|
try {
|
|
226
|
-
await validateAndExtractJwtOrServerAuthObject(
|
|
296
|
+
await validateAndExtractJwtOrServerAuthObject(
|
|
297
|
+
req,
|
|
298
|
+
publicKey,
|
|
299
|
+
serverAuthIntrospector,
|
|
300
|
+
customErrorFunction,
|
|
301
|
+
options,
|
|
302
|
+
);
|
|
227
303
|
const isRoleExist = doesJwtUserHasRole(req, role);
|
|
228
304
|
_isLoginRequired(isRoleExist, customErrorFunction);
|
|
229
305
|
next();
|
|
@@ -313,7 +389,9 @@ function validateAndExtractUserAuthorizationObject(req, publicKey, customErrorFu
|
|
|
313
389
|
if (header.present && isUserAuthorizationTokenAllowed(header.token, config)) {
|
|
314
390
|
_extractUserAuthorizationObjectNoThrow(req, header.token);
|
|
315
391
|
} else {
|
|
316
|
-
if (req)
|
|
392
|
+
if (req) {
|
|
393
|
+
req.userAuthorization = null;
|
|
394
|
+
}
|
|
317
395
|
throwError(customErrorFunction);
|
|
318
396
|
}
|
|
319
397
|
|
|
@@ -337,7 +415,9 @@ function validateAndExtractUserAuthorizationObjectNoThrow(req, publicKey, option
|
|
|
337
415
|
// Pattern: Guard Clause - skips optional context handling unless a caller explicitly configured it.
|
|
338
416
|
function validateAndExtractOptionalUserAuthorizationObject(req, options, customErrorFunction) {
|
|
339
417
|
const config = createOptionalUserAuthorizationConfig(options);
|
|
340
|
-
if (!config)
|
|
418
|
+
if (!config) {
|
|
419
|
+
return req;
|
|
420
|
+
}
|
|
341
421
|
|
|
342
422
|
const header = readUserAuthorizationHeader(req, config);
|
|
343
423
|
if (!header.present) {
|
|
@@ -358,7 +438,9 @@ function validateAndExtractOptionalUserAuthorizationObject(req, options, customE
|
|
|
358
438
|
// Pattern: Guard Clause - optional no-throw context extraction never changes primary auth results.
|
|
359
439
|
function validateAndExtractOptionalUserAuthorizationObjectNoThrow(req, options) {
|
|
360
440
|
const config = createOptionalUserAuthorizationConfig(options);
|
|
361
|
-
if (!config)
|
|
441
|
+
if (!config) {
|
|
442
|
+
return req;
|
|
443
|
+
}
|
|
362
444
|
|
|
363
445
|
const header = readUserAuthorizationHeader(req, config);
|
|
364
446
|
if (header.present && isUserAuthorizationTokenAllowed(header.token, config)) {
|
|
@@ -383,9 +465,12 @@ function _isLoginRequired(hasRequiredRole, customErrorFunction) {
|
|
|
383
465
|
}
|
|
384
466
|
}
|
|
385
467
|
|
|
468
|
+
// Pattern: Fail-Closed Probe - accepts a bearer JWT only when signature and lifetime are valid.
|
|
386
469
|
function tryValidateAndExtractJwtObject(req, publicKey) {
|
|
387
470
|
const jwtString = _validateJwtNoThrow(req);
|
|
388
|
-
if (!jwtString || !isJwtString(jwtString))
|
|
471
|
+
if (!jwtString || !isJwtString(jwtString)) {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
389
474
|
|
|
390
475
|
const isJwtSignatureValid = jwtVerifySignedToken(jwtString, publicKey);
|
|
391
476
|
if (!isJwtSignatureValid) {
|
|
@@ -394,13 +479,19 @@ function tryValidateAndExtractJwtObject(req, publicKey) {
|
|
|
394
479
|
}
|
|
395
480
|
|
|
396
481
|
_extractJwtObjectNoThrow(req, jwtString);
|
|
397
|
-
|
|
482
|
+
if (!isJwtPayloadWithinLifetime(req?.jwt?.payload)) {
|
|
483
|
+
req.jwt = null;
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
return true;
|
|
398
487
|
}
|
|
399
488
|
|
|
400
489
|
function _validateServerAuthToken(req, customErrorFunction) {
|
|
401
490
|
const authorizationHeader = req?.get?.('Authorization') || req?.get?.('authorization');
|
|
402
491
|
const token = _extractJwtNoThrow(authorizationHeader);
|
|
403
|
-
if (token)
|
|
492
|
+
if (token) {
|
|
493
|
+
return token;
|
|
494
|
+
}
|
|
404
495
|
|
|
405
496
|
throwError(customErrorFunction);
|
|
406
497
|
return null;
|
|
@@ -422,7 +513,9 @@ async function introspectServerAuthToken(serverAuthIntrospector, token, req, cus
|
|
|
422
513
|
function attachServerAuthClaims(req, claims, customErrorFunction) {
|
|
423
514
|
const payload = createServerAuthPayload(claims);
|
|
424
515
|
if (!payload.sub) {
|
|
425
|
-
if (req)
|
|
516
|
+
if (req) {
|
|
517
|
+
req.jwt = null;
|
|
518
|
+
}
|
|
426
519
|
throwError(customErrorFunction);
|
|
427
520
|
}
|
|
428
521
|
|
|
@@ -439,10 +532,9 @@ function attachServerAuthClaims(req, claims, customErrorFunction) {
|
|
|
439
532
|
// Pattern: Projection - copies only authoritative verification names and preserves their exact values.
|
|
440
533
|
function pickEmailVerificationClaims(claims) {
|
|
441
534
|
return Object.fromEntries(
|
|
442
|
-
EMAIL_VERIFICATION_CLAIM_NAMES.filter(claimName =>
|
|
443
|
-
claimName,
|
|
444
|
-
|
|
445
|
-
]),
|
|
535
|
+
EMAIL_VERIFICATION_CLAIM_NAMES.filter(claimName =>
|
|
536
|
+
Object.prototype.hasOwnProperty.call(claims, claimName),
|
|
537
|
+
).map(claimName => [claimName, claims[claimName]]),
|
|
446
538
|
);
|
|
447
539
|
}
|
|
448
540
|
|
|
@@ -462,54 +554,101 @@ function createServerAuthPayload(claims) {
|
|
|
462
554
|
}
|
|
463
555
|
|
|
464
556
|
function readEpochSeconds(value) {
|
|
465
|
-
if (value === undefined || value === null || value === '')
|
|
466
|
-
|
|
557
|
+
if (value === undefined || value === null || value === '') {
|
|
558
|
+
return undefined;
|
|
559
|
+
}
|
|
560
|
+
if (typeof value === 'number') {
|
|
561
|
+
return normalizeSeconds(value);
|
|
562
|
+
}
|
|
467
563
|
const millis = Date.parse(value);
|
|
468
564
|
return Number.isFinite(millis) ? Math.floor(millis / 1000) : undefined;
|
|
469
565
|
}
|
|
470
566
|
|
|
471
567
|
function normalizeSeconds(value) {
|
|
472
|
-
if (!Number.isFinite(value))
|
|
568
|
+
if (!Number.isFinite(value)) {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
473
571
|
return value > 1000000000000 ? Math.floor(value / 1000) : Math.floor(value);
|
|
474
572
|
}
|
|
475
573
|
|
|
574
|
+
// Pattern: Policy - validates service identity, audience, and registered token lifetime.
|
|
476
575
|
function _isServiceJwtFor(payload, expectedIssuer, expectedAudience) {
|
|
477
|
-
if (!payload)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if (
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (
|
|
484
|
-
|
|
576
|
+
if (!payload) {
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
if (payload.iss !== expectedIssuer) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
if (payload.sub !== expectedIssuer) {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
if (!payloadAudienceMatches(payload.aud, expectedAudience)) {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
return isJwtPayloadWithinLifetime(payload);
|
|
485
589
|
}
|
|
486
590
|
|
|
487
591
|
function payloadAudienceMatches(actualAudience, expectedAudience) {
|
|
488
|
-
if (Array.isArray(actualAudience))
|
|
592
|
+
if (Array.isArray(actualAudience)) {
|
|
593
|
+
return actualAudience.includes(expectedAudience);
|
|
594
|
+
}
|
|
489
595
|
return actualAudience === expectedAudience;
|
|
490
596
|
}
|
|
491
597
|
|
|
492
598
|
function isJwtPayloadExpired(payload) {
|
|
493
|
-
if (!payload.exp)
|
|
599
|
+
if (!payload.exp) {
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
494
602
|
const exp = normalizeSeconds(payload.exp);
|
|
495
|
-
if (!Number.isInteger(exp))
|
|
603
|
+
if (!Number.isInteger(exp)) {
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
496
606
|
return Math.floor(Date.now() / 1000) >= exp;
|
|
497
607
|
}
|
|
498
608
|
|
|
499
609
|
function isJwtPayloadIssuedInFuture(payload) {
|
|
500
|
-
if (!payload.iat)
|
|
610
|
+
if (!payload.iat) {
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
501
613
|
const iat = normalizeSeconds(payload.iat);
|
|
502
|
-
if (!Number.isInteger(iat))
|
|
614
|
+
if (!Number.isInteger(iat)) {
|
|
615
|
+
return true;
|
|
616
|
+
}
|
|
503
617
|
return Math.floor(Date.now() / 1000) < iat;
|
|
504
618
|
}
|
|
505
619
|
|
|
506
620
|
function isJwtPayloadNotYetValid(payload) {
|
|
507
|
-
if (!payload.nbf)
|
|
621
|
+
if (!payload.nbf) {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
508
624
|
const nbf = normalizeSeconds(payload.nbf);
|
|
509
|
-
if (!Number.isInteger(nbf))
|
|
625
|
+
if (!Number.isInteger(nbf)) {
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
510
628
|
return Math.floor(Date.now() / 1000) < nbf;
|
|
511
629
|
}
|
|
512
630
|
|
|
631
|
+
// Pattern: Specification - centralizes the registered time-claim policy for signed tokens.
|
|
632
|
+
function isJwtPayloadWithinLifetime(payload) {
|
|
633
|
+
return (
|
|
634
|
+
Boolean(payload) &&
|
|
635
|
+
!isJwtPayloadIssuedInFuture(payload) &&
|
|
636
|
+
!isJwtPayloadExpired(payload) &&
|
|
637
|
+
!isJwtPayloadNotYetValid(payload)
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Pattern: Guard Clause - clears an extracted token before surfacing an authentication failure.
|
|
642
|
+
function requireActiveExtractedToken(req, propertyName, customErrorFunction) {
|
|
643
|
+
if (isJwtPayloadWithinLifetime(req?.[propertyName]?.payload)) {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (req) {
|
|
647
|
+
req[propertyName] = null;
|
|
648
|
+
}
|
|
649
|
+
throwError(customErrorFunction);
|
|
650
|
+
}
|
|
651
|
+
|
|
513
652
|
// Pattern: Factory - normalizes direct user authorization options into one config shape.
|
|
514
653
|
function createUserAuthorizationConfig(publicKey, options) {
|
|
515
654
|
return normalizeUserAuthorizationConfig({
|
|
@@ -520,7 +659,9 @@ function createUserAuthorizationConfig(publicKey, options) {
|
|
|
520
659
|
|
|
521
660
|
// Pattern: Factory - detects optional user authorization configuration without affecting legacy callers.
|
|
522
661
|
function createOptionalUserAuthorizationConfig(options) {
|
|
523
|
-
if (!options || !Object.prototype.hasOwnProperty.call(options, 'userAuthorization'))
|
|
662
|
+
if (!options || !Object.prototype.hasOwnProperty.call(options, 'userAuthorization')) {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
524
665
|
return normalizeUserAuthorizationConfig(options.userAuthorization || {});
|
|
525
666
|
}
|
|
526
667
|
|
|
@@ -529,7 +670,10 @@ function normalizeUserAuthorizationConfig(options) {
|
|
|
529
670
|
return {
|
|
530
671
|
publicKey: options?.publicKey,
|
|
531
672
|
headerName: options?.headerName || DEFAULT_USER_AUTHORIZATION_HEADER_NAME,
|
|
532
|
-
maxTokenLength: normalizePositiveInteger(
|
|
673
|
+
maxTokenLength: normalizePositiveInteger(
|
|
674
|
+
options?.maxTokenLength,
|
|
675
|
+
DEFAULT_USER_AUTHORIZATION_MAX_TOKEN_LENGTH,
|
|
676
|
+
),
|
|
533
677
|
expectedType: options?.expectedType,
|
|
534
678
|
expectedIssuer: options?.expectedIssuer,
|
|
535
679
|
expectedAudience: options?.expectedAudience,
|
|
@@ -546,35 +690,53 @@ function normalizePositiveInteger(value, defaultValue) {
|
|
|
546
690
|
function readUserAuthorizationHeader(req, config) {
|
|
547
691
|
const rawToken = getRequestHeader(req, config.headerName);
|
|
548
692
|
const present = rawToken !== undefined && rawToken !== null;
|
|
549
|
-
if (!present || typeof rawToken !== 'string')
|
|
693
|
+
if (!present || typeof rawToken !== 'string') {
|
|
694
|
+
return { present, token: null };
|
|
695
|
+
}
|
|
550
696
|
|
|
551
697
|
const token = rawToken.trim();
|
|
552
|
-
if (!token || token.length > config.maxTokenLength || !isJwtString(token))
|
|
698
|
+
if (!token || token.length > config.maxTokenLength || !isJwtString(token)) {
|
|
699
|
+
return { present: true, token: null };
|
|
700
|
+
}
|
|
553
701
|
return { present: true, token };
|
|
554
702
|
}
|
|
555
703
|
|
|
556
704
|
// Pattern: Adapter - supports Express case-insensitive headers and simple test doubles.
|
|
557
705
|
function getRequestHeader(req, headerName) {
|
|
558
706
|
const value = req?.get?.(headerName);
|
|
559
|
-
if (value !== undefined && value !== null)
|
|
707
|
+
if (value !== undefined && value !== null) {
|
|
708
|
+
return value;
|
|
709
|
+
}
|
|
560
710
|
|
|
561
711
|
const lowerCaseHeaderName = headerName.toLowerCase();
|
|
562
|
-
if (lowerCaseHeaderName === headerName)
|
|
712
|
+
if (lowerCaseHeaderName === headerName) {
|
|
713
|
+
return value;
|
|
714
|
+
}
|
|
563
715
|
return req?.get?.(lowerCaseHeaderName);
|
|
564
716
|
}
|
|
565
717
|
|
|
566
718
|
// Pattern: Single Responsibility - verifies signature and registered JWT time claims for user authorization.
|
|
567
719
|
function isUserAuthorizationTokenAllowed(token, config) {
|
|
568
|
-
if (!token || !config.publicKey || !jwtVerifySignedToken(token, config.publicKey))
|
|
720
|
+
if (!token || !config.publicKey || !jwtVerifySignedToken(token, config.publicKey)) {
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
569
723
|
|
|
570
724
|
const payload = readVerifiedJwtPayload(token);
|
|
571
|
-
if (!payload)
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
if (
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
if (
|
|
725
|
+
if (!payload) {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
if (!isJwtPayloadWithinLifetime(payload)) {
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
if (config.expectedType && payload.typ !== config.expectedType) {
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
if (config.expectedIssuer && payload.iss !== config.expectedIssuer) {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
if (!expectedAudienceMatches(payload.aud, config.expectedAudience)) {
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
578
740
|
|
|
579
741
|
return true;
|
|
580
742
|
}
|
|
@@ -586,14 +748,20 @@ function readVerifiedJwtPayload(token) {
|
|
|
586
748
|
|
|
587
749
|
// Pattern: Pure Function - supports either one expected audience or a small allowed set.
|
|
588
750
|
function expectedAudienceMatches(actualAudience, expectedAudience) {
|
|
589
|
-
if (expectedAudience === undefined || expectedAudience === null || expectedAudience === '')
|
|
590
|
-
|
|
751
|
+
if (expectedAudience === undefined || expectedAudience === null || expectedAudience === '') {
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
if (Array.isArray(expectedAudience)) {
|
|
755
|
+
return expectedAudience.some(audience => payloadAudienceMatches(actualAudience, audience));
|
|
756
|
+
}
|
|
591
757
|
return payloadAudienceMatches(actualAudience, expectedAudience);
|
|
592
758
|
}
|
|
593
759
|
|
|
594
760
|
function _validateGenericNoThrow(req, headerName, extractor) {
|
|
595
761
|
let jwtRaw = req.get(headerName);
|
|
596
|
-
if (!jwtRaw)
|
|
762
|
+
if (!jwtRaw) {
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
597
765
|
let jwt = extractor(jwtRaw);
|
|
598
766
|
return isJwtString(jwt) ? jwt : null;
|
|
599
767
|
}
|
|
@@ -601,11 +769,14 @@ function _validateGenericNoThrow(req, headerName, extractor) {
|
|
|
601
769
|
function _validateGeneric(req, headerName, extractor, customErrorFunction) {
|
|
602
770
|
let jwtRaw = req.get(headerName);
|
|
603
771
|
let jwt = extractor(jwtRaw, customErrorFunction);
|
|
604
|
-
if (isJwtString(jwt))
|
|
772
|
+
if (isJwtString(jwt)) {
|
|
773
|
+
return jwt;
|
|
774
|
+
}
|
|
605
775
|
throwError(customErrorFunction);
|
|
606
776
|
return null;
|
|
607
777
|
}
|
|
608
778
|
|
|
779
|
+
// Pattern: Fail-Closed Extraction - optional tokens survive only valid signatures and lifetimes.
|
|
609
780
|
function _validateAndExtractGenericNoThrow(req, publicKey, validator, extractor, propertyName) {
|
|
610
781
|
try {
|
|
611
782
|
const jwtString = validator(req);
|
|
@@ -613,11 +784,16 @@ function _validateAndExtractGenericNoThrow(req, publicKey, validator, extractor,
|
|
|
613
784
|
|
|
614
785
|
if (jwtString && isJwtSignatureValid) {
|
|
615
786
|
extractor(req, jwtString);
|
|
787
|
+
if (!isJwtPayloadWithinLifetime(req?.[propertyName]?.payload)) {
|
|
788
|
+
req[propertyName] = null;
|
|
789
|
+
}
|
|
616
790
|
} else {
|
|
617
791
|
req[propertyName] = null;
|
|
618
792
|
}
|
|
619
793
|
} catch (err) {
|
|
620
|
-
if (req)
|
|
794
|
+
if (req) {
|
|
795
|
+
req[propertyName] = null;
|
|
796
|
+
}
|
|
621
797
|
throw err;
|
|
622
798
|
}
|
|
623
799
|
return req;
|
|
@@ -629,7 +805,9 @@ function _extractGenericObjectNoThrow(req, jwt, attacher, propertyName) {
|
|
|
629
805
|
attacher(obj);
|
|
630
806
|
req[propertyName] = obj;
|
|
631
807
|
} else {
|
|
632
|
-
if (req)
|
|
808
|
+
if (req) {
|
|
809
|
+
req[propertyName] = null;
|
|
810
|
+
}
|
|
633
811
|
}
|
|
634
812
|
}
|
|
635
813
|
|
package/lib/jwtRoles.js
CHANGED
|
@@ -44,8 +44,12 @@ function getContext(req) {
|
|
|
44
44
|
|
|
45
45
|
function getUserAuthorization(req) {
|
|
46
46
|
const userAuthorization = req?.userAuthorization;
|
|
47
|
-
if (!userAuthorization || typeof userAuthorization !== 'object')
|
|
48
|
-
|
|
47
|
+
if (!userAuthorization || typeof userAuthorization !== 'object') {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (!userAuthorization.payload || typeof userAuthorization.payload !== 'object') {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
49
53
|
|
|
50
54
|
return userAuthorization;
|
|
51
55
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ESLint } from 'eslint';
|
|
2
|
+
|
|
3
|
+
const eslint = new ESLint();
|
|
4
|
+
|
|
5
|
+
// Pattern: Pure Function - builds one deterministic command without shell interpolation.
|
|
6
|
+
const createCommand = (command, filePaths) =>
|
|
7
|
+
`${command} ${filePaths.map(filePath => JSON.stringify(filePath)).join(' ')}`;
|
|
8
|
+
|
|
9
|
+
// Pattern: Adapter - derives lint-staged input from ESLint's authoritative ignore rules.
|
|
10
|
+
const removeEslintIgnoredFiles = async filePaths => {
|
|
11
|
+
const ignoredFileStates = await Promise.all(
|
|
12
|
+
filePaths.map(filePath => eslint.isPathIgnored(filePath)),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
return filePaths.flatMap((filePath, index) => (ignoredFileStates[index] ? [] : [filePath]));
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Pattern: Pipeline - preserves ESLint-before-Prettier ordering for staged code.
|
|
19
|
+
const createJavaScriptTasks = async filePaths => {
|
|
20
|
+
const lintableFilePaths = await removeEslintIgnoredFiles(filePaths);
|
|
21
|
+
const tasks = [];
|
|
22
|
+
|
|
23
|
+
if (lintableFilePaths.length > 0) {
|
|
24
|
+
tasks.push(createCommand('eslint --fix --max-warnings 0', lintableFilePaths));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
tasks.push(createCommand('prettier --write', filePaths));
|
|
28
|
+
return tasks;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const lintStagedConfig = {
|
|
32
|
+
'*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}': createJavaScriptTasks,
|
|
33
|
+
'*.{json,jsonc,md,mdx,css,scss,yaml,yml}': ['prettier --write'],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default lintStagedConfig;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carecard/jwt-read",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.20.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/CareCard-ca/pkg-jwt-read.git"
|
|
@@ -10,15 +10,17 @@
|
|
|
10
10
|
"types": "index.d.ts",
|
|
11
11
|
"scripts": {
|
|
12
12
|
"test": "node scripts/runPackageTask.mjs test",
|
|
13
|
-
"test:order": "node --test scripts/testOrder/randomizeTestOrder.test.mjs scripts/
|
|
13
|
+
"test:order": "node --test scripts/testOrder/randomizeTestOrder.test.mjs scripts/testParallel/runIndexedMochaTests.test.mjs scripts/packageTaskRunner.test.mjs scripts/canonicalTestCommand.test.mjs",
|
|
14
14
|
"test:types": "node scripts/runPackageTask.mjs test:types",
|
|
15
15
|
"test:coverage": "node scripts/runPackageTask.mjs test:coverage",
|
|
16
16
|
"test:All": "node scripts/runPackageTask.mjs test:All",
|
|
17
|
-
"
|
|
18
|
-
"format
|
|
17
|
+
"validate:audits": "node scripts/runPackageTask.mjs validate:audits",
|
|
18
|
+
"format": "prettier --write \"**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}\"",
|
|
19
|
+
"format:check": "prettier --check \"**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}\"",
|
|
20
|
+
"lint-staged": "lint-staged",
|
|
19
21
|
"prepare": "husky",
|
|
20
|
-
"lint:fix": "eslint --fix",
|
|
21
|
-
"lint": "eslint"
|
|
22
|
+
"lint:fix": "eslint . --fix --max-warnings 0",
|
|
23
|
+
"lint": "eslint . --max-warnings 0"
|
|
22
24
|
},
|
|
23
25
|
"keywords": [
|
|
24
26
|
"auth",
|
|
@@ -29,27 +31,31 @@
|
|
|
29
31
|
"author": "CareCard team",
|
|
30
32
|
"license": "ISC",
|
|
31
33
|
"devDependencies": {
|
|
34
|
+
"@eslint/js": "9.39.5",
|
|
32
35
|
"@types/express": "5.0.6",
|
|
33
36
|
"@types/mocha": "10.0.10",
|
|
34
37
|
"@types/node": "25.9.3",
|
|
35
|
-
"eslint": "
|
|
38
|
+
"@typescript-eslint/parser": "8.67.0",
|
|
39
|
+
"eslint": "9.39.5",
|
|
40
|
+
"globals": "17.7.0",
|
|
36
41
|
"husky": "9.1.7",
|
|
37
|
-
"lint-staged": "17.0
|
|
42
|
+
"lint-staged": "17.2.0",
|
|
38
43
|
"mocha": "11.7.6",
|
|
39
44
|
"nyc": "18.0.0",
|
|
40
|
-
"prettier": "3.
|
|
45
|
+
"prettier": "3.9.6",
|
|
41
46
|
"ts-node": "10.9.2",
|
|
42
47
|
"typescript": "6.0.3"
|
|
43
48
|
},
|
|
44
49
|
"dependencies": {
|
|
45
|
-
"@carecard/auth-util": "3.
|
|
46
|
-
"@carecard/common-util": "3.
|
|
47
|
-
"@carecard/validate": "3.
|
|
50
|
+
"@carecard/auth-util": "3.20.0",
|
|
51
|
+
"@carecard/common-util": "3.20.0",
|
|
52
|
+
"@carecard/validate": "3.20.0"
|
|
48
53
|
},
|
|
49
54
|
"overrides": {
|
|
50
55
|
"diff": "8.0.4",
|
|
51
|
-
"
|
|
56
|
+
"brace-expansion": "5.0.9",
|
|
57
|
+
"minimatch": "10.2.6",
|
|
52
58
|
"serialize-javascript": "7.0.5",
|
|
53
|
-
"js-yaml": "4.3.
|
|
59
|
+
"js-yaml": "4.3.1"
|
|
54
60
|
}
|
|
55
61
|
}
|