@naman_deep_singh/security 1.4.0 → 1.5.1
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/README.md +1 -1
- package/dist/cjs/core/crypto/cryptoManager.js +21 -21
- package/dist/cjs/core/jwt/decode.js +2 -2
- package/dist/cjs/core/jwt/generateTokens.js +2 -2
- package/dist/cjs/core/jwt/jwtManager.js +31 -12
- package/dist/cjs/core/jwt/parseDuration.js +2 -2
- package/dist/cjs/core/jwt/signToken.js +2 -2
- package/dist/cjs/core/jwt/validateToken.js +10 -4
- package/dist/cjs/core/jwt/verify.d.ts +2 -2
- package/dist/cjs/core/jwt/verify.js +15 -13
- package/dist/cjs/core/password/hash.js +4 -4
- package/dist/cjs/core/password/passwordManager.js +13 -5
- package/dist/cjs/core/password/strength.js +12 -6
- package/dist/cjs/core/password/utils.js +1 -1
- package/dist/cjs/core/password/verify.js +4 -4
- package/dist/esm/core/crypto/cryptoManager.js +22 -22
- package/dist/esm/core/jwt/decode.js +2 -2
- package/dist/esm/core/jwt/generateTokens.js +2 -2
- package/dist/esm/core/jwt/jwtManager.js +31 -12
- package/dist/esm/core/jwt/parseDuration.js +3 -3
- package/dist/esm/core/jwt/signToken.js +2 -2
- package/dist/esm/core/jwt/validateToken.js +10 -4
- package/dist/esm/core/jwt/verify.d.ts +2 -2
- package/dist/esm/core/jwt/verify.js +15 -13
- package/dist/esm/core/password/hash.js +4 -4
- package/dist/esm/core/password/passwordManager.js +13 -5
- package/dist/esm/core/password/strength.js +12 -6
- package/dist/esm/core/password/utils.js +1 -1
- package/dist/esm/core/password/verify.js +4 -4
- package/dist/types/core/jwt/verify.d.ts +2 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -38,10 +38,10 @@ class CryptoManager {
|
|
|
38
38
|
try {
|
|
39
39
|
return (0, index_1.encrypt)(plaintext, key);
|
|
40
40
|
}
|
|
41
|
-
catch (
|
|
42
|
-
throw new errors_utils_1.
|
|
43
|
-
|
|
44
|
-
},
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw new errors_utils_1.CryptoIntegrityError({
|
|
43
|
+
reason: 'Encryption failed',
|
|
44
|
+
}, error instanceof Error ? error : undefined);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
@@ -51,10 +51,10 @@ class CryptoManager {
|
|
|
51
51
|
try {
|
|
52
52
|
return (0, index_1.decrypt)(encryptedData, key);
|
|
53
53
|
}
|
|
54
|
-
catch (
|
|
55
|
-
throw new errors_utils_1.
|
|
56
|
-
|
|
57
|
-
},
|
|
54
|
+
catch (error) {
|
|
55
|
+
throw new errors_utils_1.CryptoIntegrityError({
|
|
56
|
+
reason: 'Decryption failed',
|
|
57
|
+
}, error instanceof Error ? error : undefined);
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
@@ -85,11 +85,11 @@ class CryptoManager {
|
|
|
85
85
|
deriveKey(password, salt, iterations = 100000, keyLength = 32) {
|
|
86
86
|
return new Promise((resolve, reject) => {
|
|
87
87
|
const crypto = require('crypto');
|
|
88
|
-
crypto.pbkdf2(password, salt, iterations, keyLength, 'sha256', (
|
|
89
|
-
if (
|
|
90
|
-
reject(new errors_utils_1.
|
|
91
|
-
|
|
92
|
-
},
|
|
88
|
+
crypto.pbkdf2(password, salt, iterations, keyLength, 'sha256', (error, derivedKey) => {
|
|
89
|
+
if (error) {
|
|
90
|
+
reject(new errors_utils_1.CryptoIntegrityError({
|
|
91
|
+
reason: 'Key derivation failed',
|
|
92
|
+
}, error instanceof Error ? error : undefined));
|
|
93
93
|
}
|
|
94
94
|
else {
|
|
95
95
|
resolve(derivedKey.toString('hex'));
|
|
@@ -166,10 +166,10 @@ class CryptoManager {
|
|
|
166
166
|
const signature = sign.sign(privateKey, 'base64');
|
|
167
167
|
resolve(signature);
|
|
168
168
|
}
|
|
169
|
-
catch (
|
|
170
|
-
reject(new errors_utils_1.
|
|
171
|
-
|
|
172
|
-
},
|
|
169
|
+
catch (error) {
|
|
170
|
+
reject(new errors_utils_1.CryptoIntegrityError({
|
|
171
|
+
reason: 'RSA signing failed',
|
|
172
|
+
}, error instanceof Error ? error : undefined));
|
|
173
173
|
}
|
|
174
174
|
});
|
|
175
175
|
}
|
|
@@ -186,10 +186,10 @@ class CryptoManager {
|
|
|
186
186
|
const isValid = verify.verify(publicKey, signature, 'base64');
|
|
187
187
|
resolve(isValid);
|
|
188
188
|
}
|
|
189
|
-
catch (
|
|
190
|
-
reject(new errors_utils_1.
|
|
191
|
-
|
|
192
|
-
},
|
|
189
|
+
catch (error) {
|
|
190
|
+
reject(new errors_utils_1.CryptoIntegrityError({
|
|
191
|
+
reason: 'RSA verification failed',
|
|
192
|
+
}, error instanceof Error ? error : undefined));
|
|
193
193
|
}
|
|
194
194
|
});
|
|
195
195
|
}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.decodeToken = decodeToken;
|
|
4
4
|
exports.decodeTokenStrict = decodeTokenStrict;
|
|
5
|
+
const errors_utils_1 = require("@naman_deep_singh/errors-utils");
|
|
5
6
|
// src/jwt/decodeToken.ts
|
|
6
7
|
const jsonwebtoken_1 = require("jsonwebtoken");
|
|
7
|
-
const errors_utils_1 = require("@naman_deep_singh/errors-utils");
|
|
8
8
|
/**
|
|
9
9
|
* Flexible decode
|
|
10
10
|
* Returns: null | string | JwtPayload
|
|
@@ -21,7 +21,7 @@ function decodeTokenStrict(token) {
|
|
|
21
21
|
const decoded = (0, jsonwebtoken_1.decode)(token);
|
|
22
22
|
if (!decoded || typeof decoded === 'string') {
|
|
23
23
|
throw new errors_utils_1.BadRequestError({
|
|
24
|
-
|
|
24
|
+
reason: 'Invalid JWT payload structure',
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
return decoded;
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateTokens = void 0;
|
|
4
4
|
exports.rotateRefreshToken = rotateRefreshToken;
|
|
5
|
+
const errors_utils_1 = require("@naman_deep_singh/errors-utils");
|
|
5
6
|
const signToken_1 = require("./signToken");
|
|
6
7
|
const verify_1 = require("./verify");
|
|
7
|
-
const errors_utils_1 = require("@naman_deep_singh/errors-utils");
|
|
8
8
|
// Helper function to create branded tokens
|
|
9
9
|
/* const createBrandedToken = <T extends string>(token: string, _brand: T): T => {
|
|
10
10
|
return token as T
|
|
@@ -26,7 +26,7 @@ function rotateRefreshToken(oldToken, secret) {
|
|
|
26
26
|
const decoded = (0, verify_1.verifyToken)(oldToken, secret);
|
|
27
27
|
if (typeof decoded === 'string') {
|
|
28
28
|
throw new errors_utils_1.TokenMalformedError({
|
|
29
|
-
|
|
29
|
+
reason: 'Invalid token payload — expected JWT payload object',
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
const payload = { ...decoded };
|
|
@@ -31,33 +31,37 @@ class JWTManager {
|
|
|
31
31
|
catch (error) {
|
|
32
32
|
if (error instanceof errors_utils_1.BadRequestError || error instanceof errors_utils_1.ValidationError)
|
|
33
33
|
throw error;
|
|
34
|
-
throw new errors_utils_1.BadRequestError({
|
|
34
|
+
throw new errors_utils_1.BadRequestError({ reason: 'Failed to generate tokens' }, error instanceof Error ? error : undefined);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
/** Generate access token */
|
|
38
38
|
async generateAccessToken(payload) {
|
|
39
39
|
try {
|
|
40
40
|
this.validatePayload(payload);
|
|
41
|
-
const token = (0, signToken_1.signToken)(payload, this.accessSecret, this.accessExpiry, {
|
|
41
|
+
const token = (0, signToken_1.signToken)(payload, this.accessSecret, this.accessExpiry, {
|
|
42
|
+
algorithm: 'HS256',
|
|
43
|
+
});
|
|
42
44
|
return token;
|
|
43
45
|
}
|
|
44
46
|
catch (error) {
|
|
45
47
|
if (error instanceof errors_utils_1.BadRequestError || error instanceof errors_utils_1.ValidationError)
|
|
46
48
|
throw error;
|
|
47
|
-
throw new errors_utils_1.BadRequestError({
|
|
49
|
+
throw new errors_utils_1.BadRequestError({ reason: 'Failed to generate access token' }, error instanceof Error ? error : undefined);
|
|
48
50
|
}
|
|
49
51
|
}
|
|
50
52
|
/** Generate refresh token */
|
|
51
53
|
async generateRefreshToken(payload) {
|
|
52
54
|
try {
|
|
53
55
|
this.validatePayload(payload);
|
|
54
|
-
const token = (0, signToken_1.signToken)(payload, this.refreshSecret, this.refreshExpiry, {
|
|
56
|
+
const token = (0, signToken_1.signToken)(payload, this.refreshSecret, this.refreshExpiry, {
|
|
57
|
+
algorithm: 'HS256',
|
|
58
|
+
});
|
|
55
59
|
return token;
|
|
56
60
|
}
|
|
57
61
|
catch (error) {
|
|
58
62
|
if (error instanceof errors_utils_1.BadRequestError || error instanceof errors_utils_1.ValidationError)
|
|
59
63
|
throw error;
|
|
60
|
-
throw new errors_utils_1.BadRequestError({
|
|
64
|
+
throw new errors_utils_1.BadRequestError({ reason: 'Failed to generate refresh token' }, error instanceof Error ? error : undefined);
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
67
|
/** Verify access token */
|
|
@@ -92,7 +96,9 @@ class JWTManager {
|
|
|
92
96
|
/** Rotate refresh token */
|
|
93
97
|
async rotateRefreshToken(oldToken) {
|
|
94
98
|
if (!oldToken || typeof oldToken !== 'string') {
|
|
95
|
-
throw new errors_utils_1.ValidationError({
|
|
99
|
+
throw new errors_utils_1.ValidationError({
|
|
100
|
+
reason: 'Old refresh token must be a non-empty string',
|
|
101
|
+
});
|
|
96
102
|
}
|
|
97
103
|
const decoded = await this.verifyRefreshToken(oldToken);
|
|
98
104
|
const payload = { ...decoded };
|
|
@@ -138,29 +144,42 @@ class JWTManager {
|
|
|
138
144
|
/** Private helper methods */
|
|
139
145
|
validatePayload(payload) {
|
|
140
146
|
if (!payload || typeof payload !== 'object') {
|
|
141
|
-
throw new errors_utils_1.ValidationError({
|
|
147
|
+
throw new errors_utils_1.ValidationError({
|
|
148
|
+
reason: 'Payload must be a non-null object',
|
|
149
|
+
});
|
|
142
150
|
}
|
|
143
151
|
if (Object.keys(payload).length === 0) {
|
|
144
|
-
throw new errors_utils_1.ValidationError({
|
|
152
|
+
throw new errors_utils_1.ValidationError({ reason: 'Payload cannot be empty' });
|
|
145
153
|
}
|
|
146
154
|
}
|
|
147
155
|
async verifyTokenWithCache(token, secret, type) {
|
|
148
156
|
if (!token || typeof token !== 'string') {
|
|
149
|
-
throw new errors_utils_1.ValidationError({
|
|
157
|
+
throw new errors_utils_1.ValidationError({
|
|
158
|
+
reason: `${type} token must be a non-empty string`,
|
|
159
|
+
});
|
|
150
160
|
}
|
|
151
161
|
const cacheKey = `${type}_${token}`;
|
|
152
162
|
if (this.cache) {
|
|
153
163
|
const cached = this.cache.get(cacheKey);
|
|
154
164
|
if (cached && Date.now() - cached.timestamp <= this.cacheTTL) {
|
|
155
165
|
if (!cached.valid)
|
|
156
|
-
throw new errors_utils_1.UnauthorizedError({
|
|
166
|
+
throw new errors_utils_1.UnauthorizedError({
|
|
167
|
+
reason: `${type} token is invalid or expired`,
|
|
168
|
+
});
|
|
157
169
|
return cached.payload;
|
|
158
170
|
}
|
|
159
171
|
}
|
|
160
172
|
const { valid, payload, error } = (0, verify_1.safeVerifyToken)(token, secret);
|
|
161
173
|
if (!valid || !payload || typeof payload === 'string') {
|
|
162
|
-
this.cache?.set(cacheKey, {
|
|
163
|
-
|
|
174
|
+
this.cache?.set(cacheKey, {
|
|
175
|
+
valid: false,
|
|
176
|
+
payload: {},
|
|
177
|
+
timestamp: Date.now(),
|
|
178
|
+
});
|
|
179
|
+
throw new errors_utils_1.UnauthorizedError({
|
|
180
|
+
reason: `${type} token is invalid or expired`,
|
|
181
|
+
cause: error,
|
|
182
|
+
});
|
|
164
183
|
}
|
|
165
184
|
this.cache?.set(cacheKey, { valid: true, payload, timestamp: Date.now() });
|
|
166
185
|
return payload;
|
|
@@ -19,12 +19,12 @@ function parseDuration(input) {
|
|
|
19
19
|
const value = Number.parseInt(match[1], 10);
|
|
20
20
|
const unit = match[2].toLowerCase();
|
|
21
21
|
if (!TIME_UNITS[unit]) {
|
|
22
|
-
throw new errors_utils_1.ValidationError({
|
|
22
|
+
throw new errors_utils_1.ValidationError({ reason: `Invalid time unit: ${unit}` });
|
|
23
23
|
}
|
|
24
24
|
totalSeconds += value * TIME_UNITS[unit];
|
|
25
25
|
}
|
|
26
26
|
if (totalSeconds === 0) {
|
|
27
|
-
throw new errors_utils_1.ValidationError({
|
|
27
|
+
throw new errors_utils_1.ValidationError({ reason: `Invalid expiry format: "${input}"` });
|
|
28
28
|
}
|
|
29
29
|
return totalSeconds;
|
|
30
30
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.signToken = void 0;
|
|
4
|
+
const errors_utils_1 = require("@naman_deep_singh/errors-utils");
|
|
4
5
|
const jsonwebtoken_1 = require("jsonwebtoken");
|
|
5
6
|
const parseDuration_1 = require("./parseDuration");
|
|
6
|
-
const errors_utils_1 = require("@naman_deep_singh/errors-utils");
|
|
7
7
|
function getExpiryTimestamp(seconds) {
|
|
8
8
|
return Math.floor(Date.now() / 1000) + seconds;
|
|
9
9
|
}
|
|
10
10
|
const signToken = (payload, secret, expiresIn = '1h', options = {}) => {
|
|
11
11
|
const seconds = (0, parseDuration_1.parseDuration)(expiresIn);
|
|
12
12
|
if (!seconds || seconds < 10) {
|
|
13
|
-
throw new errors_utils_1.ValidationError({
|
|
13
|
+
throw new errors_utils_1.ValidationError({ reason: 'Token expiry too small' });
|
|
14
14
|
}
|
|
15
15
|
const tokenPayload = {
|
|
16
16
|
...payload,
|
|
@@ -8,24 +8,30 @@ const errors_utils_1 = require("@naman_deep_singh/errors-utils");
|
|
|
8
8
|
* Throws ValidationError if validation fails.
|
|
9
9
|
*/
|
|
10
10
|
function validateTokenPayload(payload, rules = { requiredFields: ['exp', 'iat'] }) {
|
|
11
|
-
const { requiredFields = [], forbiddenFields = [], validateTypes = {} } = rules;
|
|
11
|
+
const { requiredFields = [], forbiddenFields = [], validateTypes = {}, } = rules;
|
|
12
12
|
// 1. Required fields
|
|
13
13
|
for (const field of requiredFields) {
|
|
14
14
|
if (!(field in payload)) {
|
|
15
|
-
throw new errors_utils_1.ValidationError(
|
|
15
|
+
throw new errors_utils_1.ValidationError({
|
|
16
|
+
reason: `Missing required field: ${field}`,
|
|
17
|
+
});
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
// 2. Forbidden fields
|
|
19
21
|
for (const field of forbiddenFields) {
|
|
20
22
|
if (field in payload) {
|
|
21
|
-
throw new errors_utils_1.ValidationError(
|
|
23
|
+
throw new errors_utils_1.ValidationError({
|
|
24
|
+
reason: `Forbidden field in token: ${field}`,
|
|
25
|
+
});
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
28
|
// 3. Type validation
|
|
25
29
|
for (const key in validateTypes) {
|
|
26
30
|
const expectedType = validateTypes[key];
|
|
27
31
|
if (key in payload && typeof payload[key] !== expectedType) {
|
|
28
|
-
throw new errors_utils_1.ValidationError(
|
|
32
|
+
throw new errors_utils_1.ValidationError({
|
|
33
|
+
reason: `Invalid type for ${key}. Expected ${expectedType}, got ${typeof payload[key]}`,
|
|
34
|
+
});
|
|
29
35
|
}
|
|
30
36
|
}
|
|
31
37
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type JwtPayload, type Secret, VerifyOptions } from 'jsonwebtoken';
|
|
2
|
-
import { VerificationResult } from './types';
|
|
1
|
+
import { type JwtPayload, type Secret, type VerifyOptions } from 'jsonwebtoken';
|
|
2
|
+
import type { VerificationResult } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* Verify token (throws UnauthorizedError if invalid or expired)
|
|
5
5
|
*/
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.safeVerifyTokenWithOptions = exports.safeVerifyToken = exports.verifyTokenWithOptions = exports.verifyToken = void 0;
|
|
4
|
-
const jsonwebtoken_1 = require("jsonwebtoken");
|
|
5
4
|
const errors_utils_1 = require("@naman_deep_singh/errors-utils");
|
|
5
|
+
const jsonwebtoken_1 = require("jsonwebtoken");
|
|
6
6
|
/**
|
|
7
7
|
* Verify token (throws UnauthorizedError if invalid or expired)
|
|
8
8
|
*/
|
|
@@ -12,12 +12,12 @@ const verifyToken = (token, secret) => {
|
|
|
12
12
|
}
|
|
13
13
|
catch (error) {
|
|
14
14
|
if (error.name === 'TokenExpiredError') {
|
|
15
|
-
throw new errors_utils_1.UnauthorizedError({
|
|
15
|
+
throw new errors_utils_1.UnauthorizedError({ reason: 'Token has expired' }, error);
|
|
16
16
|
}
|
|
17
17
|
if (error.name === 'JsonWebTokenError') {
|
|
18
|
-
throw new errors_utils_1.UnauthorizedError({
|
|
18
|
+
throw new errors_utils_1.UnauthorizedError({ reason: 'Invalid token' }, error);
|
|
19
19
|
}
|
|
20
|
-
throw new errors_utils_1.UnauthorizedError({
|
|
20
|
+
throw new errors_utils_1.UnauthorizedError({ reason: 'Failed to verify token' }, error);
|
|
21
21
|
}
|
|
22
22
|
};
|
|
23
23
|
exports.verifyToken = verifyToken;
|
|
@@ -30,12 +30,12 @@ const verifyTokenWithOptions = (token, secret, options = {}) => {
|
|
|
30
30
|
}
|
|
31
31
|
catch (error) {
|
|
32
32
|
if (error.name === 'TokenExpiredError') {
|
|
33
|
-
throw new errors_utils_1.UnauthorizedError({
|
|
33
|
+
throw new errors_utils_1.UnauthorizedError({ reason: 'Token has expired' }, error);
|
|
34
34
|
}
|
|
35
35
|
if (error.name === 'JsonWebTokenError') {
|
|
36
|
-
throw new errors_utils_1.UnauthorizedError({
|
|
36
|
+
throw new errors_utils_1.UnauthorizedError({ reason: 'Invalid token' }, error);
|
|
37
37
|
}
|
|
38
|
-
throw new errors_utils_1.UnauthorizedError({
|
|
38
|
+
throw new errors_utils_1.UnauthorizedError({ reason: 'Failed to verify token' }, error);
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
41
|
exports.verifyTokenWithOptions = verifyTokenWithOptions;
|
|
@@ -50,13 +50,13 @@ const safeVerifyToken = (token, secret) => {
|
|
|
50
50
|
catch (error) {
|
|
51
51
|
let wrappedError;
|
|
52
52
|
if (error.name === 'TokenExpiredError') {
|
|
53
|
-
wrappedError = new errors_utils_1.UnauthorizedError({
|
|
53
|
+
wrappedError = new errors_utils_1.UnauthorizedError({ reason: 'Token has expired' }, error);
|
|
54
54
|
}
|
|
55
55
|
else if (error.name === 'JsonWebTokenError') {
|
|
56
|
-
wrappedError = new errors_utils_1.UnauthorizedError({
|
|
56
|
+
wrappedError = new errors_utils_1.UnauthorizedError({ reason: 'Invalid token' }, error);
|
|
57
57
|
}
|
|
58
58
|
else {
|
|
59
|
-
wrappedError = new errors_utils_1.UnauthorizedError({
|
|
59
|
+
wrappedError = new errors_utils_1.UnauthorizedError({ reason: 'Failed to verify token' }, error);
|
|
60
60
|
}
|
|
61
61
|
return { valid: false, error: wrappedError };
|
|
62
62
|
}
|
|
@@ -73,13 +73,15 @@ const safeVerifyTokenWithOptions = (token, secret, options = {}) => {
|
|
|
73
73
|
catch (error) {
|
|
74
74
|
let wrappedError;
|
|
75
75
|
if (error.name === 'TokenExpiredError') {
|
|
76
|
-
wrappedError = new errors_utils_1.UnauthorizedError({
|
|
76
|
+
wrappedError = new errors_utils_1.UnauthorizedError({ reason: 'Token has expired' }, error instanceof Error ? error : undefined);
|
|
77
77
|
}
|
|
78
78
|
else if (error.name === 'JsonWebTokenError') {
|
|
79
|
-
wrappedError = new errors_utils_1.UnauthorizedError({
|
|
79
|
+
wrappedError = new errors_utils_1.UnauthorizedError({
|
|
80
|
+
reason: 'Invalid token',
|
|
81
|
+
}, error instanceof Error ? error : undefined);
|
|
80
82
|
}
|
|
81
83
|
else {
|
|
82
|
-
wrappedError = new errors_utils_1.UnauthorizedError({
|
|
84
|
+
wrappedError = new errors_utils_1.UnauthorizedError({ reason: 'Failed to verify token' }, error instanceof Error ? error : undefined);
|
|
83
85
|
}
|
|
84
86
|
return { valid: false, error: wrappedError };
|
|
85
87
|
}
|
|
@@ -18,8 +18,8 @@ const hashPassword = async (password, saltRounds = 10) => {
|
|
|
18
18
|
const salt = await bcryptjs_1.default.genSalt(saltRounds);
|
|
19
19
|
return bcryptjs_1.default.hash(password, salt);
|
|
20
20
|
}
|
|
21
|
-
catch (
|
|
22
|
-
throw new errors_utils_1.InternalServerError({
|
|
21
|
+
catch (error) {
|
|
22
|
+
throw new errors_utils_1.InternalServerError({ reason: 'Password hashing failed' }, error instanceof Error ? error : undefined);
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
25
|
exports.hashPassword = hashPassword;
|
|
@@ -35,8 +35,8 @@ const hashPasswordSync = (password, saltRounds = 10) => {
|
|
|
35
35
|
const salt = bcryptjs_1.default.genSaltSync(saltRounds);
|
|
36
36
|
return bcryptjs_1.default.hashSync(password, salt);
|
|
37
37
|
}
|
|
38
|
-
catch (
|
|
39
|
-
throw new errors_utils_1.InternalServerError({
|
|
38
|
+
catch (error) {
|
|
39
|
+
throw new errors_utils_1.InternalServerError({ reason: 'Password hashing failed' }, error instanceof Error ? error : undefined);
|
|
40
40
|
}
|
|
41
41
|
};
|
|
42
42
|
exports.hashPasswordSync = hashPasswordSync;
|
|
@@ -37,10 +37,11 @@ class PasswordManager {
|
|
|
37
37
|
return { hash, salt: finalSalt };
|
|
38
38
|
}
|
|
39
39
|
catch (error) {
|
|
40
|
-
if (error instanceof errors_utils_1.BadRequestError ||
|
|
40
|
+
if (error instanceof errors_utils_1.BadRequestError ||
|
|
41
|
+
error instanceof errors_utils_1.ValidationError) {
|
|
41
42
|
throw error;
|
|
42
43
|
}
|
|
43
|
-
throw new errors_utils_1.BadRequestError({
|
|
44
|
+
throw new errors_utils_1.BadRequestError({ reason: 'Failed to hash password' }, error instanceof Error ? error : undefined);
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
/**
|
|
@@ -63,7 +64,9 @@ class PasswordManager {
|
|
|
63
64
|
generate(length = 16, options = {}) {
|
|
64
65
|
const config = { ...this.defaultConfig, ...options };
|
|
65
66
|
if (length < config.minLength || length > config.maxLength) {
|
|
66
|
-
throw new errors_utils_1.ValidationError({
|
|
67
|
+
throw new errors_utils_1.ValidationError({
|
|
68
|
+
reason: `Password length must be between ${config.minLength} and ${config.maxLength}`,
|
|
69
|
+
});
|
|
67
70
|
}
|
|
68
71
|
let charset = 'abcdefghijklmnopqrstuvwxyz';
|
|
69
72
|
if (config.requireUppercase)
|
|
@@ -114,7 +117,11 @@ class PasswordManager {
|
|
|
114
117
|
errors.push(rule.message);
|
|
115
118
|
});
|
|
116
119
|
}
|
|
117
|
-
return {
|
|
120
|
+
return {
|
|
121
|
+
isValid: errors.length === 0,
|
|
122
|
+
errors,
|
|
123
|
+
strength: this.checkStrength(password),
|
|
124
|
+
};
|
|
118
125
|
}
|
|
119
126
|
/**
|
|
120
127
|
* Check password strength
|
|
@@ -190,7 +197,8 @@ class PasswordManager {
|
|
|
190
197
|
label = 'strong';
|
|
191
198
|
suggestions.push('Your password is very secure');
|
|
192
199
|
break;
|
|
193
|
-
default:
|
|
200
|
+
default:
|
|
201
|
+
label = 'very-weak';
|
|
194
202
|
}
|
|
195
203
|
return { score, label, feedback, suggestions };
|
|
196
204
|
}
|
|
@@ -4,18 +4,24 @@ exports.isPasswordStrong = void 0;
|
|
|
4
4
|
const errors_utils_1 = require("@naman_deep_singh/errors-utils");
|
|
5
5
|
const isPasswordStrong = (password, options = {}) => {
|
|
6
6
|
if (!password)
|
|
7
|
-
throw new errors_utils_1.BadRequestError({
|
|
7
|
+
throw new errors_utils_1.BadRequestError({ reason: 'Invalid password provided' });
|
|
8
8
|
const { minLength = 8, requireUppercase = true, requireLowercase = true, requireNumbers = true, requireSymbols = false, } = options;
|
|
9
9
|
if (password.length < minLength)
|
|
10
|
-
throw new errors_utils_1.ValidationError(
|
|
10
|
+
throw new errors_utils_1.ValidationError({
|
|
11
|
+
reason: `Password must be at least ${minLength} characters long`,
|
|
12
|
+
});
|
|
11
13
|
if (requireUppercase && !/[A-Z]/.test(password))
|
|
12
|
-
throw new errors_utils_1.ValidationError({
|
|
14
|
+
throw new errors_utils_1.ValidationError({
|
|
15
|
+
reason: 'Password must include uppercase letters',
|
|
16
|
+
});
|
|
13
17
|
if (requireLowercase && !/[a-z]/.test(password))
|
|
14
|
-
throw new errors_utils_1.ValidationError({
|
|
18
|
+
throw new errors_utils_1.ValidationError({
|
|
19
|
+
reason: 'Password must include lowercase letters',
|
|
20
|
+
});
|
|
15
21
|
if (requireNumbers && !/[0-9]/.test(password))
|
|
16
|
-
throw new errors_utils_1.ValidationError({
|
|
22
|
+
throw new errors_utils_1.ValidationError({ reason: 'Password must include numbers' });
|
|
17
23
|
if (requireSymbols && !/[^A-Za-z0-9]/.test(password))
|
|
18
|
-
throw new errors_utils_1.ValidationError({
|
|
24
|
+
throw new errors_utils_1.ValidationError({ reason: 'Password must include symbols' });
|
|
19
25
|
return true;
|
|
20
26
|
};
|
|
21
27
|
exports.isPasswordStrong = isPasswordStrong;
|
|
@@ -14,7 +14,7 @@ const errors_utils_1 = require("@naman_deep_singh/errors-utils");
|
|
|
14
14
|
*/
|
|
15
15
|
function ensureValidPassword(password) {
|
|
16
16
|
if (!password || typeof password !== 'string') {
|
|
17
|
-
throw new errors_utils_1.BadRequestError({
|
|
17
|
+
throw new errors_utils_1.BadRequestError({ reason: 'Invalid password provided' });
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
@@ -15,11 +15,11 @@ const verifyPassword = async (password, hash) => {
|
|
|
15
15
|
try {
|
|
16
16
|
const result = await bcryptjs_1.default.compare(password, hash);
|
|
17
17
|
if (!result)
|
|
18
|
-
throw new errors_utils_1.UnauthorizedError({
|
|
18
|
+
throw new errors_utils_1.UnauthorizedError({ reason: 'Password verification failed' });
|
|
19
19
|
return result;
|
|
20
20
|
}
|
|
21
21
|
catch {
|
|
22
|
-
throw new errors_utils_1.UnauthorizedError({
|
|
22
|
+
throw new errors_utils_1.UnauthorizedError({ reason: 'Password verification failed' });
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
25
|
exports.verifyPassword = verifyPassword;
|
|
@@ -33,11 +33,11 @@ const verifyPasswordSync = (password, hash) => {
|
|
|
33
33
|
try {
|
|
34
34
|
const result = bcryptjs_1.default.compareSync(password, hash);
|
|
35
35
|
if (!result)
|
|
36
|
-
throw new errors_utils_1.UnauthorizedError({
|
|
36
|
+
throw new errors_utils_1.UnauthorizedError({ reason: 'Password verification failed' });
|
|
37
37
|
return result;
|
|
38
38
|
}
|
|
39
39
|
catch (_error) {
|
|
40
|
-
throw new errors_utils_1.UnauthorizedError({
|
|
40
|
+
throw new errors_utils_1.UnauthorizedError({ reason: 'Password verification failed' });
|
|
41
41
|
}
|
|
42
42
|
};
|
|
43
43
|
exports.verifyPasswordSync = verifyPasswordSync;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CryptoIntegrityError } from '@naman_deep_singh/errors-utils';
|
|
2
2
|
import { decrypt as functionalDecrypt, encrypt as functionalEncrypt, hmacSign as functionalHmacSign, hmacVerify as functionalHmacVerify, randomToken as functionalRandomToken, } from './index';
|
|
3
3
|
/**
|
|
4
4
|
* Default configuration
|
|
@@ -35,10 +35,10 @@ export class CryptoManager {
|
|
|
35
35
|
try {
|
|
36
36
|
return functionalEncrypt(plaintext, key);
|
|
37
37
|
}
|
|
38
|
-
catch (
|
|
39
|
-
throw new
|
|
40
|
-
|
|
41
|
-
},
|
|
38
|
+
catch (error) {
|
|
39
|
+
throw new CryptoIntegrityError({
|
|
40
|
+
reason: 'Encryption failed',
|
|
41
|
+
}, error instanceof Error ? error : undefined);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
@@ -48,10 +48,10 @@ export class CryptoManager {
|
|
|
48
48
|
try {
|
|
49
49
|
return functionalDecrypt(encryptedData, key);
|
|
50
50
|
}
|
|
51
|
-
catch (
|
|
52
|
-
throw new
|
|
53
|
-
|
|
54
|
-
},
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw new CryptoIntegrityError({
|
|
53
|
+
reason: 'Decryption failed',
|
|
54
|
+
}, error instanceof Error ? error : undefined);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
@@ -82,11 +82,11 @@ export class CryptoManager {
|
|
|
82
82
|
deriveKey(password, salt, iterations = 100000, keyLength = 32) {
|
|
83
83
|
return new Promise((resolve, reject) => {
|
|
84
84
|
const crypto = require('crypto');
|
|
85
|
-
crypto.pbkdf2(password, salt, iterations, keyLength, 'sha256', (
|
|
86
|
-
if (
|
|
87
|
-
reject(new
|
|
88
|
-
|
|
89
|
-
},
|
|
85
|
+
crypto.pbkdf2(password, salt, iterations, keyLength, 'sha256', (error, derivedKey) => {
|
|
86
|
+
if (error) {
|
|
87
|
+
reject(new CryptoIntegrityError({
|
|
88
|
+
reason: 'Key derivation failed',
|
|
89
|
+
}, error instanceof Error ? error : undefined));
|
|
90
90
|
}
|
|
91
91
|
else {
|
|
92
92
|
resolve(derivedKey.toString('hex'));
|
|
@@ -163,10 +163,10 @@ export class CryptoManager {
|
|
|
163
163
|
const signature = sign.sign(privateKey, 'base64');
|
|
164
164
|
resolve(signature);
|
|
165
165
|
}
|
|
166
|
-
catch (
|
|
167
|
-
reject(new
|
|
168
|
-
|
|
169
|
-
},
|
|
166
|
+
catch (error) {
|
|
167
|
+
reject(new CryptoIntegrityError({
|
|
168
|
+
reason: 'RSA signing failed',
|
|
169
|
+
}, error instanceof Error ? error : undefined));
|
|
170
170
|
}
|
|
171
171
|
});
|
|
172
172
|
}
|
|
@@ -183,10 +183,10 @@ export class CryptoManager {
|
|
|
183
183
|
const isValid = verify.verify(publicKey, signature, 'base64');
|
|
184
184
|
resolve(isValid);
|
|
185
185
|
}
|
|
186
|
-
catch (
|
|
187
|
-
reject(new
|
|
188
|
-
|
|
189
|
-
},
|
|
186
|
+
catch (error) {
|
|
187
|
+
reject(new CryptoIntegrityError({
|
|
188
|
+
reason: 'RSA verification failed',
|
|
189
|
+
}, error instanceof Error ? error : undefined));
|
|
190
190
|
}
|
|
191
191
|
});
|
|
192
192
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { BadRequestError } from '@naman_deep_singh/errors-utils';
|
|
1
2
|
// src/jwt/decodeToken.ts
|
|
2
3
|
import { decode } from 'jsonwebtoken';
|
|
3
|
-
import { BadRequestError } from '@naman_deep_singh/errors-utils';
|
|
4
4
|
/**
|
|
5
5
|
* Flexible decode
|
|
6
6
|
* Returns: null | string | JwtPayload
|
|
@@ -17,7 +17,7 @@ export function decodeTokenStrict(token) {
|
|
|
17
17
|
const decoded = decode(token);
|
|
18
18
|
if (!decoded || typeof decoded === 'string') {
|
|
19
19
|
throw new BadRequestError({
|
|
20
|
-
|
|
20
|
+
reason: 'Invalid JWT payload structure',
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
return decoded;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { TokenMalformedError } from '@naman_deep_singh/errors-utils';
|
|
1
2
|
import { signToken } from './signToken';
|
|
2
3
|
import { verifyToken } from './verify';
|
|
3
|
-
import { TokenMalformedError } from '@naman_deep_singh/errors-utils';
|
|
4
4
|
// Helper function to create branded tokens
|
|
5
5
|
/* const createBrandedToken = <T extends string>(token: string, _brand: T): T => {
|
|
6
6
|
return token as T
|
|
@@ -21,7 +21,7 @@ export function rotateRefreshToken(oldToken, secret) {
|
|
|
21
21
|
const decoded = verifyToken(oldToken, secret);
|
|
22
22
|
if (typeof decoded === 'string') {
|
|
23
23
|
throw new TokenMalformedError({
|
|
24
|
-
|
|
24
|
+
reason: 'Invalid token payload — expected JWT payload object',
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
const payload = { ...decoded };
|
|
@@ -25,33 +25,37 @@ export class JWTManager {
|
|
|
25
25
|
catch (error) {
|
|
26
26
|
if (error instanceof BadRequestError || error instanceof ValidationError)
|
|
27
27
|
throw error;
|
|
28
|
-
throw new BadRequestError({
|
|
28
|
+
throw new BadRequestError({ reason: 'Failed to generate tokens' }, error instanceof Error ? error : undefined);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
/** Generate access token */
|
|
32
32
|
async generateAccessToken(payload) {
|
|
33
33
|
try {
|
|
34
34
|
this.validatePayload(payload);
|
|
35
|
-
const token = signToken(payload, this.accessSecret, this.accessExpiry, {
|
|
35
|
+
const token = signToken(payload, this.accessSecret, this.accessExpiry, {
|
|
36
|
+
algorithm: 'HS256',
|
|
37
|
+
});
|
|
36
38
|
return token;
|
|
37
39
|
}
|
|
38
40
|
catch (error) {
|
|
39
41
|
if (error instanceof BadRequestError || error instanceof ValidationError)
|
|
40
42
|
throw error;
|
|
41
|
-
throw new BadRequestError({
|
|
43
|
+
throw new BadRequestError({ reason: 'Failed to generate access token' }, error instanceof Error ? error : undefined);
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
46
|
/** Generate refresh token */
|
|
45
47
|
async generateRefreshToken(payload) {
|
|
46
48
|
try {
|
|
47
49
|
this.validatePayload(payload);
|
|
48
|
-
const token = signToken(payload, this.refreshSecret, this.refreshExpiry, {
|
|
50
|
+
const token = signToken(payload, this.refreshSecret, this.refreshExpiry, {
|
|
51
|
+
algorithm: 'HS256',
|
|
52
|
+
});
|
|
49
53
|
return token;
|
|
50
54
|
}
|
|
51
55
|
catch (error) {
|
|
52
56
|
if (error instanceof BadRequestError || error instanceof ValidationError)
|
|
53
57
|
throw error;
|
|
54
|
-
throw new BadRequestError({
|
|
58
|
+
throw new BadRequestError({ reason: 'Failed to generate refresh token' }, error instanceof Error ? error : undefined);
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
/** Verify access token */
|
|
@@ -86,7 +90,9 @@ export class JWTManager {
|
|
|
86
90
|
/** Rotate refresh token */
|
|
87
91
|
async rotateRefreshToken(oldToken) {
|
|
88
92
|
if (!oldToken || typeof oldToken !== 'string') {
|
|
89
|
-
throw new ValidationError({
|
|
93
|
+
throw new ValidationError({
|
|
94
|
+
reason: 'Old refresh token must be a non-empty string',
|
|
95
|
+
});
|
|
90
96
|
}
|
|
91
97
|
const decoded = await this.verifyRefreshToken(oldToken);
|
|
92
98
|
const payload = { ...decoded };
|
|
@@ -132,29 +138,42 @@ export class JWTManager {
|
|
|
132
138
|
/** Private helper methods */
|
|
133
139
|
validatePayload(payload) {
|
|
134
140
|
if (!payload || typeof payload !== 'object') {
|
|
135
|
-
throw new ValidationError({
|
|
141
|
+
throw new ValidationError({
|
|
142
|
+
reason: 'Payload must be a non-null object',
|
|
143
|
+
});
|
|
136
144
|
}
|
|
137
145
|
if (Object.keys(payload).length === 0) {
|
|
138
|
-
throw new ValidationError({
|
|
146
|
+
throw new ValidationError({ reason: 'Payload cannot be empty' });
|
|
139
147
|
}
|
|
140
148
|
}
|
|
141
149
|
async verifyTokenWithCache(token, secret, type) {
|
|
142
150
|
if (!token || typeof token !== 'string') {
|
|
143
|
-
throw new ValidationError({
|
|
151
|
+
throw new ValidationError({
|
|
152
|
+
reason: `${type} token must be a non-empty string`,
|
|
153
|
+
});
|
|
144
154
|
}
|
|
145
155
|
const cacheKey = `${type}_${token}`;
|
|
146
156
|
if (this.cache) {
|
|
147
157
|
const cached = this.cache.get(cacheKey);
|
|
148
158
|
if (cached && Date.now() - cached.timestamp <= this.cacheTTL) {
|
|
149
159
|
if (!cached.valid)
|
|
150
|
-
throw new UnauthorizedError({
|
|
160
|
+
throw new UnauthorizedError({
|
|
161
|
+
reason: `${type} token is invalid or expired`,
|
|
162
|
+
});
|
|
151
163
|
return cached.payload;
|
|
152
164
|
}
|
|
153
165
|
}
|
|
154
166
|
const { valid, payload, error } = safeVerifyToken(token, secret);
|
|
155
167
|
if (!valid || !payload || typeof payload === 'string') {
|
|
156
|
-
this.cache?.set(cacheKey, {
|
|
157
|
-
|
|
168
|
+
this.cache?.set(cacheKey, {
|
|
169
|
+
valid: false,
|
|
170
|
+
payload: {},
|
|
171
|
+
timestamp: Date.now(),
|
|
172
|
+
});
|
|
173
|
+
throw new UnauthorizedError({
|
|
174
|
+
reason: `${type} token is invalid or expired`,
|
|
175
|
+
cause: error,
|
|
176
|
+
});
|
|
158
177
|
}
|
|
159
178
|
this.cache?.set(cacheKey, { valid: true, payload, timestamp: Date.now() });
|
|
160
179
|
return payload;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ValidationError } from
|
|
1
|
+
import { ValidationError } from '@naman_deep_singh/errors-utils';
|
|
2
2
|
const TIME_UNITS = {
|
|
3
3
|
s: 1,
|
|
4
4
|
m: 60,
|
|
@@ -16,12 +16,12 @@ export function parseDuration(input) {
|
|
|
16
16
|
const value = Number.parseInt(match[1], 10);
|
|
17
17
|
const unit = match[2].toLowerCase();
|
|
18
18
|
if (!TIME_UNITS[unit]) {
|
|
19
|
-
throw new ValidationError({
|
|
19
|
+
throw new ValidationError({ reason: `Invalid time unit: ${unit}` });
|
|
20
20
|
}
|
|
21
21
|
totalSeconds += value * TIME_UNITS[unit];
|
|
22
22
|
}
|
|
23
23
|
if (totalSeconds === 0) {
|
|
24
|
-
throw new ValidationError({
|
|
24
|
+
throw new ValidationError({ reason: `Invalid expiry format: "${input}"` });
|
|
25
25
|
}
|
|
26
26
|
return totalSeconds;
|
|
27
27
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
+
import { ValidationError } from '@naman_deep_singh/errors-utils';
|
|
1
2
|
import { sign } from 'jsonwebtoken';
|
|
2
3
|
import { parseDuration } from './parseDuration';
|
|
3
|
-
import { ValidationError } from '@naman_deep_singh/errors-utils';
|
|
4
4
|
function getExpiryTimestamp(seconds) {
|
|
5
5
|
return Math.floor(Date.now() / 1000) + seconds;
|
|
6
6
|
}
|
|
7
7
|
export const signToken = (payload, secret, expiresIn = '1h', options = {}) => {
|
|
8
8
|
const seconds = parseDuration(expiresIn);
|
|
9
9
|
if (!seconds || seconds < 10) {
|
|
10
|
-
throw new ValidationError({
|
|
10
|
+
throw new ValidationError({ reason: 'Token expiry too small' });
|
|
11
11
|
}
|
|
12
12
|
const tokenPayload = {
|
|
13
13
|
...payload,
|
|
@@ -4,24 +4,30 @@ import { ValidationError } from '@naman_deep_singh/errors-utils';
|
|
|
4
4
|
* Throws ValidationError if validation fails.
|
|
5
5
|
*/
|
|
6
6
|
export function validateTokenPayload(payload, rules = { requiredFields: ['exp', 'iat'] }) {
|
|
7
|
-
const { requiredFields = [], forbiddenFields = [], validateTypes = {} } = rules;
|
|
7
|
+
const { requiredFields = [], forbiddenFields = [], validateTypes = {}, } = rules;
|
|
8
8
|
// 1. Required fields
|
|
9
9
|
for (const field of requiredFields) {
|
|
10
10
|
if (!(field in payload)) {
|
|
11
|
-
throw new ValidationError(
|
|
11
|
+
throw new ValidationError({
|
|
12
|
+
reason: `Missing required field: ${field}`,
|
|
13
|
+
});
|
|
12
14
|
}
|
|
13
15
|
}
|
|
14
16
|
// 2. Forbidden fields
|
|
15
17
|
for (const field of forbiddenFields) {
|
|
16
18
|
if (field in payload) {
|
|
17
|
-
throw new ValidationError(
|
|
19
|
+
throw new ValidationError({
|
|
20
|
+
reason: `Forbidden field in token: ${field}`,
|
|
21
|
+
});
|
|
18
22
|
}
|
|
19
23
|
}
|
|
20
24
|
// 3. Type validation
|
|
21
25
|
for (const key in validateTypes) {
|
|
22
26
|
const expectedType = validateTypes[key];
|
|
23
27
|
if (key in payload && typeof payload[key] !== expectedType) {
|
|
24
|
-
throw new ValidationError(
|
|
28
|
+
throw new ValidationError({
|
|
29
|
+
reason: `Invalid type for ${key}. Expected ${expectedType}, got ${typeof payload[key]}`,
|
|
30
|
+
});
|
|
25
31
|
}
|
|
26
32
|
}
|
|
27
33
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type JwtPayload, type Secret, VerifyOptions } from 'jsonwebtoken';
|
|
2
|
-
import { VerificationResult } from './types';
|
|
1
|
+
import { type JwtPayload, type Secret, type VerifyOptions } from 'jsonwebtoken';
|
|
2
|
+
import type { VerificationResult } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* Verify token (throws UnauthorizedError if invalid or expired)
|
|
5
5
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { verify } from 'jsonwebtoken';
|
|
2
1
|
import { UnauthorizedError } from '@naman_deep_singh/errors-utils';
|
|
2
|
+
import { verify, } from 'jsonwebtoken';
|
|
3
3
|
/**
|
|
4
4
|
* Verify token (throws UnauthorizedError if invalid or expired)
|
|
5
5
|
*/
|
|
@@ -9,12 +9,12 @@ export const verifyToken = (token, secret) => {
|
|
|
9
9
|
}
|
|
10
10
|
catch (error) {
|
|
11
11
|
if (error.name === 'TokenExpiredError') {
|
|
12
|
-
throw new UnauthorizedError({
|
|
12
|
+
throw new UnauthorizedError({ reason: 'Token has expired' }, error);
|
|
13
13
|
}
|
|
14
14
|
if (error.name === 'JsonWebTokenError') {
|
|
15
|
-
throw new UnauthorizedError({
|
|
15
|
+
throw new UnauthorizedError({ reason: 'Invalid token' }, error);
|
|
16
16
|
}
|
|
17
|
-
throw new UnauthorizedError({
|
|
17
|
+
throw new UnauthorizedError({ reason: 'Failed to verify token' }, error);
|
|
18
18
|
}
|
|
19
19
|
};
|
|
20
20
|
/**
|
|
@@ -26,12 +26,12 @@ export const verifyTokenWithOptions = (token, secret, options = {}) => {
|
|
|
26
26
|
}
|
|
27
27
|
catch (error) {
|
|
28
28
|
if (error.name === 'TokenExpiredError') {
|
|
29
|
-
throw new UnauthorizedError({
|
|
29
|
+
throw new UnauthorizedError({ reason: 'Token has expired' }, error);
|
|
30
30
|
}
|
|
31
31
|
if (error.name === 'JsonWebTokenError') {
|
|
32
|
-
throw new UnauthorizedError({
|
|
32
|
+
throw new UnauthorizedError({ reason: 'Invalid token' }, error);
|
|
33
33
|
}
|
|
34
|
-
throw new UnauthorizedError({
|
|
34
|
+
throw new UnauthorizedError({ reason: 'Failed to verify token' }, error);
|
|
35
35
|
}
|
|
36
36
|
};
|
|
37
37
|
/**
|
|
@@ -45,13 +45,13 @@ export const safeVerifyToken = (token, secret) => {
|
|
|
45
45
|
catch (error) {
|
|
46
46
|
let wrappedError;
|
|
47
47
|
if (error.name === 'TokenExpiredError') {
|
|
48
|
-
wrappedError = new UnauthorizedError({
|
|
48
|
+
wrappedError = new UnauthorizedError({ reason: 'Token has expired' }, error);
|
|
49
49
|
}
|
|
50
50
|
else if (error.name === 'JsonWebTokenError') {
|
|
51
|
-
wrappedError = new UnauthorizedError({
|
|
51
|
+
wrappedError = new UnauthorizedError({ reason: 'Invalid token' }, error);
|
|
52
52
|
}
|
|
53
53
|
else {
|
|
54
|
-
wrappedError = new UnauthorizedError({
|
|
54
|
+
wrappedError = new UnauthorizedError({ reason: 'Failed to verify token' }, error);
|
|
55
55
|
}
|
|
56
56
|
return { valid: false, error: wrappedError };
|
|
57
57
|
}
|
|
@@ -67,13 +67,15 @@ export const safeVerifyTokenWithOptions = (token, secret, options = {}) => {
|
|
|
67
67
|
catch (error) {
|
|
68
68
|
let wrappedError;
|
|
69
69
|
if (error.name === 'TokenExpiredError') {
|
|
70
|
-
wrappedError = new UnauthorizedError({
|
|
70
|
+
wrappedError = new UnauthorizedError({ reason: 'Token has expired' }, error instanceof Error ? error : undefined);
|
|
71
71
|
}
|
|
72
72
|
else if (error.name === 'JsonWebTokenError') {
|
|
73
|
-
wrappedError = new UnauthorizedError({
|
|
73
|
+
wrappedError = new UnauthorizedError({
|
|
74
|
+
reason: 'Invalid token',
|
|
75
|
+
}, error instanceof Error ? error : undefined);
|
|
74
76
|
}
|
|
75
77
|
else {
|
|
76
|
-
wrappedError = new UnauthorizedError({
|
|
78
|
+
wrappedError = new UnauthorizedError({ reason: 'Failed to verify token' }, error instanceof Error ? error : undefined);
|
|
77
79
|
}
|
|
78
80
|
return { valid: false, error: wrappedError };
|
|
79
81
|
}
|
|
@@ -10,8 +10,8 @@ export const hashPassword = async (password, saltRounds = 10) => {
|
|
|
10
10
|
const salt = await bcrypt.genSalt(saltRounds);
|
|
11
11
|
return bcrypt.hash(password, salt);
|
|
12
12
|
}
|
|
13
|
-
catch (
|
|
14
|
-
throw new InternalServerError({
|
|
13
|
+
catch (error) {
|
|
14
|
+
throw new InternalServerError({ reason: 'Password hashing failed' }, error instanceof Error ? error : undefined);
|
|
15
15
|
}
|
|
16
16
|
};
|
|
17
17
|
export function hashPasswordWithPepper(password, pepper) {
|
|
@@ -26,8 +26,8 @@ export const hashPasswordSync = (password, saltRounds = 10) => {
|
|
|
26
26
|
const salt = bcrypt.genSaltSync(saltRounds);
|
|
27
27
|
return bcrypt.hashSync(password, salt);
|
|
28
28
|
}
|
|
29
|
-
catch (
|
|
30
|
-
throw new InternalServerError({
|
|
29
|
+
catch (error) {
|
|
30
|
+
throw new InternalServerError({ reason: 'Password hashing failed' }, error instanceof Error ? error : undefined);
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
33
|
export function hashPasswordWithPepperSync(password, pepper) {
|
|
@@ -31,10 +31,11 @@ export class PasswordManager {
|
|
|
31
31
|
return { hash, salt: finalSalt };
|
|
32
32
|
}
|
|
33
33
|
catch (error) {
|
|
34
|
-
if (error instanceof BadRequestError ||
|
|
34
|
+
if (error instanceof BadRequestError ||
|
|
35
|
+
error instanceof ValidationError) {
|
|
35
36
|
throw error;
|
|
36
37
|
}
|
|
37
|
-
throw new BadRequestError({
|
|
38
|
+
throw new BadRequestError({ reason: 'Failed to hash password' }, error instanceof Error ? error : undefined);
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
/**
|
|
@@ -57,7 +58,9 @@ export class PasswordManager {
|
|
|
57
58
|
generate(length = 16, options = {}) {
|
|
58
59
|
const config = { ...this.defaultConfig, ...options };
|
|
59
60
|
if (length < config.minLength || length > config.maxLength) {
|
|
60
|
-
throw new ValidationError({
|
|
61
|
+
throw new ValidationError({
|
|
62
|
+
reason: `Password length must be between ${config.minLength} and ${config.maxLength}`,
|
|
63
|
+
});
|
|
61
64
|
}
|
|
62
65
|
let charset = 'abcdefghijklmnopqrstuvwxyz';
|
|
63
66
|
if (config.requireUppercase)
|
|
@@ -108,7 +111,11 @@ export class PasswordManager {
|
|
|
108
111
|
errors.push(rule.message);
|
|
109
112
|
});
|
|
110
113
|
}
|
|
111
|
-
return {
|
|
114
|
+
return {
|
|
115
|
+
isValid: errors.length === 0,
|
|
116
|
+
errors,
|
|
117
|
+
strength: this.checkStrength(password),
|
|
118
|
+
};
|
|
112
119
|
}
|
|
113
120
|
/**
|
|
114
121
|
* Check password strength
|
|
@@ -184,7 +191,8 @@ export class PasswordManager {
|
|
|
184
191
|
label = 'strong';
|
|
185
192
|
suggestions.push('Your password is very secure');
|
|
186
193
|
break;
|
|
187
|
-
default:
|
|
194
|
+
default:
|
|
195
|
+
label = 'very-weak';
|
|
188
196
|
}
|
|
189
197
|
return { score, label, feedback, suggestions };
|
|
190
198
|
}
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import { BadRequestError, ValidationError, } from '@naman_deep_singh/errors-utils';
|
|
2
2
|
export const isPasswordStrong = (password, options = {}) => {
|
|
3
3
|
if (!password)
|
|
4
|
-
throw new BadRequestError({
|
|
4
|
+
throw new BadRequestError({ reason: 'Invalid password provided' });
|
|
5
5
|
const { minLength = 8, requireUppercase = true, requireLowercase = true, requireNumbers = true, requireSymbols = false, } = options;
|
|
6
6
|
if (password.length < minLength)
|
|
7
|
-
throw new ValidationError(
|
|
7
|
+
throw new ValidationError({
|
|
8
|
+
reason: `Password must be at least ${minLength} characters long`,
|
|
9
|
+
});
|
|
8
10
|
if (requireUppercase && !/[A-Z]/.test(password))
|
|
9
|
-
throw new ValidationError({
|
|
11
|
+
throw new ValidationError({
|
|
12
|
+
reason: 'Password must include uppercase letters',
|
|
13
|
+
});
|
|
10
14
|
if (requireLowercase && !/[a-z]/.test(password))
|
|
11
|
-
throw new ValidationError({
|
|
15
|
+
throw new ValidationError({
|
|
16
|
+
reason: 'Password must include lowercase letters',
|
|
17
|
+
});
|
|
12
18
|
if (requireNumbers && !/[0-9]/.test(password))
|
|
13
|
-
throw new ValidationError({
|
|
19
|
+
throw new ValidationError({ reason: 'Password must include numbers' });
|
|
14
20
|
if (requireSymbols && !/[^A-Za-z0-9]/.test(password))
|
|
15
|
-
throw new ValidationError({
|
|
21
|
+
throw new ValidationError({ reason: 'Password must include symbols' });
|
|
16
22
|
return true;
|
|
17
23
|
};
|
|
@@ -5,7 +5,7 @@ import { BadRequestError } from '@naman_deep_singh/errors-utils';
|
|
|
5
5
|
*/
|
|
6
6
|
export function ensureValidPassword(password) {
|
|
7
7
|
if (!password || typeof password !== 'string') {
|
|
8
|
-
throw new BadRequestError({
|
|
8
|
+
throw new BadRequestError({ reason: 'Invalid password provided' });
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
@@ -7,11 +7,11 @@ export const verifyPassword = async (password, hash) => {
|
|
|
7
7
|
try {
|
|
8
8
|
const result = await bcrypt.compare(password, hash);
|
|
9
9
|
if (!result)
|
|
10
|
-
throw new UnauthorizedError({
|
|
10
|
+
throw new UnauthorizedError({ reason: 'Password verification failed' });
|
|
11
11
|
return result;
|
|
12
12
|
}
|
|
13
13
|
catch {
|
|
14
|
-
throw new UnauthorizedError({
|
|
14
|
+
throw new UnauthorizedError({ reason: 'Password verification failed' });
|
|
15
15
|
}
|
|
16
16
|
};
|
|
17
17
|
export async function verifyPasswordWithPepper(password, pepper, hash) {
|
|
@@ -24,11 +24,11 @@ export const verifyPasswordSync = (password, hash) => {
|
|
|
24
24
|
try {
|
|
25
25
|
const result = bcrypt.compareSync(password, hash);
|
|
26
26
|
if (!result)
|
|
27
|
-
throw new UnauthorizedError({
|
|
27
|
+
throw new UnauthorizedError({ reason: 'Password verification failed' });
|
|
28
28
|
return result;
|
|
29
29
|
}
|
|
30
30
|
catch (_error) {
|
|
31
|
-
throw new UnauthorizedError({
|
|
31
|
+
throw new UnauthorizedError({ reason: 'Password verification failed' });
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
34
|
export async function verifyPasswordWithPepperSync(password, pepper, hash) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type JwtPayload, type Secret, VerifyOptions } from 'jsonwebtoken';
|
|
2
|
-
import { VerificationResult } from './types';
|
|
1
|
+
import { type JwtPayload, type Secret, type VerifyOptions } from 'jsonwebtoken';
|
|
2
|
+
import type { VerificationResult } from './types';
|
|
3
3
|
/**
|
|
4
4
|
* Verify token (throws UnauthorizedError if invalid or expired)
|
|
5
5
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naman_deep_singh/security",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Security utilities for password hashing and JWT token management with TypeScript",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"author": "Naman Deep Singh",
|
|
29
29
|
"license": "ISC",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@naman_deep_singh/errors-utils": "^1.
|
|
32
|
-
"@naman_deep_singh/js-extensions": "^1.
|
|
31
|
+
"@naman_deep_singh/errors-utils": "^1.4.2",
|
|
32
|
+
"@naman_deep_singh/js-extensions": "^1.4.0",
|
|
33
33
|
"bcryptjs": "^3.0.3",
|
|
34
34
|
"jsonwebtoken": "^9.0.2"
|
|
35
35
|
},
|