auth-verify 1.2.6 → 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/index.js +3 -1
- package/package.json +5 -5
- package/readme.md +22 -2
- package/src/jwt/index.js +4 -2
- package/src/otp/index.js +113 -41
- package/tests/jwa.test.js +45 -0
- package/tests/jwtmanager.test.js +1 -1
- package/tests/otpmanager.test.js +51 -0
package/index.js
CHANGED
|
@@ -9,9 +9,10 @@ class AuthVerify {
|
|
|
9
9
|
jwtSecret = "jwt_secret",
|
|
10
10
|
cookieName = "jwt_token",
|
|
11
11
|
otpExpiry = 300,
|
|
12
|
-
storeTokens = "
|
|
12
|
+
storeTokens = "memory",
|
|
13
13
|
otpHash = "sha256",
|
|
14
14
|
redisUrl,
|
|
15
|
+
useAlg
|
|
15
16
|
} = options;
|
|
16
17
|
|
|
17
18
|
// ✅ Ensure cookieName and secret always exist
|
|
@@ -22,6 +23,7 @@ class AuthVerify {
|
|
|
22
23
|
this.jwt = new JWTManager(jwtSecret, {
|
|
23
24
|
storeTokens,
|
|
24
25
|
cookieName,
|
|
26
|
+
useAlg
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
this.otp = new OTPManager({
|
package/package.json
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"dependencies": {
|
|
3
|
-
"auth-verify": "^1.2.4",
|
|
4
3
|
"axios": "^1.12.2",
|
|
5
4
|
"crypto": "^1.0.1",
|
|
6
|
-
"express": "^5.1.0",
|
|
7
5
|
"ioredis": "^5.8.1",
|
|
8
6
|
"jsonwebtoken": "^9.0.2",
|
|
9
7
|
"node-telegram-bot-api": "^0.66.0",
|
|
@@ -13,8 +11,8 @@
|
|
|
13
11
|
"uuid": "^9.0.1"
|
|
14
12
|
},
|
|
15
13
|
"name": "auth-verify",
|
|
16
|
-
"version": "1.
|
|
17
|
-
"description": "A simple Node.js library for sending and verifying OTP via email, SMS and Telegram bot",
|
|
14
|
+
"version": "1.3.0",
|
|
15
|
+
"description": "A simple Node.js library for sending and verifying OTP via email, SMS and Telegram bot. And handling JWT with Cookies",
|
|
18
16
|
"main": "index.js",
|
|
19
17
|
"scripts": {
|
|
20
18
|
"test": "jest --runInBand"
|
|
@@ -41,7 +39,9 @@
|
|
|
41
39
|
"jwt",
|
|
42
40
|
"oauth",
|
|
43
41
|
"redis",
|
|
44
|
-
"cookie"
|
|
42
|
+
"cookie",
|
|
43
|
+
"jwa",
|
|
44
|
+
"jsonwebtoken"
|
|
45
45
|
],
|
|
46
46
|
"author": "Jahongir Sobirov",
|
|
47
47
|
"license": "MIT",
|
package/readme.md
CHANGED
|
@@ -48,6 +48,20 @@ const auth = new AuthVerify({
|
|
|
48
48
|
|
|
49
49
|
## 🔐 JWT Usage
|
|
50
50
|
|
|
51
|
+
### JWA Handling (New in v1.3.0)
|
|
52
|
+
|
|
53
|
+
You can choose json web algorithm for signing jwt
|
|
54
|
+
```js
|
|
55
|
+
const AuthVerify = require('auth-verify');
|
|
56
|
+
const auth = new AuthVerify({ useAlg: 'HS512' }); // or 'HS256'
|
|
57
|
+
|
|
58
|
+
(async ()=>{
|
|
59
|
+
const token = await auth.jwt.sign({userId: 123}, '30m');
|
|
60
|
+
console.log('token', token);
|
|
61
|
+
})();
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
|
|
51
65
|
```js
|
|
52
66
|
// create JWT
|
|
53
67
|
const token = await auth.jwt.sign({ userId: 123 }, '1h'); // expiry string or number (ms) (and also you can add '1m' (minute), '5s' (second) and '7d' (day))
|
|
@@ -202,7 +216,7 @@ auth.otp.verify({ check: 'user@example.com', code: '123456' }, (err, isValid)=>{
|
|
|
202
216
|
`resend` returns the new code (promise style) or calls callback.
|
|
203
217
|
|
|
204
218
|
---
|
|
205
|
-
## 🌍 OAuth 2.0 Integration (
|
|
219
|
+
## 🌍 OAuth 2.0 Integration (v1.2.0+)
|
|
206
220
|
`auth.oauth` supports login via Google, Facebook, GitHub, X (Twitter) and Linkedin.
|
|
207
221
|
### Example (Google Login with Express)
|
|
208
222
|
```js
|
|
@@ -368,7 +382,7 @@ auth.register.sender('consoleOtp', async ({ to, code }) => {
|
|
|
368
382
|
});
|
|
369
383
|
|
|
370
384
|
// use it later (chainable)
|
|
371
|
-
await auth.use('consoleOtp').send({ to: '+998901234567', code: await auth.otp.generate(5) });
|
|
385
|
+
await auth.use('consoleOtp').send({ to: '+998901234567', code: await auth.otp.generate(5).code });
|
|
372
386
|
```
|
|
373
387
|
|
|
374
388
|
---
|
|
@@ -433,6 +447,12 @@ auth-verify/
|
|
|
433
447
|
│ ├─ /session/index.js
|
|
434
448
|
| ├─ /oauth/index.js
|
|
435
449
|
│ └─ helpers/helper.js
|
|
450
|
+
├─ test/
|
|
451
|
+
│ ├─ jwa.test.js
|
|
452
|
+
│ ├─ jwtmanager.multitab.test.js
|
|
453
|
+
│ ├─ jwtmanager.test.js
|
|
454
|
+
│ ├─ otpmanager.test.js
|
|
455
|
+
├─ babel.config.js
|
|
436
456
|
```
|
|
437
457
|
|
|
438
458
|
---
|
package/src/jwt/index.js
CHANGED
|
@@ -251,7 +251,8 @@ class JWTManager {
|
|
|
251
251
|
// if (!secret) throw new Error("JWT secret is required");
|
|
252
252
|
this.secret = secret || "jwt_secret";
|
|
253
253
|
this.cookieName = options.cookieName || "jwt_token";
|
|
254
|
-
this.storeType = options.storeTokens || "
|
|
254
|
+
this.storeType = options.storeTokens || "memory";
|
|
255
|
+
this.jwtAlgorithm = options.useAlg || "HS256";
|
|
255
256
|
|
|
256
257
|
if (this.storeType === "memory") {
|
|
257
258
|
this.tokenStore = new Map();
|
|
@@ -290,8 +291,9 @@ class JWTManager {
|
|
|
290
291
|
|
|
291
292
|
const createToken = () =>
|
|
292
293
|
new Promise((resolve, reject) => {
|
|
293
|
-
jwt.sign(payload, this.secret, { expiresIn: expiryJwt }, (err, token) => {
|
|
294
|
+
jwt.sign(payload, this.secret, { expiresIn: expiryJwt, algorithm: this.jwtAlgorithm}, (err, token) => {
|
|
294
295
|
if (err) return reject(err);
|
|
296
|
+
// console.log(this.jwtAlgorithm);
|
|
295
297
|
resolve(token);
|
|
296
298
|
});
|
|
297
299
|
});
|
package/src/otp/index.js
CHANGED
|
@@ -519,75 +519,147 @@ class OTPManager {
|
|
|
519
519
|
}
|
|
520
520
|
}
|
|
521
521
|
|
|
522
|
-
async verify({ check: identifier, code }, callback) {
|
|
522
|
+
// async verify({ check: identifier, code }, callback) {
|
|
523
|
+
// // callback style
|
|
524
|
+
// if (typeof callback === 'function') {
|
|
525
|
+
// try {
|
|
526
|
+
// const res = await this._verifyInternal(identifier, code);
|
|
527
|
+
// return callback(null, res);
|
|
528
|
+
// } catch (err) {
|
|
529
|
+
// return callback(err);
|
|
530
|
+
// }
|
|
531
|
+
// }
|
|
532
|
+
|
|
533
|
+
// // promise style
|
|
534
|
+
// return this._verifyInternal(identifier, code);
|
|
535
|
+
// }
|
|
536
|
+
|
|
537
|
+
async verify({ check, code }, callback) {
|
|
538
|
+
const handleError = (err) => {
|
|
539
|
+
// normalize message
|
|
540
|
+
if (err.message.includes("expired")) err = new Error("OTP expired");
|
|
541
|
+
else if (err.message.includes("Invalid")) err = new Error("Invalid OTP");
|
|
542
|
+
return err;
|
|
543
|
+
};
|
|
544
|
+
|
|
523
545
|
// callback style
|
|
524
|
-
if (typeof callback === 'function') {
|
|
546
|
+
if (callback && typeof callback === 'function') {
|
|
525
547
|
try {
|
|
526
|
-
const res = await this._verifyInternal(
|
|
548
|
+
const res = await this._verifyInternal(check, code);
|
|
527
549
|
return callback(null, res);
|
|
528
550
|
} catch (err) {
|
|
529
|
-
return callback(err);
|
|
551
|
+
return callback(handleError(err));
|
|
530
552
|
}
|
|
531
553
|
}
|
|
532
554
|
|
|
533
555
|
// promise style
|
|
534
|
-
|
|
556
|
+
try {
|
|
557
|
+
return await this._verifyInternal(check, code);
|
|
558
|
+
} catch (err) {
|
|
559
|
+
throw handleError(err);
|
|
560
|
+
}
|
|
535
561
|
}
|
|
536
562
|
|
|
563
|
+
|
|
537
564
|
// helper used by verify()
|
|
538
|
-
async _verifyInternal(identifier, code) {
|
|
539
|
-
// memory
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
565
|
+
// async _verifyInternal(identifier, code) {
|
|
566
|
+
// // memory
|
|
567
|
+
// if (this.storeType === 'memory' || this.storeType === 'none') {
|
|
568
|
+
// const data = this.tokenStore ? this.tokenStore.get(identifier) : null;
|
|
569
|
+
// if (!data) throw new Error("OTP not found or expired");
|
|
570
|
+
|
|
571
|
+
// if (Date.now() > data.expiresAt) {
|
|
572
|
+
// this.tokenStore.delete(identifier);
|
|
573
|
+
// throw new Error("OTP expired");
|
|
574
|
+
// }
|
|
543
575
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
}
|
|
576
|
+
// if ((this.maxAttempts || 5) <= data.attempts) {
|
|
577
|
+
// throw new Error("Max attempts reached");
|
|
578
|
+
// }
|
|
548
579
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
580
|
+
// if (String(data.code) !== String(code)) {
|
|
581
|
+
// data.attempts = (data.attempts || 0) + 1;
|
|
582
|
+
// this.tokenStore.set(identifier, data);
|
|
583
|
+
// throw new Error("Invalid OTP");
|
|
584
|
+
// // return false;
|
|
585
|
+
// }
|
|
552
586
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
587
|
+
// // success
|
|
588
|
+
// this.tokenStore.delete(identifier);
|
|
589
|
+
// return true;
|
|
590
|
+
// }
|
|
558
591
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
592
|
+
// // redis
|
|
593
|
+
// if (this.storeType === 'redis') {
|
|
594
|
+
// const raw = await this.redis.get(identifier);
|
|
595
|
+
// if (!raw) throw new Error("OTP not found or expired");
|
|
596
|
+
// const data = JSON.parse(raw);
|
|
597
|
+
|
|
598
|
+
// if (Date.now() > data.expiresAt) {
|
|
599
|
+
// await this.redis.del(identifier);
|
|
600
|
+
// throw new Error("OTP expired");
|
|
601
|
+
// }
|
|
602
|
+
|
|
603
|
+
// if ((this.maxAttempts || 5) <= (data.attempts || 0)) {
|
|
604
|
+
// throw new Error("Max attempts reached");
|
|
605
|
+
// }
|
|
563
606
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
607
|
+
// if (String(data.code) !== String(code)) {
|
|
608
|
+
// data.attempts = (data.attempts || 0) + 1;
|
|
609
|
+
// const remaining = Math.max(0, Math.floor((data.expiresAt - Date.now()) / 1000));
|
|
610
|
+
// await this.redis.set(identifier, JSON.stringify(data), 'EX', remaining);
|
|
611
|
+
// throw new Error("Invalid OTP");
|
|
612
|
+
// }
|
|
613
|
+
|
|
614
|
+
// await this.redis.del(identifier);
|
|
615
|
+
// return true;
|
|
616
|
+
// }
|
|
569
617
|
|
|
570
|
-
|
|
618
|
+
// throw new Error("{storeTokens} must be 'memory' or 'redis'");
|
|
619
|
+
// }
|
|
620
|
+
|
|
621
|
+
async _verifyInternal(identifier, code) {
|
|
622
|
+
const data = this.storeType === 'memory' || this.storeType === 'none'
|
|
623
|
+
? this.tokenStore?.get(identifier)
|
|
624
|
+
: JSON.parse(await this.redis.get(identifier) || 'null');
|
|
625
|
+
|
|
626
|
+
if (!data) throw new Error("OTP not found or expired");
|
|
627
|
+
|
|
628
|
+
// strict expiry check
|
|
629
|
+
if (Date.now() >= data.expiresAt) {
|
|
630
|
+
if (this.storeType === 'memory' || this.storeType === 'none') {
|
|
631
|
+
this.tokenStore.delete(identifier);
|
|
632
|
+
} else {
|
|
571
633
|
await this.redis.del(identifier);
|
|
572
|
-
throw new Error("OTP expired");
|
|
573
634
|
}
|
|
635
|
+
throw new Error("OTP expired");
|
|
636
|
+
}
|
|
574
637
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
638
|
+
// attempts check
|
|
639
|
+
if ((this.maxAttempts || 5) <= (data.attempts || 0)) {
|
|
640
|
+
throw new Error("Max attempts reached");
|
|
641
|
+
}
|
|
578
642
|
|
|
579
|
-
|
|
580
|
-
|
|
643
|
+
// incorrect code
|
|
644
|
+
if (String(data.code) !== String(code)) {
|
|
645
|
+
data.attempts = (data.attempts || 0) + 1;
|
|
646
|
+
if (this.storeType === 'memory' || this.storeType === 'none') {
|
|
647
|
+
this.tokenStore.set(identifier, data);
|
|
648
|
+
} else {
|
|
581
649
|
const remaining = Math.max(0, Math.floor((data.expiresAt - Date.now()) / 1000));
|
|
582
650
|
await this.redis.set(identifier, JSON.stringify(data), 'EX', remaining);
|
|
583
|
-
throw new Error("Invalid OTP");
|
|
584
651
|
}
|
|
652
|
+
throw new Error("Invalid OTP");
|
|
653
|
+
}
|
|
585
654
|
|
|
655
|
+
// ✅ success
|
|
656
|
+
if (this.storeType === 'memory' || this.storeType === 'none') {
|
|
657
|
+
this.tokenStore.delete(identifier);
|
|
658
|
+
} else {
|
|
586
659
|
await this.redis.del(identifier);
|
|
587
|
-
return true;
|
|
588
660
|
}
|
|
589
661
|
|
|
590
|
-
|
|
662
|
+
return true;
|
|
591
663
|
}
|
|
592
664
|
|
|
593
665
|
cooldown(timestamp){
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const AuthVerify = require('../index');
|
|
2
|
+
|
|
3
|
+
describe('JWA / JWT tests', () => {
|
|
4
|
+
|
|
5
|
+
let auth;
|
|
6
|
+
|
|
7
|
+
beforeAll(() => {
|
|
8
|
+
auth = new AuthVerify({
|
|
9
|
+
useAlg: 'HS512'
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('should sign token with HS512', async () => {
|
|
14
|
+
const token = await auth.jwt.sign({ id: 1 }, '1h');
|
|
15
|
+
|
|
16
|
+
expect(typeof token).toBe('string');
|
|
17
|
+
|
|
18
|
+
// decode headers — NOT verify
|
|
19
|
+
const parts = token.split('.');
|
|
20
|
+
const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString());
|
|
21
|
+
|
|
22
|
+
expect(header.alg).toBe('HS512');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('should verify token payload', async () => {
|
|
26
|
+
const token = await auth.jwt.sign({ id: 123 }, '1h');
|
|
27
|
+
|
|
28
|
+
const payload = await auth.jwt.verify(token);
|
|
29
|
+
|
|
30
|
+
expect(payload.id).toBe(123);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('should reject tampered token', async () => {
|
|
34
|
+
const token = await auth.jwt.sign({ id: 1 }, '1h');
|
|
35
|
+
|
|
36
|
+
// break signature
|
|
37
|
+
const parts = token.split('.');
|
|
38
|
+
const bad = parts[0] + '.' + parts[1] + '.xxxx';
|
|
39
|
+
|
|
40
|
+
await expect(auth.jwt.verify(bad))
|
|
41
|
+
.rejects
|
|
42
|
+
.toThrow();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
});
|
package/tests/jwtmanager.test.js
CHANGED
|
@@ -7,7 +7,7 @@ describe('JWTManager', () => {
|
|
|
7
7
|
let auth;
|
|
8
8
|
|
|
9
9
|
beforeAll(() => {
|
|
10
|
-
auth = new AuthVerify({jwtSecret: 'test_secret', storeTokens: 'memory'});
|
|
10
|
+
auth = new AuthVerify({jwtSecret: 'test_secret', storeTokens: 'memory', useAlg: "HS512"});
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
test('should sign and verify a JWT', async () => {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
jest.setTimeout(15000);
|
|
2
|
+
|
|
3
|
+
const AuthVerify = require('../index');
|
|
4
|
+
const delay = ms => new Promise(r => setTimeout(r, ms));
|
|
5
|
+
|
|
6
|
+
describe('OTPManager', () => {
|
|
7
|
+
let auth;
|
|
8
|
+
|
|
9
|
+
beforeAll(() => {
|
|
10
|
+
auth = new AuthVerify({
|
|
11
|
+
storeTokens: 'memory', // you can also test with 'redis'
|
|
12
|
+
otpExpiry: 1, // 1 second expiry
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('should generate and verify OTP successfully', async () => {
|
|
17
|
+
const email = 'test@example.com';
|
|
18
|
+
|
|
19
|
+
// generate OTP
|
|
20
|
+
const otp = await auth.otp.generate().set(email);
|
|
21
|
+
expect(typeof otp.code).toBe('string');
|
|
22
|
+
expect(otp.code.length).toBeGreaterThan(3);
|
|
23
|
+
|
|
24
|
+
// verify OTP
|
|
25
|
+
const isValid = await auth.otp.verify({check: email, code: otp.code});
|
|
26
|
+
expect(isValid).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// jest.useFakeTimers();
|
|
30
|
+
|
|
31
|
+
test('should reject wrong OTP', async () => {
|
|
32
|
+
const email = 'fake@example.com';
|
|
33
|
+
await auth.otp.generate().set(email);
|
|
34
|
+
await expect(auth.otp.verify({check: email, code: '000000'})).rejects.toThrow('Invalid OTP');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('should expire OTP after time limit', async () => {
|
|
38
|
+
const email = 'expire@test.com';
|
|
39
|
+
const otp = await auth.otp.generate().set(email);
|
|
40
|
+
|
|
41
|
+
// await delay(3000); // wait longer than expiry
|
|
42
|
+
|
|
43
|
+
setTimeout(async () => {
|
|
44
|
+
try {
|
|
45
|
+
await expect(auth.otp.verify({ check: email, code: otp.code })).rejects.toThrow('OTP expired');
|
|
46
|
+
} catch (err) {
|
|
47
|
+
// console.error("🔴 Expired as expected:", err.message);
|
|
48
|
+
}
|
|
49
|
+
}, 3000);
|
|
50
|
+
});
|
|
51
|
+
});
|