@naman_deep_singh/security 1.2.0 → 1.3.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 +355 -176
- package/dist/cjs/core/crypto/cryptoManager.d.ts +111 -0
- package/dist/cjs/core/crypto/cryptoManager.js +185 -0
- package/dist/cjs/core/crypto/index.d.ts +5 -4
- package/dist/cjs/core/crypto/index.js +12 -4
- package/dist/cjs/core/jwt/jwtManager.d.ts +66 -0
- package/dist/cjs/core/jwt/jwtManager.js +319 -0
- package/dist/cjs/core/password/passwordManager.d.ts +29 -0
- package/dist/cjs/core/password/passwordManager.js +242 -0
- package/dist/cjs/index.d.ts +4 -0
- package/dist/cjs/interfaces/jwt.interface.d.ts +47 -0
- package/dist/cjs/interfaces/jwt.interface.js +2 -0
- package/dist/cjs/interfaces/password.interface.d.ts +60 -0
- package/dist/cjs/interfaces/password.interface.js +2 -0
- package/dist/esm/core/crypto/cryptoManager.d.ts +111 -0
- package/dist/esm/core/crypto/cryptoManager.js +180 -0
- package/dist/esm/core/crypto/index.d.ts +5 -4
- package/dist/esm/core/crypto/index.js +5 -4
- package/dist/esm/core/jwt/jwtManager.d.ts +66 -0
- package/dist/esm/core/jwt/jwtManager.js +312 -0
- package/dist/esm/core/password/passwordManager.d.ts +29 -0
- package/dist/esm/core/password/passwordManager.js +235 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/interfaces/jwt.interface.d.ts +47 -0
- package/dist/esm/interfaces/jwt.interface.js +1 -0
- package/dist/esm/interfaces/password.interface.d.ts +60 -0
- package/dist/esm/interfaces/password.interface.js +1 -0
- package/dist/types/core/crypto/cryptoManager.d.ts +111 -0
- package/dist/types/core/crypto/index.d.ts +5 -4
- package/dist/types/core/jwt/jwtManager.d.ts +66 -0
- package/dist/types/core/password/passwordManager.d.ts +29 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/interfaces/jwt.interface.d.ts +47 -0
- package/dist/types/interfaces/password.interface.d.ts +60 -0
- package/package.json +1 -1
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import jwt from "jsonwebtoken";
|
|
2
|
+
import { signToken } from "./signToken";
|
|
3
|
+
import { verifyToken, safeVerifyToken } from "./verify";
|
|
4
|
+
import { BadRequestError, UnauthorizedError, ValidationError } from "@naman_deep_singh/errors-utils";
|
|
5
|
+
// Simple LRU cache for token validation
|
|
6
|
+
class TokenCache {
|
|
7
|
+
constructor(maxSize = 100, ttl = 5 * 60 * 1000) {
|
|
8
|
+
this.cache = new Map();
|
|
9
|
+
this.maxSize = maxSize;
|
|
10
|
+
this.ttl = ttl;
|
|
11
|
+
}
|
|
12
|
+
get(key) {
|
|
13
|
+
const entry = this.cache.get(key);
|
|
14
|
+
if (!entry)
|
|
15
|
+
return null;
|
|
16
|
+
if (Date.now() - entry.timestamp > this.ttl) {
|
|
17
|
+
this.cache.delete(key);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
// Move to end (most recently used)
|
|
21
|
+
this.cache.delete(key);
|
|
22
|
+
this.cache.set(key, entry);
|
|
23
|
+
return { valid: entry.valid, payload: entry.payload };
|
|
24
|
+
}
|
|
25
|
+
set(key, value) {
|
|
26
|
+
if (this.cache.has(key)) {
|
|
27
|
+
this.cache.delete(key);
|
|
28
|
+
}
|
|
29
|
+
else if (this.cache.size >= this.maxSize) {
|
|
30
|
+
// Remove least recently used (first item)
|
|
31
|
+
const firstKey = this.cache.keys().next().value;
|
|
32
|
+
if (firstKey !== undefined) {
|
|
33
|
+
this.cache.delete(firstKey);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
this.cache.set(key, { ...value, timestamp: Date.now() });
|
|
37
|
+
}
|
|
38
|
+
clear() {
|
|
39
|
+
this.cache.clear();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export class JWTManager {
|
|
43
|
+
constructor(config) {
|
|
44
|
+
this.accessSecret = config.accessSecret;
|
|
45
|
+
this.refreshSecret = config.refreshSecret;
|
|
46
|
+
this.accessExpiry = config.accessExpiry || "15m";
|
|
47
|
+
this.refreshExpiry = config.refreshExpiry || "7d";
|
|
48
|
+
if (config.enableCaching) {
|
|
49
|
+
this.cache = new TokenCache(config.maxCacheSize || 100);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Generate both access and refresh tokens
|
|
54
|
+
*/
|
|
55
|
+
async generateTokens(payload) {
|
|
56
|
+
try {
|
|
57
|
+
this.validatePayload(payload);
|
|
58
|
+
const accessToken = await this.generateAccessToken(payload);
|
|
59
|
+
const refreshToken = await this.generateRefreshToken(payload);
|
|
60
|
+
return {
|
|
61
|
+
accessToken,
|
|
62
|
+
refreshToken,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error instanceof BadRequestError || error instanceof ValidationError) {
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
throw new BadRequestError("Failed to generate tokens");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Generate access token
|
|
74
|
+
*/
|
|
75
|
+
async generateAccessToken(payload) {
|
|
76
|
+
try {
|
|
77
|
+
this.validatePayload(payload);
|
|
78
|
+
const token = signToken(payload, this.accessSecret, this.accessExpiry, { algorithm: "HS256" });
|
|
79
|
+
return token;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error instanceof BadRequestError || error instanceof ValidationError) {
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
throw new BadRequestError("Failed to generate access token");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Generate refresh token
|
|
90
|
+
*/
|
|
91
|
+
async generateRefreshToken(payload) {
|
|
92
|
+
try {
|
|
93
|
+
this.validatePayload(payload);
|
|
94
|
+
const token = signToken(payload, this.refreshSecret, this.refreshExpiry, { algorithm: "HS256" });
|
|
95
|
+
return token;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error instanceof BadRequestError || error instanceof ValidationError) {
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
throw new BadRequestError("Failed to generate refresh token");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Verify access token
|
|
106
|
+
*/
|
|
107
|
+
async verifyAccessToken(token) {
|
|
108
|
+
try {
|
|
109
|
+
if (!token || typeof token !== "string") {
|
|
110
|
+
throw new ValidationError("Access token must be a non-empty string");
|
|
111
|
+
}
|
|
112
|
+
const cacheKey = `access_${token}`;
|
|
113
|
+
if (this.cache) {
|
|
114
|
+
const cached = this.cache.get(cacheKey);
|
|
115
|
+
if (cached) {
|
|
116
|
+
if (!cached.valid) {
|
|
117
|
+
throw new UnauthorizedError("Access token is invalid or expired");
|
|
118
|
+
}
|
|
119
|
+
return cached.payload;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const decoded = verifyToken(token, this.accessSecret);
|
|
123
|
+
if (this.cache) {
|
|
124
|
+
this.cache.set(cacheKey, { valid: true, payload: decoded });
|
|
125
|
+
}
|
|
126
|
+
return decoded;
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (error instanceof ValidationError || error instanceof UnauthorizedError) {
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
if (error instanceof Error && error.name === "TokenExpiredError") {
|
|
133
|
+
throw new UnauthorizedError("Access token has expired");
|
|
134
|
+
}
|
|
135
|
+
if (error instanceof Error && error.name === "JsonWebTokenError") {
|
|
136
|
+
throw new UnauthorizedError("Access token is invalid");
|
|
137
|
+
}
|
|
138
|
+
throw new UnauthorizedError("Failed to verify access token");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Verify refresh token
|
|
143
|
+
*/
|
|
144
|
+
async verifyRefreshToken(token) {
|
|
145
|
+
try {
|
|
146
|
+
if (!token || typeof token !== "string") {
|
|
147
|
+
throw new ValidationError("Refresh token must be a non-empty string");
|
|
148
|
+
}
|
|
149
|
+
const cacheKey = `refresh_${token}`;
|
|
150
|
+
if (this.cache) {
|
|
151
|
+
const cached = this.cache.get(cacheKey);
|
|
152
|
+
if (cached) {
|
|
153
|
+
if (!cached.valid) {
|
|
154
|
+
throw new UnauthorizedError("Refresh token is invalid or expired");
|
|
155
|
+
}
|
|
156
|
+
return cached.payload;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const decoded = verifyToken(token, this.refreshSecret);
|
|
160
|
+
if (this.cache) {
|
|
161
|
+
this.cache.set(cacheKey, { valid: true, payload: decoded });
|
|
162
|
+
}
|
|
163
|
+
return decoded;
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (error instanceof ValidationError || error instanceof UnauthorizedError) {
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
if (error instanceof Error && error.name === "TokenExpiredError") {
|
|
170
|
+
throw new UnauthorizedError("Refresh token has expired");
|
|
171
|
+
}
|
|
172
|
+
if (error instanceof Error && error.name === "JsonWebTokenError") {
|
|
173
|
+
throw new UnauthorizedError("Refresh token is invalid");
|
|
174
|
+
}
|
|
175
|
+
throw new UnauthorizedError("Failed to verify refresh token");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Decode token without verification
|
|
180
|
+
*/
|
|
181
|
+
decodeToken(token, complete = false) {
|
|
182
|
+
try {
|
|
183
|
+
if (!token || typeof token !== "string") {
|
|
184
|
+
throw new ValidationError("Token must be a non-empty string");
|
|
185
|
+
}
|
|
186
|
+
return jwt.decode(token, { complete });
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
if (error instanceof ValidationError) {
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Extract token from Authorization header
|
|
197
|
+
*/
|
|
198
|
+
extractTokenFromHeader(authHeader) {
|
|
199
|
+
try {
|
|
200
|
+
if (!authHeader || typeof authHeader !== "string") {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
const parts = authHeader.split(" ");
|
|
204
|
+
if (parts.length !== 2 || parts[0] !== "Bearer") {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
return parts[1];
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Validate token without throwing exceptions
|
|
215
|
+
*/
|
|
216
|
+
validateToken(token, secret, options = {}) {
|
|
217
|
+
try {
|
|
218
|
+
if (!token || typeof token !== "string") {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
const result = safeVerifyToken(token, secret);
|
|
222
|
+
return result.valid;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Rotate refresh token
|
|
230
|
+
*/
|
|
231
|
+
async rotateRefreshToken(oldToken) {
|
|
232
|
+
try {
|
|
233
|
+
if (!oldToken || typeof oldToken !== "string") {
|
|
234
|
+
throw new ValidationError("Old refresh token must be a non-empty string");
|
|
235
|
+
}
|
|
236
|
+
const decoded = await this.verifyRefreshToken(oldToken);
|
|
237
|
+
if (typeof decoded === "string") {
|
|
238
|
+
throw new ValidationError("Invalid token payload — expected JWT payload object");
|
|
239
|
+
}
|
|
240
|
+
// Create new payload without issued/expired timestamps
|
|
241
|
+
const payload = { ...decoded };
|
|
242
|
+
delete payload.iat;
|
|
243
|
+
delete payload.exp;
|
|
244
|
+
// Generate new refresh token
|
|
245
|
+
const newToken = signToken(payload, this.refreshSecret, this.refreshExpiry);
|
|
246
|
+
return newToken;
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
if (error instanceof ValidationError || error instanceof UnauthorizedError) {
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
throw new BadRequestError("Failed to rotate refresh token");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Check if token is expired
|
|
257
|
+
*/
|
|
258
|
+
isTokenExpired(token) {
|
|
259
|
+
try {
|
|
260
|
+
const decoded = this.decodeToken(token);
|
|
261
|
+
if (!decoded || !decoded.exp) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
const currentTime = Math.floor(Date.now() / 1000);
|
|
265
|
+
return decoded.exp < currentTime;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Get token expiration date
|
|
273
|
+
*/
|
|
274
|
+
getTokenExpiration(token) {
|
|
275
|
+
try {
|
|
276
|
+
const decoded = this.decodeToken(token);
|
|
277
|
+
if (!decoded || !decoded.exp) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
return new Date(decoded.exp * 1000);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Clear token cache
|
|
288
|
+
*/
|
|
289
|
+
clearCache() {
|
|
290
|
+
this.cache?.clear();
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Get cache statistics
|
|
294
|
+
*/
|
|
295
|
+
getCacheStats() {
|
|
296
|
+
if (!this.cache)
|
|
297
|
+
return null;
|
|
298
|
+
return {
|
|
299
|
+
size: this.cache.cache.size,
|
|
300
|
+
maxSize: this.cache.maxSize
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
// Private helper methods
|
|
304
|
+
validatePayload(payload) {
|
|
305
|
+
if (!payload || typeof payload !== "object") {
|
|
306
|
+
throw new ValidationError("Payload must be a non-null object");
|
|
307
|
+
}
|
|
308
|
+
if (Object.keys(payload).length === 0) {
|
|
309
|
+
throw new ValidationError("Payload cannot be empty");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { IPasswordManager, PasswordConfig, PasswordValidationResult, HashedPassword, PasswordStrength } from "../../interfaces/password.interface";
|
|
2
|
+
export declare class PasswordManager implements IPasswordManager {
|
|
3
|
+
private defaultConfig;
|
|
4
|
+
constructor(config?: PasswordConfig);
|
|
5
|
+
/**
|
|
6
|
+
* Hash a password asynchronously using bcrypt
|
|
7
|
+
*/
|
|
8
|
+
hash(password: string, salt?: string): Promise<HashedPassword>;
|
|
9
|
+
/**
|
|
10
|
+
* Verify password against hash and salt
|
|
11
|
+
*/
|
|
12
|
+
verify(password: string, hash: string, salt: string): Promise<boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* Generate a random password
|
|
15
|
+
*/
|
|
16
|
+
generate(length?: number, options?: PasswordConfig): string;
|
|
17
|
+
/**
|
|
18
|
+
* Validate password against configuration
|
|
19
|
+
*/
|
|
20
|
+
validate(password: string, config?: PasswordConfig): PasswordValidationResult;
|
|
21
|
+
/**
|
|
22
|
+
* Check password strength
|
|
23
|
+
*/
|
|
24
|
+
checkStrength(password: string): PasswordStrength;
|
|
25
|
+
/**
|
|
26
|
+
* Check if password hash needs upgrade (different salt rounds)
|
|
27
|
+
*/
|
|
28
|
+
needsUpgrade(hash: string, currentConfig: PasswordConfig): boolean;
|
|
29
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import bcrypt from "bcryptjs";
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
import { ensureValidPassword, estimatePasswordEntropy } from "./utils";
|
|
4
|
+
import { BadRequestError, ValidationError } from "@naman_deep_singh/errors-utils";
|
|
5
|
+
export class PasswordManager {
|
|
6
|
+
constructor(config = {}) {
|
|
7
|
+
this.defaultConfig = {
|
|
8
|
+
saltRounds: 10,
|
|
9
|
+
minLength: 8,
|
|
10
|
+
maxLength: 128,
|
|
11
|
+
requireUppercase: true,
|
|
12
|
+
requireLowercase: true,
|
|
13
|
+
requireNumbers: true,
|
|
14
|
+
requireSpecialChars: false,
|
|
15
|
+
...config
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Hash a password asynchronously using bcrypt
|
|
20
|
+
*/
|
|
21
|
+
async hash(password, salt) {
|
|
22
|
+
try {
|
|
23
|
+
ensureValidPassword(password);
|
|
24
|
+
// Validate password meets basic requirements
|
|
25
|
+
this.validate(password);
|
|
26
|
+
const saltRounds = this.defaultConfig.saltRounds;
|
|
27
|
+
let passwordSalt = salt;
|
|
28
|
+
if (!passwordSalt) {
|
|
29
|
+
passwordSalt = await bcrypt.genSalt(saltRounds);
|
|
30
|
+
}
|
|
31
|
+
const hash = await bcrypt.hash(password, passwordSalt);
|
|
32
|
+
return {
|
|
33
|
+
hash,
|
|
34
|
+
salt: passwordSalt
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error instanceof BadRequestError || error instanceof ValidationError) {
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
throw new BadRequestError("Failed to hash password");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Verify password against hash and salt
|
|
46
|
+
*/
|
|
47
|
+
async verify(password, hash, salt) {
|
|
48
|
+
try {
|
|
49
|
+
if (!password || !hash || !salt) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
// First verify with the provided salt
|
|
53
|
+
const isValid = await bcrypt.compare(password, hash);
|
|
54
|
+
// If invalid and different salt was used, try regenerating hash with new salt
|
|
55
|
+
if (!isValid && salt !== this.defaultConfig.saltRounds?.toString()) {
|
|
56
|
+
const newHash = await bcrypt.hash(password, salt);
|
|
57
|
+
return newHash === hash;
|
|
58
|
+
}
|
|
59
|
+
return isValid;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Generate a random password
|
|
67
|
+
*/
|
|
68
|
+
generate(length = 16, options = {}) {
|
|
69
|
+
const config = { ...this.defaultConfig, ...options };
|
|
70
|
+
if (length < config.minLength || length > config.maxLength) {
|
|
71
|
+
throw new ValidationError(`Password length must be between ${config.minLength} and ${config.maxLength}`);
|
|
72
|
+
}
|
|
73
|
+
let charset = "abcdefghijklmnopqrstuvwxyz";
|
|
74
|
+
if (config.requireUppercase)
|
|
75
|
+
charset += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
76
|
+
if (config.requireNumbers)
|
|
77
|
+
charset += "0123456789";
|
|
78
|
+
if (config.requireSpecialChars)
|
|
79
|
+
charset += "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
|
80
|
+
let password = "";
|
|
81
|
+
const randomBytes = crypto.randomBytes(length);
|
|
82
|
+
for (let i = 0; i < length; i++) {
|
|
83
|
+
password += charset[randomBytes[i] % charset.length];
|
|
84
|
+
}
|
|
85
|
+
// Ensure all requirements are met
|
|
86
|
+
if (config.requireUppercase && !/[A-Z]/.test(password)) {
|
|
87
|
+
password = password.replace(/[a-z]/, 'A');
|
|
88
|
+
}
|
|
89
|
+
if (config.requireLowercase && !/[a-z]/.test(password)) {
|
|
90
|
+
password = password.replace(/[A-Z]/, 'a');
|
|
91
|
+
}
|
|
92
|
+
if (config.requireNumbers && !/[0-9]/.test(password)) {
|
|
93
|
+
password = password.replace(/[A-Za-z]/, '0');
|
|
94
|
+
}
|
|
95
|
+
if (config.requireSpecialChars && !/[^A-Za-z0-9]/.test(password)) {
|
|
96
|
+
password = password.replace(/[A-Za-z0-9]/, '!');
|
|
97
|
+
}
|
|
98
|
+
return password;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Validate password against configuration
|
|
102
|
+
*/
|
|
103
|
+
validate(password, config = {}) {
|
|
104
|
+
const finalConfig = { ...this.defaultConfig, ...config };
|
|
105
|
+
const errors = [];
|
|
106
|
+
// Basic validation
|
|
107
|
+
if (!password || typeof password !== "string") {
|
|
108
|
+
errors.push("Password must be a non-empty string");
|
|
109
|
+
}
|
|
110
|
+
// Length validation
|
|
111
|
+
if (password.length < finalConfig.minLength) {
|
|
112
|
+
errors.push(`Password must be at least ${finalConfig.minLength} characters long`);
|
|
113
|
+
}
|
|
114
|
+
if (password.length > finalConfig.maxLength) {
|
|
115
|
+
errors.push(`Password must not exceed ${finalConfig.maxLength} characters`);
|
|
116
|
+
}
|
|
117
|
+
// Complexity requirements
|
|
118
|
+
if (finalConfig.requireUppercase && !/[A-Z]/.test(password)) {
|
|
119
|
+
errors.push("Password must contain at least one uppercase letter");
|
|
120
|
+
}
|
|
121
|
+
if (finalConfig.requireLowercase && !/[a-z]/.test(password)) {
|
|
122
|
+
errors.push("Password must contain at least one lowercase letter");
|
|
123
|
+
}
|
|
124
|
+
if (finalConfig.requireNumbers && !/[0-9]/.test(password)) {
|
|
125
|
+
errors.push("Password must contain at least one number");
|
|
126
|
+
}
|
|
127
|
+
if (finalConfig.requireSpecialChars && !/[^A-Za-z0-9]/.test(password)) {
|
|
128
|
+
errors.push("Password must contain at least one special character");
|
|
129
|
+
}
|
|
130
|
+
// Custom rules
|
|
131
|
+
if (finalConfig.customRules) {
|
|
132
|
+
finalConfig.customRules.forEach(rule => {
|
|
133
|
+
if (!rule.test(password)) {
|
|
134
|
+
errors.push(rule.message);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const strength = this.checkStrength(password);
|
|
139
|
+
const isValid = errors.length === 0;
|
|
140
|
+
return {
|
|
141
|
+
isValid,
|
|
142
|
+
errors,
|
|
143
|
+
strength
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Check password strength
|
|
148
|
+
*/
|
|
149
|
+
checkStrength(password) {
|
|
150
|
+
const entropy = estimatePasswordEntropy(password);
|
|
151
|
+
let score = 0;
|
|
152
|
+
const feedback = [];
|
|
153
|
+
const suggestions = [];
|
|
154
|
+
// Length scoring
|
|
155
|
+
if (password.length >= 8)
|
|
156
|
+
score++;
|
|
157
|
+
if (password.length >= 12)
|
|
158
|
+
score++;
|
|
159
|
+
if (password.length >= 16)
|
|
160
|
+
score++;
|
|
161
|
+
// Character variety scoring
|
|
162
|
+
if (/[a-z]/.test(password))
|
|
163
|
+
score++;
|
|
164
|
+
if (/[A-Z]/.test(password))
|
|
165
|
+
score++;
|
|
166
|
+
if (/[0-9]/.test(password))
|
|
167
|
+
score++;
|
|
168
|
+
if (/[^A-Za-z0-9]/.test(password))
|
|
169
|
+
score++;
|
|
170
|
+
// Common patterns deduction
|
|
171
|
+
if (/^[A-Za-z]+$/.test(password)) {
|
|
172
|
+
score--;
|
|
173
|
+
feedback.push("Consider adding numbers and symbols");
|
|
174
|
+
}
|
|
175
|
+
if (/^[0-9]+$/.test(password)) {
|
|
176
|
+
score -= 2;
|
|
177
|
+
feedback.push("Avoid using only numbers");
|
|
178
|
+
}
|
|
179
|
+
if (/([a-zA-Z0-9])\1{2,}/.test(password)) {
|
|
180
|
+
score--;
|
|
181
|
+
feedback.push("Avoid repeated characters");
|
|
182
|
+
}
|
|
183
|
+
if (/(?:012|123|234|345|456|567|678|789)/.test(password)) {
|
|
184
|
+
score--;
|
|
185
|
+
feedback.push("Avoid sequential patterns");
|
|
186
|
+
}
|
|
187
|
+
// Common passwords check
|
|
188
|
+
const commonPasswords = ['password', '123456', 'qwerty', 'admin', 'letmein'];
|
|
189
|
+
if (commonPasswords.some(common => password.toLowerCase().includes(common))) {
|
|
190
|
+
score = 0;
|
|
191
|
+
feedback.push("Avoid common passwords");
|
|
192
|
+
}
|
|
193
|
+
// Clamp score and determine label
|
|
194
|
+
score = Math.max(0, Math.min(4, score));
|
|
195
|
+
let label;
|
|
196
|
+
switch (score) {
|
|
197
|
+
case 0:
|
|
198
|
+
label = 'very-weak';
|
|
199
|
+
suggestions.push("Use a longer password with mixed characters");
|
|
200
|
+
break;
|
|
201
|
+
case 1:
|
|
202
|
+
label = 'weak';
|
|
203
|
+
suggestions.push("Add more character variety");
|
|
204
|
+
break;
|
|
205
|
+
case 2:
|
|
206
|
+
label = 'fair';
|
|
207
|
+
suggestions.push("Consider adding more length or character types");
|
|
208
|
+
break;
|
|
209
|
+
case 3:
|
|
210
|
+
label = 'good';
|
|
211
|
+
suggestions.push("Your password is reasonably secure");
|
|
212
|
+
break;
|
|
213
|
+
case 4:
|
|
214
|
+
label = 'strong';
|
|
215
|
+
suggestions.push("Your password is very secure");
|
|
216
|
+
break;
|
|
217
|
+
default:
|
|
218
|
+
label = 'very-weak';
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
score,
|
|
222
|
+
label,
|
|
223
|
+
feedback,
|
|
224
|
+
suggestions
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Check if password hash needs upgrade (different salt rounds)
|
|
229
|
+
*/
|
|
230
|
+
needsUpgrade(hash, currentConfig) {
|
|
231
|
+
// Simple heuristic: if the hash doesn't match current salt rounds pattern
|
|
232
|
+
// In practice, you'd need to store the salt rounds with the hash
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from "./core/jwt";
|
|
|
3
3
|
export * from "./core/crypto";
|
|
4
4
|
export { BadRequestError, UnauthorizedError, ValidationError, InternalServerError } from "@naman_deep_singh/errors-utils";
|
|
5
5
|
import * as JWTUtils from "./core/jwt";
|
|
6
|
+
import * as CryptoUtils from "./core/crypto";
|
|
6
7
|
declare const _default: {
|
|
7
8
|
decrypt: (data: string, secret: string) => string;
|
|
8
9
|
encrypt: (text: string, secret: string) => string;
|
|
@@ -10,6 +11,9 @@ declare const _default: {
|
|
|
10
11
|
hmacVerify: (message: string, secret: string, signature: string) => boolean;
|
|
11
12
|
randomToken: (length?: number) => string;
|
|
12
13
|
generateStrongPassword: (length?: number) => string;
|
|
14
|
+
CryptoManager: typeof CryptoUtils.CryptoManager;
|
|
15
|
+
createCryptoManager: (config?: CryptoUtils.CryptoManagerConfig) => CryptoUtils.CryptoManager;
|
|
16
|
+
cryptoManager: CryptoUtils.CryptoManager;
|
|
13
17
|
decodeToken(token: string): null | string | import("node_modules/@types/jsonwebtoken").JwtPayload;
|
|
14
18
|
decodeTokenStrict(token: string): import("node_modules/@types/jsonwebtoken").JwtPayload;
|
|
15
19
|
extractToken(sources: JWTUtils.TokenSources): string | null;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { JwtPayload, Secret } from "jsonwebtoken";
|
|
2
|
+
export interface AccessToken extends String {
|
|
3
|
+
readonly __type: 'AccessToken';
|
|
4
|
+
}
|
|
5
|
+
export interface RefreshToken extends String {
|
|
6
|
+
readonly __type: 'RefreshToken';
|
|
7
|
+
}
|
|
8
|
+
export interface TokenPair {
|
|
9
|
+
accessToken: AccessToken;
|
|
10
|
+
refreshToken: RefreshToken;
|
|
11
|
+
}
|
|
12
|
+
export interface JWTConfig {
|
|
13
|
+
accessSecret: Secret;
|
|
14
|
+
refreshSecret: Secret;
|
|
15
|
+
accessExpiry?: string | number;
|
|
16
|
+
refreshExpiry?: string | number;
|
|
17
|
+
enableCaching?: boolean;
|
|
18
|
+
maxCacheSize?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface TokenValidationOptions {
|
|
21
|
+
ignoreExpiration?: boolean;
|
|
22
|
+
ignoreNotBefore?: boolean;
|
|
23
|
+
audience?: string | string[];
|
|
24
|
+
issuer?: string;
|
|
25
|
+
algorithms?: string[];
|
|
26
|
+
}
|
|
27
|
+
export interface TokenGenerationOptions {
|
|
28
|
+
algorithm?: string;
|
|
29
|
+
expiresIn?: string | number;
|
|
30
|
+
audience?: string | string[];
|
|
31
|
+
issuer?: string;
|
|
32
|
+
subject?: string;
|
|
33
|
+
kid?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface ITokenManager {
|
|
36
|
+
generateTokens(payload: Record<string, unknown>): Promise<TokenPair>;
|
|
37
|
+
generateAccessToken(payload: Record<string, unknown>): Promise<AccessToken>;
|
|
38
|
+
generateRefreshToken(payload: Record<string, unknown>): Promise<RefreshToken>;
|
|
39
|
+
verifyAccessToken(token: string): Promise<JwtPayload | string>;
|
|
40
|
+
verifyRefreshToken(token: string): Promise<JwtPayload | string>;
|
|
41
|
+
decodeToken(token: string, complete?: boolean): JwtPayload | string | null;
|
|
42
|
+
extractTokenFromHeader(authHeader: string): string | null;
|
|
43
|
+
validateToken(token: string, secret: Secret, options?: TokenValidationOptions): boolean;
|
|
44
|
+
rotateRefreshToken(oldToken: string): Promise<RefreshToken>;
|
|
45
|
+
isTokenExpired(token: string): boolean;
|
|
46
|
+
getTokenExpiration(token: string): Date | null;
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|