@carecard/auth-util 3.19.0 → 3.21.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/lib/jwtUtilAuth.js +27 -3
- package/lib/pwdUtilAuth.js +44 -19
- package/package.json +3 -3
package/lib/jwtUtilAuth.js
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
2
|
|
|
3
|
+
const supportedVerificationHashes = new Set(crypto.getHashes().map(hash => hash.toLowerCase()));
|
|
4
|
+
|
|
5
|
+
// Pattern: Normalizer - preserves explicit registered times while supplying omitted defaults.
|
|
3
6
|
const _normalizePayload = payloadObject => {
|
|
4
7
|
const payload = { ...payloadObject };
|
|
5
8
|
const now = Math.floor(Date.now() / 1000);
|
|
6
9
|
const fieldsToNormalize = ['iat', 'exp', 'nbf', 'auth_time'];
|
|
7
10
|
const msThreshold = 1000000000000;
|
|
8
11
|
|
|
9
|
-
if (!payload.iat) {
|
|
12
|
+
if (!Number.isFinite(payload.iat)) {
|
|
10
13
|
payload.iat = now;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
fieldsToNormalize.forEach(field => {
|
|
14
|
-
if (payload[field] && payload[field] > msThreshold) {
|
|
17
|
+
if (Number.isFinite(payload[field]) && payload[field] > msThreshold) {
|
|
15
18
|
payload[field] = Math.floor(payload[field] / 1000);
|
|
16
19
|
}
|
|
17
20
|
});
|
|
18
21
|
|
|
19
|
-
if (!payload.exp) {
|
|
22
|
+
if (!Number.isFinite(payload.exp)) {
|
|
20
23
|
payload.exp = payload.iat + 3600;
|
|
21
24
|
}
|
|
22
25
|
return payload;
|
|
@@ -52,6 +55,19 @@ const _verify = (token, signature, alg, publicKey) => {
|
|
|
52
55
|
return verify.verify(publicKey, signature, 'base64url');
|
|
53
56
|
};
|
|
54
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
|
+
};
|
|
65
|
+
|
|
66
|
+
// Pattern: Type Guard - decoded JWT parts must honor their documented object contract.
|
|
67
|
+
const isDecodedJwtObject = value => {
|
|
68
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
69
|
+
};
|
|
70
|
+
|
|
55
71
|
const isNonEmptyString = value => {
|
|
56
72
|
return typeof value === 'string' && value.trim().length > 0;
|
|
57
73
|
};
|
|
@@ -155,6 +171,7 @@ function createServiceAuthorizationHeader(options = {}) {
|
|
|
155
171
|
* @param publicKey
|
|
156
172
|
* @return {boolean}
|
|
157
173
|
*/
|
|
174
|
+
// Pattern: Fail-Closed Verification - malformed token metadata returns false before crypto use.
|
|
158
175
|
const verifyJwtSignature = (jwt, publicKey) => {
|
|
159
176
|
try {
|
|
160
177
|
if (!jwt || !publicKey) {
|
|
@@ -168,6 +185,9 @@ const verifyJwtSignature = (jwt, publicKey) => {
|
|
|
168
185
|
const [header, payload, signature] = splitJWT;
|
|
169
186
|
const token = header + '.' + payload;
|
|
170
187
|
const headerObject = _decode(header);
|
|
188
|
+
if (!isDecodedJwtObject(headerObject) || !isSupportedVerificationAlgorithm(headerObject.alg)) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
171
191
|
|
|
172
192
|
return _verify(token, signature, headerObject.alg, publicKey);
|
|
173
193
|
} catch (error) {
|
|
@@ -183,6 +203,7 @@ const verifyJwtSignature = (jwt, publicKey) => {
|
|
|
183
203
|
* @param jwt
|
|
184
204
|
* @return {{payload: *, header: *}|null}
|
|
185
205
|
*/
|
|
206
|
+
// Pattern: Boundary Validation - returns only decoded header and payload objects.
|
|
186
207
|
const getHeaderPayloadFromJwt = jwt => {
|
|
187
208
|
try {
|
|
188
209
|
if (typeof jwt !== 'string') {
|
|
@@ -195,6 +216,9 @@ const getHeaderPayloadFromJwt = jwt => {
|
|
|
195
216
|
|
|
196
217
|
const headerObject = _decode(splitJWT[0]);
|
|
197
218
|
const payloadObject = _decode(splitJWT[1]);
|
|
219
|
+
if (!isDecodedJwtObject(headerObject) || !isDecodedJwtObject(payloadObject)) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
198
222
|
|
|
199
223
|
return { header: headerObject, payload: payloadObject };
|
|
200
224
|
} catch (error) {
|
package/lib/pwdUtilAuth.js
CHANGED
|
@@ -1,26 +1,45 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
const PASSWORD_HASH_VERSION = '1';
|
|
4
|
+
|
|
5
|
+
// Pattern: Pure Function - creates a digest in which both the pepper and random salt participate.
|
|
6
|
+
function createSaltedPasswordDigest(password, secret, algorithm, salt) {
|
|
7
|
+
return crypto
|
|
8
|
+
.createHmac(algorithm, secret)
|
|
9
|
+
.update(Buffer.from(salt, 'base64'))
|
|
10
|
+
.update(password)
|
|
11
|
+
.digest('base64');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Pattern: Pure Function - recreates the historical digest for existing stored credentials.
|
|
15
|
+
function createLegacyPasswordDigest(password, secret, algorithm) {
|
|
16
|
+
return crypto.createHmac(algorithm, secret).update(password).digest('base64');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Pattern: Serializer - preserves the published password-hash storage contract.
|
|
20
|
+
function serializePasswordHash(algorithmBase64, hashBase64, salt) {
|
|
21
|
+
return `$${PASSWORD_HASH_VERSION}$${algorithmBase64}$${hashBase64}$${salt}$`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Pattern: Constant-Time Comparison - avoids credential-dependent string comparison timing.
|
|
25
|
+
function passwordHashesMatch(candidateHash, savedPasswordHash) {
|
|
26
|
+
const candidateBuffer = Buffer.from(candidateHash);
|
|
27
|
+
const savedBuffer = Buffer.from(savedPasswordHash);
|
|
28
|
+
return (
|
|
29
|
+
candidateBuffer.length === savedBuffer.length &&
|
|
30
|
+
crypto.timingSafeEqual(candidateBuffer, savedBuffer)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Pattern: Factory - creates a password hash whose random salt changes the credential digest.
|
|
10
35
|
const createPasswordHashWithRandomSalt = (password, secret, algorithm) => {
|
|
11
36
|
const salt = crypto.randomBytes(32).toString('base64');
|
|
12
37
|
const algorithmBase64 = Buffer.from(algorithm).toString('base64');
|
|
13
|
-
const hashBase64 =
|
|
14
|
-
return
|
|
38
|
+
const hashBase64 = createSaltedPasswordDigest(password, secret, algorithm, salt);
|
|
39
|
+
return serializePasswordHash(algorithmBase64, hashBase64, salt);
|
|
15
40
|
};
|
|
16
41
|
|
|
17
|
-
|
|
18
|
-
* Creates hash based on saved hash in database.
|
|
19
|
-
* @param password
|
|
20
|
-
* @param savedPasswordHash
|
|
21
|
-
* @param secret
|
|
22
|
-
* @return {string}
|
|
23
|
-
*/
|
|
42
|
+
// Pattern: Compatibility Verification - verifies salted hashes while retaining existing credentials.
|
|
24
43
|
const createPasswordHashBasedOnSavedAlgorithmSalt = (password, savedPasswordHash, secret) => {
|
|
25
44
|
if (typeof savedPasswordHash !== 'string') {
|
|
26
45
|
return null;
|
|
@@ -31,12 +50,18 @@ const createPasswordHashBasedOnSavedAlgorithmSalt = (password, savedPasswordHash
|
|
|
31
50
|
return null;
|
|
32
51
|
}
|
|
33
52
|
|
|
53
|
+
const version = splitStringArray[1];
|
|
34
54
|
const algBase64 = splitStringArray[2];
|
|
35
55
|
const salt = splitStringArray[4];
|
|
56
|
+
if (version !== PASSWORD_HASH_VERSION || !algBase64 || !salt) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
36
59
|
const algorithm = Buffer.from(algBase64, 'base64').toString('utf8');
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
60
|
+
const saltedDigest = createSaltedPasswordDigest(password, secret, algorithm, salt);
|
|
61
|
+
const saltedHash = serializePasswordHash(algBase64, saltedDigest, salt);
|
|
62
|
+
const legacyDigest = createLegacyPasswordDigest(password, secret, algorithm);
|
|
63
|
+
const legacyHash = serializePasswordHash(algBase64, legacyDigest, salt);
|
|
64
|
+
return passwordHashesMatch(legacyHash, savedPasswordHash) ? legacyHash : saltedHash;
|
|
40
65
|
};
|
|
41
66
|
|
|
42
67
|
module.exports = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carecard/auth-util",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.21.0",
|
|
4
4
|
"repository": "https://github.com/CareCard-ca/pkg-auth-util.git",
|
|
5
5
|
"description": "Auth utility functions",
|
|
6
6
|
"main": "index.js",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"typescript": "6.0.3"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@carecard/common-util": "3.
|
|
51
|
-
"@carecard/validate": "3.
|
|
50
|
+
"@carecard/common-util": "3.21.0",
|
|
51
|
+
"@carecard/validate": "3.21.0",
|
|
52
52
|
"@types/express": "5.0.6"
|
|
53
53
|
},
|
|
54
54
|
"overrides": {
|