@bhandari88/express-auth 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 +404 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -1
- package/dist/middleware/validation.middleware.d.ts +114 -0
- package/dist/middleware/validation.middleware.js +290 -0
- package/dist/utils/encryption.d.ts +145 -0
- package/dist/utils/encryption.js +291 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +3 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ npm install @bhandari88/express-auth
|
|
|
34
34
|
- ✅ **Easy Integration**
|
|
35
35
|
- Plug-and-play API
|
|
36
36
|
- Express middleware for protected routes
|
|
37
|
+
- Request data validation middleware with custom messages
|
|
37
38
|
- Role-based access control
|
|
38
39
|
- Automatic route setup
|
|
39
40
|
- TypeScript support
|
|
@@ -44,6 +45,14 @@ npm install @bhandari88/express-auth
|
|
|
44
45
|
- Optional refresh tokens
|
|
45
46
|
- Customizable user fields
|
|
46
47
|
|
|
48
|
+
- ✅ **End-to-End Encryption** (NEW in v1.3.0)
|
|
49
|
+
- AES-256-GCM authenticated encryption
|
|
50
|
+
- Automatic chunking for large data
|
|
51
|
+
- Password-based key derivation (PBKDF2)
|
|
52
|
+
- Secure IV/nonce generation
|
|
53
|
+
- Support for strings and Buffers
|
|
54
|
+
- Easy-to-use API for encryption/decryption
|
|
55
|
+
|
|
47
56
|
## Peer Dependencies
|
|
48
57
|
|
|
49
58
|
Make sure you have the following peer dependencies installed:
|
|
@@ -170,6 +179,394 @@ app.get('/api/admin',
|
|
|
170
179
|
);
|
|
171
180
|
```
|
|
172
181
|
|
|
182
|
+
### 5. Use Validation Middleware
|
|
183
|
+
|
|
184
|
+
The package includes a powerful validation middleware for request data validation with custom error messages. No third-party dependencies required!
|
|
185
|
+
|
|
186
|
+
#### Basic Usage
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
import { validate, validateBody, validateQuery, validateParams } from '@bhandari88/express-auth';
|
|
190
|
+
|
|
191
|
+
// Validate request body
|
|
192
|
+
app.post('/api/users', validateBody({
|
|
193
|
+
email: {
|
|
194
|
+
rules: [
|
|
195
|
+
{ type: 'required', message: 'Email is required' },
|
|
196
|
+
{ type: 'email', message: 'Invalid email format' }
|
|
197
|
+
]
|
|
198
|
+
},
|
|
199
|
+
password: {
|
|
200
|
+
rules: [
|
|
201
|
+
{ type: 'required', message: 'Password is required' },
|
|
202
|
+
{ type: 'minLength', value: 8, message: 'Password must be at least 8 characters' }
|
|
203
|
+
]
|
|
204
|
+
},
|
|
205
|
+
age: {
|
|
206
|
+
rules: [
|
|
207
|
+
{ type: 'number', message: 'Age must be a number' },
|
|
208
|
+
{ type: 'min', value: 18, message: 'Age must be at least 18' },
|
|
209
|
+
{ type: 'max', value: 120, message: 'Age must be at most 120' }
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
}), (req, res) => {
|
|
213
|
+
// req.body is validated
|
|
214
|
+
res.json({ message: 'User created', data: req.body });
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
#### Shorthand Syntax
|
|
219
|
+
|
|
220
|
+
You can also use a simpler array syntax:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
app.post('/api/register', validateBody({
|
|
224
|
+
username: [
|
|
225
|
+
{ type: 'required', message: 'Username is required' },
|
|
226
|
+
{ type: 'minLength', value: 3, message: 'Username must be at least 3 characters' },
|
|
227
|
+
{ type: 'maxLength', value: 30, message: 'Username must be at most 30 characters' },
|
|
228
|
+
{ type: 'pattern', value: /^[a-zA-Z0-9_-]+$/, message: 'Username can only contain letters, numbers, underscores, and hyphens' }
|
|
229
|
+
],
|
|
230
|
+
email: [
|
|
231
|
+
{ type: 'required' },
|
|
232
|
+
{ type: 'email', message: 'Invalid email address' }
|
|
233
|
+
]
|
|
234
|
+
}), handler);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
#### Validate Query Parameters
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
app.get('/api/users', validateQuery({
|
|
241
|
+
page: [
|
|
242
|
+
{ type: 'number', message: 'Page must be a number' },
|
|
243
|
+
{ type: 'min', value: 1, message: 'Page must be at least 1' }
|
|
244
|
+
],
|
|
245
|
+
limit: [
|
|
246
|
+
{ type: 'number' },
|
|
247
|
+
{ type: 'min', value: 1 },
|
|
248
|
+
{ type: 'max', value: 100, message: 'Limit cannot exceed 100' }
|
|
249
|
+
],
|
|
250
|
+
status: [
|
|
251
|
+
{ type: 'enum', values: ['active', 'inactive', 'pending'], message: 'Status must be active, inactive, or pending' }
|
|
252
|
+
]
|
|
253
|
+
}), (req, res) => {
|
|
254
|
+
// req.query is validated
|
|
255
|
+
res.json({ users: [] });
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
#### Validate Route Parameters
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
app.get('/api/users/:id', validateParams({
|
|
263
|
+
id: [
|
|
264
|
+
{ type: 'required', message: 'User ID is required' },
|
|
265
|
+
{ type: 'pattern', value: /^[a-f\d]{24}$/i, message: 'Invalid user ID format' }
|
|
266
|
+
]
|
|
267
|
+
}), (req, res) => {
|
|
268
|
+
// req.params is validated
|
|
269
|
+
res.json({ user: {} });
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
#### Validate Multiple Sources
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
app.put('/api/users/:id', validate({
|
|
277
|
+
id: [
|
|
278
|
+
{ type: 'required' },
|
|
279
|
+
{ type: 'pattern', value: /^[a-f\d]{24}$/i }
|
|
280
|
+
],
|
|
281
|
+
email: [
|
|
282
|
+
{ type: 'email' }
|
|
283
|
+
],
|
|
284
|
+
status: [
|
|
285
|
+
{ type: 'enum', values: ['active', 'inactive'] }
|
|
286
|
+
]
|
|
287
|
+
}, {
|
|
288
|
+
source: ['params', 'body'] // Validate both params and body
|
|
289
|
+
}), handler);
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
#### Custom Validation
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
app.post('/api/custom', validateBody({
|
|
296
|
+
password: [
|
|
297
|
+
{ type: 'required' },
|
|
298
|
+
{ type: 'minLength', value: 8 },
|
|
299
|
+
{
|
|
300
|
+
type: 'custom',
|
|
301
|
+
validator: (value) => {
|
|
302
|
+
// Custom validation logic
|
|
303
|
+
return /[A-Z]/.test(value) && /[a-z]/.test(value) && /[0-9]/.test(value);
|
|
304
|
+
},
|
|
305
|
+
message: 'Password must contain uppercase, lowercase, and number'
|
|
306
|
+
}
|
|
307
|
+
],
|
|
308
|
+
confirmPassword: [
|
|
309
|
+
{ type: 'required' },
|
|
310
|
+
{
|
|
311
|
+
type: 'custom',
|
|
312
|
+
validator: (value, data) => {
|
|
313
|
+
// Access other fields from the request
|
|
314
|
+
return value === data.password;
|
|
315
|
+
},
|
|
316
|
+
message: 'Passwords do not match'
|
|
317
|
+
}
|
|
318
|
+
]
|
|
319
|
+
}), handler);
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
#### Available Validation Rules
|
|
323
|
+
|
|
324
|
+
| Rule | Description | Example |
|
|
325
|
+
|------|-------------|---------|
|
|
326
|
+
| `required` | Field must be present and not empty | `{ type: 'required', message: 'Field is required' }` |
|
|
327
|
+
| `string` | Value must be a string | `{ type: 'string' }` |
|
|
328
|
+
| `number` | Value must be a number | `{ type: 'number' }` |
|
|
329
|
+
| `boolean` | Value must be a boolean | `{ type: 'boolean' }` |
|
|
330
|
+
| `email` | Value must be a valid email | `{ type: 'email', message: 'Invalid email' }` |
|
|
331
|
+
| `min` | Number must be >= value | `{ type: 'min', value: 1 }` |
|
|
332
|
+
| `max` | Number must be <= value | `{ type: 'max', value: 100 }` |
|
|
333
|
+
| `minLength` | String length must be >= value | `{ type: 'minLength', value: 8 }` |
|
|
334
|
+
| `maxLength` | String length must be <= value | `{ type: 'maxLength', value: 255 }` |
|
|
335
|
+
| `pattern` | String must match regex | `{ type: 'pattern', value: /^[a-z]+$/ }` |
|
|
336
|
+
| `enum` | Value must be in array | `{ type: 'enum', values: ['a', 'b', 'c'] }` |
|
|
337
|
+
| `array` | Value must be an array | `{ type: 'array' }` |
|
|
338
|
+
| `object` | Value must be an object | `{ type: 'object' }` |
|
|
339
|
+
| `custom` | Custom validation function | `{ type: 'custom', validator: (val) => boolean }` |
|
|
340
|
+
|
|
341
|
+
#### Validation Options
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
validate(schema, {
|
|
345
|
+
source: 'body' | 'query' | 'params' | ['body', 'query'], // What to validate (default: 'body')
|
|
346
|
+
abortEarly: boolean // Stop on first error or collect all errors (default: false)
|
|
347
|
+
})
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
#### Error Response Format
|
|
351
|
+
|
|
352
|
+
When validation fails, the middleware returns a 400 status with:
|
|
353
|
+
|
|
354
|
+
```json
|
|
355
|
+
{
|
|
356
|
+
"success": false,
|
|
357
|
+
"error": "Validation failed",
|
|
358
|
+
"errors": {
|
|
359
|
+
"email": "Invalid email format",
|
|
360
|
+
"password": "Password must be at least 8 characters"
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Or with `abortEarly: true`, only the first error:
|
|
366
|
+
|
|
367
|
+
```json
|
|
368
|
+
{
|
|
369
|
+
"success": false,
|
|
370
|
+
"error": "Validation failed",
|
|
371
|
+
"errors": {
|
|
372
|
+
"email": "Email is required"
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### 6. End-to-End Data Encryption & Decryption
|
|
378
|
+
|
|
379
|
+
The package includes a powerful encryption service for secure end-to-end encryption and decryption of large data using AES-256-GCM with automatic chunking support.
|
|
380
|
+
|
|
381
|
+
#### Basic Usage
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
import { EncryptionService } from '@bhandari88/express-auth';
|
|
385
|
+
|
|
386
|
+
// Create encryption service with default settings
|
|
387
|
+
const encryption = new EncryptionService();
|
|
388
|
+
|
|
389
|
+
// Encrypt data
|
|
390
|
+
const data = 'Sensitive information that needs to be encrypted';
|
|
391
|
+
const password = 'my-secure-password-123';
|
|
392
|
+
|
|
393
|
+
const encrypted = await encryption.encrypt(data, password);
|
|
394
|
+
console.log(encrypted);
|
|
395
|
+
// {
|
|
396
|
+
// encrypted: 'base64-encrypted-data...',
|
|
397
|
+
// salt: 'base64-salt...',
|
|
398
|
+
// iv: 'base64-iv...',
|
|
399
|
+
// authTag: 'base64-auth-tag...',
|
|
400
|
+
// algorithm: 'aes-256-gcm'
|
|
401
|
+
// }
|
|
402
|
+
|
|
403
|
+
// Decrypt data
|
|
404
|
+
const decrypted = await encryption.decrypt(encrypted, password);
|
|
405
|
+
console.log(decrypted.toString('utf8')); // 'Sensitive information that needs to be encrypted'
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
#### Encrypt/Decrypt Strings (Convenience Methods)
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
// Encrypt to string (includes all metadata as JSON)
|
|
412
|
+
const encryptedString = await encryption.encryptToString(data, password);
|
|
413
|
+
// Store encryptedString in database, file, etc.
|
|
414
|
+
|
|
415
|
+
// Decrypt from string
|
|
416
|
+
const decryptedString = await encryption.decryptFromString(encryptedString, password);
|
|
417
|
+
console.log(decryptedString); // Original data
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
#### Encrypt/Decrypt Large Data
|
|
421
|
+
|
|
422
|
+
The encryption service automatically handles large data by chunking it into smaller pieces. This is transparent to you:
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
// Encrypt large file or data
|
|
426
|
+
const fs = require('fs');
|
|
427
|
+
const largeData = fs.readFileSync('large-file.pdf');
|
|
428
|
+
|
|
429
|
+
const encrypted = await encryption.encrypt(largeData, password);
|
|
430
|
+
// Automatically chunked and encrypted
|
|
431
|
+
|
|
432
|
+
// Decrypt large data
|
|
433
|
+
const decrypted = await encryption.decrypt(encrypted, password);
|
|
434
|
+
fs.writeFileSync('decrypted-file.pdf', decrypted);
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
#### Encrypt/Decrypt Buffers
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
// Encrypt Buffer
|
|
441
|
+
const buffer = Buffer.from('Binary data', 'utf8');
|
|
442
|
+
const encrypted = await encryption.encrypt(buffer, password);
|
|
443
|
+
|
|
444
|
+
// Decrypt to Buffer
|
|
445
|
+
const decrypted = await encryption.decrypt(encrypted, password);
|
|
446
|
+
console.log(decrypted); // Buffer
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
#### Custom Configuration
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
import { EncryptionService, EncryptionConfig } from '@bhandari88/express-auth';
|
|
453
|
+
|
|
454
|
+
const config: EncryptionConfig = {
|
|
455
|
+
algorithm: 'aes-256-gcm', // 'aes-256-gcm' | 'aes-192-gcm' | 'aes-128-gcm'
|
|
456
|
+
keyDerivationIterations: 100000, // PBKDF2 iterations (higher = more secure but slower)
|
|
457
|
+
saltLength: 32, // Salt length in bytes
|
|
458
|
+
ivLength: 16, // IV length in bytes
|
|
459
|
+
authTagLength: 16, // Auth tag length in bytes
|
|
460
|
+
chunkSize: 64 * 1024, // Chunk size for large data (64KB default)
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const encryption = new EncryptionService(config);
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
#### Generate Secure Passwords
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
import { EncryptionService } from '@bhandari88/express-auth';
|
|
470
|
+
|
|
471
|
+
// Generate secure random password (base64)
|
|
472
|
+
const password = EncryptionService.generateSecurePassword(32);
|
|
473
|
+
console.log(password); // Random base64 string
|
|
474
|
+
|
|
475
|
+
// Generate secure random password (hex)
|
|
476
|
+
const hexPassword = EncryptionService.generateSecurePasswordHex(32);
|
|
477
|
+
console.log(hexPassword); // Random hex string
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
#### Express Route Example
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
import express from 'express';
|
|
484
|
+
import { EncryptionService } from '@bhandari88/express-auth';
|
|
485
|
+
|
|
486
|
+
const app = express();
|
|
487
|
+
app.use(express.json());
|
|
488
|
+
|
|
489
|
+
const encryption = new EncryptionService();
|
|
490
|
+
|
|
491
|
+
// Encrypt endpoint
|
|
492
|
+
app.post('/api/encrypt', async (req, res) => {
|
|
493
|
+
try {
|
|
494
|
+
const { data, password } = req.body;
|
|
495
|
+
|
|
496
|
+
if (!data || !password) {
|
|
497
|
+
return res.status(400).json({
|
|
498
|
+
success: false,
|
|
499
|
+
error: 'Data and password are required'
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const encrypted = await encryption.encryptToString(data, password);
|
|
504
|
+
res.json({ success: true, encrypted });
|
|
505
|
+
} catch (error) {
|
|
506
|
+
res.status(500).json({
|
|
507
|
+
success: false,
|
|
508
|
+
error: error instanceof Error ? error.message : 'Encryption failed'
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// Decrypt endpoint
|
|
514
|
+
app.post('/api/decrypt', async (req, res) => {
|
|
515
|
+
try {
|
|
516
|
+
const { encrypted, password } = req.body;
|
|
517
|
+
|
|
518
|
+
if (!encrypted || !password) {
|
|
519
|
+
return res.status(400).json({
|
|
520
|
+
success: false,
|
|
521
|
+
error: 'Encrypted data and password are required'
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const decrypted = await encryption.decryptFromString(encrypted, password);
|
|
526
|
+
res.json({ success: true, decrypted });
|
|
527
|
+
} catch (error) {
|
|
528
|
+
res.status(500).json({
|
|
529
|
+
success: false,
|
|
530
|
+
error: error instanceof Error ? error.message : 'Decryption failed'
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
#### Security Features
|
|
537
|
+
|
|
538
|
+
- **AES-256-GCM**: Industry-standard authenticated encryption
|
|
539
|
+
- **Automatic Chunking**: Handles large data efficiently without memory issues
|
|
540
|
+
- **PBKDF2 Key Derivation**: 100,000 iterations by default (configurable)
|
|
541
|
+
- **Random Salt & IV**: Unique salt and IV for each encryption
|
|
542
|
+
- **Authentication Tags**: Prevents tampering with encrypted data
|
|
543
|
+
- **No External Dependencies**: Uses Node.js built-in crypto module
|
|
544
|
+
|
|
545
|
+
#### Encryption Configuration Options
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
interface EncryptionConfig {
|
|
549
|
+
algorithm?: 'aes-256-gcm' | 'aes-192-gcm' | 'aes-128-gcm'; // Default: 'aes-256-gcm'
|
|
550
|
+
keyDerivationIterations?: number; // Default: 100000
|
|
551
|
+
saltLength?: number; // Default: 32 bytes
|
|
552
|
+
ivLength?: number; // Default: 16 bytes
|
|
553
|
+
authTagLength?: number; // Default: 16 bytes
|
|
554
|
+
chunkSize?: number; // Default: 64KB
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
#### Encryption Result Format
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
interface EncryptionResult {
|
|
562
|
+
encrypted: string; // Base64 encoded encrypted data
|
|
563
|
+
salt: string; // Base64 encoded salt used for key derivation
|
|
564
|
+
iv: string; // Base64 encoded IV/nonce
|
|
565
|
+
authTag?: string; // Base64 encoded authentication tag (for single chunk)
|
|
566
|
+
algorithm: string; // Algorithm used ('aes-256-gcm')
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
173
570
|
## API Endpoints
|
|
174
571
|
|
|
175
572
|
### Register User
|
|
@@ -389,6 +786,13 @@ interface AuthConfig {
|
|
|
389
786
|
|
|
390
787
|
5. **Input Validation**: All inputs are validated and sanitized automatically
|
|
391
788
|
|
|
789
|
+
6. **Data Encryption**:
|
|
790
|
+
- Use AES-256-GCM for authenticated encryption
|
|
791
|
+
- Minimum 8 character password for encryption
|
|
792
|
+
- Store encrypted data securely (database, files, etc.)
|
|
793
|
+
- Never share encryption passwords in plain text
|
|
794
|
+
- Use strong, randomly generated passwords when possible
|
|
795
|
+
|
|
392
796
|
## Types
|
|
393
797
|
|
|
394
798
|
```typescript
|
package/dist/index.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ export * from './types';
|
|
|
56
56
|
export { JwtService } from './utils/jwt';
|
|
57
57
|
export { PasswordService } from './utils/password';
|
|
58
58
|
export { ValidatorService } from './utils/validator';
|
|
59
|
+
export { EncryptionService, EncryptionConfig, EncryptionResult } from './utils/encryption';
|
|
59
60
|
export { AuthMiddleware } from './middleware/auth.middleware';
|
|
61
|
+
export { validate, validateBody, validateQuery, validateParams, ValidationOptions, ValidationSource, ValidationSchema, ValidationRule, FieldValidation } from './middleware/validation.middleware';
|
|
60
62
|
export { LocalAuthHandler } from './handlers/local-auth';
|
|
61
63
|
export { SocialAuthHandler } from './handlers/social-auth';
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.SocialAuthHandler = exports.LocalAuthHandler = exports.AuthMiddleware = exports.ValidatorService = exports.PasswordService = exports.JwtService = exports.Auth = void 0;
|
|
17
|
+
exports.SocialAuthHandler = exports.LocalAuthHandler = exports.validateParams = exports.validateQuery = exports.validateBody = exports.validate = exports.AuthMiddleware = exports.EncryptionService = exports.ValidatorService = exports.PasswordService = exports.JwtService = exports.Auth = void 0;
|
|
18
18
|
const jwt_1 = require("./utils/jwt");
|
|
19
19
|
const password_1 = require("./utils/password");
|
|
20
20
|
const local_auth_1 = require("./handlers/local-auth");
|
|
@@ -224,8 +224,15 @@ var password_2 = require("./utils/password");
|
|
|
224
224
|
Object.defineProperty(exports, "PasswordService", { enumerable: true, get: function () { return password_2.PasswordService; } });
|
|
225
225
|
var validator_1 = require("./utils/validator");
|
|
226
226
|
Object.defineProperty(exports, "ValidatorService", { enumerable: true, get: function () { return validator_1.ValidatorService; } });
|
|
227
|
+
var encryption_1 = require("./utils/encryption");
|
|
228
|
+
Object.defineProperty(exports, "EncryptionService", { enumerable: true, get: function () { return encryption_1.EncryptionService; } });
|
|
227
229
|
var auth_middleware_2 = require("./middleware/auth.middleware");
|
|
228
230
|
Object.defineProperty(exports, "AuthMiddleware", { enumerable: true, get: function () { return auth_middleware_2.AuthMiddleware; } });
|
|
231
|
+
var validation_middleware_1 = require("./middleware/validation.middleware");
|
|
232
|
+
Object.defineProperty(exports, "validate", { enumerable: true, get: function () { return validation_middleware_1.validate; } });
|
|
233
|
+
Object.defineProperty(exports, "validateBody", { enumerable: true, get: function () { return validation_middleware_1.validateBody; } });
|
|
234
|
+
Object.defineProperty(exports, "validateQuery", { enumerable: true, get: function () { return validation_middleware_1.validateQuery; } });
|
|
235
|
+
Object.defineProperty(exports, "validateParams", { enumerable: true, get: function () { return validation_middleware_1.validateParams; } });
|
|
229
236
|
var local_auth_2 = require("./handlers/local-auth");
|
|
230
237
|
Object.defineProperty(exports, "LocalAuthHandler", { enumerable: true, get: function () { return local_auth_2.LocalAuthHandler; } });
|
|
231
238
|
var social_auth_2 = require("./handlers/social-auth");
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { RequestHandler } from 'express';
|
|
2
|
+
export type ValidationSource = 'body' | 'query' | 'params';
|
|
3
|
+
export type ValidationRule = {
|
|
4
|
+
type: 'required';
|
|
5
|
+
message?: string;
|
|
6
|
+
} | {
|
|
7
|
+
type: 'string';
|
|
8
|
+
message?: string;
|
|
9
|
+
} | {
|
|
10
|
+
type: 'number';
|
|
11
|
+
message?: string;
|
|
12
|
+
} | {
|
|
13
|
+
type: 'boolean';
|
|
14
|
+
message?: string;
|
|
15
|
+
} | {
|
|
16
|
+
type: 'email';
|
|
17
|
+
message?: string;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'min';
|
|
20
|
+
value: number;
|
|
21
|
+
message?: string;
|
|
22
|
+
} | {
|
|
23
|
+
type: 'max';
|
|
24
|
+
value: number;
|
|
25
|
+
message?: string;
|
|
26
|
+
} | {
|
|
27
|
+
type: 'minLength';
|
|
28
|
+
value: number;
|
|
29
|
+
message?: string;
|
|
30
|
+
} | {
|
|
31
|
+
type: 'maxLength';
|
|
32
|
+
value: number;
|
|
33
|
+
message?: string;
|
|
34
|
+
} | {
|
|
35
|
+
type: 'pattern';
|
|
36
|
+
value: RegExp;
|
|
37
|
+
message?: string;
|
|
38
|
+
} | {
|
|
39
|
+
type: 'enum';
|
|
40
|
+
values: any[];
|
|
41
|
+
message?: string;
|
|
42
|
+
} | {
|
|
43
|
+
type: 'custom';
|
|
44
|
+
validator: (value: any) => boolean | Promise<boolean>;
|
|
45
|
+
message?: string;
|
|
46
|
+
} | {
|
|
47
|
+
type: 'array';
|
|
48
|
+
message?: string;
|
|
49
|
+
} | {
|
|
50
|
+
type: 'object';
|
|
51
|
+
message?: string;
|
|
52
|
+
};
|
|
53
|
+
export interface FieldValidation {
|
|
54
|
+
rules: ValidationRule[];
|
|
55
|
+
message?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface ValidationSchema {
|
|
58
|
+
[field: string]: FieldValidation | ValidationRule[];
|
|
59
|
+
}
|
|
60
|
+
export interface ValidationOptions {
|
|
61
|
+
source?: ValidationSource | ValidationSource[];
|
|
62
|
+
abortEarly?: boolean;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Validation middleware factory for request data validation
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* import { validate } from '@bhandari88/express-auth';
|
|
70
|
+
*
|
|
71
|
+
* // Simple validation
|
|
72
|
+
* app.post('/login', validate({
|
|
73
|
+
* email: {
|
|
74
|
+
* rules: [
|
|
75
|
+
* { type: 'required', message: 'Email is required' },
|
|
76
|
+
* { type: 'email', message: 'Invalid email format' }
|
|
77
|
+
* ]
|
|
78
|
+
* },
|
|
79
|
+
* password: {
|
|
80
|
+
* rules: [
|
|
81
|
+
* { type: 'required', message: 'Password is required' },
|
|
82
|
+
* { type: 'minLength', value: 8, message: 'Password must be at least 8 characters' }
|
|
83
|
+
* ]
|
|
84
|
+
* }
|
|
85
|
+
* }), (req, res) => {
|
|
86
|
+
* // req.body is validated
|
|
87
|
+
* });
|
|
88
|
+
*
|
|
89
|
+
* // Validate query parameters
|
|
90
|
+
* app.get('/users', validate({
|
|
91
|
+
* page: {
|
|
92
|
+
* rules: [
|
|
93
|
+
* { type: 'number', message: 'Page must be a number' },
|
|
94
|
+
* { type: 'min', value: 1, message: 'Page must be at least 1' }
|
|
95
|
+
* ]
|
|
96
|
+
* }
|
|
97
|
+
* }, { source: 'query' }), (req, res) => {
|
|
98
|
+
* // req.query is validated
|
|
99
|
+
* });
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export declare function validate(schema: ValidationSchema, options?: ValidationOptions): RequestHandler;
|
|
103
|
+
/**
|
|
104
|
+
* Validate request body
|
|
105
|
+
*/
|
|
106
|
+
export declare function validateBody(schema: ValidationSchema, options?: Omit<ValidationOptions, 'source'>): RequestHandler;
|
|
107
|
+
/**
|
|
108
|
+
* Validate request query parameters
|
|
109
|
+
*/
|
|
110
|
+
export declare function validateQuery(schema: ValidationSchema, options?: Omit<ValidationOptions, 'source'>): RequestHandler;
|
|
111
|
+
/**
|
|
112
|
+
* Validate request route parameters
|
|
113
|
+
*/
|
|
114
|
+
export declare function validateParams(schema: ValidationSchema, options?: Omit<ValidationOptions, 'source'>): RequestHandler;
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validate = validate;
|
|
4
|
+
exports.validateBody = validateBody;
|
|
5
|
+
exports.validateQuery = validateQuery;
|
|
6
|
+
exports.validateParams = validateParams;
|
|
7
|
+
/**
|
|
8
|
+
* Validation middleware factory for request data validation
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { validate } from '@bhandari88/express-auth';
|
|
13
|
+
*
|
|
14
|
+
* // Simple validation
|
|
15
|
+
* app.post('/login', validate({
|
|
16
|
+
* email: {
|
|
17
|
+
* rules: [
|
|
18
|
+
* { type: 'required', message: 'Email is required' },
|
|
19
|
+
* { type: 'email', message: 'Invalid email format' }
|
|
20
|
+
* ]
|
|
21
|
+
* },
|
|
22
|
+
* password: {
|
|
23
|
+
* rules: [
|
|
24
|
+
* { type: 'required', message: 'Password is required' },
|
|
25
|
+
* { type: 'minLength', value: 8, message: 'Password must be at least 8 characters' }
|
|
26
|
+
* ]
|
|
27
|
+
* }
|
|
28
|
+
* }), (req, res) => {
|
|
29
|
+
* // req.body is validated
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* // Validate query parameters
|
|
33
|
+
* app.get('/users', validate({
|
|
34
|
+
* page: {
|
|
35
|
+
* rules: [
|
|
36
|
+
* { type: 'number', message: 'Page must be a number' },
|
|
37
|
+
* { type: 'min', value: 1, message: 'Page must be at least 1' }
|
|
38
|
+
* ]
|
|
39
|
+
* }
|
|
40
|
+
* }, { source: 'query' }), (req, res) => {
|
|
41
|
+
* // req.query is validated
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
function validate(schema, options = {}) {
|
|
46
|
+
const { source = 'body', abortEarly = false, } = options;
|
|
47
|
+
const sources = Array.isArray(source) ? source : [source];
|
|
48
|
+
return async (req, res, next) => {
|
|
49
|
+
try {
|
|
50
|
+
const errors = [];
|
|
51
|
+
// Collect data from all specified sources
|
|
52
|
+
const dataToValidate = {};
|
|
53
|
+
for (const src of sources) {
|
|
54
|
+
if (src === 'body') {
|
|
55
|
+
Object.assign(dataToValidate, req.body);
|
|
56
|
+
}
|
|
57
|
+
else if (src === 'query') {
|
|
58
|
+
Object.assign(dataToValidate, req.query);
|
|
59
|
+
}
|
|
60
|
+
else if (src === 'params') {
|
|
61
|
+
Object.assign(dataToValidate, req.params);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Validate each field in the schema
|
|
65
|
+
for (const [field, fieldValidation] of Object.entries(schema)) {
|
|
66
|
+
const value = dataToValidate[field];
|
|
67
|
+
// Get rules array
|
|
68
|
+
let rules;
|
|
69
|
+
let fieldMessage;
|
|
70
|
+
if (Array.isArray(fieldValidation)) {
|
|
71
|
+
rules = fieldValidation;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
rules = fieldValidation.rules;
|
|
75
|
+
fieldMessage = fieldValidation.message;
|
|
76
|
+
}
|
|
77
|
+
// Validate each rule
|
|
78
|
+
for (const rule of rules) {
|
|
79
|
+
const error = await validateRule(field, value, rule, fieldMessage);
|
|
80
|
+
if (error) {
|
|
81
|
+
errors.push(error);
|
|
82
|
+
if (abortEarly) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (abortEarly && errors.length > 0) {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (errors.length > 0) {
|
|
92
|
+
res.status(400).json({
|
|
93
|
+
success: false,
|
|
94
|
+
error: 'Validation failed',
|
|
95
|
+
errors: errors.length === 1
|
|
96
|
+
? { [errors[0].field]: errors[0].message }
|
|
97
|
+
: errors.reduce((acc, err) => {
|
|
98
|
+
acc[err.field] = err.message;
|
|
99
|
+
return acc;
|
|
100
|
+
}, {}),
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
next();
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
res.status(500).json({
|
|
108
|
+
success: false,
|
|
109
|
+
error: 'Validation error',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Validate a single rule
|
|
116
|
+
*/
|
|
117
|
+
async function validateRule(field, value, rule, fieldMessage) {
|
|
118
|
+
const getMessage = (defaultMsg) => {
|
|
119
|
+
return rule.message || fieldMessage || defaultMsg;
|
|
120
|
+
};
|
|
121
|
+
switch (rule.type) {
|
|
122
|
+
case 'required':
|
|
123
|
+
if (value === undefined || value === null || value === '') {
|
|
124
|
+
return {
|
|
125
|
+
field,
|
|
126
|
+
message: getMessage(`${field} is required`),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
case 'string':
|
|
131
|
+
if (value !== undefined && typeof value !== 'string') {
|
|
132
|
+
return {
|
|
133
|
+
field,
|
|
134
|
+
message: getMessage(`${field} must be a string`),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
case 'number':
|
|
139
|
+
if (value !== undefined && (typeof value !== 'number' || isNaN(value))) {
|
|
140
|
+
// Try to parse string numbers
|
|
141
|
+
if (typeof value === 'string' && !isNaN(Number(value))) {
|
|
142
|
+
return null; // Valid number string
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
field,
|
|
146
|
+
message: getMessage(`${field} must be a number`),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
case 'boolean':
|
|
151
|
+
if (value !== undefined && typeof value !== 'boolean') {
|
|
152
|
+
// Accept string booleans
|
|
153
|
+
if (typeof value === 'string' && (value === 'true' || value === 'false')) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
field,
|
|
158
|
+
message: getMessage(`${field} must be a boolean`),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
case 'email':
|
|
163
|
+
if (value !== undefined && value !== '') {
|
|
164
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
165
|
+
if (!emailRegex.test(String(value))) {
|
|
166
|
+
return {
|
|
167
|
+
field,
|
|
168
|
+
message: getMessage(`${field} must be a valid email`),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
case 'min':
|
|
174
|
+
if (value !== undefined && value !== '') {
|
|
175
|
+
const numValue = typeof value === 'string' ? Number(value) : value;
|
|
176
|
+
if (typeof numValue === 'number' && !isNaN(numValue) && numValue < rule.value) {
|
|
177
|
+
return {
|
|
178
|
+
field,
|
|
179
|
+
message: getMessage(`${field} must be at least ${rule.value}`),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
case 'max':
|
|
185
|
+
if (value !== undefined && value !== '') {
|
|
186
|
+
const numValue = typeof value === 'string' ? Number(value) : value;
|
|
187
|
+
if (typeof numValue === 'number' && !isNaN(numValue) && numValue > rule.value) {
|
|
188
|
+
return {
|
|
189
|
+
field,
|
|
190
|
+
message: getMessage(`${field} must be at most ${rule.value}`),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
case 'minLength':
|
|
196
|
+
if (value !== undefined && value !== '') {
|
|
197
|
+
const strValue = String(value);
|
|
198
|
+
if (strValue.length < rule.value) {
|
|
199
|
+
return {
|
|
200
|
+
field,
|
|
201
|
+
message: getMessage(`${field} must be at least ${rule.value} characters`),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
case 'maxLength':
|
|
207
|
+
if (value !== undefined && value !== '') {
|
|
208
|
+
const strValue = String(value);
|
|
209
|
+
if (strValue.length > rule.value) {
|
|
210
|
+
return {
|
|
211
|
+
field,
|
|
212
|
+
message: getMessage(`${field} must be at most ${rule.value} characters`),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
case 'pattern':
|
|
218
|
+
if (value !== undefined && value !== '') {
|
|
219
|
+
if (!rule.value.test(String(value))) {
|
|
220
|
+
return {
|
|
221
|
+
field,
|
|
222
|
+
message: getMessage(`${field} format is invalid`),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
case 'enum':
|
|
228
|
+
if (value !== undefined && value !== '') {
|
|
229
|
+
if (!rule.values.includes(value)) {
|
|
230
|
+
return {
|
|
231
|
+
field,
|
|
232
|
+
message: getMessage(`${field} must be one of: ${rule.values.join(', ')}`),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
case 'array':
|
|
238
|
+
if (value !== undefined && !Array.isArray(value)) {
|
|
239
|
+
return {
|
|
240
|
+
field,
|
|
241
|
+
message: getMessage(`${field} must be an array`),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
break;
|
|
245
|
+
case 'object':
|
|
246
|
+
if (value !== undefined && (typeof value !== 'object' || Array.isArray(value) || value === null)) {
|
|
247
|
+
return {
|
|
248
|
+
field,
|
|
249
|
+
message: getMessage(`${field} must be an object`),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
case 'custom':
|
|
254
|
+
try {
|
|
255
|
+
const isValid = await rule.validator(value);
|
|
256
|
+
if (!isValid) {
|
|
257
|
+
return {
|
|
258
|
+
field,
|
|
259
|
+
message: getMessage(`${field} is invalid`),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
return {
|
|
265
|
+
field,
|
|
266
|
+
message: getMessage(`${field} validation failed`),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Validate request body
|
|
275
|
+
*/
|
|
276
|
+
function validateBody(schema, options = {}) {
|
|
277
|
+
return validate(schema, { ...options, source: 'body' });
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Validate request query parameters
|
|
281
|
+
*/
|
|
282
|
+
function validateQuery(schema, options = {}) {
|
|
283
|
+
return validate(schema, { ...options, source: 'query' });
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Validate request route parameters
|
|
287
|
+
*/
|
|
288
|
+
function validateParams(schema, options = {}) {
|
|
289
|
+
return validate(schema, { ...options, source: 'params' });
|
|
290
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration options for encryption service
|
|
3
|
+
*/
|
|
4
|
+
export interface EncryptionConfig {
|
|
5
|
+
/**
|
|
6
|
+
* Algorithm to use for encryption (default: 'aes-256-gcm')
|
|
7
|
+
*/
|
|
8
|
+
algorithm?: 'aes-256-gcm' | 'aes-192-gcm' | 'aes-128-gcm';
|
|
9
|
+
/**
|
|
10
|
+
* Key derivation iterations for PBKDF2 (default: 100000)
|
|
11
|
+
* Higher values are more secure but slower
|
|
12
|
+
*/
|
|
13
|
+
keyDerivationIterations?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Salt length in bytes (default: 32)
|
|
16
|
+
*/
|
|
17
|
+
saltLength?: number;
|
|
18
|
+
/**
|
|
19
|
+
* IV length in bytes (default: 16 for GCM)
|
|
20
|
+
*/
|
|
21
|
+
ivLength?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Authentication tag length in bytes (default: 16 for GCM)
|
|
24
|
+
*/
|
|
25
|
+
authTagLength?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Chunk size for large data encryption in bytes (default: 64KB)
|
|
28
|
+
* Data larger than this will be encrypted in chunks
|
|
29
|
+
*/
|
|
30
|
+
chunkSize?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Encryption result containing encrypted data and metadata
|
|
34
|
+
*/
|
|
35
|
+
export interface EncryptionResult {
|
|
36
|
+
/**
|
|
37
|
+
* Encrypted data as base64 string
|
|
38
|
+
*/
|
|
39
|
+
encrypted: string;
|
|
40
|
+
/**
|
|
41
|
+
* Salt used for key derivation (base64)
|
|
42
|
+
*/
|
|
43
|
+
salt: string;
|
|
44
|
+
/**
|
|
45
|
+
* IV/nonce used for encryption (base64)
|
|
46
|
+
*/
|
|
47
|
+
iv: string;
|
|
48
|
+
/**
|
|
49
|
+
* Authentication tag for GCM mode (base64)
|
|
50
|
+
*/
|
|
51
|
+
authTag?: string;
|
|
52
|
+
/**
|
|
53
|
+
* Algorithm used
|
|
54
|
+
*/
|
|
55
|
+
algorithm: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Service for end-to-end encryption and decryption of large data
|
|
59
|
+
* Uses AES-256-GCM for authenticated encryption with chunking support
|
|
60
|
+
*/
|
|
61
|
+
export declare class EncryptionService {
|
|
62
|
+
private algorithm;
|
|
63
|
+
private keyDerivationIterations;
|
|
64
|
+
private saltLength;
|
|
65
|
+
private ivLength;
|
|
66
|
+
private authTagLength;
|
|
67
|
+
private chunkSize;
|
|
68
|
+
private keyLength;
|
|
69
|
+
constructor(config?: EncryptionConfig);
|
|
70
|
+
/**
|
|
71
|
+
* Derive encryption key from password using PBKDF2
|
|
72
|
+
*/
|
|
73
|
+
private deriveKey;
|
|
74
|
+
/**
|
|
75
|
+
* Encrypt data (string or Buffer) with password-based encryption
|
|
76
|
+
* Automatically handles chunking for large data
|
|
77
|
+
*
|
|
78
|
+
* @param data - Data to encrypt (string or Buffer)
|
|
79
|
+
* @param password - Password for encryption
|
|
80
|
+
* @returns Encryption result with encrypted data and metadata
|
|
81
|
+
*/
|
|
82
|
+
encrypt(data: string | Buffer, password: string): Promise<EncryptionResult>;
|
|
83
|
+
/**
|
|
84
|
+
* Encrypt a single chunk of data
|
|
85
|
+
*/
|
|
86
|
+
private encryptChunk;
|
|
87
|
+
/**
|
|
88
|
+
* Encrypt large data in chunks
|
|
89
|
+
*/
|
|
90
|
+
private encryptLargeData;
|
|
91
|
+
/**
|
|
92
|
+
* Decrypt data that was encrypted with encrypt()
|
|
93
|
+
*
|
|
94
|
+
* @param encryptedData - Encryption result from encrypt()
|
|
95
|
+
* @param password - Password used for encryption
|
|
96
|
+
* @returns Decrypted data as Buffer
|
|
97
|
+
*/
|
|
98
|
+
decrypt(encryptedData: EncryptionResult, password: string): Promise<Buffer>;
|
|
99
|
+
/**
|
|
100
|
+
* Decrypt a single chunk
|
|
101
|
+
*/
|
|
102
|
+
private decryptChunk;
|
|
103
|
+
/**
|
|
104
|
+
* Decrypt large data that was encrypted in chunks
|
|
105
|
+
*/
|
|
106
|
+
private decryptLargeData;
|
|
107
|
+
/**
|
|
108
|
+
* Encrypt data and return as string (convenience method)
|
|
109
|
+
*
|
|
110
|
+
* @param data - Data to encrypt (string or Buffer)
|
|
111
|
+
* @param password - Password for encryption
|
|
112
|
+
* @returns Encrypted data as base64 string (includes all metadata)
|
|
113
|
+
*/
|
|
114
|
+
encryptToString(data: string | Buffer, password: string): Promise<string>;
|
|
115
|
+
/**
|
|
116
|
+
* Decrypt data from string format (convenience method)
|
|
117
|
+
*
|
|
118
|
+
* @param encryptedString - Encrypted data as JSON string from encryptToString()
|
|
119
|
+
* @param password - Password used for encryption
|
|
120
|
+
* @returns Decrypted data as string
|
|
121
|
+
*/
|
|
122
|
+
decryptFromString(encryptedString: string, password: string): Promise<string>;
|
|
123
|
+
/**
|
|
124
|
+
* Decrypt data from string format and return as Buffer
|
|
125
|
+
*
|
|
126
|
+
* @param encryptedString - Encrypted data as JSON string from encryptToString()
|
|
127
|
+
* @param password - Password used for encryption
|
|
128
|
+
* @returns Decrypted data as Buffer
|
|
129
|
+
*/
|
|
130
|
+
decryptFromStringToBuffer(encryptedString: string, password: string): Promise<Buffer>;
|
|
131
|
+
/**
|
|
132
|
+
* Generate a secure random password/key
|
|
133
|
+
*
|
|
134
|
+
* @param length - Length of password in bytes (default: 32)
|
|
135
|
+
* @returns Random password as base64 string
|
|
136
|
+
*/
|
|
137
|
+
static generateSecurePassword(length?: number): string;
|
|
138
|
+
/**
|
|
139
|
+
* Generate a secure random password/key as hex string
|
|
140
|
+
*
|
|
141
|
+
* @param length - Length of password in bytes (default: 32)
|
|
142
|
+
* @returns Random password as hex string
|
|
143
|
+
*/
|
|
144
|
+
static generateSecurePasswordHex(length?: number): string;
|
|
145
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.EncryptionService = void 0;
|
|
37
|
+
const crypto = __importStar(require("crypto"));
|
|
38
|
+
/**
|
|
39
|
+
* Service for end-to-end encryption and decryption of large data
|
|
40
|
+
* Uses AES-256-GCM for authenticated encryption with chunking support
|
|
41
|
+
*/
|
|
42
|
+
class EncryptionService {
|
|
43
|
+
constructor(config = {}) {
|
|
44
|
+
this.algorithm = config.algorithm || 'aes-256-gcm';
|
|
45
|
+
this.keyDerivationIterations = config.keyDerivationIterations || 100000;
|
|
46
|
+
this.saltLength = config.saltLength || 32;
|
|
47
|
+
this.ivLength = config.ivLength || 16;
|
|
48
|
+
this.authTagLength = config.authTagLength || 16;
|
|
49
|
+
this.chunkSize = config.chunkSize || 64 * 1024; // 64KB default
|
|
50
|
+
// Determine key length based on algorithm
|
|
51
|
+
if (this.algorithm.includes('256')) {
|
|
52
|
+
this.keyLength = 32; // 256 bits
|
|
53
|
+
}
|
|
54
|
+
else if (this.algorithm.includes('192')) {
|
|
55
|
+
this.keyLength = 24; // 192 bits
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
this.keyLength = 16; // 128 bits
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Derive encryption key from password using PBKDF2
|
|
63
|
+
*/
|
|
64
|
+
async deriveKey(password, salt) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
crypto.pbkdf2(password, salt, this.keyDerivationIterations, this.keyLength, 'sha256', (err, derivedKey) => {
|
|
67
|
+
if (err)
|
|
68
|
+
reject(err);
|
|
69
|
+
else
|
|
70
|
+
resolve(derivedKey);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Encrypt data (string or Buffer) with password-based encryption
|
|
76
|
+
* Automatically handles chunking for large data
|
|
77
|
+
*
|
|
78
|
+
* @param data - Data to encrypt (string or Buffer)
|
|
79
|
+
* @param password - Password for encryption
|
|
80
|
+
* @returns Encryption result with encrypted data and metadata
|
|
81
|
+
*/
|
|
82
|
+
async encrypt(data, password) {
|
|
83
|
+
if (!data) {
|
|
84
|
+
throw new Error('Data to encrypt cannot be empty');
|
|
85
|
+
}
|
|
86
|
+
if (!password || password.length < 8) {
|
|
87
|
+
throw new Error('Password must be at least 8 characters long');
|
|
88
|
+
}
|
|
89
|
+
// Convert string to Buffer if needed
|
|
90
|
+
const dataBuffer = Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8');
|
|
91
|
+
// Generate random salt and IV
|
|
92
|
+
const salt = crypto.randomBytes(this.saltLength);
|
|
93
|
+
const iv = crypto.randomBytes(this.ivLength);
|
|
94
|
+
// Derive encryption key from password
|
|
95
|
+
const key = await this.deriveKey(password, salt);
|
|
96
|
+
// For small data, encrypt directly
|
|
97
|
+
if (dataBuffer.length <= this.chunkSize) {
|
|
98
|
+
const result = this.encryptChunk(dataBuffer, key, iv);
|
|
99
|
+
result.salt = salt.toString('base64');
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
// For large data, encrypt in chunks
|
|
103
|
+
const result = await this.encryptLargeData(dataBuffer, key, iv);
|
|
104
|
+
result.salt = salt.toString('base64');
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Encrypt a single chunk of data
|
|
109
|
+
*/
|
|
110
|
+
encryptChunk(data, key, iv) {
|
|
111
|
+
const cipher = crypto.createCipheriv(this.algorithm, key, iv);
|
|
112
|
+
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
|
113
|
+
const authTag = cipher.getAuthTag();
|
|
114
|
+
return {
|
|
115
|
+
encrypted: encrypted.toString('base64'),
|
|
116
|
+
salt: '', // Will be set by caller
|
|
117
|
+
iv: iv.toString('base64'),
|
|
118
|
+
authTag: authTag.toString('base64'),
|
|
119
|
+
algorithm: this.algorithm,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Encrypt large data in chunks
|
|
124
|
+
*/
|
|
125
|
+
async encryptLargeData(data, key, iv) {
|
|
126
|
+
const chunks = [];
|
|
127
|
+
const totalChunks = Math.ceil(data.length / this.chunkSize);
|
|
128
|
+
// Generate a unique IV for each chunk (derive from base IV)
|
|
129
|
+
const chunkIVs = [];
|
|
130
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
131
|
+
// Derive chunk IV from base IV and chunk index
|
|
132
|
+
const chunkIVBuffer = Buffer.allocUnsafe(this.ivLength);
|
|
133
|
+
crypto.createHash('sha256')
|
|
134
|
+
.update(iv)
|
|
135
|
+
.update(Buffer.from(i.toString()))
|
|
136
|
+
.digest()
|
|
137
|
+
.copy(chunkIVBuffer, 0, 0, this.ivLength);
|
|
138
|
+
chunkIVs.push(chunkIVBuffer);
|
|
139
|
+
}
|
|
140
|
+
// Encrypt each chunk
|
|
141
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
142
|
+
const start = i * this.chunkSize;
|
|
143
|
+
const end = Math.min(start + this.chunkSize, data.length);
|
|
144
|
+
const chunk = data.slice(start, end);
|
|
145
|
+
const chunkIV = chunkIVs[i];
|
|
146
|
+
const cipher = crypto.createCipheriv(this.algorithm, key, chunkIV);
|
|
147
|
+
const encrypted = Buffer.concat([cipher.update(chunk), cipher.final()]);
|
|
148
|
+
const authTag = cipher.getAuthTag();
|
|
149
|
+
// Store chunk with its auth tag: base64(encrypted:authTag)
|
|
150
|
+
const chunkWithTag = Buffer.concat([encrypted, authTag]);
|
|
151
|
+
chunks.push(chunkWithTag.toString('base64'));
|
|
152
|
+
}
|
|
153
|
+
// Combine all chunks with delimiter
|
|
154
|
+
const encryptedData = chunks.join('|');
|
|
155
|
+
return {
|
|
156
|
+
encrypted: encryptedData,
|
|
157
|
+
salt: '', // Will be set by caller
|
|
158
|
+
iv: iv.toString('base64'),
|
|
159
|
+
authTag: undefined, // Not used for chunked encryption (each chunk has its own tag)
|
|
160
|
+
algorithm: this.algorithm,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Decrypt data that was encrypted with encrypt()
|
|
165
|
+
*
|
|
166
|
+
* @param encryptedData - Encryption result from encrypt()
|
|
167
|
+
* @param password - Password used for encryption
|
|
168
|
+
* @returns Decrypted data as Buffer
|
|
169
|
+
*/
|
|
170
|
+
async decrypt(encryptedData, password) {
|
|
171
|
+
if (!encryptedData || !encryptedData.encrypted) {
|
|
172
|
+
throw new Error('Invalid encrypted data');
|
|
173
|
+
}
|
|
174
|
+
if (!password || password.length < 8) {
|
|
175
|
+
throw new Error('Password must be at least 8 characters long');
|
|
176
|
+
}
|
|
177
|
+
// Reconstruct salt and IV
|
|
178
|
+
if (!encryptedData.salt) {
|
|
179
|
+
throw new Error('Salt is required for decryption');
|
|
180
|
+
}
|
|
181
|
+
const salt = Buffer.from(encryptedData.salt, 'base64');
|
|
182
|
+
const iv = Buffer.from(encryptedData.iv, 'base64');
|
|
183
|
+
// Derive encryption key from password
|
|
184
|
+
const key = await this.deriveKey(password, salt);
|
|
185
|
+
// Check if data is chunked (contains '|' delimiter)
|
|
186
|
+
if (encryptedData.encrypted.includes('|')) {
|
|
187
|
+
return this.decryptLargeData(encryptedData.encrypted, key, iv);
|
|
188
|
+
}
|
|
189
|
+
// Decrypt single chunk
|
|
190
|
+
return this.decryptChunk(encryptedData, key, iv);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Decrypt a single chunk
|
|
194
|
+
*/
|
|
195
|
+
decryptChunk(encryptedData, key, iv) {
|
|
196
|
+
const encrypted = Buffer.from(encryptedData.encrypted, 'base64');
|
|
197
|
+
if (!encryptedData.authTag) {
|
|
198
|
+
throw new Error('Authentication tag is required for decryption');
|
|
199
|
+
}
|
|
200
|
+
const authTag = Buffer.from(encryptedData.authTag, 'base64');
|
|
201
|
+
const decipher = crypto.createDecipheriv(encryptedData.algorithm || this.algorithm, key, iv);
|
|
202
|
+
decipher.setAuthTag(authTag);
|
|
203
|
+
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Decrypt large data that was encrypted in chunks
|
|
207
|
+
*/
|
|
208
|
+
decryptLargeData(encryptedData, key, iv) {
|
|
209
|
+
const chunks = encryptedData.split('|');
|
|
210
|
+
const decryptedChunks = [];
|
|
211
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
212
|
+
// Derive chunk IV from base IV and chunk index
|
|
213
|
+
const chunkIVBuffer = Buffer.allocUnsafe(this.ivLength);
|
|
214
|
+
crypto.createHash('sha256')
|
|
215
|
+
.update(iv)
|
|
216
|
+
.update(Buffer.from(i.toString()))
|
|
217
|
+
.digest()
|
|
218
|
+
.copy(chunkIVBuffer, 0, 0, this.ivLength);
|
|
219
|
+
// Extract encrypted data and auth tag
|
|
220
|
+
const chunkWithTag = Buffer.from(chunks[i], 'base64');
|
|
221
|
+
const encrypted = chunkWithTag.slice(0, -this.authTagLength);
|
|
222
|
+
const authTag = chunkWithTag.slice(-this.authTagLength);
|
|
223
|
+
// Decrypt chunk
|
|
224
|
+
const decipher = crypto.createDecipheriv(this.algorithm, key, chunkIVBuffer);
|
|
225
|
+
decipher.setAuthTag(authTag);
|
|
226
|
+
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
227
|
+
decryptedChunks.push(decrypted);
|
|
228
|
+
}
|
|
229
|
+
return Buffer.concat(decryptedChunks);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Encrypt data and return as string (convenience method)
|
|
233
|
+
*
|
|
234
|
+
* @param data - Data to encrypt (string or Buffer)
|
|
235
|
+
* @param password - Password for encryption
|
|
236
|
+
* @returns Encrypted data as base64 string (includes all metadata)
|
|
237
|
+
*/
|
|
238
|
+
async encryptToString(data, password) {
|
|
239
|
+
const result = await this.encrypt(data, password);
|
|
240
|
+
// Include salt in result for proper decryption
|
|
241
|
+
if (!result.salt) {
|
|
242
|
+
// Generate salt for this encryption
|
|
243
|
+
const salt = crypto.randomBytes(this.saltLength);
|
|
244
|
+
result.salt = salt.toString('base64');
|
|
245
|
+
}
|
|
246
|
+
// Return as JSON string (can be easily stored/transmitted)
|
|
247
|
+
return JSON.stringify(result);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Decrypt data from string format (convenience method)
|
|
251
|
+
*
|
|
252
|
+
* @param encryptedString - Encrypted data as JSON string from encryptToString()
|
|
253
|
+
* @param password - Password used for encryption
|
|
254
|
+
* @returns Decrypted data as string
|
|
255
|
+
*/
|
|
256
|
+
async decryptFromString(encryptedString, password) {
|
|
257
|
+
const encryptedData = JSON.parse(encryptedString);
|
|
258
|
+
const decrypted = await this.decrypt(encryptedData, password);
|
|
259
|
+
return decrypted.toString('utf8');
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Decrypt data from string format and return as Buffer
|
|
263
|
+
*
|
|
264
|
+
* @param encryptedString - Encrypted data as JSON string from encryptToString()
|
|
265
|
+
* @param password - Password used for encryption
|
|
266
|
+
* @returns Decrypted data as Buffer
|
|
267
|
+
*/
|
|
268
|
+
async decryptFromStringToBuffer(encryptedString, password) {
|
|
269
|
+
const encryptedData = JSON.parse(encryptedString);
|
|
270
|
+
return this.decrypt(encryptedData, password);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Generate a secure random password/key
|
|
274
|
+
*
|
|
275
|
+
* @param length - Length of password in bytes (default: 32)
|
|
276
|
+
* @returns Random password as base64 string
|
|
277
|
+
*/
|
|
278
|
+
static generateSecurePassword(length = 32) {
|
|
279
|
+
return crypto.randomBytes(length).toString('base64');
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Generate a secure random password/key as hex string
|
|
283
|
+
*
|
|
284
|
+
* @param length - Length of password in bytes (default: 32)
|
|
285
|
+
* @returns Random password as hex string
|
|
286
|
+
*/
|
|
287
|
+
static generateSecurePasswordHex(length = 32) {
|
|
288
|
+
return crypto.randomBytes(length).toString('hex');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
exports.EncryptionService = EncryptionService;
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ValidatorService = exports.PasswordService = exports.JwtService = void 0;
|
|
3
|
+
exports.EncryptionService = exports.ValidatorService = exports.PasswordService = exports.JwtService = void 0;
|
|
4
4
|
var jwt_1 = require("./jwt");
|
|
5
5
|
Object.defineProperty(exports, "JwtService", { enumerable: true, get: function () { return jwt_1.JwtService; } });
|
|
6
6
|
var password_1 = require("./password");
|
|
7
7
|
Object.defineProperty(exports, "PasswordService", { enumerable: true, get: function () { return password_1.PasswordService; } });
|
|
8
8
|
var validator_1 = require("./validator");
|
|
9
9
|
Object.defineProperty(exports, "ValidatorService", { enumerable: true, get: function () { return validator_1.ValidatorService; } });
|
|
10
|
+
var encryption_1 = require("./encryption");
|
|
11
|
+
Object.defineProperty(exports, "EncryptionService", { enumerable: true, get: function () { return encryption_1.EncryptionService; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bhandari88/express-auth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Plug-and-play authentication handler for Express.js with TypeScript supporting email, username, phone, and social login",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -23,19 +23,19 @@
|
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"jsonwebtoken": "^9.0.2",
|
|
25
25
|
"passport": "^0.7.0",
|
|
26
|
-
"passport-facebook": "^3.0.0",
|
|
27
26
|
"passport-google-oauth20": "^2.0.0",
|
|
27
|
+
"passport-facebook": "^3.0.0",
|
|
28
28
|
"validator": "^13.11.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/express": "^4.17.21",
|
|
32
32
|
"@types/jsonwebtoken": "^9.0.5",
|
|
33
|
-
"@types/node": "^20.10.5",
|
|
34
33
|
"@types/passport": "^1.0.16",
|
|
35
|
-
"@types/passport-facebook": "^3.0.3",
|
|
36
34
|
"@types/passport-google-oauth20": "^2.0.14",
|
|
35
|
+
"@types/passport-facebook": "^3.0.3",
|
|
37
36
|
"@types/validator": "^13.11.7",
|
|
38
|
-
"
|
|
37
|
+
"@types/node": "^20.10.5",
|
|
38
|
+
"typescript": "^5.3.3"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"express": "^4.18.0"
|