@naman_deep_singh/security 1.1.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 +358 -175
- 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/extractToken.d.ts +2 -2
- package/dist/cjs/core/jwt/extractToken.js +12 -7
- package/dist/cjs/core/jwt/generateTokens.d.ts +3 -6
- package/dist/cjs/core/jwt/generateTokens.js +10 -3
- package/dist/cjs/core/jwt/index.d.ts +1 -0
- package/dist/cjs/core/jwt/index.js +1 -0
- package/dist/cjs/core/jwt/jwtManager.d.ts +66 -0
- package/dist/cjs/core/jwt/jwtManager.js +319 -0
- package/dist/cjs/core/jwt/signToken.d.ts +1 -1
- package/dist/cjs/core/jwt/types.d.ts +22 -0
- package/dist/cjs/core/jwt/types.js +2 -0
- package/dist/cjs/core/jwt/validateToken.d.ts +1 -1
- package/dist/cjs/core/jwt/verify.d.ts +12 -7
- package/dist/cjs/core/jwt/verify.js +23 -3
- 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 +11 -9
- 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/extractToken.d.ts +2 -2
- package/dist/esm/core/jwt/extractToken.js +12 -7
- package/dist/esm/core/jwt/generateTokens.d.ts +3 -6
- package/dist/esm/core/jwt/generateTokens.js +10 -3
- package/dist/esm/core/jwt/index.d.ts +1 -0
- package/dist/esm/core/jwt/index.js +1 -0
- package/dist/esm/core/jwt/jwtManager.d.ts +66 -0
- package/dist/esm/core/jwt/jwtManager.js +312 -0
- package/dist/esm/core/jwt/signToken.d.ts +1 -1
- package/dist/esm/core/jwt/types.d.ts +22 -0
- package/dist/esm/core/jwt/types.js +1 -0
- package/dist/esm/core/jwt/validateToken.d.ts +1 -1
- package/dist/esm/core/jwt/verify.d.ts +12 -7
- package/dist/esm/core/jwt/verify.js +20 -2
- 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 +11 -9
- 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/extractToken.d.ts +2 -2
- package/dist/types/core/jwt/generateTokens.d.ts +3 -6
- package/dist/types/core/jwt/index.d.ts +1 -0
- package/dist/types/core/jwt/jwtManager.d.ts +66 -0
- package/dist/types/core/jwt/signToken.d.ts +1 -1
- package/dist/types/core/jwt/types.d.ts +22 -0
- package/dist/types/core/jwt/validateToken.d.ts +1 -1
- package/dist/types/core/jwt/verify.d.ts +12 -7
- package/dist/types/core/password/passwordManager.d.ts +29 -0
- package/dist/types/index.d.ts +11 -9
- 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,180 @@
|
|
|
1
|
+
import { encrypt as functionalEncrypt, decrypt as functionalDecrypt, hmacSign as functionalHmacSign, hmacVerify as functionalHmacVerify, randomToken as functionalRandomToken } from './index';
|
|
2
|
+
/**
|
|
3
|
+
* Default configuration
|
|
4
|
+
*/
|
|
5
|
+
const DEFAULT_CONFIG = {
|
|
6
|
+
defaultAlgorithm: 'aes-256-gcm',
|
|
7
|
+
defaultEncoding: 'utf8',
|
|
8
|
+
hmacAlgorithm: 'sha256'
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* CryptoManager - Class-based wrapper for all cryptographic operations
|
|
12
|
+
* Provides a consistent interface for encryption, decryption, HMAC generation, and secure random generation
|
|
13
|
+
*/
|
|
14
|
+
export class CryptoManager {
|
|
15
|
+
constructor(config = {}) {
|
|
16
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Update configuration
|
|
20
|
+
*/
|
|
21
|
+
updateConfig(config) {
|
|
22
|
+
this.config = { ...this.config, ...config };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get current configuration
|
|
26
|
+
*/
|
|
27
|
+
getConfig() {
|
|
28
|
+
return { ...this.config };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Encrypt data using the default or specified algorithm
|
|
32
|
+
*/
|
|
33
|
+
encrypt(plaintext, key, options) {
|
|
34
|
+
// For now, use the basic encrypt function
|
|
35
|
+
// TODO: Enhance to support different algorithms and options
|
|
36
|
+
return functionalEncrypt(plaintext, key);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Decrypt data using the default or specified algorithm
|
|
40
|
+
*/
|
|
41
|
+
decrypt(encryptedData, key, options) {
|
|
42
|
+
// For now, use the basic decrypt function
|
|
43
|
+
// TODO: Enhance to support different algorithms and options
|
|
44
|
+
return functionalDecrypt(encryptedData, key);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Generate HMAC signature
|
|
48
|
+
*/
|
|
49
|
+
generateHmac(data, secret, options) {
|
|
50
|
+
// Use the basic HMAC sign function for now
|
|
51
|
+
// TODO: Add support for different algorithms
|
|
52
|
+
return functionalHmacSign(data, secret);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Generate cryptographically secure random bytes
|
|
56
|
+
*/
|
|
57
|
+
generateSecureRandom(length, encoding = 'hex') {
|
|
58
|
+
// Use the basic random token function
|
|
59
|
+
return functionalRandomToken(length);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Verify HMAC signature
|
|
63
|
+
*/
|
|
64
|
+
verifyHmac(data, secret, signature, options) {
|
|
65
|
+
// Use the basic HMAC verify function
|
|
66
|
+
return functionalHmacVerify(data, secret, signature);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Create a key derivation function using PBKDF2
|
|
70
|
+
*/
|
|
71
|
+
deriveKey(password, salt, iterations = 100000, keyLength = 32) {
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const crypto = require('crypto');
|
|
74
|
+
crypto.pbkdf2(password, salt, iterations, keyLength, 'sha256', (err, derivedKey) => {
|
|
75
|
+
if (err) {
|
|
76
|
+
reject(err);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
resolve(derivedKey.toString('hex'));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Hash data using SHA-256
|
|
86
|
+
*/
|
|
87
|
+
sha256(data, encoding = 'hex') {
|
|
88
|
+
const crypto = require('crypto');
|
|
89
|
+
return crypto.createHash('sha256').update(data).digest(encoding);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Hash data using SHA-512
|
|
93
|
+
*/
|
|
94
|
+
sha512(data, encoding = 'hex') {
|
|
95
|
+
const crypto = require('crypto');
|
|
96
|
+
return crypto.createHash('sha512').update(data).digest(encoding);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Generate a secure key pair for asymmetric encryption
|
|
100
|
+
*/
|
|
101
|
+
generateKeyPair(options) {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
const crypto = require('crypto');
|
|
104
|
+
const keyPair = crypto.generateKeyPairSync('rsa', {
|
|
105
|
+
modulusLength: options?.modulusLength || 2048,
|
|
106
|
+
publicKeyEncoding: options?.publicKeyEncoding || { type: 'spki', format: 'pem' },
|
|
107
|
+
privateKeyEncoding: options?.privateKeyEncoding || { type: 'pkcs8', format: 'pem' }
|
|
108
|
+
});
|
|
109
|
+
resolve(keyPair);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Encrypt data using RSA public key
|
|
114
|
+
*/
|
|
115
|
+
rsaEncrypt(data, publicKey) {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const crypto = require('crypto');
|
|
118
|
+
const buffer = Buffer.from(data, 'utf8');
|
|
119
|
+
const encrypted = crypto.publicEncrypt(publicKey, buffer);
|
|
120
|
+
resolve(encrypted.toString('base64'));
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Decrypt data using RSA private key
|
|
125
|
+
*/
|
|
126
|
+
rsaDecrypt(encryptedData, privateKey) {
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
const crypto = require('crypto');
|
|
129
|
+
const buffer = Buffer.from(encryptedData, 'base64');
|
|
130
|
+
const decrypted = crypto.privateDecrypt(privateKey, buffer);
|
|
131
|
+
resolve(decrypted.toString('utf8'));
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Create digital signature using RSA private key
|
|
136
|
+
*/
|
|
137
|
+
rsaSign(data, privateKey, algorithm = 'sha256') {
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const crypto = require('crypto');
|
|
140
|
+
const sign = crypto.createSign(algorithm);
|
|
141
|
+
sign.update(data);
|
|
142
|
+
sign.end();
|
|
143
|
+
try {
|
|
144
|
+
const signature = sign.sign(privateKey, 'base64');
|
|
145
|
+
resolve(signature);
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
reject(error);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Verify digital signature using RSA public key
|
|
154
|
+
*/
|
|
155
|
+
rsaVerify(data, signature, publicKey, algorithm = 'sha256') {
|
|
156
|
+
return new Promise((resolve, reject) => {
|
|
157
|
+
const crypto = require('crypto');
|
|
158
|
+
const verify = crypto.createVerify(algorithm);
|
|
159
|
+
verify.update(data);
|
|
160
|
+
verify.end();
|
|
161
|
+
try {
|
|
162
|
+
const isValid = verify.verify(publicKey, signature, 'base64');
|
|
163
|
+
resolve(isValid);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
reject(error);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Create a CryptoManager instance with default configuration
|
|
173
|
+
*/
|
|
174
|
+
export const createCryptoManager = (config) => {
|
|
175
|
+
return new CryptoManager(config);
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Default CryptoManager instance
|
|
179
|
+
*/
|
|
180
|
+
export const cryptoManager = new CryptoManager();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export
|
|
1
|
+
export { decrypt } from "./decrypt";
|
|
2
|
+
export { encrypt } from "./encrypt";
|
|
3
|
+
export { hmacSign, hmacVerify } from "./hmac";
|
|
4
|
+
export { randomToken, generateStrongPassword } from "./random";
|
|
5
|
+
export * from "./cryptoManager";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export
|
|
1
|
+
export { decrypt } from "./decrypt";
|
|
2
|
+
export { encrypt } from "./encrypt";
|
|
3
|
+
export { hmacSign, hmacVerify } from "./hmac";
|
|
4
|
+
export { randomToken, generateStrongPassword } from "./random";
|
|
5
|
+
export * from "./cryptoManager";
|
|
@@ -2,8 +2,8 @@ export interface TokenSources {
|
|
|
2
2
|
header?: string | undefined | null;
|
|
3
3
|
cookies?: Record<string, string> | undefined;
|
|
4
4
|
query?: Record<string, string | undefined> | undefined;
|
|
5
|
-
body?: Record<string,
|
|
6
|
-
wsMessage?: string | Record<string,
|
|
5
|
+
body?: Record<string, unknown> | undefined;
|
|
6
|
+
wsMessage?: string | Record<string, unknown> | undefined;
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* Universal token extractor
|
|
@@ -20,7 +20,7 @@ export function extractToken(sources) {
|
|
|
20
20
|
if (query?.token)
|
|
21
21
|
return query.token;
|
|
22
22
|
// 4. Body: { token: "" }
|
|
23
|
-
if (body?.token)
|
|
23
|
+
if (body?.token && typeof body.token === 'string')
|
|
24
24
|
return body.token;
|
|
25
25
|
// 5. WebSocket message extraction (NEW)
|
|
26
26
|
if (wsMessage) {
|
|
@@ -30,12 +30,17 @@ export function extractToken(sources) {
|
|
|
30
30
|
if (typeof wsMessage === "string") {
|
|
31
31
|
msg = JSON.parse(wsMessage);
|
|
32
32
|
}
|
|
33
|
-
//
|
|
34
|
-
if (typeof msg
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
// Ensure msg is an object before property access
|
|
34
|
+
if (typeof msg === 'object' && msg !== null) {
|
|
35
|
+
const m = msg;
|
|
36
|
+
if (typeof m['token'] === 'string')
|
|
37
|
+
return m['token'];
|
|
38
|
+
const auth = m['auth'];
|
|
39
|
+
if (typeof auth === 'object' && auth !== null) {
|
|
40
|
+
const a = auth;
|
|
41
|
+
if (typeof a['token'] === 'string')
|
|
42
|
+
return a['token'];
|
|
43
|
+
}
|
|
39
44
|
}
|
|
40
45
|
}
|
|
41
46
|
catch {
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { Secret } from "jsonwebtoken";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
6
|
-
export declare const generateTokens: (payload: object, accessSecret: Secret, refreshSecret: Secret, accessExpiry?: string | number, refreshExpiry?: string | number) => TokenPair;
|
|
7
|
-
export declare function rotateRefreshToken(oldToken: string, secret: Secret): string;
|
|
2
|
+
import { RefreshToken, TokenPair } from "./types";
|
|
3
|
+
export declare const generateTokens: (payload: Record<string, unknown>, accessSecret: Secret, refreshSecret: Secret, accessExpiry?: string | number, refreshExpiry?: string | number) => TokenPair;
|
|
4
|
+
export declare function rotateRefreshToken(oldToken: string, secret: Secret): RefreshToken;
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { signToken } from "./signToken";
|
|
2
2
|
import { verifyToken } from "./verify";
|
|
3
|
+
// Helper function to create branded tokens
|
|
4
|
+
const createBrandedToken = (token, _brand) => {
|
|
5
|
+
return token;
|
|
6
|
+
};
|
|
3
7
|
export const generateTokens = (payload, accessSecret, refreshSecret, accessExpiry = "15m", refreshExpiry = "7d") => {
|
|
8
|
+
const accessToken = signToken(payload, accessSecret, accessExpiry, { algorithm: "HS256" });
|
|
9
|
+
const refreshToken = signToken(payload, refreshSecret, refreshExpiry, { algorithm: "HS256" });
|
|
4
10
|
return {
|
|
5
|
-
accessToken:
|
|
6
|
-
refreshToken:
|
|
11
|
+
accessToken: accessToken,
|
|
12
|
+
refreshToken: refreshToken,
|
|
7
13
|
};
|
|
8
14
|
};
|
|
9
15
|
export function rotateRefreshToken(oldToken, secret) {
|
|
@@ -14,5 +20,6 @@ export function rotateRefreshToken(oldToken, secret) {
|
|
|
14
20
|
const payload = { ...decoded };
|
|
15
21
|
delete payload.iat;
|
|
16
22
|
delete payload.exp;
|
|
17
|
-
|
|
23
|
+
const newToken = signToken(payload, secret, "7d");
|
|
24
|
+
return newToken;
|
|
18
25
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { JwtPayload, Secret } from "jsonwebtoken";
|
|
2
|
+
import { ITokenManager, TokenPair, AccessToken, RefreshToken, JWTConfig, TokenValidationOptions } from "../../interfaces/jwt.interface";
|
|
3
|
+
export declare class JWTManager implements ITokenManager {
|
|
4
|
+
private accessSecret;
|
|
5
|
+
private refreshSecret;
|
|
6
|
+
private accessExpiry;
|
|
7
|
+
private refreshExpiry;
|
|
8
|
+
private cache?;
|
|
9
|
+
constructor(config: JWTConfig);
|
|
10
|
+
/**
|
|
11
|
+
* Generate both access and refresh tokens
|
|
12
|
+
*/
|
|
13
|
+
generateTokens(payload: Record<string, unknown>): Promise<TokenPair>;
|
|
14
|
+
/**
|
|
15
|
+
* Generate access token
|
|
16
|
+
*/
|
|
17
|
+
generateAccessToken(payload: Record<string, unknown>): Promise<AccessToken>;
|
|
18
|
+
/**
|
|
19
|
+
* Generate refresh token
|
|
20
|
+
*/
|
|
21
|
+
generateRefreshToken(payload: Record<string, unknown>): Promise<RefreshToken>;
|
|
22
|
+
/**
|
|
23
|
+
* Verify access token
|
|
24
|
+
*/
|
|
25
|
+
verifyAccessToken(token: string): Promise<JwtPayload | string>;
|
|
26
|
+
/**
|
|
27
|
+
* Verify refresh token
|
|
28
|
+
*/
|
|
29
|
+
verifyRefreshToken(token: string): Promise<JwtPayload | string>;
|
|
30
|
+
/**
|
|
31
|
+
* Decode token without verification
|
|
32
|
+
*/
|
|
33
|
+
decodeToken(token: string, complete?: boolean): JwtPayload | string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Extract token from Authorization header
|
|
36
|
+
*/
|
|
37
|
+
extractTokenFromHeader(authHeader: string): string | null;
|
|
38
|
+
/**
|
|
39
|
+
* Validate token without throwing exceptions
|
|
40
|
+
*/
|
|
41
|
+
validateToken(token: string, secret: Secret, options?: TokenValidationOptions): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Rotate refresh token
|
|
44
|
+
*/
|
|
45
|
+
rotateRefreshToken(oldToken: string): Promise<RefreshToken>;
|
|
46
|
+
/**
|
|
47
|
+
* Check if token is expired
|
|
48
|
+
*/
|
|
49
|
+
isTokenExpired(token: string): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Get token expiration date
|
|
52
|
+
*/
|
|
53
|
+
getTokenExpiration(token: string): Date | null;
|
|
54
|
+
/**
|
|
55
|
+
* Clear token cache
|
|
56
|
+
*/
|
|
57
|
+
clearCache(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Get cache statistics
|
|
60
|
+
*/
|
|
61
|
+
getCacheStats(): {
|
|
62
|
+
size: number;
|
|
63
|
+
maxSize: number;
|
|
64
|
+
} | null;
|
|
65
|
+
private validatePayload;
|
|
66
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { Secret, SignOptions } from "jsonwebtoken";
|
|
2
|
-
export declare const signToken: (payload: Record<string,
|
|
2
|
+
export declare const signToken: (payload: Record<string, unknown>, secret: Secret, expiresIn?: string | number, options?: SignOptions) => string;
|