@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,3 +1,4 @@
|
|
|
1
|
+
import { InternalServerError } from '@naman_deep_singh/errors-utils';
|
|
1
2
|
import { decrypt as functionalDecrypt, encrypt as functionalEncrypt, hmacSign as functionalHmacSign, hmacVerify as functionalHmacVerify, randomToken as functionalRandomToken, } from './index';
|
|
2
3
|
/**
|
|
3
4
|
* Default configuration
|
|
@@ -31,17 +32,27 @@ export class CryptoManager {
|
|
|
31
32
|
* Encrypt data using the default or specified algorithm
|
|
32
33
|
*/
|
|
33
34
|
encrypt(plaintext, key, _options) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
try {
|
|
36
|
+
return functionalEncrypt(plaintext, key);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
throw new InternalServerError(undefined, {
|
|
40
|
+
message: 'Encryption failed',
|
|
41
|
+
}, err instanceof Error ? err : undefined);
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
44
|
/**
|
|
39
45
|
* Decrypt data using the default or specified algorithm
|
|
40
46
|
*/
|
|
41
47
|
decrypt(encryptedData, key, _options) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
try {
|
|
49
|
+
return functionalDecrypt(encryptedData, key);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
throw new InternalServerError(undefined, {
|
|
53
|
+
message: 'Decryption failed',
|
|
54
|
+
}, err instanceof Error ? err : undefined);
|
|
55
|
+
}
|
|
45
56
|
}
|
|
46
57
|
/**
|
|
47
58
|
* Generate HMAC signature
|
|
@@ -73,7 +84,9 @@ export class CryptoManager {
|
|
|
73
84
|
const crypto = require('crypto');
|
|
74
85
|
crypto.pbkdf2(password, salt, iterations, keyLength, 'sha256', (err, derivedKey) => {
|
|
75
86
|
if (err) {
|
|
76
|
-
reject(
|
|
87
|
+
reject(new InternalServerError(undefined, {
|
|
88
|
+
message: 'Key derivation failed',
|
|
89
|
+
}, err instanceof Error ? err : undefined));
|
|
77
90
|
}
|
|
78
91
|
else {
|
|
79
92
|
resolve(derivedKey.toString('hex'));
|
|
@@ -143,15 +156,17 @@ export class CryptoManager {
|
|
|
143
156
|
rsaSign(data, privateKey, algorithm = 'sha256') {
|
|
144
157
|
return new Promise((resolve, reject) => {
|
|
145
158
|
const crypto = require('crypto');
|
|
146
|
-
const sign = crypto.createSign(algorithm);
|
|
147
|
-
sign.update(data);
|
|
148
|
-
sign.end();
|
|
149
159
|
try {
|
|
160
|
+
const sign = crypto.createSign(algorithm);
|
|
161
|
+
sign.update(data);
|
|
162
|
+
sign.end();
|
|
150
163
|
const signature = sign.sign(privateKey, 'base64');
|
|
151
164
|
resolve(signature);
|
|
152
165
|
}
|
|
153
|
-
catch (
|
|
154
|
-
reject(
|
|
166
|
+
catch (err) {
|
|
167
|
+
reject(new InternalServerError(undefined, {
|
|
168
|
+
message: 'RSA signing failed',
|
|
169
|
+
}, err instanceof Error ? err : undefined));
|
|
155
170
|
}
|
|
156
171
|
});
|
|
157
172
|
}
|
|
@@ -161,15 +176,17 @@ export class CryptoManager {
|
|
|
161
176
|
rsaVerify(data, signature, publicKey, algorithm = 'sha256') {
|
|
162
177
|
return new Promise((resolve, reject) => {
|
|
163
178
|
const crypto = require('crypto');
|
|
164
|
-
const verify = crypto.createVerify(algorithm);
|
|
165
|
-
verify.update(data);
|
|
166
|
-
verify.end();
|
|
167
179
|
try {
|
|
180
|
+
const verify = crypto.createVerify(algorithm);
|
|
181
|
+
verify.update(data);
|
|
182
|
+
verify.end();
|
|
168
183
|
const isValid = verify.verify(publicKey, signature, 'base64');
|
|
169
184
|
resolve(isValid);
|
|
170
185
|
}
|
|
171
|
-
catch (
|
|
172
|
-
reject(
|
|
186
|
+
catch (err) {
|
|
187
|
+
reject(new InternalServerError(undefined, {
|
|
188
|
+
message: 'RSA verification failed',
|
|
189
|
+
}, err instanceof Error ? err : undefined));
|
|
173
190
|
}
|
|
174
191
|
});
|
|
175
192
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/jwt/decodeToken.ts
|
|
2
2
|
import { decode } from 'jsonwebtoken';
|
|
3
|
+
import { BadRequestError } from '@naman_deep_singh/errors-utils';
|
|
3
4
|
/**
|
|
4
5
|
* Flexible decode
|
|
5
6
|
* Returns: null | string | JwtPayload
|
|
@@ -15,7 +16,9 @@ export function decodeToken(token) {
|
|
|
15
16
|
export function decodeTokenStrict(token) {
|
|
16
17
|
const decoded = decode(token);
|
|
17
18
|
if (!decoded || typeof decoded === 'string') {
|
|
18
|
-
throw new
|
|
19
|
+
throw new BadRequestError({
|
|
20
|
+
message: 'Invalid JWT payload structure',
|
|
21
|
+
});
|
|
19
22
|
}
|
|
20
23
|
return decoded;
|
|
21
24
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { signToken } from './signToken';
|
|
2
2
|
import { verifyToken } from './verify';
|
|
3
|
+
import { TokenMalformedError } from '@naman_deep_singh/errors-utils';
|
|
3
4
|
// Helper function to create branded tokens
|
|
4
5
|
/* const createBrandedToken = <T extends string>(token: string, _brand: T): T => {
|
|
5
6
|
return token as T
|
|
@@ -19,7 +20,9 @@ export const generateTokens = (payload, accessSecret, refreshSecret, accessExpir
|
|
|
19
20
|
export function rotateRefreshToken(oldToken, secret) {
|
|
20
21
|
const decoded = verifyToken(oldToken, secret);
|
|
21
22
|
if (typeof decoded === 'string') {
|
|
22
|
-
throw new
|
|
23
|
+
throw new TokenMalformedError({
|
|
24
|
+
message: 'Invalid token payload — expected JWT payload object',
|
|
25
|
+
});
|
|
23
26
|
}
|
|
24
27
|
const payload = { ...decoded };
|
|
25
28
|
delete payload.iat;
|
|
@@ -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
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import jwt from 'jsonwebtoken';
|
|
2
2
|
import { signToken } from './signToken';
|
|
3
|
-
import { safeVerifyToken
|
|
3
|
+
import { safeVerifyToken } from './verify';
|
|
4
4
|
import { BadRequestError, UnauthorizedError, ValidationError, } from '@naman_deep_singh/errors-utils';
|
|
5
5
|
import { LRUCache } from '@naman_deep_singh/js-extensions';
|
|
6
6
|
export class JWTManager {
|
|
@@ -9,288 +9,154 @@ export class JWTManager {
|
|
|
9
9
|
this.refreshSecret = config.refreshSecret;
|
|
10
10
|
this.accessExpiry = config.accessExpiry || '15m';
|
|
11
11
|
this.refreshExpiry = config.refreshExpiry || '7d';
|
|
12
|
-
this.cacheTTL = 5 * 60 * 1000; // 5 minutes
|
|
12
|
+
this.cacheTTL = 5 * 60 * 1000; // 5 minutes
|
|
13
13
|
if (config.enableCaching) {
|
|
14
14
|
this.cache = new LRUCache(config.maxCacheSize || 100);
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
-
/**
|
|
18
|
-
* Generate both access and refresh tokens
|
|
19
|
-
*/
|
|
17
|
+
/** Generate both access and refresh tokens */
|
|
20
18
|
async generateTokens(payload) {
|
|
21
19
|
try {
|
|
22
20
|
this.validatePayload(payload);
|
|
23
21
|
const accessToken = await this.generateAccessToken(payload);
|
|
24
22
|
const refreshToken = await this.generateRefreshToken(payload);
|
|
25
|
-
return {
|
|
26
|
-
accessToken,
|
|
27
|
-
refreshToken,
|
|
28
|
-
};
|
|
23
|
+
return { accessToken, refreshToken };
|
|
29
24
|
}
|
|
30
25
|
catch (error) {
|
|
31
|
-
if (error instanceof BadRequestError ||
|
|
32
|
-
error instanceof ValidationError) {
|
|
26
|
+
if (error instanceof BadRequestError || error instanceof ValidationError)
|
|
33
27
|
throw error;
|
|
34
|
-
}
|
|
35
|
-
throw new BadRequestError('Failed to generate tokens');
|
|
28
|
+
throw new BadRequestError({ message: 'Failed to generate tokens' }, error instanceof Error ? error : undefined);
|
|
36
29
|
}
|
|
37
30
|
}
|
|
38
|
-
/**
|
|
39
|
-
* Generate access token
|
|
40
|
-
*/
|
|
31
|
+
/** Generate access token */
|
|
41
32
|
async generateAccessToken(payload) {
|
|
42
33
|
try {
|
|
43
34
|
this.validatePayload(payload);
|
|
44
|
-
const token = signToken(payload, this.accessSecret, this.accessExpiry, {
|
|
45
|
-
algorithm: 'HS256',
|
|
46
|
-
});
|
|
35
|
+
const token = signToken(payload, this.accessSecret, this.accessExpiry, { algorithm: 'HS256' });
|
|
47
36
|
return token;
|
|
48
37
|
}
|
|
49
38
|
catch (error) {
|
|
50
|
-
if (error instanceof BadRequestError ||
|
|
51
|
-
error instanceof ValidationError) {
|
|
39
|
+
if (error instanceof BadRequestError || error instanceof ValidationError)
|
|
52
40
|
throw error;
|
|
53
|
-
}
|
|
54
|
-
throw new BadRequestError('Failed to generate access token');
|
|
41
|
+
throw new BadRequestError({ message: 'Failed to generate access token' }, error instanceof Error ? error : undefined);
|
|
55
42
|
}
|
|
56
43
|
}
|
|
57
|
-
/**
|
|
58
|
-
* Generate refresh token
|
|
59
|
-
*/
|
|
44
|
+
/** Generate refresh token */
|
|
60
45
|
async generateRefreshToken(payload) {
|
|
61
46
|
try {
|
|
62
47
|
this.validatePayload(payload);
|
|
63
|
-
const token = signToken(payload, this.refreshSecret, this.refreshExpiry, {
|
|
64
|
-
algorithm: 'HS256',
|
|
65
|
-
});
|
|
48
|
+
const token = signToken(payload, this.refreshSecret, this.refreshExpiry, { algorithm: 'HS256' });
|
|
66
49
|
return token;
|
|
67
50
|
}
|
|
68
51
|
catch (error) {
|
|
69
|
-
if (error instanceof BadRequestError ||
|
|
70
|
-
error instanceof ValidationError) {
|
|
52
|
+
if (error instanceof BadRequestError || error instanceof ValidationError)
|
|
71
53
|
throw error;
|
|
72
|
-
}
|
|
73
|
-
throw new BadRequestError('Failed to generate refresh token');
|
|
54
|
+
throw new BadRequestError({ message: 'Failed to generate refresh token' }, error instanceof Error ? error : undefined);
|
|
74
55
|
}
|
|
75
56
|
}
|
|
76
|
-
/**
|
|
77
|
-
* Verify access token
|
|
78
|
-
*/
|
|
57
|
+
/** Verify access token */
|
|
79
58
|
async verifyAccessToken(token) {
|
|
80
|
-
|
|
81
|
-
if (!token || typeof token !== 'string') {
|
|
82
|
-
throw new ValidationError('Access token must be a non-empty string');
|
|
83
|
-
}
|
|
84
|
-
const cacheKey = `access_${token}`;
|
|
85
|
-
if (this.cache) {
|
|
86
|
-
const cached = this.cache.get(cacheKey);
|
|
87
|
-
if (cached && Date.now() - cached.timestamp <= this.cacheTTL) {
|
|
88
|
-
if (!cached.valid) {
|
|
89
|
-
throw new UnauthorizedError('Access token is invalid or expired');
|
|
90
|
-
}
|
|
91
|
-
return cached.payload;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
const decoded = verifyToken(token, this.accessSecret);
|
|
95
|
-
if (this.cache) {
|
|
96
|
-
this.cache.set(cacheKey, {
|
|
97
|
-
valid: true,
|
|
98
|
-
payload: decoded,
|
|
99
|
-
timestamp: Date.now(),
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
return decoded;
|
|
103
|
-
}
|
|
104
|
-
catch (error) {
|
|
105
|
-
if (error instanceof ValidationError ||
|
|
106
|
-
error instanceof UnauthorizedError) {
|
|
107
|
-
throw error;
|
|
108
|
-
}
|
|
109
|
-
if (error instanceof Error && error.name === 'TokenExpiredError') {
|
|
110
|
-
throw new UnauthorizedError('Access token has expired');
|
|
111
|
-
}
|
|
112
|
-
if (error instanceof Error && error.name === 'JsonWebTokenError') {
|
|
113
|
-
throw new UnauthorizedError('Access token is invalid');
|
|
114
|
-
}
|
|
115
|
-
throw new UnauthorizedError('Failed to verify access token');
|
|
116
|
-
}
|
|
59
|
+
return this.verifyTokenWithCache(token, this.accessSecret, 'access');
|
|
117
60
|
}
|
|
118
|
-
/**
|
|
119
|
-
* Verify refresh token
|
|
120
|
-
*/
|
|
61
|
+
/** Verify refresh token */
|
|
121
62
|
async verifyRefreshToken(token) {
|
|
122
|
-
|
|
123
|
-
if (!token || typeof token !== 'string') {
|
|
124
|
-
throw new ValidationError('Refresh token must be a non-empty string');
|
|
125
|
-
}
|
|
126
|
-
const cacheKey = `refresh_${token}`;
|
|
127
|
-
if (this.cache) {
|
|
128
|
-
const cached = this.cache.get(cacheKey);
|
|
129
|
-
if (cached) {
|
|
130
|
-
if (!cached.valid) {
|
|
131
|
-
throw new UnauthorizedError('Refresh token is invalid or expired');
|
|
132
|
-
}
|
|
133
|
-
return cached.payload;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
const decoded = verifyToken(token, this.refreshSecret);
|
|
137
|
-
if (this.cache) {
|
|
138
|
-
this.cache.set(cacheKey, {
|
|
139
|
-
valid: true,
|
|
140
|
-
payload: decoded,
|
|
141
|
-
timestamp: Date.now(),
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
return decoded;
|
|
145
|
-
}
|
|
146
|
-
catch (error) {
|
|
147
|
-
if (error instanceof ValidationError ||
|
|
148
|
-
error instanceof UnauthorizedError) {
|
|
149
|
-
throw error;
|
|
150
|
-
}
|
|
151
|
-
if (error instanceof Error && error.name === 'TokenExpiredError') {
|
|
152
|
-
throw new UnauthorizedError('Refresh token has expired');
|
|
153
|
-
}
|
|
154
|
-
if (error instanceof Error && error.name === 'JsonWebTokenError') {
|
|
155
|
-
throw new UnauthorizedError('Refresh token is invalid');
|
|
156
|
-
}
|
|
157
|
-
throw new UnauthorizedError('Failed to verify refresh token');
|
|
158
|
-
}
|
|
63
|
+
return this.verifyTokenWithCache(token, this.refreshSecret, 'refresh');
|
|
159
64
|
}
|
|
160
|
-
/**
|
|
161
|
-
* Decode token without verification
|
|
162
|
-
*/
|
|
65
|
+
/** Decode token without verification */
|
|
163
66
|
decodeToken(token, complete = false) {
|
|
164
|
-
|
|
165
|
-
if (!token || typeof token !== 'string') {
|
|
166
|
-
throw new ValidationError('Token must be a non-empty string');
|
|
167
|
-
}
|
|
168
|
-
return jwt.decode(token, { complete });
|
|
169
|
-
}
|
|
170
|
-
catch (error) {
|
|
171
|
-
if (error instanceof ValidationError) {
|
|
172
|
-
throw error;
|
|
173
|
-
}
|
|
67
|
+
if (!token || typeof token !== 'string')
|
|
174
68
|
return null;
|
|
175
|
-
}
|
|
69
|
+
return jwt.decode(token, { complete });
|
|
176
70
|
}
|
|
177
|
-
/**
|
|
178
|
-
* Extract token from Authorization header
|
|
179
|
-
*/
|
|
71
|
+
/** Extract token from Authorization header */
|
|
180
72
|
extractTokenFromHeader(authHeader) {
|
|
181
|
-
|
|
182
|
-
if (!authHeader || typeof authHeader !== 'string') {
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
const parts = authHeader.split(' ');
|
|
186
|
-
if (parts.length !== 2 || parts[0] !== 'Bearer') {
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
return parts[1];
|
|
190
|
-
}
|
|
191
|
-
catch {
|
|
73
|
+
if (!authHeader || typeof authHeader !== 'string')
|
|
192
74
|
return null;
|
|
193
|
-
|
|
75
|
+
const parts = authHeader.split(' ');
|
|
76
|
+
if (parts.length !== 2 || parts[0] !== 'Bearer')
|
|
77
|
+
return null;
|
|
78
|
+
return parts[1];
|
|
194
79
|
}
|
|
195
|
-
/**
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
validateToken(token, secret, _options = {}) {
|
|
199
|
-
try {
|
|
200
|
-
if (!token || typeof token !== 'string') {
|
|
201
|
-
return false;
|
|
202
|
-
}
|
|
203
|
-
const result = safeVerifyToken(token, secret);
|
|
204
|
-
return result.valid;
|
|
205
|
-
}
|
|
206
|
-
catch {
|
|
80
|
+
/** Validate token without throwing exceptions */
|
|
81
|
+
validateToken(token, secret) {
|
|
82
|
+
if (!token || typeof token !== 'string')
|
|
207
83
|
return false;
|
|
208
|
-
|
|
84
|
+
return safeVerifyToken(token, secret).valid;
|
|
209
85
|
}
|
|
210
|
-
/**
|
|
211
|
-
* Rotate refresh token
|
|
212
|
-
*/
|
|
86
|
+
/** Rotate refresh token */
|
|
213
87
|
async rotateRefreshToken(oldToken) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const payload = { ...decoded };
|
|
224
|
-
delete payload.iat;
|
|
225
|
-
delete payload.exp;
|
|
226
|
-
// Generate new refresh token
|
|
227
|
-
const newToken = signToken(payload, this.refreshSecret, this.refreshExpiry);
|
|
228
|
-
return newToken;
|
|
229
|
-
}
|
|
230
|
-
catch (error) {
|
|
231
|
-
if (error instanceof ValidationError ||
|
|
232
|
-
error instanceof UnauthorizedError) {
|
|
233
|
-
throw error;
|
|
234
|
-
}
|
|
235
|
-
throw new BadRequestError('Failed to rotate refresh token');
|
|
236
|
-
}
|
|
88
|
+
if (!oldToken || typeof oldToken !== 'string') {
|
|
89
|
+
throw new ValidationError({ message: 'Old refresh token must be a non-empty string' });
|
|
90
|
+
}
|
|
91
|
+
const decoded = await this.verifyRefreshToken(oldToken);
|
|
92
|
+
const payload = { ...decoded };
|
|
93
|
+
delete payload.iat;
|
|
94
|
+
delete payload.exp;
|
|
95
|
+
const newToken = signToken(payload, this.refreshSecret, this.refreshExpiry);
|
|
96
|
+
return newToken;
|
|
237
97
|
}
|
|
238
|
-
/**
|
|
239
|
-
* Check if token is expired
|
|
240
|
-
*/
|
|
98
|
+
/** Check if token is expired */
|
|
241
99
|
isTokenExpired(token) {
|
|
242
100
|
try {
|
|
243
101
|
const decoded = this.decodeToken(token);
|
|
244
|
-
if (!decoded || !decoded.exp)
|
|
102
|
+
if (!decoded || !decoded.exp)
|
|
245
103
|
return true;
|
|
246
|
-
|
|
247
|
-
const currentTime = Math.floor(Date.now() / 1000);
|
|
248
|
-
return decoded.exp < currentTime;
|
|
104
|
+
return decoded.exp < Math.floor(Date.now() / 1000);
|
|
249
105
|
}
|
|
250
106
|
catch {
|
|
251
107
|
return true;
|
|
252
108
|
}
|
|
253
109
|
}
|
|
254
|
-
/**
|
|
255
|
-
* Get token expiration date
|
|
256
|
-
*/
|
|
110
|
+
/** Get token expiration date */
|
|
257
111
|
getTokenExpiration(token) {
|
|
258
112
|
try {
|
|
259
113
|
const decoded = this.decodeToken(token);
|
|
260
|
-
if (!decoded || !decoded.exp)
|
|
114
|
+
if (!decoded || !decoded.exp)
|
|
261
115
|
return null;
|
|
262
|
-
}
|
|
263
116
|
return new Date(decoded.exp * 1000);
|
|
264
117
|
}
|
|
265
118
|
catch {
|
|
266
119
|
return null;
|
|
267
120
|
}
|
|
268
121
|
}
|
|
269
|
-
/**
|
|
270
|
-
* Clear token cache
|
|
271
|
-
*/
|
|
122
|
+
/** Clear token cache */
|
|
272
123
|
clearCache() {
|
|
273
124
|
this.cache?.clear();
|
|
274
125
|
}
|
|
275
|
-
/**
|
|
276
|
-
* Get cache statistics
|
|
277
|
-
*/
|
|
126
|
+
/** Get cache statistics */
|
|
278
127
|
getCacheStats() {
|
|
279
128
|
if (!this.cache)
|
|
280
129
|
return null;
|
|
281
|
-
|
|
282
|
-
return {
|
|
283
|
-
size: -1, // Size not available from LRUCache
|
|
284
|
-
maxSize: this.cache.maxSize,
|
|
285
|
-
};
|
|
130
|
+
return { size: -1, maxSize: this.cache.maxSize };
|
|
286
131
|
}
|
|
287
|
-
|
|
132
|
+
/** Private helper methods */
|
|
288
133
|
validatePayload(payload) {
|
|
289
134
|
if (!payload || typeof payload !== 'object') {
|
|
290
|
-
throw new ValidationError('Payload must be a non-null object');
|
|
135
|
+
throw new ValidationError({ message: 'Payload must be a non-null object' });
|
|
291
136
|
}
|
|
292
137
|
if (Object.keys(payload).length === 0) {
|
|
293
|
-
throw new ValidationError('Payload cannot be empty');
|
|
138
|
+
throw new ValidationError({ message: 'Payload cannot be empty' });
|
|
294
139
|
}
|
|
295
140
|
}
|
|
141
|
+
async verifyTokenWithCache(token, secret, type) {
|
|
142
|
+
if (!token || typeof token !== 'string') {
|
|
143
|
+
throw new ValidationError({ message: `${type} token must be a non-empty string` });
|
|
144
|
+
}
|
|
145
|
+
const cacheKey = `${type}_${token}`;
|
|
146
|
+
if (this.cache) {
|
|
147
|
+
const cached = this.cache.get(cacheKey);
|
|
148
|
+
if (cached && Date.now() - cached.timestamp <= this.cacheTTL) {
|
|
149
|
+
if (!cached.valid)
|
|
150
|
+
throw new UnauthorizedError({ message: `${type} token is invalid or expired` });
|
|
151
|
+
return cached.payload;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const { valid, payload, error } = safeVerifyToken(token, secret);
|
|
155
|
+
if (!valid || !payload || typeof payload === 'string') {
|
|
156
|
+
this.cache?.set(cacheKey, { valid: false, payload: {}, timestamp: Date.now() });
|
|
157
|
+
throw new UnauthorizedError({ message: `${type} token is invalid or expired`, cause: error });
|
|
158
|
+
}
|
|
159
|
+
this.cache?.set(cacheKey, { valid: true, payload, timestamp: Date.now() });
|
|
160
|
+
return payload;
|
|
161
|
+
}
|
|
296
162
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ValidationError } from "@naman_deep_singh/errors-utils";
|
|
1
2
|
const TIME_UNITS = {
|
|
2
3
|
s: 1,
|
|
3
4
|
m: 60,
|
|
@@ -15,12 +16,12 @@ export function parseDuration(input) {
|
|
|
15
16
|
const value = Number.parseInt(match[1], 10);
|
|
16
17
|
const unit = match[2].toLowerCase();
|
|
17
18
|
if (!TIME_UNITS[unit]) {
|
|
18
|
-
throw new
|
|
19
|
+
throw new ValidationError({ message: `Invalid time unit: ${unit}` });
|
|
19
20
|
}
|
|
20
21
|
totalSeconds += value * TIME_UNITS[unit];
|
|
21
22
|
}
|
|
22
23
|
if (totalSeconds === 0) {
|
|
23
|
-
throw new
|
|
24
|
+
throw new ValidationError({ message: `Invalid expiry format: "${input}"` });
|
|
24
25
|
}
|
|
25
26
|
return totalSeconds;
|
|
26
27
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { sign } from 'jsonwebtoken';
|
|
2
2
|
import { parseDuration } from './parseDuration';
|
|
3
|
+
import { ValidationError } from '@naman_deep_singh/errors-utils';
|
|
3
4
|
function getExpiryTimestamp(seconds) {
|
|
4
5
|
return Math.floor(Date.now() / 1000) + seconds;
|
|
5
6
|
}
|
|
6
7
|
export const signToken = (payload, secret, expiresIn = '1h', options = {}) => {
|
|
7
8
|
const seconds = parseDuration(expiresIn);
|
|
8
9
|
if (!seconds || seconds < 10) {
|
|
9
|
-
throw new
|
|
10
|
+
throw new ValidationError({ message: 'Token expiry too small' });
|
|
10
11
|
}
|
|
11
12
|
const tokenPayload = {
|
|
12
13
|
...payload,
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import type { JwtPayload } from '
|
|
1
|
+
import type { JwtPayload } from 'jsonwebtoken';
|
|
2
2
|
export interface TokenRequirements {
|
|
3
3
|
requiredFields?: string[];
|
|
4
4
|
forbiddenFields?: string[];
|
|
5
5
|
validateTypes?: Record<string, 'string' | 'number' | 'boolean'>;
|
|
6
6
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Validates a JWT payload according to the provided rules.
|
|
9
|
+
* Throws ValidationError if validation fails.
|
|
10
|
+
*/
|
|
11
|
+
export declare function validateTokenPayload(payload: Record<string, unknown>, rules?: TokenRequirements): void;
|
|
12
|
+
/**
|
|
13
|
+
* Checks if a JWT payload is expired.
|
|
14
|
+
* Returns true if expired or missing 'exp'.
|
|
15
|
+
*/
|
|
13
16
|
export declare function isTokenExpired(payload: JwtPayload): boolean;
|