@carecard/jwt-read 3.1.14 → 3.1.16
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/config.toml +2 -0
- package/.agents/skills/carecard-workspace-standards/SKILL.md +410 -0
- package/.agents/skills/carecard-workspace-standards/agents/openai.yaml +4 -0
- package/.agents/skills/github-pr-create-update/SKILL.md +188 -0
- package/.agents/skills/github-pr-create-update/agents/openai.yaml +5 -0
- package/.agents/skills/github-pr-merge-cleanup/SKILL.md +175 -0
- package/.agents/skills/github-pr-merge-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/pkg-jwt-read-jwt-middleware-library/SKILL.md +261 -0
- package/.agents/skills/pkg-jwt-read-jwt-middleware-library/agents/openai.yaml +4 -0
- package/.agents/skills/software-design-patterns-and-clean-code/SKILL.md +73 -0
- package/.codex/config.toml +2 -0
- package/index.d.ts +82 -1
- package/index.js +5 -0
- package/lib/jwtLib.js +187 -0
- package/package.json +8 -8
- package/readme.md +75 -1
package/lib/jwtLib.js
CHANGED
|
@@ -104,6 +104,33 @@ function validateAndExtractJwtObjectNoThrow(req, publicKey) {
|
|
|
104
104
|
return _validateAndExtractGenericNoThrow(req, publicKey, _validateJwtNoThrow, _extractJwtObjectNoThrow, 'jwt');
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
function validateAndExtractServiceJwtObject(req, publicKey, expectedIssuer, expectedAudience, customErrorFunction) {
|
|
108
|
+
const jwtString = _validateJwt(req, customErrorFunction);
|
|
109
|
+
const isJwtSignatureValid = jwtVerifySignedToken(jwtString, publicKey);
|
|
110
|
+
|
|
111
|
+
if (jwtString && isJwtSignatureValid) {
|
|
112
|
+
_extractJwtObject(req, jwtString, customErrorFunction);
|
|
113
|
+
} else if (req) {
|
|
114
|
+
req.jwt = null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!req?.jwt || !_isServiceJwtFor(req.jwt.payload, expectedIssuer, expectedAudience)) {
|
|
118
|
+
if (req) req.jwt = null;
|
|
119
|
+
throwError(customErrorFunction);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return req;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function validateAndExtractJwtOrServerAuthObject(req, publicKey, serverAuthIntrospector, customErrorFunction) {
|
|
126
|
+
if (tryValidateAndExtractJwtObject(req, publicKey)) return req;
|
|
127
|
+
|
|
128
|
+
const serverAuthToken = _validateServerAuthToken(req, customErrorFunction);
|
|
129
|
+
const claims = await introspectServerAuthToken(serverAuthIntrospector, serverAuthToken, req, customErrorFunction);
|
|
130
|
+
attachServerAuthClaims(req, claims, customErrorFunction);
|
|
131
|
+
return req;
|
|
132
|
+
}
|
|
133
|
+
|
|
107
134
|
function validateAndExtractWebTokenObjectNoThrow(req, publicKey, headerName) {
|
|
108
135
|
return _validateAndExtractGenericNoThrow(req, publicKey, r => _validateWebTokenNoThrow(r, headerName), _extractJwtObjectNoThrow, 'jwt');
|
|
109
136
|
}
|
|
@@ -136,6 +163,41 @@ function verifyJwt(publicKey, customErrorFunction) {
|
|
|
136
163
|
};
|
|
137
164
|
}
|
|
138
165
|
|
|
166
|
+
function verifyServiceJwt(publicKey, expectedIssuer, expectedAudience, customErrorFunction) {
|
|
167
|
+
return function (req, res, next) {
|
|
168
|
+
try {
|
|
169
|
+
validateAndExtractServiceJwtObject(req, publicKey, expectedIssuer, expectedAudience, customErrorFunction);
|
|
170
|
+
next();
|
|
171
|
+
} catch (err) {
|
|
172
|
+
next(err);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function verifyJwtOrServerAuth(publicKey, serverAuthIntrospector, customErrorFunction) {
|
|
178
|
+
return async function (req, res, next) {
|
|
179
|
+
try {
|
|
180
|
+
await validateAndExtractJwtOrServerAuthObject(req, publicKey, serverAuthIntrospector, customErrorFunction);
|
|
181
|
+
next();
|
|
182
|
+
} catch (err) {
|
|
183
|
+
next(err);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function verifyJwtOrServerAuthAndHasRole(role, publicKey, serverAuthIntrospector, customErrorFunction) {
|
|
189
|
+
return async function (req, res, next) {
|
|
190
|
+
try {
|
|
191
|
+
await validateAndExtractJwtOrServerAuthObject(req, publicKey, serverAuthIntrospector, customErrorFunction);
|
|
192
|
+
const isRoleExist = doesJwtUserHasRole(req, role);
|
|
193
|
+
_isLoginRequired(isRoleExist, customErrorFunction);
|
|
194
|
+
next();
|
|
195
|
+
} catch (err) {
|
|
196
|
+
next(err);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
139
201
|
function verifyWebToken(publicKey, headerName, customErrorFunction) {
|
|
140
202
|
return function (req, res, next) {
|
|
141
203
|
try {
|
|
@@ -193,6 +255,122 @@ function _isLoginRequired(hasRequiredRole, customErrorFunction) {
|
|
|
193
255
|
}
|
|
194
256
|
}
|
|
195
257
|
|
|
258
|
+
function tryValidateAndExtractJwtObject(req, publicKey) {
|
|
259
|
+
const jwtString = _validateJwtNoThrow(req);
|
|
260
|
+
if (!jwtString || !isJwtString(jwtString)) return false;
|
|
261
|
+
|
|
262
|
+
const isJwtSignatureValid = jwtVerifySignedToken(jwtString, publicKey);
|
|
263
|
+
if (!isJwtSignatureValid) {
|
|
264
|
+
if (req) req.jwt = null;
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
_extractJwtObjectNoThrow(req, jwtString);
|
|
269
|
+
return Boolean(req?.jwt);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function _validateServerAuthToken(req, customErrorFunction) {
|
|
273
|
+
const authorizationHeader = req?.get?.('Authorization') || req?.get?.('authorization');
|
|
274
|
+
const token = _extractJwtNoThrow(authorizationHeader);
|
|
275
|
+
if (token) return token;
|
|
276
|
+
|
|
277
|
+
throwError(customErrorFunction);
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function introspectServerAuthToken(serverAuthIntrospector, token, req, customErrorFunction) {
|
|
282
|
+
if (typeof serverAuthIntrospector !== 'function') {
|
|
283
|
+
throwError(customErrorFunction);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const claims = await serverAuthIntrospector(token, req);
|
|
287
|
+
if (!claims || claims.valid === false) {
|
|
288
|
+
throwError(customErrorFunction);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return claims;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function attachServerAuthClaims(req, claims, customErrorFunction) {
|
|
295
|
+
const payload = createServerAuthPayload(claims);
|
|
296
|
+
if (!payload.sub) {
|
|
297
|
+
if (req) req.jwt = null;
|
|
298
|
+
throwError(customErrorFunction);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
req.jwt = {
|
|
302
|
+
header: {
|
|
303
|
+
alg: 'opaque',
|
|
304
|
+
typ: 'ServerAuth',
|
|
305
|
+
},
|
|
306
|
+
payload,
|
|
307
|
+
};
|
|
308
|
+
_attachJwtMethods(req.jwt);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function createServerAuthPayload(claims) {
|
|
312
|
+
return {
|
|
313
|
+
sub: claims.sub || claims.userId || claims.user_id,
|
|
314
|
+
email: claims.email || '',
|
|
315
|
+
email_verified: claims.emailVerified || claims.email_verified || claims.emailConfirmed || claims.email_confirmed || false,
|
|
316
|
+
roles: Array.isArray(claims.roles) ? claims.roles : [],
|
|
317
|
+
authMode: 'server-auth',
|
|
318
|
+
auth_mode: 'server-auth',
|
|
319
|
+
sessionId: claims.sessionId || claims.session_id,
|
|
320
|
+
session_id: claims.session_id || claims.sessionId,
|
|
321
|
+
exp: readEpochSeconds(claims.exp || claims.expiresAt || claims.expires_at),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function readEpochSeconds(value) {
|
|
326
|
+
if (value === undefined || value === null || value === '') return undefined;
|
|
327
|
+
if (typeof value === 'number') return normalizeSeconds(value);
|
|
328
|
+
const millis = Date.parse(value);
|
|
329
|
+
return Number.isFinite(millis) ? Math.floor(millis / 1000) : undefined;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function normalizeSeconds(value) {
|
|
333
|
+
if (!Number.isFinite(value)) return null;
|
|
334
|
+
return value > 1000000000000 ? Math.floor(value / 1000) : Math.floor(value);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function _isServiceJwtFor(payload, expectedIssuer, expectedAudience) {
|
|
338
|
+
if (!payload) return false;
|
|
339
|
+
if (payload.iss !== expectedIssuer) return false;
|
|
340
|
+
if (payload.sub !== expectedIssuer) return false;
|
|
341
|
+
if (!payloadAudienceMatches(payload.aud, expectedAudience)) return false;
|
|
342
|
+
if (isJwtPayloadIssuedInFuture(payload)) return false;
|
|
343
|
+
if (isJwtPayloadExpired(payload)) return false;
|
|
344
|
+
if (isJwtPayloadNotYetValid(payload)) return false;
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function payloadAudienceMatches(actualAudience, expectedAudience) {
|
|
349
|
+
if (Array.isArray(actualAudience)) return actualAudience.includes(expectedAudience);
|
|
350
|
+
return actualAudience === expectedAudience;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function isJwtPayloadExpired(payload) {
|
|
354
|
+
if (!payload.exp) return true;
|
|
355
|
+
const exp = normalizeSeconds(payload.exp);
|
|
356
|
+
if (!Number.isInteger(exp)) return true;
|
|
357
|
+
return Math.floor(Date.now() / 1000) >= exp;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function isJwtPayloadIssuedInFuture(payload) {
|
|
361
|
+
if (!payload.iat) return true;
|
|
362
|
+
const iat = normalizeSeconds(payload.iat);
|
|
363
|
+
if (!Number.isInteger(iat)) return true;
|
|
364
|
+
return Math.floor(Date.now() / 1000) < iat;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function isJwtPayloadNotYetValid(payload) {
|
|
368
|
+
if (!payload.nbf) return false;
|
|
369
|
+
const nbf = normalizeSeconds(payload.nbf);
|
|
370
|
+
if (!Number.isInteger(nbf)) return true;
|
|
371
|
+
return Math.floor(Date.now() / 1000) < nbf;
|
|
372
|
+
}
|
|
373
|
+
|
|
196
374
|
function _validateGenericNoThrow(req, headerName, extractor) {
|
|
197
375
|
let jwtRaw = req.get(headerName);
|
|
198
376
|
if (!jwtRaw) return null;
|
|
@@ -362,6 +540,8 @@ module.exports = {
|
|
|
362
540
|
_isJwtSignatureValidNoThrow,
|
|
363
541
|
_extractJwtObject,
|
|
364
542
|
validateAndExtractJwtObject,
|
|
543
|
+
validateAndExtractServiceJwtObject,
|
|
544
|
+
validateAndExtractJwtOrServerAuthObject,
|
|
365
545
|
validateAndExtractWebToken,
|
|
366
546
|
jwtAgeInSeconds,
|
|
367
547
|
isJwtExpired,
|
|
@@ -369,6 +549,9 @@ module.exports = {
|
|
|
369
549
|
jwtClientId,
|
|
370
550
|
visitorClientId,
|
|
371
551
|
verifyJwtAndRole,
|
|
552
|
+
verifyServiceJwt,
|
|
553
|
+
verifyJwtOrServerAuth,
|
|
554
|
+
verifyJwtOrServerAuthAndHasRole,
|
|
372
555
|
verifyJwt,
|
|
373
556
|
verifyWebTokenNoThrow,
|
|
374
557
|
verifyWebToken,
|
|
@@ -388,6 +571,10 @@ module.exports = {
|
|
|
388
571
|
_extractVisitorObjectNoThrow,
|
|
389
572
|
_extractWebToken,
|
|
390
573
|
_isLoginRequired,
|
|
574
|
+
tryValidateAndExtractJwtObject,
|
|
575
|
+
attachServerAuthClaims,
|
|
576
|
+
createServerAuthPayload,
|
|
577
|
+
_isServiceJwtFor,
|
|
391
578
|
_attachJwtMethods,
|
|
392
579
|
_attachVisitorMethods,
|
|
393
580
|
_validateWebToken,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carecard/jwt-read",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.16",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/CareCard-ca/pkg-jwt-read.git"
|
|
@@ -30,19 +30,19 @@
|
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/express": "5.0.6",
|
|
32
32
|
"@types/mocha": "10.0.10",
|
|
33
|
-
"@types/node": "25.
|
|
33
|
+
"@types/node": "25.9.1",
|
|
34
34
|
"eslint": "9.39.4",
|
|
35
35
|
"husky": "9.1.7",
|
|
36
|
-
"lint-staged": "17.0.
|
|
37
|
-
"mocha": "11.7.
|
|
36
|
+
"lint-staged": "17.0.5",
|
|
37
|
+
"mocha": "11.7.6",
|
|
38
38
|
"nyc": "18.0.0",
|
|
39
39
|
"prettier": "3.8.3",
|
|
40
40
|
"ts-node": "10.9.2",
|
|
41
|
-
"typescript": "
|
|
41
|
+
"typescript": "6.0.3"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@carecard/common-util": "3.1.
|
|
45
|
-
"@carecard/auth-util": "3.1.
|
|
46
|
-
"@carecard/validate": "3.1.
|
|
44
|
+
"@carecard/common-util": "3.1.15",
|
|
45
|
+
"@carecard/auth-util": "3.1.15",
|
|
46
|
+
"@carecard/validate": "3.1.24"
|
|
47
47
|
}
|
|
48
48
|
}
|
package/readme.md
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|

|
|
4
4
|

|
|
5
5
|
|
|
6
|
-
Utility package for reading, parsing, and verifying JWTs in the CareCard
|
|
6
|
+
Utility package for reading, parsing, and verifying JWTs in the CareCard
|
|
7
|
+
ecosystem. It also provides the shared request middleware used by `ms-*`
|
|
8
|
+
services to accept either an `ms-auth` JWT or an opaque server-auth token
|
|
9
|
+
introspected by `ms-auth`.
|
|
7
10
|
|
|
8
11
|
## Features
|
|
9
12
|
|
|
@@ -12,6 +15,9 @@ Utility package for reading, parsing, and verifying JWTs in the CareCard ecosyst
|
|
|
12
15
|
- **Role Mapping**: Simple utility for translating internal role codes to human-readable names.
|
|
13
16
|
- **Claims Extraction**: Easy extraction of `sub` (clientId) and other JWT payload claims.
|
|
14
17
|
- **Expiration Management**: Helpers to check if a JWT is expired and calculate its remaining TTL.
|
|
18
|
+
- **Service JWTs**: Helpers for verifying and extracting microservice-to-microservice JWTs with standard `iss`, `sub`, `aud`, `iat`, and `exp` claims.
|
|
19
|
+
- **JWT or Server Auth**: Middleware helpers that verify normal JWTs locally and
|
|
20
|
+
call a service-provided introspector for opaque server-auth tokens.
|
|
15
21
|
|
|
16
22
|
## Installation
|
|
17
23
|
|
|
@@ -59,6 +65,74 @@ console.log(getNameOfRole('ad')); // Result: 'admin'
|
|
|
59
65
|
console.log(getCodeOfRole('super_admin')); // Result: 'su'
|
|
60
66
|
```
|
|
61
67
|
|
|
68
|
+
### Auth RLS Role Semantics
|
|
69
|
+
|
|
70
|
+
`ms-auth` treats a JWT or server-auth payload containing `roles: ["ad"]` as the
|
|
71
|
+
auth-service super-admin signal for its RLS policies. Consumers may map `ad` to
|
|
72
|
+
UI/domain names such as `super_admin`, but middleware should preserve the
|
|
73
|
+
original roles array on the request context so services can make
|
|
74
|
+
database-context decisions consistently.
|
|
75
|
+
|
|
76
|
+
Docs that mention `ms-auth` controller internals should use concise action
|
|
77
|
+
names such as `loginUser`, `registerUser`, `getUserDetail`, and `renewJwt`.
|
|
78
|
+
Access level is conveyed by route middleware and endpoint placement, not by
|
|
79
|
+
`public`/`protected`/`admin`/`Handler` suffixes.
|
|
80
|
+
|
|
81
|
+
### Service-To-Service JWTs
|
|
82
|
+
|
|
83
|
+
Use service JWT verification helpers for backend service calls. The sending
|
|
84
|
+
service signs the token with `@carecard/auth-util`. The receiving service uses
|
|
85
|
+
this package to verify the token with the sending service public key and check
|
|
86
|
+
the expected issuer and audience.
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
const { jwtCreateServiceAuthorizationHeader } = require('@carecard/auth-util');
|
|
90
|
+
const { jwtVerifyService } = require('@carecard/jwt-read');
|
|
91
|
+
|
|
92
|
+
const authorization = jwtCreateServiceAuthorizationHeader({
|
|
93
|
+
issuer: 'ms-institutions',
|
|
94
|
+
audience: 'ms-auth',
|
|
95
|
+
privateKey: institutionsPrivateKey,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
app.use(jwtVerifyService(institutionsPublicKey, 'ms-institutions', 'ms-auth', throwNotAuthorizedError));
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Service JWT payloads follow standard JWT semantics:
|
|
102
|
+
|
|
103
|
+
- `iss`: sending service
|
|
104
|
+
- `sub`: sending service identity
|
|
105
|
+
- `aud`: receiving service
|
|
106
|
+
- `iat`: issued-at NumericDate
|
|
107
|
+
- `exp`: expiration NumericDate
|
|
108
|
+
|
|
109
|
+
### JWT Or Server-Auth Middleware
|
|
110
|
+
|
|
111
|
+
Use the `OrServerAuth` helpers on app-facing `ms-*` routes that should accept
|
|
112
|
+
both current authentication modes. The JWT path verifies locally with the
|
|
113
|
+
`ms-auth` public key. The server-auth path calls the provided introspector,
|
|
114
|
+
which should send the opaque token to
|
|
115
|
+
`POST /api/v1/ms-auth/server-auth/introspect` with the receiving service's
|
|
116
|
+
service JWT.
|
|
117
|
+
|
|
118
|
+
```javascript
|
|
119
|
+
const { jwtGetRoleCode, jwtVerifyOrServerAuth, jwtVerifyOrServerAuthAndHasRole } = require('@carecard/jwt-read');
|
|
120
|
+
|
|
121
|
+
const verifyUser = jwtVerifyOrServerAuth(msAuthPublicKey, token => introspectServerAuthTokenWithMsAuth(token), throwNotAuthorizedError);
|
|
122
|
+
|
|
123
|
+
const verifyAdmin = jwtVerifyOrServerAuthAndHasRole(
|
|
124
|
+
jwtGetRoleCode('admin'),
|
|
125
|
+
msAuthPublicKey,
|
|
126
|
+
token => introspectServerAuthTokenWithMsAuth(token),
|
|
127
|
+
throwNotAuthorizedError,
|
|
128
|
+
);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The introspector must return claims for valid tokens. This package normalizes
|
|
132
|
+
those claims onto `req.jwt.payload` with `authMode: "server-auth"` and
|
|
133
|
+
`auth_mode: "server-auth"` so services can keep their existing JWT-backed
|
|
134
|
+
database context and role checks.
|
|
135
|
+
|
|
62
136
|
## Testing
|
|
63
137
|
|
|
64
138
|
Run tests using:
|