@naman_deep_singh/security 1.3.3 → 1.4.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/README.md +153 -355
- package/dist/cjs/core/crypto/cryptoManager.js +34 -17
- package/dist/cjs/core/jwt/decode.js +4 -1
- package/dist/cjs/core/jwt/generateTokens.js +4 -1
- package/dist/cjs/core/jwt/jwtManager.d.ts +19 -43
- package/dist/cjs/core/jwt/jwtManager.js +72 -206
- package/dist/cjs/core/jwt/parseDuration.js +3 -2
- package/dist/cjs/core/jwt/signToken.js +2 -1
- package/dist/cjs/core/jwt/validateToken.d.ts +10 -7
- package/dist/cjs/core/jwt/validateToken.js +14 -11
- package/dist/cjs/core/jwt/verify.d.ts +9 -10
- package/dist/cjs/core/jwt/verify.js +57 -14
- package/dist/cjs/core/password/hash.js +2 -2
- package/dist/cjs/core/password/passwordManager.d.ts +1 -1
- package/dist/cjs/core/password/passwordManager.js +35 -87
- package/dist/cjs/core/password/strength.js +5 -5
- package/dist/cjs/core/password/utils.d.ts +12 -0
- package/dist/cjs/core/password/utils.js +16 -1
- package/dist/cjs/core/password/verify.js +4 -4
- package/dist/cjs/index.d.ts +2 -7
- package/dist/esm/core/crypto/cryptoManager.js +34 -17
- package/dist/esm/core/jwt/decode.js +4 -1
- package/dist/esm/core/jwt/generateTokens.js +4 -1
- package/dist/esm/core/jwt/jwtManager.d.ts +19 -43
- package/dist/esm/core/jwt/jwtManager.js +73 -207
- package/dist/esm/core/jwt/parseDuration.js +3 -2
- package/dist/esm/core/jwt/signToken.js +2 -1
- package/dist/esm/core/jwt/validateToken.d.ts +10 -7
- package/dist/esm/core/jwt/validateToken.js +14 -11
- package/dist/esm/core/jwt/verify.d.ts +9 -10
- package/dist/esm/core/jwt/verify.js +55 -12
- package/dist/esm/core/password/hash.js +2 -2
- package/dist/esm/core/password/passwordManager.d.ts +1 -1
- package/dist/esm/core/password/passwordManager.js +35 -87
- package/dist/esm/core/password/strength.js +5 -5
- package/dist/esm/core/password/utils.d.ts +12 -0
- package/dist/esm/core/password/utils.js +16 -1
- package/dist/esm/core/password/verify.js +4 -4
- package/dist/esm/index.d.ts +2 -7
- package/dist/types/core/jwt/jwtManager.d.ts +19 -43
- package/dist/types/core/jwt/validateToken.d.ts +10 -7
- package/dist/types/core/jwt/verify.d.ts +9 -10
- package/dist/types/core/password/passwordManager.d.ts +1 -1
- package/dist/types/core/password/utils.d.ts +12 -0
- package/dist/types/index.d.ts +2 -7
- package/package.json +2 -2
|
@@ -1,31 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { ValidationError } from '@naman_deep_singh/errors-utils';
|
|
2
|
+
/**
|
|
3
|
+
* Validates a JWT payload according to the provided rules.
|
|
4
|
+
* Throws ValidationError if validation fails.
|
|
5
|
+
*/
|
|
6
|
+
export function validateTokenPayload(payload, rules = { requiredFields: ['exp', 'iat'] }) {
|
|
7
|
+
const { requiredFields = [], forbiddenFields = [], validateTypes = {} } = rules;
|
|
5
8
|
// 1. Required fields
|
|
6
9
|
for (const field of requiredFields) {
|
|
7
10
|
if (!(field in payload)) {
|
|
8
|
-
|
|
11
|
+
throw new ValidationError(`Missing required field: ${field}`);
|
|
9
12
|
}
|
|
10
13
|
}
|
|
11
14
|
// 2. Forbidden fields
|
|
12
15
|
for (const field of forbiddenFields) {
|
|
13
16
|
if (field in payload) {
|
|
14
|
-
|
|
17
|
+
throw new ValidationError(`Forbidden field in token: ${field}`);
|
|
15
18
|
}
|
|
16
19
|
}
|
|
17
20
|
// 3. Type validation
|
|
18
21
|
for (const key in validateTypes) {
|
|
19
22
|
const expectedType = validateTypes[key];
|
|
20
23
|
if (key in payload && typeof payload[key] !== expectedType) {
|
|
21
|
-
|
|
22
|
-
valid: false,
|
|
23
|
-
error: `Invalid type for ${key}. Expected ${expectedType}.`,
|
|
24
|
-
};
|
|
24
|
+
throw new ValidationError(`Invalid type for ${key}. Expected ${expectedType}, got ${typeof payload[key]}`);
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
return { valid: true };
|
|
28
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Checks if a JWT payload is expired.
|
|
30
|
+
* Returns true if expired or missing 'exp'.
|
|
31
|
+
*/
|
|
29
32
|
export function isTokenExpired(payload) {
|
|
30
33
|
if (!payload.exp)
|
|
31
34
|
return true;
|
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
3
|
-
import type { VerificationResult } from './types';
|
|
1
|
+
import { type JwtPayload, type Secret, VerifyOptions } from 'jsonwebtoken';
|
|
2
|
+
import { VerificationResult } from './types';
|
|
4
3
|
/**
|
|
5
|
-
* Verify token (throws if invalid or expired)
|
|
4
|
+
* Verify token (throws UnauthorizedError if invalid or expired)
|
|
6
5
|
*/
|
|
7
6
|
export declare const verifyToken: (token: string, secret: Secret) => string | JwtPayload;
|
|
8
7
|
/**
|
|
9
|
-
*
|
|
8
|
+
* Verify token with options
|
|
10
9
|
*/
|
|
11
|
-
export declare const
|
|
10
|
+
export declare const verifyTokenWithOptions: (token: string, secret: Secret, options?: VerifyOptions) => string | JwtPayload;
|
|
12
11
|
/**
|
|
13
|
-
*
|
|
12
|
+
* Safe verify — never throws, returns structured result with UnauthorizedError on failure
|
|
14
13
|
*/
|
|
15
|
-
export declare const
|
|
14
|
+
export declare const safeVerifyToken: (token: string, secret: Secret) => VerificationResult;
|
|
16
15
|
/**
|
|
17
|
-
* Safe verify with
|
|
16
|
+
* Safe verify with options — never throws, returns structured result with UnauthorizedError on failure
|
|
18
17
|
*/
|
|
19
|
-
export declare const safeVerifyTokenWithOptions: (token: string, secret: Secret, options?:
|
|
18
|
+
export declare const safeVerifyTokenWithOptions: (token: string, secret: Secret, options?: VerifyOptions) => VerificationResult;
|
|
@@ -1,30 +1,63 @@
|
|
|
1
1
|
import { verify } from 'jsonwebtoken';
|
|
2
|
+
import { UnauthorizedError } from '@naman_deep_singh/errors-utils';
|
|
2
3
|
/**
|
|
3
|
-
* Verify token (throws if invalid or expired)
|
|
4
|
+
* Verify token (throws UnauthorizedError if invalid or expired)
|
|
4
5
|
*/
|
|
5
6
|
export const verifyToken = (token, secret) => {
|
|
6
|
-
|
|
7
|
+
try {
|
|
8
|
+
return verify(token, secret);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
if (error.name === 'TokenExpiredError') {
|
|
12
|
+
throw new UnauthorizedError({ message: 'Token has expired' }, error);
|
|
13
|
+
}
|
|
14
|
+
if (error.name === 'JsonWebTokenError') {
|
|
15
|
+
throw new UnauthorizedError({ message: 'Invalid token' }, error);
|
|
16
|
+
}
|
|
17
|
+
throw new UnauthorizedError({ message: 'Failed to verify token' }, error);
|
|
18
|
+
}
|
|
7
19
|
};
|
|
8
20
|
/**
|
|
9
|
-
*
|
|
21
|
+
* Verify token with options
|
|
10
22
|
*/
|
|
11
|
-
export const
|
|
23
|
+
export const verifyTokenWithOptions = (token, secret, options = {}) => {
|
|
12
24
|
try {
|
|
13
|
-
|
|
14
|
-
return { valid: true, payload: decoded };
|
|
25
|
+
return verify(token, secret, options);
|
|
15
26
|
}
|
|
16
27
|
catch (error) {
|
|
17
|
-
|
|
28
|
+
if (error.name === 'TokenExpiredError') {
|
|
29
|
+
throw new UnauthorizedError({ message: 'Token has expired' }, error);
|
|
30
|
+
}
|
|
31
|
+
if (error.name === 'JsonWebTokenError') {
|
|
32
|
+
throw new UnauthorizedError({ message: 'Invalid token' }, error);
|
|
33
|
+
}
|
|
34
|
+
throw new UnauthorizedError({ message: 'Failed to verify token' }, error);
|
|
18
35
|
}
|
|
19
36
|
};
|
|
20
37
|
/**
|
|
21
|
-
*
|
|
38
|
+
* Safe verify — never throws, returns structured result with UnauthorizedError on failure
|
|
22
39
|
*/
|
|
23
|
-
export const
|
|
24
|
-
|
|
40
|
+
export const safeVerifyToken = (token, secret) => {
|
|
41
|
+
try {
|
|
42
|
+
const decoded = verify(token, secret);
|
|
43
|
+
return { valid: true, payload: decoded };
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
let wrappedError;
|
|
47
|
+
if (error.name === 'TokenExpiredError') {
|
|
48
|
+
wrappedError = new UnauthorizedError({ message: 'Token has expired' }, error);
|
|
49
|
+
}
|
|
50
|
+
else if (error.name === 'JsonWebTokenError') {
|
|
51
|
+
wrappedError = new UnauthorizedError({ message: 'Invalid token' }, error);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
wrappedError = new UnauthorizedError({ message: 'Failed to verify token' }, error);
|
|
55
|
+
}
|
|
56
|
+
return { valid: false, error: wrappedError };
|
|
57
|
+
}
|
|
25
58
|
};
|
|
26
59
|
/**
|
|
27
|
-
* Safe verify with
|
|
60
|
+
* Safe verify with options — never throws, returns structured result with UnauthorizedError on failure
|
|
28
61
|
*/
|
|
29
62
|
export const safeVerifyTokenWithOptions = (token, secret, options = {}) => {
|
|
30
63
|
try {
|
|
@@ -32,6 +65,16 @@ export const safeVerifyTokenWithOptions = (token, secret, options = {}) => {
|
|
|
32
65
|
return { valid: true, payload: decoded };
|
|
33
66
|
}
|
|
34
67
|
catch (error) {
|
|
35
|
-
|
|
68
|
+
let wrappedError;
|
|
69
|
+
if (error.name === 'TokenExpiredError') {
|
|
70
|
+
wrappedError = new UnauthorizedError({ message: 'Token has expired' }, error);
|
|
71
|
+
}
|
|
72
|
+
else if (error.name === 'JsonWebTokenError') {
|
|
73
|
+
wrappedError = new UnauthorizedError({ message: 'Invalid token' }, error);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
wrappedError = new UnauthorizedError({ message: 'Failed to verify token' }, error);
|
|
77
|
+
}
|
|
78
|
+
return { valid: false, error: wrappedError };
|
|
36
79
|
}
|
|
37
80
|
};
|
|
@@ -11,7 +11,7 @@ export const hashPassword = async (password, saltRounds = 10) => {
|
|
|
11
11
|
return bcrypt.hash(password, salt);
|
|
12
12
|
}
|
|
13
13
|
catch (_err) {
|
|
14
|
-
throw new InternalServerError('Password hashing failed');
|
|
14
|
+
throw new InternalServerError({ message: 'Password hashing failed' });
|
|
15
15
|
}
|
|
16
16
|
};
|
|
17
17
|
export function hashPasswordWithPepper(password, pepper) {
|
|
@@ -27,7 +27,7 @@ export const hashPasswordSync = (password, saltRounds = 10) => {
|
|
|
27
27
|
return bcrypt.hashSync(password, salt);
|
|
28
28
|
}
|
|
29
29
|
catch (_error) {
|
|
30
|
-
throw new InternalServerError('Password hashing failed');
|
|
30
|
+
throw new InternalServerError({ message: 'Password hashing failed' });
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
33
|
export function hashPasswordWithPepperSync(password, pepper) {
|
|
@@ -23,7 +23,7 @@ export declare class PasswordManager implements IPasswordManager {
|
|
|
23
23
|
*/
|
|
24
24
|
checkStrength(password: string): PasswordStrength;
|
|
25
25
|
/**
|
|
26
|
-
* Check if password hash needs upgrade (
|
|
26
|
+
* Check if password hash needs upgrade (saltRounds change)
|
|
27
27
|
*/
|
|
28
28
|
needsUpgrade(_hash: string, _currentConfig: PasswordConfig): boolean;
|
|
29
29
|
}
|
|
@@ -21,25 +21,20 @@ export class PasswordManager {
|
|
|
21
21
|
async hash(password, salt) {
|
|
22
22
|
try {
|
|
23
23
|
ensureValidPassword(password);
|
|
24
|
-
// Validate password meets basic requirements
|
|
25
24
|
this.validate(password);
|
|
26
25
|
const saltRounds = this.defaultConfig.saltRounds;
|
|
27
|
-
let
|
|
28
|
-
if (!
|
|
29
|
-
|
|
26
|
+
let finalSalt = salt;
|
|
27
|
+
if (!finalSalt) {
|
|
28
|
+
finalSalt = await bcrypt.genSalt(saltRounds);
|
|
30
29
|
}
|
|
31
|
-
const hash = await bcrypt.hash(password,
|
|
32
|
-
return {
|
|
33
|
-
hash,
|
|
34
|
-
salt: passwordSalt,
|
|
35
|
-
};
|
|
30
|
+
const hash = await bcrypt.hash(password, finalSalt);
|
|
31
|
+
return { hash, salt: finalSalt };
|
|
36
32
|
}
|
|
37
33
|
catch (error) {
|
|
38
|
-
if (error instanceof BadRequestError ||
|
|
39
|
-
error instanceof ValidationError) {
|
|
34
|
+
if (error instanceof BadRequestError || error instanceof ValidationError) {
|
|
40
35
|
throw error;
|
|
41
36
|
}
|
|
42
|
-
throw new BadRequestError('Failed to hash password');
|
|
37
|
+
throw new BadRequestError({ message: 'Failed to hash password' }, error instanceof Error ? error : undefined);
|
|
43
38
|
}
|
|
44
39
|
}
|
|
45
40
|
/**
|
|
@@ -47,19 +42,12 @@ export class PasswordManager {
|
|
|
47
42
|
*/
|
|
48
43
|
async verify(password, hash, salt) {
|
|
49
44
|
try {
|
|
50
|
-
if (!password || !hash || !salt)
|
|
45
|
+
if (!password || !hash || !salt)
|
|
51
46
|
return false;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const isValid = await bcrypt.compare(password, hash);
|
|
55
|
-
// If invalid and different salt was used, try regenerating hash with new salt
|
|
56
|
-
if (!isValid && salt !== this.defaultConfig.saltRounds?.toString()) {
|
|
57
|
-
const newHash = await bcrypt.hash(password, salt);
|
|
58
|
-
return newHash === hash;
|
|
59
|
-
}
|
|
60
|
-
return isValid;
|
|
47
|
+
// bcrypt compare works directly with hash
|
|
48
|
+
return await bcrypt.compare(password, hash);
|
|
61
49
|
}
|
|
62
|
-
catch
|
|
50
|
+
catch {
|
|
63
51
|
return false;
|
|
64
52
|
}
|
|
65
53
|
}
|
|
@@ -69,7 +57,7 @@ export class PasswordManager {
|
|
|
69
57
|
generate(length = 16, options = {}) {
|
|
70
58
|
const config = { ...this.defaultConfig, ...options };
|
|
71
59
|
if (length < config.minLength || length > config.maxLength) {
|
|
72
|
-
throw new ValidationError(`Password length must be between ${config.minLength} and ${config.maxLength}`);
|
|
60
|
+
throw new ValidationError({ message: `Password length must be between ${config.minLength} and ${config.maxLength}` });
|
|
73
61
|
}
|
|
74
62
|
let charset = 'abcdefghijklmnopqrstuvwxyz';
|
|
75
63
|
if (config.requireUppercase)
|
|
@@ -78,24 +66,20 @@ export class PasswordManager {
|
|
|
78
66
|
charset += '0123456789';
|
|
79
67
|
if (config.requireSpecialChars)
|
|
80
68
|
charset += '!@#$%^&*()_+-=[]{}|;:,.<>?';
|
|
81
|
-
let password = '';
|
|
82
69
|
const randomBytes = crypto.randomBytes(length);
|
|
70
|
+
let password = '';
|
|
83
71
|
for (let i = 0; i < length; i++) {
|
|
84
72
|
password += charset[randomBytes[i] % charset.length];
|
|
85
73
|
}
|
|
86
|
-
// Ensure
|
|
87
|
-
if (config.requireUppercase && !/[A-Z]/.test(password))
|
|
74
|
+
// Ensure requirements
|
|
75
|
+
if (config.requireUppercase && !/[A-Z]/.test(password))
|
|
88
76
|
password = password.replace(/[a-z]/, 'A');
|
|
89
|
-
|
|
90
|
-
if (config.requireLowercase && !/[a-z]/.test(password)) {
|
|
77
|
+
if (config.requireLowercase && !/[a-z]/.test(password))
|
|
91
78
|
password = password.replace(/[A-Z]/, 'a');
|
|
92
|
-
|
|
93
|
-
if (config.requireNumbers && !/[0-9]/.test(password)) {
|
|
79
|
+
if (config.requireNumbers && !/[0-9]/.test(password))
|
|
94
80
|
password = password.replace(/[A-Za-z]/, '0');
|
|
95
|
-
|
|
96
|
-
if (config.requireSpecialChars && !/[^A-Za-z0-9]/.test(password)) {
|
|
81
|
+
if (config.requireSpecialChars && !/[^A-Za-z0-9]/.test(password))
|
|
97
82
|
password = password.replace(/[A-Za-z0-9]/, '!');
|
|
98
|
-
}
|
|
99
83
|
return password;
|
|
100
84
|
}
|
|
101
85
|
/**
|
|
@@ -104,45 +88,27 @@ export class PasswordManager {
|
|
|
104
88
|
validate(password, config = {}) {
|
|
105
89
|
const finalConfig = { ...this.defaultConfig, ...config };
|
|
106
90
|
const errors = [];
|
|
107
|
-
|
|
108
|
-
if (!password || typeof password !== 'string') {
|
|
91
|
+
if (!password || typeof password !== 'string')
|
|
109
92
|
errors.push('Password must be a non-empty string');
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (password.length
|
|
113
|
-
errors.push(`Password must be at least ${finalConfig.minLength} characters long`);
|
|
114
|
-
}
|
|
115
|
-
if (password.length > finalConfig.maxLength) {
|
|
93
|
+
if (password.length < finalConfig.minLength)
|
|
94
|
+
errors.push(`Password must be at least ${finalConfig.minLength} characters`);
|
|
95
|
+
if (password.length > finalConfig.maxLength)
|
|
116
96
|
errors.push(`Password must not exceed ${finalConfig.maxLength} characters`);
|
|
117
|
-
|
|
118
|
-
// Complexity requirements
|
|
119
|
-
if (finalConfig.requireUppercase && !/[A-Z]/.test(password)) {
|
|
97
|
+
if (finalConfig.requireUppercase && !/[A-Z]/.test(password))
|
|
120
98
|
errors.push('Password must contain at least one uppercase letter');
|
|
121
|
-
|
|
122
|
-
if (finalConfig.requireLowercase && !/[a-z]/.test(password)) {
|
|
99
|
+
if (finalConfig.requireLowercase && !/[a-z]/.test(password))
|
|
123
100
|
errors.push('Password must contain at least one lowercase letter');
|
|
124
|
-
|
|
125
|
-
if (finalConfig.requireNumbers && !/[0-9]/.test(password)) {
|
|
101
|
+
if (finalConfig.requireNumbers && !/[0-9]/.test(password))
|
|
126
102
|
errors.push('Password must contain at least one number');
|
|
127
|
-
|
|
128
|
-
if (finalConfig.requireSpecialChars && !/[^A-Za-z0-9]/.test(password)) {
|
|
103
|
+
if (finalConfig.requireSpecialChars && !/[^A-Za-z0-9]/.test(password))
|
|
129
104
|
errors.push('Password must contain at least one special character');
|
|
130
|
-
}
|
|
131
|
-
// Custom rules
|
|
132
105
|
if (finalConfig.customRules) {
|
|
133
106
|
finalConfig.customRules.forEach((rule) => {
|
|
134
|
-
if (!rule.test(password))
|
|
107
|
+
if (!rule.test(password))
|
|
135
108
|
errors.push(rule.message);
|
|
136
|
-
}
|
|
137
109
|
});
|
|
138
110
|
}
|
|
139
|
-
|
|
140
|
-
const isValid = errors.length === 0;
|
|
141
|
-
return {
|
|
142
|
-
isValid,
|
|
143
|
-
errors,
|
|
144
|
-
strength,
|
|
145
|
-
};
|
|
111
|
+
return { isValid: errors.length === 0, errors, strength: this.checkStrength(password) };
|
|
146
112
|
}
|
|
147
113
|
/**
|
|
148
114
|
* Check password strength
|
|
@@ -152,26 +118,20 @@ export class PasswordManager {
|
|
|
152
118
|
let score = 0;
|
|
153
119
|
const feedback = [];
|
|
154
120
|
const suggestions = [];
|
|
155
|
-
/* ---------------- Entropy baseline ---------------- */
|
|
156
121
|
if (entropy < 28) {
|
|
157
122
|
feedback.push('Password is easy to guess');
|
|
158
123
|
suggestions.push('Use more unique characters and length');
|
|
159
124
|
}
|
|
160
|
-
else if (entropy < 36)
|
|
161
|
-
score
|
|
162
|
-
|
|
163
|
-
else if (entropy < 60) {
|
|
125
|
+
else if (entropy < 36)
|
|
126
|
+
score++;
|
|
127
|
+
else if (entropy < 60)
|
|
164
128
|
score += 2;
|
|
165
|
-
|
|
166
|
-
else {
|
|
129
|
+
else
|
|
167
130
|
score += 3;
|
|
168
|
-
}
|
|
169
|
-
/* ---------------- Length scoring ---------------- */
|
|
170
131
|
if (password.length >= 12)
|
|
171
132
|
score++;
|
|
172
133
|
if (password.length >= 16)
|
|
173
134
|
score++;
|
|
174
|
-
/* ---------------- Character variety ---------------- */
|
|
175
135
|
if (/[a-z]/.test(password))
|
|
176
136
|
score++;
|
|
177
137
|
if (/[A-Z]/.test(password))
|
|
@@ -180,7 +140,6 @@ export class PasswordManager {
|
|
|
180
140
|
score++;
|
|
181
141
|
if (/[^A-Za-z0-9]/.test(password))
|
|
182
142
|
score++;
|
|
183
|
-
/* ---------------- Pattern deductions ---------------- */
|
|
184
143
|
if (/^[A-Za-z]+$/.test(password)) {
|
|
185
144
|
score--;
|
|
186
145
|
feedback.push('Consider adding numbers or symbols');
|
|
@@ -197,15 +156,12 @@ export class PasswordManager {
|
|
|
197
156
|
score--;
|
|
198
157
|
feedback.push('Avoid sequential patterns');
|
|
199
158
|
}
|
|
200
|
-
/* ---------------- Common passwords ---------------- */
|
|
201
159
|
const commonPasswords = ['password', '123456', 'qwerty', 'admin', 'letmein'];
|
|
202
160
|
if (commonPasswords.some((common) => password.toLowerCase().includes(common))) {
|
|
203
161
|
score = 0;
|
|
204
162
|
feedback.push('Avoid common passwords');
|
|
205
163
|
}
|
|
206
|
-
/* ---------------- Clamp score ---------------- */
|
|
207
164
|
score = Math.max(0, Math.min(4, score));
|
|
208
|
-
/* ---------------- Strength label ---------------- */
|
|
209
165
|
let label;
|
|
210
166
|
switch (score) {
|
|
211
167
|
case 0:
|
|
@@ -228,22 +184,14 @@ export class PasswordManager {
|
|
|
228
184
|
label = 'strong';
|
|
229
185
|
suggestions.push('Your password is very secure');
|
|
230
186
|
break;
|
|
231
|
-
default:
|
|
232
|
-
label = 'very-weak';
|
|
187
|
+
default: label = 'very-weak';
|
|
233
188
|
}
|
|
234
|
-
return {
|
|
235
|
-
score,
|
|
236
|
-
label,
|
|
237
|
-
feedback,
|
|
238
|
-
suggestions,
|
|
239
|
-
};
|
|
189
|
+
return { score, label, feedback, suggestions };
|
|
240
190
|
}
|
|
241
191
|
/**
|
|
242
|
-
* Check if password hash needs upgrade (
|
|
192
|
+
* Check if password hash needs upgrade (saltRounds change)
|
|
243
193
|
*/
|
|
244
194
|
needsUpgrade(_hash, _currentConfig) {
|
|
245
|
-
// Simple heuristic: if the hash doesn't match current salt rounds pattern
|
|
246
|
-
// In practice, you'd need to store the salt rounds with the hash
|
|
247
195
|
return false;
|
|
248
196
|
}
|
|
249
197
|
}
|
|
@@ -1,17 +1,17 @@
|
|
|
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('Invalid password provided');
|
|
4
|
+
throw new BadRequestError({ message: 'Invalid password provided' });
|
|
5
5
|
const { minLength = 8, requireUppercase = true, requireLowercase = true, requireNumbers = true, requireSymbols = false, } = options;
|
|
6
6
|
if (password.length < minLength)
|
|
7
7
|
throw new ValidationError(`Password must be at least ${minLength} characters`);
|
|
8
8
|
if (requireUppercase && !/[A-Z]/.test(password))
|
|
9
|
-
throw new ValidationError('Password must include uppercase letters');
|
|
9
|
+
throw new ValidationError({ message: 'Password must include uppercase letters' });
|
|
10
10
|
if (requireLowercase && !/[a-z]/.test(password))
|
|
11
|
-
throw new ValidationError('Password must include lowercase letters');
|
|
11
|
+
throw new ValidationError({ message: 'Password must include lowercase letters' });
|
|
12
12
|
if (requireNumbers && !/[0-9]/.test(password))
|
|
13
|
-
throw new ValidationError('Password must include numbers');
|
|
13
|
+
throw new ValidationError({ message: 'Password must include numbers' });
|
|
14
14
|
if (requireSymbols && !/[^A-Za-z0-9]/.test(password))
|
|
15
|
-
throw new ValidationError('Password must include symbols');
|
|
15
|
+
throw new ValidationError({ message: 'Password must include symbols' });
|
|
16
16
|
return true;
|
|
17
17
|
};
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensure password is a valid non-empty string
|
|
3
|
+
*/
|
|
1
4
|
export declare function ensureValidPassword(password: string): void;
|
|
5
|
+
/**
|
|
6
|
+
* Timing-safe comparison between two strings
|
|
7
|
+
*/
|
|
2
8
|
export declare function safeCompare(a: string, b: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Estimate password entropy based on character pool
|
|
11
|
+
*/
|
|
3
12
|
export declare function estimatePasswordEntropy(password: string): number;
|
|
13
|
+
/**
|
|
14
|
+
* Normalize password string to a consistent form
|
|
15
|
+
*/
|
|
4
16
|
export declare function normalizePassword(password: string): string;
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
import { BadRequestError } from '@naman_deep_singh/errors-utils';
|
|
3
|
+
/**
|
|
4
|
+
* Ensure password is a valid non-empty string
|
|
5
|
+
*/
|
|
3
6
|
export function ensureValidPassword(password) {
|
|
4
7
|
if (!password || typeof password !== 'string') {
|
|
5
|
-
throw new BadRequestError('Invalid password provided');
|
|
8
|
+
throw new BadRequestError({ message: 'Invalid password provided' });
|
|
6
9
|
}
|
|
7
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Timing-safe comparison between two strings
|
|
13
|
+
*/
|
|
8
14
|
export function safeCompare(a, b) {
|
|
9
15
|
const bufA = Buffer.from(a);
|
|
10
16
|
const bufB = Buffer.from(b);
|
|
@@ -12,6 +18,9 @@ export function safeCompare(a, b) {
|
|
|
12
18
|
return false;
|
|
13
19
|
return crypto.timingSafeEqual(bufA, bufB);
|
|
14
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Estimate password entropy based on character pool
|
|
23
|
+
*/
|
|
15
24
|
export function estimatePasswordEntropy(password) {
|
|
16
25
|
let pool = 0;
|
|
17
26
|
if (/[a-z]/.test(password))
|
|
@@ -22,8 +31,14 @@ export function estimatePasswordEntropy(password) {
|
|
|
22
31
|
pool += 10;
|
|
23
32
|
if (/[^A-Za-z0-9]/.test(password))
|
|
24
33
|
pool += 32;
|
|
34
|
+
// If no characters matched, fallback to 1 to avoid log2(0)
|
|
35
|
+
if (pool === 0)
|
|
36
|
+
pool = 1;
|
|
25
37
|
return password.length * Math.log2(pool);
|
|
26
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Normalize password string to a consistent form
|
|
41
|
+
*/
|
|
27
42
|
export function normalizePassword(password) {
|
|
28
43
|
return password.normalize('NFKC');
|
|
29
44
|
}
|
|
@@ -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('Password verification failed');
|
|
10
|
+
throw new UnauthorizedError({ message: 'Password verification failed' });
|
|
11
11
|
return result;
|
|
12
12
|
}
|
|
13
13
|
catch {
|
|
14
|
-
throw new UnauthorizedError('Password verification failed');
|
|
14
|
+
throw new UnauthorizedError({ message: '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('Password verification failed');
|
|
27
|
+
throw new UnauthorizedError({ message: 'Password verification failed' });
|
|
28
28
|
return result;
|
|
29
29
|
}
|
|
30
30
|
catch (_error) {
|
|
31
|
-
throw new UnauthorizedError('Password verification failed');
|
|
31
|
+
throw new UnauthorizedError({ message: 'Password verification failed' });
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
34
|
export async function verifyPasswordWithPepperSync(password, pepper, hash) {
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -21,16 +21,11 @@ declare const _default: {
|
|
|
21
21
|
generateTokens: (payload: Record<string, unknown>, accessSecret: import("node_modules/@types/jsonwebtoken").Secret, refreshSecret: import("node_modules/@types/jsonwebtoken").Secret, accessExpiry?: string | number, refreshExpiry?: string | number) => JWTUtils.TokenPair;
|
|
22
22
|
parseDuration(input: string | number): number;
|
|
23
23
|
signToken: (payload: Record<string, unknown>, secret: import("node_modules/@types/jsonwebtoken").Secret, expiresIn?: string | number, options?: import("node_modules/@types/jsonwebtoken").SignOptions) => string;
|
|
24
|
-
validateTokenPayload(payload: Record<string, unknown>, rules?: JWTUtils.TokenRequirements):
|
|
25
|
-
valid: true;
|
|
26
|
-
} | {
|
|
27
|
-
valid: false;
|
|
28
|
-
error: string;
|
|
29
|
-
};
|
|
24
|
+
validateTokenPayload(payload: Record<string, unknown>, rules?: JWTUtils.TokenRequirements): void;
|
|
30
25
|
isTokenExpired(payload: import("node_modules/@types/jsonwebtoken").JwtPayload): boolean;
|
|
31
26
|
verifyToken: (token: string, secret: import("node_modules/@types/jsonwebtoken").Secret) => string | import("node_modules/@types/jsonwebtoken").JwtPayload;
|
|
32
|
-
safeVerifyToken: (token: string, secret: import("node_modules/@types/jsonwebtoken").Secret) => JWTUtils.VerificationResult;
|
|
33
27
|
verifyTokenWithOptions: (token: string, secret: import("node_modules/@types/jsonwebtoken").Secret, options?: import("node_modules/@types/jsonwebtoken").VerifyOptions) => string | import("node_modules/@types/jsonwebtoken").JwtPayload;
|
|
28
|
+
safeVerifyToken: (token: string, secret: import("node_modules/@types/jsonwebtoken").Secret) => JWTUtils.VerificationResult;
|
|
34
29
|
safeVerifyTokenWithOptions: (token: string, secret: import("node_modules/@types/jsonwebtoken").Secret, options?: import("node_modules/@types/jsonwebtoken").VerifyOptions) => JWTUtils.VerificationResult;
|
|
35
30
|
hashPasswordWithPepper(password: string, pepper: string): Promise<string>;
|
|
36
31
|
hashPasswordWithPepperSync(password: string, pepper: string): string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type JwtPayload, type Secret } from 'jsonwebtoken';
|
|
2
|
-
import type { AccessToken, ITokenManager, JWTConfig, RefreshToken, TokenPair
|
|
2
|
+
import type { AccessToken, ITokenManager, JWTConfig, RefreshToken, TokenPair } from '../../interfaces/jwt.interface';
|
|
3
3
|
export declare class JWTManager implements ITokenManager {
|
|
4
4
|
private accessSecret;
|
|
5
5
|
private refreshSecret;
|
|
@@ -8,60 +8,36 @@ export declare class JWTManager implements ITokenManager {
|
|
|
8
8
|
private cache?;
|
|
9
9
|
private cacheTTL;
|
|
10
10
|
constructor(config: JWTConfig);
|
|
11
|
-
/**
|
|
12
|
-
* Generate both access and refresh tokens
|
|
13
|
-
*/
|
|
11
|
+
/** Generate both access and refresh tokens */
|
|
14
12
|
generateTokens(payload: Record<string, unknown>): Promise<TokenPair>;
|
|
15
|
-
/**
|
|
16
|
-
* Generate access token
|
|
17
|
-
*/
|
|
13
|
+
/** Generate access token */
|
|
18
14
|
generateAccessToken(payload: Record<string, unknown>): Promise<AccessToken>;
|
|
19
|
-
/**
|
|
20
|
-
* Generate refresh token
|
|
21
|
-
*/
|
|
15
|
+
/** Generate refresh token */
|
|
22
16
|
generateRefreshToken(payload: Record<string, unknown>): Promise<RefreshToken>;
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Verify refresh token
|
|
29
|
-
*/
|
|
30
|
-
verifyRefreshToken(token: string): Promise<JwtPayload | string>;
|
|
31
|
-
/**
|
|
32
|
-
* Decode token without verification
|
|
33
|
-
*/
|
|
17
|
+
/** Verify access token */
|
|
18
|
+
verifyAccessToken(token: string): Promise<JwtPayload>;
|
|
19
|
+
/** Verify refresh token */
|
|
20
|
+
verifyRefreshToken(token: string): Promise<JwtPayload>;
|
|
21
|
+
/** Decode token without verification */
|
|
34
22
|
decodeToken(token: string, complete?: boolean): JwtPayload | string | null;
|
|
35
|
-
/**
|
|
36
|
-
* Extract token from Authorization header
|
|
37
|
-
*/
|
|
23
|
+
/** Extract token from Authorization header */
|
|
38
24
|
extractTokenFromHeader(authHeader: string): string | null;
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
validateToken(token: string, secret: Secret, _options?: TokenValidationOptions): boolean;
|
|
43
|
-
/**
|
|
44
|
-
* Rotate refresh token
|
|
45
|
-
*/
|
|
25
|
+
/** Validate token without throwing exceptions */
|
|
26
|
+
validateToken(token: string, secret: Secret): boolean;
|
|
27
|
+
/** Rotate refresh token */
|
|
46
28
|
rotateRefreshToken(oldToken: string): Promise<RefreshToken>;
|
|
47
|
-
/**
|
|
48
|
-
* Check if token is expired
|
|
49
|
-
*/
|
|
29
|
+
/** Check if token is expired */
|
|
50
30
|
isTokenExpired(token: string): boolean;
|
|
51
|
-
/**
|
|
52
|
-
* Get token expiration date
|
|
53
|
-
*/
|
|
31
|
+
/** Get token expiration date */
|
|
54
32
|
getTokenExpiration(token: string): Date | null;
|
|
55
|
-
/**
|
|
56
|
-
* Clear token cache
|
|
57
|
-
*/
|
|
33
|
+
/** Clear token cache */
|
|
58
34
|
clearCache(): void;
|
|
59
|
-
/**
|
|
60
|
-
* Get cache statistics
|
|
61
|
-
*/
|
|
35
|
+
/** Get cache statistics */
|
|
62
36
|
getCacheStats(): {
|
|
63
37
|
size: number;
|
|
64
38
|
maxSize: number;
|
|
65
39
|
} | null;
|
|
40
|
+
/** Private helper methods */
|
|
66
41
|
private validatePayload;
|
|
42
|
+
private verifyTokenWithCache;
|
|
67
43
|
}
|