@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/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.14",
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.6.2",
33
+ "@types/node": "25.9.1",
34
34
  "eslint": "9.39.4",
35
35
  "husky": "9.1.7",
36
- "lint-staged": "17.0.3",
37
- "mocha": "11.7.5",
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": "5.9.3"
41
+ "typescript": "6.0.3"
42
42
  },
43
43
  "dependencies": {
44
- "@carecard/common-util": "3.1.13",
45
- "@carecard/auth-util": "3.1.13",
46
- "@carecard/validate": "3.1.16"
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
  ![Tests Passing](https://github.com/CareCard-ca/pkg-jwt-read/actions/workflows/ci.yml/badge.svg)
4
4
  ![Coverage](https://img.shields.io/badge/Coverage-80%25-orange)
5
5
 
6
- Utility package for reading, parsing, and verifying JWTs in the CareCard ecosystem.
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: