@carecard/auth-util 3.25.0 → 3.27.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/pkg-auth-util-auth-crypto-library/SKILL.md +23 -12
- package/index.d.ts +60 -80
- package/index.js +6 -15
- package/lib/jwkUtilAuth.js +130 -0
- package/lib/jwtUtilAuth.js +66 -153
- package/lib/pwdUtilAuth.js +66 -30
- package/package.json +3 -3
- package/readme.md +40 -38
- package/lib/keyGen.js +0 -24
|
@@ -103,10 +103,11 @@ depend on those folders being present.
|
|
|
103
103
|
- `index.d.ts` must stay aligned with every public export in `index.js`.
|
|
104
104
|
- `lib/jwtUtilAuth.js` owns JWT creation, service-to-service JWT creation,
|
|
105
105
|
verification, and decomposition.
|
|
106
|
-
- `lib/
|
|
106
|
+
- `lib/jwkUtilAuth.js` owns strict signing-JWK and verification-JWKS parsing.
|
|
107
|
+
- `lib/pwdUtilAuth.js` owns password keyring parsing and versioned credential
|
|
108
|
+
hashing and verification.
|
|
107
109
|
- `lib/cryptoUtilAuth.js` wraps Node.js `crypto` primitives for signing,
|
|
108
110
|
verifying, generating HMACs, and salts.
|
|
109
|
-
- `lib/keyGen.js` owns Ed25519 and RSA key generation.
|
|
110
111
|
- `lib/stringUtilAuth.js` owns legacy base64, base64-url-safe, and JWT string
|
|
111
112
|
helpers. Password-hash parsing stays private to `lib/pwdUtilAuth.js`.
|
|
112
113
|
- Keep direct exports as the public package API. Do not add password aliases,
|
|
@@ -116,10 +117,15 @@ depend on those folders being present.
|
|
|
116
117
|
|
|
117
118
|
## JWT Layer
|
|
118
119
|
|
|
119
|
-
-
|
|
120
|
-
|
|
121
|
-
-
|
|
122
|
-
|
|
120
|
+
- Accept only parsed Ed25519 signing JWKs and verification JWKS values. Do not
|
|
121
|
+
add PEM, RSA, algorithm-selection, missing-`kid`, or single-key fallbacks.
|
|
122
|
+
- Derive every `kid` as the RFC 7638 SHA-256 base64url thumbprint of canonical
|
|
123
|
+
`{ "crv": "Ed25519", "kty": "OKP", "x": ... }` members.
|
|
124
|
+
- Require exact JWK metadata: `alg: "EdDSA"`, `use: "sig"`, and one `key_ops`
|
|
125
|
+
value (`sign` for private keys, `verify` for public keys). Ensure private and
|
|
126
|
+
public key material agree before accepting signing configuration.
|
|
127
|
+
- Create JWTs with the fixed `{ alg: "EdDSA", typ: "JWT", kid }` header and
|
|
128
|
+
verify by selecting the exact public key named by `kid`.
|
|
123
129
|
- Keep service-to-service JWT creation here, not in `@carecard/jwt-read`.
|
|
124
130
|
Public service-token creation exports are `jwtCreateServiceToken` and
|
|
125
131
|
`jwtCreateServiceAuthorizationHeader`.
|
|
@@ -143,17 +149,22 @@ depend on those folders being present.
|
|
|
143
149
|
$argon2id$v=19$m=19456,t=2,p=1$base64(salt)$base64(tag)
|
|
144
150
|
```
|
|
145
151
|
|
|
146
|
-
- `
|
|
147
|
-
`
|
|
148
|
-
credential
|
|
152
|
+
- `parsePasswordHashKeyring(activeKeyId, serializedKeyring)`,
|
|
153
|
+
`createPasswordCredential(password, keyring)`, and
|
|
154
|
+
`verifyPasswordCredential(password, credential, keyring)` are the only
|
|
155
|
+
public password credential APIs. Keyring entries are comma-separated
|
|
156
|
+
`key-id:canonical-base64-32-byte-key` values.
|
|
157
|
+
- Persist the returned `hash` and `hashKeyId` together. Verification returns
|
|
158
|
+
`{ isValid, needsRehash }`; `needsRehash` is true only after a valid retiring
|
|
159
|
+
key credential is verified.
|
|
149
160
|
- Generate salts internally for every credential. Never accept caller-selected
|
|
150
161
|
salts and never persist or log the pepper.
|
|
151
162
|
- Normalize well-formed Unicode passwords to NFC before deriving the tag, but
|
|
152
163
|
do not trim or impose password policy here. `@carecard/validate` owns policy,
|
|
153
164
|
while login intentionally permits existing passwords of any length.
|
|
154
|
-
- Return
|
|
155
|
-
legacy saved hashes. Throw for
|
|
156
|
-
invalid
|
|
165
|
+
- Return an invalid verification result for unknown key IDs, malformed,
|
|
166
|
+
non-canonical, differently parameterized, or legacy saved hashes. Throw for
|
|
167
|
+
programmer/configuration errors such as an invalid keyring.
|
|
157
168
|
- Do not add legacy HMAC verification. Seeded accounts must be rehashed before
|
|
158
169
|
the Argon2id-only service starts.
|
|
159
170
|
- Prefer Node.js `crypto` primitives over new dependencies.
|
package/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface JwtHeader {
|
|
|
12
12
|
alg?: string;
|
|
13
13
|
/** The media type of the JWT. Defaults to 'JWT'. */
|
|
14
14
|
typ?: string;
|
|
15
|
+
/** RFC 7638 thumbprint identifying the signing key. */
|
|
16
|
+
kid?: string;
|
|
15
17
|
|
|
16
18
|
/** Any other custom header fields. */
|
|
17
19
|
[key: string]: unknown;
|
|
@@ -58,16 +60,14 @@ export interface ServiceJwtOptions {
|
|
|
58
60
|
issuer: string;
|
|
59
61
|
/** Receiving microservice name or names. */
|
|
60
62
|
audience: string | string[];
|
|
61
|
-
/**
|
|
62
|
-
|
|
63
|
+
/** Parsed private Ed25519 JWK owned by the issuing microservice. */
|
|
64
|
+
signingJwk: JwtSigningJwk;
|
|
63
65
|
/** Subject claim. Defaults to issuer. */
|
|
64
66
|
subject?: string;
|
|
65
67
|
/** Issued-at timestamp in seconds or milliseconds. Defaults to now. */
|
|
66
68
|
issuedAt?: number;
|
|
67
69
|
/** Token lifetime in seconds. Defaults to 60. */
|
|
68
70
|
expiresInSeconds?: number;
|
|
69
|
-
/** JWT signing algorithm. Defaults to EdDSA. */
|
|
70
|
-
algorithm?: string;
|
|
71
71
|
/** Additional non-sensitive JWT claims. */
|
|
72
72
|
claims?: Record<string, unknown>;
|
|
73
73
|
}
|
|
@@ -112,49 +112,35 @@ export interface AuthenticatedRequest extends Request {
|
|
|
112
112
|
visitor?: VisitorRequestObject | null;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
privateKey: string;
|
|
115
|
+
declare const jwtSigningJwkBrand: unique symbol;
|
|
116
|
+
declare const jwtVerificationJwksBrand: unique symbol;
|
|
117
|
+
declare const passwordHashKeyringBrand: unique symbol;
|
|
118
|
+
|
|
119
|
+
export interface JwtSigningJwk {
|
|
120
|
+
readonly kid: string;
|
|
121
|
+
readonly [jwtSigningJwkBrand]: true;
|
|
123
122
|
}
|
|
124
123
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
* @param jwt - The JWT string to verify.
|
|
146
|
-
* @param publicKey - PEM formatted public key.
|
|
147
|
-
* @returns True if the signature is valid, false for malformed or invalid tokens. Unexpected failures throw.
|
|
148
|
-
*/
|
|
149
|
-
verifyJwtSignature: (jwt: string, publicKey: string) => boolean;
|
|
150
|
-
/**
|
|
151
|
-
* Decodes a JWT and returns its header and payload as objects.
|
|
152
|
-
* Note: This does NOT verify the signature.
|
|
153
|
-
* @param jwt - The JWT string to parse.
|
|
154
|
-
* @returns An object containing the header and payload, or null if parsing fails.
|
|
155
|
-
*/
|
|
156
|
-
getHeaderPayloadFromJwt: (jwt: string) => JwtParts | null;
|
|
157
|
-
};
|
|
124
|
+
export interface JwtVerificationJwks {
|
|
125
|
+
readonly kids: readonly string[];
|
|
126
|
+
readonly [jwtVerificationJwksBrand]: true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface PasswordHashKeyring {
|
|
130
|
+
readonly activeKeyId: string;
|
|
131
|
+
readonly keyIds: readonly string[];
|
|
132
|
+
readonly [passwordHashKeyringBrand]: true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface PasswordCredential {
|
|
136
|
+
readonly hash: string;
|
|
137
|
+
readonly hashKeyId: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface PasswordCredentialVerification {
|
|
141
|
+
readonly isValid: boolean;
|
|
142
|
+
readonly needsRehash: boolean;
|
|
143
|
+
}
|
|
158
144
|
|
|
159
145
|
/**
|
|
160
146
|
* Utility functions for string manipulation, base64 encoding, and parsing auth-related strings.
|
|
@@ -217,24 +203,14 @@ export const stringUtilAuth: {
|
|
|
217
203
|
};
|
|
218
204
|
|
|
219
205
|
/**
|
|
220
|
-
*
|
|
221
|
-
* @param algorithm - The algorithm to use ('ed25519' or 'rsa'). Defaults to 'ed25519'.
|
|
222
|
-
* @returns A KeyPair object containing PEM formatted keys.
|
|
223
|
-
*/
|
|
224
|
-
export function generateKeyPair(algorithm?: 'ed25519' | 'rsa' | string): KeyPair;
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Creates a signed JWT from header and payload objects.
|
|
228
|
-
* Automatically normalizes header (sets alg/typ) and payload (sets iat/exp/etc. in seconds).
|
|
229
|
-
* @param headerObject - Header data for the JWT.
|
|
206
|
+
* Creates an EdDSA JWT with a fixed JWT header and the signing JWK's kid.
|
|
230
207
|
* @param payloadObject - Payload data for the JWT.
|
|
231
|
-
* @param
|
|
232
|
-
* @returns Signed JWT string or null
|
|
208
|
+
* @param signingJwk - A parsed private Ed25519 signing JWK.
|
|
209
|
+
* @returns Signed JWT string or null for invalid input.
|
|
233
210
|
*/
|
|
234
211
|
export function jwtCreateSignedToken(
|
|
235
|
-
headerObject: JwtHeader,
|
|
236
212
|
payloadObject: JwtPayload,
|
|
237
|
-
|
|
213
|
+
signingJwk: JwtSigningJwk,
|
|
238
214
|
): string | null;
|
|
239
215
|
|
|
240
216
|
/**
|
|
@@ -252,12 +228,18 @@ export function jwtCreateServiceToken(options: ServiceJwtOptions): string | null
|
|
|
252
228
|
export function jwtCreateServiceAuthorizationHeader(options: ServiceJwtOptions): string | null;
|
|
253
229
|
|
|
254
230
|
/**
|
|
255
|
-
* Verifies
|
|
231
|
+
* Verifies a JWT by selecting its kid from a parsed public JWKS.
|
|
256
232
|
* @param jwt - The JWT string to verify.
|
|
257
|
-
* @param
|
|
233
|
+
* @param verificationJwks - Parsed public Ed25519 verification keys.
|
|
258
234
|
* @returns True if the signature is valid, false otherwise.
|
|
259
235
|
*/
|
|
260
|
-
export function jwtVerifySignedToken(jwt: string,
|
|
236
|
+
export function jwtVerifySignedToken(jwt: string, verificationJwks: JwtVerificationJwks): boolean;
|
|
237
|
+
|
|
238
|
+
/** Parses and validates one private Ed25519 signing JWK. */
|
|
239
|
+
export function parseJwtSigningJwk(serializedJwk: string): JwtSigningJwk;
|
|
240
|
+
|
|
241
|
+
/** Parses and validates an RFC 7517 public Ed25519 JWKS. */
|
|
242
|
+
export function parseJwtVerificationJwks(serializedJwks: string): JwtVerificationJwks;
|
|
261
243
|
|
|
262
244
|
/**
|
|
263
245
|
* Decodes a JWT and returns its header and payload as objects.
|
|
@@ -267,23 +249,21 @@ export function jwtVerifySignedToken(jwt: string, publicKey: string): boolean;
|
|
|
267
249
|
*/
|
|
268
250
|
export function jwtGetHeaderPayload(jwt: string): JwtParts | null;
|
|
269
251
|
|
|
270
|
-
/**
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
*/
|
|
276
|
-
export function createPasswordHash(password: string, pepper: string): Promise<string>;
|
|
252
|
+
/** Parses a validated active/retiring password hash keyring. */
|
|
253
|
+
export function parsePasswordHashKeyring(
|
|
254
|
+
activeKeyId: string,
|
|
255
|
+
serializedKeyring: string,
|
|
256
|
+
): PasswordHashKeyring;
|
|
277
257
|
|
|
278
|
-
/**
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
*/
|
|
285
|
-
export function
|
|
258
|
+
/** Creates a freshly salted active-key Argon2id credential. */
|
|
259
|
+
export function createPasswordCredential(
|
|
260
|
+
password: string,
|
|
261
|
+
keyring: PasswordHashKeyring,
|
|
262
|
+
): Promise<PasswordCredential>;
|
|
263
|
+
|
|
264
|
+
/** Verifies a credential and reports whether it should move to the active key. */
|
|
265
|
+
export function verifyPasswordCredential(
|
|
286
266
|
password: string,
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
): Promise<
|
|
267
|
+
credential: PasswordCredential,
|
|
268
|
+
keyring: PasswordHashKeyring,
|
|
269
|
+
): Promise<PasswordCredentialVerification>;
|
package/index.js
CHANGED
|
@@ -1,27 +1,18 @@
|
|
|
1
1
|
const pwdUtilAuth = require('./lib/pwdUtilAuth');
|
|
2
2
|
const jwtUtilAuth = require('./lib/jwtUtilAuth');
|
|
3
|
-
const
|
|
3
|
+
const jwkUtilAuth = require('./lib/jwkUtilAuth');
|
|
4
4
|
|
|
5
5
|
module.exports = {
|
|
6
|
-
// New
|
|
7
|
-
generateKeyPair: keyGen.generateKeyPair,
|
|
8
6
|
jwtCreateSignedToken: jwtUtilAuth.createSignedJwtFromObject,
|
|
9
7
|
jwtCreateServiceToken: jwtUtilAuth.createServiceJwt,
|
|
10
8
|
jwtCreateServiceAuthorizationHeader: jwtUtilAuth.createServiceAuthorizationHeader,
|
|
11
9
|
jwtVerifySignedToken: jwtUtilAuth.verifyJwtSignature,
|
|
12
10
|
jwtGetHeaderPayload: jwtUtilAuth.getHeaderPayloadFromJwt,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* @deprecated use direct import of the new functions.
|
|
19
|
-
*/
|
|
20
|
-
jwtUtilAuth: {
|
|
21
|
-
createSignedJwtFromObject: jwtUtilAuth.createSignedJwtFromObject,
|
|
22
|
-
verifyJwtSignature: jwtUtilAuth.verifyJwtSignature,
|
|
23
|
-
getHeaderPayloadFromJwt: jwtUtilAuth.getHeaderPayloadFromJwt,
|
|
24
|
-
},
|
|
11
|
+
parseJwtSigningJwk: jwkUtilAuth.parseJwtSigningJwk,
|
|
12
|
+
parseJwtVerificationJwks: jwkUtilAuth.parseJwtVerificationJwks,
|
|
13
|
+
createPasswordCredential: pwdUtilAuth.createPasswordCredential,
|
|
14
|
+
parsePasswordHashKeyring: pwdUtilAuth.parsePasswordHashKeyring,
|
|
15
|
+
verifyPasswordCredential: pwdUtilAuth.verifyPasswordCredential,
|
|
25
16
|
/**
|
|
26
17
|
* @deprecated Use native Buffer methods or other modern alternatives.
|
|
27
18
|
*/
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createHash, createPrivateKey, createPublicKey } = require('node:crypto');
|
|
4
|
+
|
|
5
|
+
const ED25519_KEY_BYTES = 32;
|
|
6
|
+
const signingKeys = new WeakMap();
|
|
7
|
+
const verificationKeys = new WeakMap();
|
|
8
|
+
const SIGNING_JWK_MEMBERS = ['alg', 'crv', 'd', 'key_ops', 'kid', 'kty', 'use', 'x'];
|
|
9
|
+
const VERIFICATION_JWK_MEMBERS = ['alg', 'crv', 'key_ops', 'kid', 'kty', 'use', 'x'];
|
|
10
|
+
|
|
11
|
+
function parseJsonObject(serializedValue, configurationName) {
|
|
12
|
+
try {
|
|
13
|
+
const value = JSON.parse(serializedValue);
|
|
14
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
15
|
+
throw new TypeError();
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
} catch {
|
|
19
|
+
throw new Error(`Invalid ${configurationName} configuration.`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isCanonicalBase64Url(value) {
|
|
24
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9_-]+$/.test(value)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const decodedValue = Buffer.from(value, 'base64url');
|
|
28
|
+
return decodedValue.length === ED25519_KEY_BYTES && decodedValue.toString('base64url') === value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function deriveJwkThumbprint(jwk) {
|
|
32
|
+
const canonicalJwk = JSON.stringify({ crv: 'Ed25519', kty: 'OKP', x: jwk.x });
|
|
33
|
+
return createHash('sha256').update(canonicalJwk).digest('base64url');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hasExactKeyOperation(jwk, operation) {
|
|
37
|
+
return Array.isArray(jwk.key_ops) && jwk.key_ops.length === 1 && jwk.key_ops[0] === operation;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hasExactMembers(value, expectedMembers) {
|
|
41
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const members = Object.keys(value).sort();
|
|
45
|
+
return (
|
|
46
|
+
members.length === expectedMembers.length &&
|
|
47
|
+
members.every((member, index) => member === expectedMembers[index])
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hasValidSharedMetadata(jwk) {
|
|
52
|
+
return (
|
|
53
|
+
jwk.kty === 'OKP' &&
|
|
54
|
+
jwk.crv === 'Ed25519' &&
|
|
55
|
+
jwk.alg === 'EdDSA' &&
|
|
56
|
+
jwk.use === 'sig' &&
|
|
57
|
+
isCanonicalBase64Url(jwk.x) &&
|
|
58
|
+
jwk.kid === deriveJwkThumbprint(jwk)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createSigningKey(jwk) {
|
|
63
|
+
try {
|
|
64
|
+
const privateKey = createPrivateKey({ key: jwk, format: 'jwk' });
|
|
65
|
+
const derivedPublicJwk = createPublicKey(privateKey).export({ format: 'jwk' });
|
|
66
|
+
if (derivedPublicJwk.x !== jwk.x) {
|
|
67
|
+
throw new TypeError();
|
|
68
|
+
}
|
|
69
|
+
return privateKey;
|
|
70
|
+
} catch {
|
|
71
|
+
throw new Error('Invalid JWT signing JWK configuration.');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseJwtSigningJwk(serializedJwk) {
|
|
76
|
+
const jwk = parseJsonObject(serializedJwk, 'JWT signing JWK');
|
|
77
|
+
if (!hasExactMembers(jwk, SIGNING_JWK_MEMBERS) || !hasValidSharedMetadata(jwk)) {
|
|
78
|
+
throw new Error('Invalid JWT signing JWK configuration.');
|
|
79
|
+
}
|
|
80
|
+
if (!hasExactKeyOperation(jwk, 'sign') || !isCanonicalBase64Url(jwk.d)) {
|
|
81
|
+
throw new Error('Invalid JWT signing JWK configuration.');
|
|
82
|
+
}
|
|
83
|
+
const parsedJwk = Object.freeze({ ...jwk, key_ops: Object.freeze(['sign']) });
|
|
84
|
+
signingKeys.set(parsedJwk, createSigningKey(jwk));
|
|
85
|
+
return parsedJwk;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseVerificationJwk(jwk) {
|
|
89
|
+
if (!hasExactMembers(jwk, VERIFICATION_JWK_MEMBERS) || !hasValidSharedMetadata(jwk)) {
|
|
90
|
+
throw new Error('Invalid JWT verification JWKS configuration.');
|
|
91
|
+
}
|
|
92
|
+
if (!hasExactKeyOperation(jwk, 'verify')) {
|
|
93
|
+
throw new Error('Invalid JWT verification JWKS configuration.');
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
return [jwk.kid, createPublicKey({ key: jwk, format: 'jwk' })];
|
|
97
|
+
} catch {
|
|
98
|
+
throw new Error('Invalid JWT verification JWKS configuration.');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseJwtVerificationJwks(serializedJwks) {
|
|
103
|
+
const jwks = parseJsonObject(serializedJwks, 'JWT verification JWKS');
|
|
104
|
+
if (!hasExactMembers(jwks, ['keys']) || !Array.isArray(jwks.keys) || jwks.keys.length === 0) {
|
|
105
|
+
throw new Error('Invalid JWT verification JWKS configuration.');
|
|
106
|
+
}
|
|
107
|
+
const entries = jwks.keys.map(parseVerificationJwk);
|
|
108
|
+
const keyMap = new Map(entries);
|
|
109
|
+
if (keyMap.size !== entries.length) {
|
|
110
|
+
throw new Error('Invalid JWT verification JWKS configuration.');
|
|
111
|
+
}
|
|
112
|
+
const parsedJwks = Object.freeze({ kids: Object.freeze([...keyMap.keys()]) });
|
|
113
|
+
verificationKeys.set(parsedJwks, keyMap);
|
|
114
|
+
return parsedJwks;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getJwtSigningKey(signingJwk) {
|
|
118
|
+
return signingKeys.get(signingJwk) ?? null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getJwtVerificationKey(jwks, kid) {
|
|
122
|
+
return verificationKeys.get(jwks)?.get(kid) ?? null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
getJwtSigningKey,
|
|
127
|
+
getJwtVerificationKey,
|
|
128
|
+
parseJwtSigningJwk,
|
|
129
|
+
parseJwtVerificationJwks,
|
|
130
|
+
};
|
package/lib/jwtUtilAuth.js
CHANGED
|
@@ -1,89 +1,43 @@
|
|
|
1
|
-
|
|
1
|
+
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const { sign, verify } = require('node:crypto');
|
|
4
|
+
const { getJwtSigningKey, getJwtVerificationKey } = require('./jwkUtilAuth');
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
const _normalizePayload = payloadObject => {
|
|
6
|
+
const normalizePayload = payloadObject => {
|
|
7
7
|
const payload = { ...payloadObject };
|
|
8
8
|
const now = Math.floor(Date.now() / 1000);
|
|
9
9
|
const fieldsToNormalize = ['iat', 'exp', 'nbf', 'auth_time'];
|
|
10
10
|
const msThreshold = 1000000000000;
|
|
11
|
-
|
|
12
11
|
if (!Number.isFinite(payload.iat)) {
|
|
13
12
|
payload.iat = now;
|
|
14
13
|
}
|
|
15
|
-
|
|
16
14
|
fieldsToNormalize.forEach(field => {
|
|
17
15
|
if (Number.isFinite(payload[field]) && payload[field] > msThreshold) {
|
|
18
16
|
payload[field] = Math.floor(payload[field] / 1000);
|
|
19
17
|
}
|
|
20
18
|
});
|
|
21
|
-
|
|
22
19
|
if (!Number.isFinite(payload.exp)) {
|
|
23
20
|
payload.exp = payload.iat + 3600;
|
|
24
21
|
}
|
|
25
22
|
return payload;
|
|
26
23
|
};
|
|
27
24
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const _decode = str => {
|
|
33
|
-
return JSON.parse(Buffer.from(str, 'base64url').toString('utf8'));
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const _sign = (token, alg, privateKey) => {
|
|
37
|
-
if (alg === 'EdDSA' || alg === 'Ed25519') {
|
|
38
|
-
return crypto.sign(null, Buffer.from(token), privateKey).toString('base64url');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const sign = crypto.createSign(alg);
|
|
42
|
-
sign.write(token);
|
|
43
|
-
sign.end();
|
|
44
|
-
return sign.sign(privateKey, 'base64url');
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const _verify = (token, signature, alg, publicKey) => {
|
|
48
|
-
if (alg === 'EdDSA' || alg === 'Ed25519') {
|
|
49
|
-
return crypto.verify(null, Buffer.from(token), publicKey, Buffer.from(signature, 'base64url'));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const verify = crypto.createVerify(alg);
|
|
53
|
-
verify.update(token);
|
|
54
|
-
verify.end();
|
|
55
|
-
return verify.verify(publicKey, signature, 'base64url');
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// Pattern: Allow List - rejects algorithms that the runtime cannot safely verify.
|
|
59
|
-
const isSupportedVerificationAlgorithm = algorithm => {
|
|
60
|
-
if (algorithm === 'EdDSA' || algorithm === 'Ed25519') {
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
return typeof algorithm === 'string' && supportedVerificationHashes.has(algorithm.toLowerCase());
|
|
64
|
-
};
|
|
25
|
+
const encodeJwtPart = value => Buffer.from(JSON.stringify(value)).toString('base64url');
|
|
26
|
+
const decodeJwtPart = value => JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
|
|
65
27
|
|
|
66
|
-
// Pattern: Type Guard - decoded JWT parts must honor their documented object contract.
|
|
67
28
|
const isDecodedJwtObject = value => {
|
|
68
29
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
69
30
|
};
|
|
70
31
|
|
|
71
|
-
const isNonEmptyString = value =>
|
|
72
|
-
return typeof value === 'string' && value.trim().length > 0;
|
|
73
|
-
};
|
|
32
|
+
const isNonEmptyString = value => typeof value === 'string' && value.trim().length > 0;
|
|
74
33
|
|
|
75
|
-
const
|
|
34
|
+
const isNonEmptyAudience = audience => {
|
|
76
35
|
return (
|
|
77
|
-
|
|
78
|
-
audience.length > 0 &&
|
|
79
|
-
audience.every(audienceValue => isNonEmptyString(audienceValue))
|
|
36
|
+
isNonEmptyString(audience) ||
|
|
37
|
+
(Array.isArray(audience) && audience.length > 0 && audience.every(isNonEmptyString))
|
|
80
38
|
);
|
|
81
39
|
};
|
|
82
40
|
|
|
83
|
-
const isNonEmptyAudience = audience => {
|
|
84
|
-
return isNonEmptyString(audience) || audienceIsNonEmptyStringArray(audience);
|
|
85
|
-
};
|
|
86
|
-
|
|
87
41
|
const normalizeSeconds = value => {
|
|
88
42
|
if (!Number.isFinite(value)) {
|
|
89
43
|
return null;
|
|
@@ -91,72 +45,39 @@ const normalizeSeconds = value => {
|
|
|
91
45
|
return value > 1000000000000 ? Math.floor(value / 1000) : Math.floor(value);
|
|
92
46
|
};
|
|
93
47
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
* @param payloadObject
|
|
98
|
-
* @param privateKey
|
|
99
|
-
* @return {string|null}
|
|
100
|
-
*/
|
|
101
|
-
const createSignedJwtFromObject = (headerObject, payloadObject, privateKey) => {
|
|
102
|
-
if (!privateKey) {
|
|
48
|
+
function createSignedJwtFromObject(payloadObject, signingJwk) {
|
|
49
|
+
const privateKey = getJwtSigningKey(signingJwk);
|
|
50
|
+
if (!privateKey || !isDecodedJwtObject(payloadObject)) {
|
|
103
51
|
return null;
|
|
104
52
|
}
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const payload = _normalizePayload(payloadObject);
|
|
111
|
-
|
|
112
|
-
const headerBase64UrlSafe = _encode(header);
|
|
113
|
-
const payloadBase64UrlSafe = _encode(payload);
|
|
114
|
-
const token = headerBase64UrlSafe + '.' + payloadBase64UrlSafe;
|
|
115
|
-
|
|
116
|
-
const signature = _sign(token, header.alg, privateKey);
|
|
117
|
-
|
|
118
|
-
return token + '.' + signature;
|
|
119
|
-
};
|
|
53
|
+
const header = { alg: 'EdDSA', typ: 'JWT', kid: signingJwk.kid };
|
|
54
|
+
const token = `${encodeJwtPart(header)}.${encodeJwtPart(normalizePayload(payloadObject))}`;
|
|
55
|
+
const signature = sign(null, Buffer.from(token), privateKey).toString('base64url');
|
|
56
|
+
return `${token}.${signature}`;
|
|
57
|
+
}
|
|
120
58
|
|
|
121
59
|
function createServiceJwt({
|
|
122
60
|
issuer,
|
|
123
61
|
audience,
|
|
124
|
-
|
|
62
|
+
signingJwk,
|
|
125
63
|
subject = issuer,
|
|
126
64
|
issuedAt = Math.floor(Date.now() / 1000),
|
|
127
65
|
expiresInSeconds = 60,
|
|
128
|
-
algorithm = 'EdDSA',
|
|
129
66
|
claims = {},
|
|
130
67
|
} = {}) {
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
!isNonEmptyString(privateKey) ||
|
|
136
|
-
!Number.isInteger(expiresInSeconds) ||
|
|
137
|
-
expiresInSeconds <= 0
|
|
138
|
-
) {
|
|
68
|
+
if (!isNonEmptyString(issuer) || !isNonEmptyAudience(audience) || !isNonEmptyString(subject)) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (!Number.isInteger(expiresInSeconds) || expiresInSeconds <= 0) {
|
|
139
72
|
return null;
|
|
140
73
|
}
|
|
141
|
-
|
|
142
74
|
const iat = normalizeSeconds(issuedAt);
|
|
143
|
-
if (!Number.isInteger(iat)) {
|
|
75
|
+
if (!Number.isInteger(iat) || !isDecodedJwtObject(claims)) {
|
|
144
76
|
return null;
|
|
145
77
|
}
|
|
146
|
-
|
|
147
|
-
// Service-to-service tokens use registered JWT claims so receivers can
|
|
148
|
-
// validate sender, intended audience, subject, and lifetime consistently.
|
|
149
78
|
return createSignedJwtFromObject(
|
|
150
|
-
{
|
|
151
|
-
|
|
152
|
-
...claims,
|
|
153
|
-
iss: issuer,
|
|
154
|
-
aud: audience,
|
|
155
|
-
sub: subject,
|
|
156
|
-
iat,
|
|
157
|
-
exp: iat + expiresInSeconds,
|
|
158
|
-
},
|
|
159
|
-
privateKey,
|
|
79
|
+
{ ...claims, iss: issuer, aud: audience, sub: subject, iat, exp: iat + expiresInSeconds },
|
|
80
|
+
signingJwk,
|
|
160
81
|
);
|
|
161
82
|
}
|
|
162
83
|
|
|
@@ -165,81 +86,73 @@ function createServiceAuthorizationHeader(options = {}) {
|
|
|
165
86
|
return token ? `Bearer ${token}` : null;
|
|
166
87
|
}
|
|
167
88
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
89
|
+
function hasValidJwtHeader(header) {
|
|
90
|
+
return (
|
|
91
|
+
isDecodedJwtObject(header) &&
|
|
92
|
+
header.alg === 'EdDSA' &&
|
|
93
|
+
header.typ === 'JWT' &&
|
|
94
|
+
isNonEmptyString(header.kid)
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function decodeCanonicalSignature(signature) {
|
|
99
|
+
if (typeof signature !== 'string' || !/^[A-Za-z0-9_-]+$/.test(signature)) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const decoded = Buffer.from(signature, 'base64url');
|
|
103
|
+
return decoded.length === 64 && decoded.toString('base64url') === signature ? decoded : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function verifyJwtSignature(jwt, verificationJwks) {
|
|
176
107
|
try {
|
|
177
|
-
if (
|
|
178
|
-
return false;
|
|
179
|
-
}
|
|
180
|
-
const splitJWT = jwt.split('.');
|
|
181
|
-
if (splitJWT.length !== 3) {
|
|
108
|
+
if (typeof jwt !== 'string') {
|
|
182
109
|
return false;
|
|
183
110
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const token = header + '.' + payload;
|
|
187
|
-
const headerObject = _decode(header);
|
|
188
|
-
if (!isDecodedJwtObject(headerObject) || !isSupportedVerificationAlgorithm(headerObject.alg)) {
|
|
111
|
+
const parts = jwt.split('.');
|
|
112
|
+
if (parts.length !== 3) {
|
|
189
113
|
return false;
|
|
190
114
|
}
|
|
191
|
-
|
|
192
|
-
|
|
115
|
+
const header = decodeJwtPart(parts[0]);
|
|
116
|
+
const signature = decodeCanonicalSignature(parts[2]);
|
|
117
|
+
const publicKey = hasValidJwtHeader(header)
|
|
118
|
+
? getJwtVerificationKey(verificationJwks, header.kid)
|
|
119
|
+
: null;
|
|
120
|
+
const signedData = Buffer.from(`${parts[0]}.${parts[1]}`);
|
|
121
|
+
return Boolean(publicKey && signature && verify(null, signedData, publicKey, signature));
|
|
193
122
|
} catch (error) {
|
|
194
123
|
if (!(error instanceof SyntaxError)) {
|
|
195
124
|
throw error;
|
|
196
125
|
}
|
|
197
126
|
return false;
|
|
198
127
|
}
|
|
199
|
-
}
|
|
128
|
+
}
|
|
200
129
|
|
|
201
|
-
|
|
202
|
-
* Returns header and payload object for jwt.
|
|
203
|
-
* @param jwt
|
|
204
|
-
* @return {{payload: *, header: *}|null}
|
|
205
|
-
*/
|
|
206
|
-
// Pattern: Boundary Validation - returns only decoded header and payload objects.
|
|
207
|
-
const getHeaderPayloadFromJwt = jwt => {
|
|
130
|
+
function getHeaderPayloadFromJwt(jwt) {
|
|
208
131
|
try {
|
|
209
|
-
if (typeof jwt !== 'string') {
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
const splitJWT = jwt.split('.');
|
|
213
|
-
if (splitJWT.length !== 3) {
|
|
214
|
-
return null;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const headerObject = _decode(splitJWT[0]);
|
|
218
|
-
const payloadObject = _decode(splitJWT[1]);
|
|
219
|
-
if (!isDecodedJwtObject(headerObject) || !isDecodedJwtObject(payloadObject)) {
|
|
132
|
+
if (typeof jwt !== 'string' || jwt.split('.').length !== 3) {
|
|
220
133
|
return null;
|
|
221
134
|
}
|
|
222
|
-
|
|
223
|
-
|
|
135
|
+
const [headerPart, payloadPart] = jwt.split('.');
|
|
136
|
+
const header = decodeJwtPart(headerPart);
|
|
137
|
+
const payload = decodeJwtPart(payloadPart);
|
|
138
|
+
return isDecodedJwtObject(header) && isDecodedJwtObject(payload) ? { header, payload } : null;
|
|
224
139
|
} catch (error) {
|
|
225
140
|
if (!(error instanceof SyntaxError)) {
|
|
226
141
|
throw error;
|
|
227
142
|
}
|
|
228
143
|
return null;
|
|
229
144
|
}
|
|
230
|
-
}
|
|
145
|
+
}
|
|
231
146
|
|
|
232
147
|
module.exports = {
|
|
233
148
|
createSignedJwtFromObject,
|
|
234
149
|
createServiceJwt,
|
|
235
150
|
createServiceAuthorizationHeader,
|
|
236
|
-
verifyJwtSignature,
|
|
237
151
|
getHeaderPayloadFromJwt,
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
_verify,
|
|
152
|
+
verifyJwtSignature,
|
|
153
|
+
_normalizePayload: normalizePayload,
|
|
154
|
+
_encode: encodeJwtPart,
|
|
155
|
+
_decode: decodeJwtPart,
|
|
243
156
|
_isNonEmptyAudience: isNonEmptyAudience,
|
|
244
157
|
_normalizeSeconds: normalizeSeconds,
|
|
245
158
|
};
|
package/lib/pwdUtilAuth.js
CHANGED
|
@@ -11,22 +11,12 @@ const ARGON2_PASSES = 2;
|
|
|
11
11
|
const ARGON2_PARALLELISM = 1;
|
|
12
12
|
const PASSWORD_SALT_BYTES = 16;
|
|
13
13
|
const PASSWORD_TAG_BYTES = 32;
|
|
14
|
-
const
|
|
14
|
+
const PASSWORD_KEY_BYTES = 32;
|
|
15
|
+
const PASSWORD_KEY_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
16
|
+
const passwordKeyringSecrets = new WeakMap();
|
|
15
17
|
const PASSWORD_HASH_PATTERN =
|
|
16
18
|
/^\$argon2id\$v=19\$m=19456,t=2,p=1\$([A-Za-z0-9+/]{22})\$([A-Za-z0-9+/]{43})$/;
|
|
17
19
|
|
|
18
|
-
// Pattern: Configuration Guard - converts only a sufficiently strong string pepper to bytes.
|
|
19
|
-
function createPepperBuffer(pepper) {
|
|
20
|
-
if (typeof pepper !== 'string') {
|
|
21
|
-
throw new TypeError('Password pepper must be a string.');
|
|
22
|
-
}
|
|
23
|
-
const pepperBuffer = Buffer.from(pepper, 'utf8');
|
|
24
|
-
if (pepperBuffer.length < MINIMUM_PEPPER_BYTES) {
|
|
25
|
-
throw new RangeError('Password pepper must be at least 32 bytes.');
|
|
26
|
-
}
|
|
27
|
-
return pepperBuffer;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
20
|
// Pattern: Canonicalization - converts well-formed NFC password text to stable UTF-8 bytes.
|
|
31
21
|
function createPasswordBuffer(password) {
|
|
32
22
|
if (typeof password !== 'string' || !password.isWellFormed()) {
|
|
@@ -88,26 +78,72 @@ function serializePasswordHash(salt, tag) {
|
|
|
88
78
|
return `$${ARGON2_ALGORITHM}$v=${ARGON2_VERSION}$${parameters}$${encodePhcBase64(salt)}$${encodePhcBase64(tag)}`;
|
|
89
79
|
}
|
|
90
80
|
|
|
91
|
-
// Pattern:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
81
|
+
// Pattern: Strict Configuration Parser - creates an opaque versioned password keyring.
|
|
82
|
+
function parsePasswordHashKeyring(activeKeyId, serializedKeyring) {
|
|
83
|
+
if (!PASSWORD_KEY_ID_PATTERN.test(activeKeyId) || typeof serializedKeyring !== 'string') {
|
|
84
|
+
throw new Error('Invalid password hash keyring configuration.');
|
|
85
|
+
}
|
|
86
|
+
if (!serializedKeyring || /\s/.test(serializedKeyring)) {
|
|
87
|
+
throw new Error('Invalid password hash keyring configuration.');
|
|
88
|
+
}
|
|
89
|
+
const entries = serializedKeyring.split(',').map(parsePasswordKeyEntry);
|
|
90
|
+
const keyMap = new Map(entries);
|
|
91
|
+
if (keyMap.size !== entries.length || !keyMap.has(activeKeyId)) {
|
|
92
|
+
throw new Error('Invalid password hash keyring configuration.');
|
|
93
|
+
}
|
|
94
|
+
const keyring = Object.freeze({ activeKeyId, keyIds: Object.freeze([...keyMap.keys()]) });
|
|
95
|
+
passwordKeyringSecrets.set(keyring, keyMap);
|
|
96
|
+
return keyring;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
function parsePasswordKeyEntry(entry) {
|
|
100
|
+
const separatorIndex = entry.indexOf(':');
|
|
101
|
+
if (separatorIndex <= 0 || separatorIndex !== entry.lastIndexOf(':')) {
|
|
102
|
+
throw new Error('Invalid password hash keyring configuration.');
|
|
103
|
+
}
|
|
104
|
+
const keyId = entry.slice(0, separatorIndex);
|
|
105
|
+
const encodedKey = entry.slice(separatorIndex + 1);
|
|
106
|
+
const key = Buffer.from(encodedKey, 'base64');
|
|
107
|
+
if (!PASSWORD_KEY_ID_PATTERN.test(keyId) || !isCanonicalPasswordKey(key, encodedKey)) {
|
|
108
|
+
throw new Error('Invalid password hash keyring configuration.');
|
|
104
109
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
return [keyId, key];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isCanonicalPasswordKey(key, encodedKey) {
|
|
114
|
+
return key.length === PASSWORD_KEY_BYTES && key.toString('base64') === encodedKey;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getPasswordKeyringSecrets(keyring) {
|
|
118
|
+
const keys = passwordKeyringSecrets.get(keyring);
|
|
119
|
+
if (!keys) {
|
|
120
|
+
throw new TypeError('A parsed password hash keyring is required.');
|
|
121
|
+
}
|
|
122
|
+
return keys;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function createPasswordCredential(password, keyring) {
|
|
126
|
+
const keys = getPasswordKeyringSecrets(keyring);
|
|
127
|
+
const pepper = keys.get(keyring.activeKeyId);
|
|
128
|
+
const salt = randomBytes(PASSWORD_SALT_BYTES);
|
|
129
|
+
const tag = await derivePasswordTag(password, salt, pepper);
|
|
130
|
+
return Object.freeze({ hash: serializePasswordHash(salt, tag), hashKeyId: keyring.activeKeyId });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function verifyPasswordCredential(password, credential, keyring) {
|
|
134
|
+
const keys = getPasswordKeyringSecrets(keyring);
|
|
135
|
+
const pepper = keys.get(credential?.hashKeyId);
|
|
136
|
+
const parsedHash = parsePasswordHash(credential?.hash);
|
|
137
|
+
if (!pepper || !parsedHash || typeof password !== 'string' || !password.isWellFormed()) {
|
|
138
|
+
return { isValid: false, needsRehash: false };
|
|
108
139
|
}
|
|
109
|
-
const candidateTag = await derivePasswordTag(password, parsedHash.salt,
|
|
110
|
-
|
|
140
|
+
const candidateTag = await derivePasswordTag(password, parsedHash.salt, pepper);
|
|
141
|
+
const isValid = timingSafeEqual(candidateTag, parsedHash.tag);
|
|
142
|
+
return { isValid, needsRehash: isValid && credential.hashKeyId !== keyring.activeKeyId };
|
|
111
143
|
}
|
|
112
144
|
|
|
113
|
-
module.exports = {
|
|
145
|
+
module.exports = {
|
|
146
|
+
createPasswordCredential,
|
|
147
|
+
parsePasswordHashKeyring,
|
|
148
|
+
verifyPasswordCredential,
|
|
149
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carecard/auth-util",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.27.0",
|
|
4
4
|
"repository": "https://github.com/CareCard-ca/pkg-auth-util.git",
|
|
5
5
|
"description": "Auth utility functions",
|
|
6
6
|
"main": "index.js",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"typescript": "6.0.3"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@carecard/common-util": "3.
|
|
54
|
-
"@carecard/validate": "3.
|
|
53
|
+
"@carecard/common-util": "3.27.0",
|
|
54
|
+
"@carecard/validate": "3.27.0",
|
|
55
55
|
"@types/express": "5.0.6"
|
|
56
56
|
},
|
|
57
57
|
"overrides": {
|
package/readme.md
CHANGED
|
@@ -25,9 +25,8 @@ Non-negotiable code organization rule: Functions with the same or equivalent beh
|
|
|
25
25
|
|
|
26
26
|
## Features
|
|
27
27
|
|
|
28
|
-
- **JWT Utilities**: Create
|
|
29
|
-
- **Password Utilities**:
|
|
30
|
-
- **Key Generation**: Generate Ed25519 and RSA key pairs for JWT signing.
|
|
28
|
+
- **JWT Utilities**: Create and verify EdDSA JWTs through strict Ed25519 JWK and JWKS configuration.
|
|
29
|
+
- **Password Utilities**: Rotate versioned Argon2id password peppers while preserving a strict saved-credential format.
|
|
31
30
|
- **Crypto Utilities**: Low-level cryptographic primitives for signing, verification, and hashing.
|
|
32
31
|
- **String Utilities**: Base64 and Base64UrlSafe encoding/decoding, and custom string parsing.
|
|
33
32
|
|
|
@@ -46,18 +45,17 @@ const {
|
|
|
46
45
|
jwtCreateSignedToken,
|
|
47
46
|
jwtGetHeaderPayload,
|
|
48
47
|
jwtVerifySignedToken,
|
|
48
|
+
parseJwtSigningJwk,
|
|
49
|
+
parseJwtVerificationJwks,
|
|
49
50
|
} = require('@carecard/auth-util');
|
|
50
51
|
|
|
51
|
-
const header = { alg: 'EdDSA', typ: 'JWT' };
|
|
52
52
|
const payload = { sub: '1234567890', name: 'John Doe' };
|
|
53
|
-
const
|
|
53
|
+
const signingJwk = parseJwtSigningJwk(process.env.JWT_SIGNING_JWK);
|
|
54
|
+
const verificationJwks = parseJwtVerificationJwks(process.env.JWT_VERIFICATION_JWKS);
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
const token = jwtCreateSignedToken(header, payload, privateKey);
|
|
56
|
+
const token = jwtCreateSignedToken(payload, signingJwk);
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
const publicKey = '...'; // Your public PEM key
|
|
60
|
-
const isValid = jwtVerifySignedToken(token, publicKey);
|
|
58
|
+
const isValid = jwtVerifySignedToken(token, verificationJwks);
|
|
61
59
|
|
|
62
60
|
// Get header and payload from a JWT
|
|
63
61
|
const { header: decodedHeader, payload: decodedPayload } = jwtGetHeaderPayload(token);
|
|
@@ -74,47 +72,51 @@ const {
|
|
|
74
72
|
const token = jwtCreateServiceToken({
|
|
75
73
|
issuer: 'ms-institutions',
|
|
76
74
|
audience: 'ms-auth',
|
|
77
|
-
|
|
75
|
+
signingJwk: institutionsSigningJwk,
|
|
78
76
|
});
|
|
79
77
|
|
|
80
78
|
const authorization = jwtCreateServiceAuthorizationHeader({
|
|
81
79
|
issuer: 'ms-institutions',
|
|
82
80
|
audience: 'ms-auth',
|
|
83
|
-
|
|
81
|
+
signingJwk: institutionsSigningJwk,
|
|
84
82
|
});
|
|
85
83
|
```
|
|
86
84
|
|
|
87
85
|
### Password Utilities
|
|
88
86
|
|
|
89
87
|
```javascript
|
|
90
|
-
const {
|
|
88
|
+
const {
|
|
89
|
+
createPasswordCredential,
|
|
90
|
+
parsePasswordHashKeyring,
|
|
91
|
+
verifyPasswordCredential,
|
|
92
|
+
} = require('@carecard/auth-util');
|
|
91
93
|
|
|
92
94
|
const password = 'correct horse battery staple';
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
const keyring = parsePasswordHashKeyring(
|
|
96
|
+
process.env.MS_AUTH_PASSWORD_HASH_ACTIVE_KEY_ID,
|
|
97
|
+
process.env.MS_AUTH_PASSWORD_HASH_KEYRING,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const credential = await createPasswordCredential(password, keyring);
|
|
101
|
+
|
|
102
|
+
const result = await verifyPasswordCredential(password, credential, keyring);
|
|
103
|
+
if (result.isValid && result.needsRehash) {
|
|
104
|
+
const replacement = await createPasswordCredential(password, keyring);
|
|
105
|
+
// Persist replacement.hash and replacement.hashKeyId atomically.
|
|
106
|
+
}
|
|
100
107
|
```
|
|
101
108
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
### Key Generation
|
|
109
|
+
Keyring entries use `key-id:canonical-base64-32-byte-key`, separated by commas.
|
|
110
|
+
Credential rows persist both the PHC hash and its `hashKeyId`; pepper keys stay
|
|
111
|
+
outside persisted credentials and logs. Passwords retain spaces and normalize
|
|
112
|
+
well-formed Unicode to NFC. `@carecard/validate` owns password policy.
|
|
108
113
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
// Generate RSA keys
|
|
116
|
-
const rsaKeys = generateKeyPair('rsa');
|
|
117
|
-
```
|
|
114
|
+
JWT signing configuration is one strict private Ed25519 JWK. Verification
|
|
115
|
+
configuration is an RFC 7517 JWKS containing one or more public Ed25519 JWKs.
|
|
116
|
+
Every key uses `alg: "EdDSA"`, `use: "sig"`, one exact `key_ops` operation, and
|
|
117
|
+
an RFC 7638 SHA-256 thumbprint `kid`. Add replacement public keys before
|
|
118
|
+
switching signers, and retain retiring public keys for the maximum token
|
|
119
|
+
lifetime plus clock skew.
|
|
118
120
|
|
|
119
121
|
### String Utilities (`stringUtilAuth`)
|
|
120
122
|
|
|
@@ -159,9 +161,9 @@ npm run test:types
|
|
|
159
161
|
|
|
160
162
|
The package is organized into several modules:
|
|
161
163
|
|
|
162
|
-
- `
|
|
163
|
-
- `
|
|
164
|
-
- `
|
|
164
|
+
- `jwkUtilAuth`: Strictly parses private signing JWKs and public verification JWKS values.
|
|
165
|
+
- `jwtUtilAuth`: Creates fixed-header Ed25519 JWTs and verifies signatures by `kid`.
|
|
166
|
+
- `pwdUtilAuth`: Creates and verifies versioned Argon2id password credentials.
|
|
165
167
|
- `cryptoUtilAuth`: Core cryptographic operations using Node.js `crypto` module.
|
|
166
168
|
- `stringUtilAuth`: String manipulation and format conversions.
|
|
167
169
|
|
package/lib/keyGen.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
const { generateKeyPairSync } = require('node:crypto');
|
|
2
|
-
|
|
3
|
-
const generateKeyPair = (algorithm = 'ed25519') => {
|
|
4
|
-
const options = {
|
|
5
|
-
publicKeyEncoding: {
|
|
6
|
-
type: 'spki',
|
|
7
|
-
format: 'pem',
|
|
8
|
-
},
|
|
9
|
-
privateKeyEncoding: {
|
|
10
|
-
type: 'pkcs8',
|
|
11
|
-
format: 'pem',
|
|
12
|
-
},
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
if (algorithm === 'rsa') {
|
|
16
|
-
options.modulusLength = 2048;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return generateKeyPairSync(algorithm, options);
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
module.exports = {
|
|
23
|
-
generateKeyPair,
|
|
24
|
-
};
|