auth-verify 1.2.5 → 1.2.7
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 +32 -50
- package/package.json +1 -2
- package/readme.md +5 -0
- package/src/jwt/index.js +4 -3
- package/src/otp/index.js +113 -41
- package/tests/jwtmanager.multitab.test.js +34 -0
- package/tests/otpmanager.test.js +51 -0
package/index.js
CHANGED
|
@@ -1,51 +1,43 @@
|
|
|
1
1
|
const JWTManager = require("./src/jwt");
|
|
2
2
|
const OTPManager = require("./src/otp");
|
|
3
3
|
const SessionManager = require("./src/session");
|
|
4
|
-
const OAuthManager = require("./src/oauth")
|
|
4
|
+
const OAuthManager = require("./src/oauth");
|
|
5
5
|
|
|
6
6
|
class AuthVerify {
|
|
7
7
|
constructor(options = {}) {
|
|
8
8
|
let {
|
|
9
|
-
jwtSecret,
|
|
9
|
+
jwtSecret = "jwt_secret",
|
|
10
|
+
cookieName = "jwt_token",
|
|
10
11
|
otpExpiry = 300,
|
|
11
12
|
storeTokens = "none",
|
|
12
13
|
otpHash = "sha256",
|
|
13
14
|
redisUrl,
|
|
14
15
|
} = options;
|
|
15
16
|
|
|
16
|
-
//
|
|
17
|
-
|
|
17
|
+
// ✅ Ensure cookieName and secret always exist
|
|
18
|
+
this.cookieName = cookieName;
|
|
19
|
+
this.jwtSecret = jwtSecret;
|
|
20
|
+
|
|
21
|
+
// ✅ Pass both into JWTManager
|
|
22
|
+
this.jwt = new JWTManager(jwtSecret, {
|
|
23
|
+
storeTokens,
|
|
24
|
+
cookieName,
|
|
25
|
+
});
|
|
18
26
|
|
|
19
|
-
this.senderName;
|
|
20
|
-
this.jwt = new JWTManager(jwtSecret, { storeTokens });
|
|
21
27
|
this.otp = new OTPManager({
|
|
22
28
|
storeTokens,
|
|
23
29
|
otpExpiry,
|
|
24
30
|
otpHash,
|
|
25
31
|
redisUrl,
|
|
26
32
|
});
|
|
33
|
+
|
|
27
34
|
this.session = new SessionManager({ storeTokens, redisUrl });
|
|
35
|
+
this.oauth = new OAuthManager();
|
|
28
36
|
|
|
29
37
|
this.senders = new Map();
|
|
30
|
-
// this.register = {
|
|
31
|
-
// sender: (name, fn)=>{
|
|
32
|
-
// if (!name || typeof fn !== "function") {
|
|
33
|
-
// throw new Error("Sender registration requires a name and a function");
|
|
34
|
-
// }else{
|
|
35
|
-
// try{
|
|
36
|
-
// this.senders.set(name, fn);
|
|
37
|
-
// }catch(err){
|
|
38
|
-
// throw new Error(err);
|
|
39
|
-
// }
|
|
40
|
-
// }
|
|
41
|
-
// }
|
|
42
|
-
// }
|
|
43
|
-
// ✅ No getters — directly reference otp.dev (it's a plain object)
|
|
44
|
-
|
|
45
|
-
this.oauth = new OAuthManager();
|
|
46
38
|
}
|
|
47
|
-
|
|
48
|
-
// Session helpers
|
|
39
|
+
|
|
40
|
+
// --- Session helpers ---
|
|
49
41
|
async createSession(userId, options = {}) {
|
|
50
42
|
return this.session.create(userId, options);
|
|
51
43
|
}
|
|
@@ -58,33 +50,23 @@ class AuthVerify {
|
|
|
58
50
|
return this.session.destroy(sessionId);
|
|
59
51
|
}
|
|
60
52
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
53
|
+
// --- Sender registration ---
|
|
54
|
+
register = {
|
|
55
|
+
sender: (name, fn) => {
|
|
56
|
+
if (!name || typeof fn !== "function") {
|
|
57
|
+
throw new Error("Sender registration requires a name and a function");
|
|
58
|
+
}
|
|
59
|
+
this.senders.set(name, fn);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
66
62
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
this.senders.set(name, fn);
|
|
73
|
-
// console.log(`✅ Sender registered: ${name}`);
|
|
74
|
-
}
|
|
63
|
+
use(name) {
|
|
64
|
+
const senderFn = this.senders.get(name);
|
|
65
|
+
if (!senderFn) throw new Error(`Sender "${name}" not found`);
|
|
66
|
+
return {
|
|
67
|
+
send: async (options) => await senderFn(options),
|
|
75
68
|
};
|
|
76
|
-
|
|
77
|
-
// use a sender by name
|
|
78
|
-
use(name) {
|
|
79
|
-
const senderFn = this.senders.get(name);
|
|
80
|
-
if (!senderFn) throw new Error(`Sender "${name}" not found`);
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
send: async (options) => {
|
|
84
|
-
return await senderFn(options); // call user function
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
}
|
|
69
|
+
}
|
|
88
70
|
}
|
|
89
71
|
|
|
90
|
-
module.exports = AuthVerify;
|
|
72
|
+
module.exports = AuthVerify;
|
package/package.json
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"dependencies": {
|
|
3
|
-
"auth-verify": "^1.2.4",
|
|
4
3
|
"axios": "^1.12.2",
|
|
5
4
|
"crypto": "^1.0.1",
|
|
6
5
|
"ioredis": "^5.8.1",
|
|
@@ -12,7 +11,7 @@
|
|
|
12
11
|
"uuid": "^9.0.1"
|
|
13
12
|
},
|
|
14
13
|
"name": "auth-verify",
|
|
15
|
-
"version": "1.2.
|
|
14
|
+
"version": "1.2.7",
|
|
16
15
|
"description": "A simple Node.js library for sending and verifying OTP via email, SMS and Telegram bot",
|
|
17
16
|
"main": "index.js",
|
|
18
17
|
"scripts": {
|
package/readme.md
CHANGED
package/src/jwt/index.js
CHANGED
|
@@ -249,7 +249,8 @@ const CookieManager = require("./cookie");
|
|
|
249
249
|
class JWTManager {
|
|
250
250
|
constructor(secret, options = {}) {
|
|
251
251
|
// if (!secret) throw new Error("JWT secret is required");
|
|
252
|
-
this.secret = secret || "
|
|
252
|
+
this.secret = secret || "jwt_secret";
|
|
253
|
+
this.cookieName = options.cookieName || "jwt_token";
|
|
253
254
|
this.storeType = options.storeTokens || "none";
|
|
254
255
|
|
|
255
256
|
if (this.storeType === "memory") {
|
|
@@ -307,7 +308,7 @@ class JWTManager {
|
|
|
307
308
|
|
|
308
309
|
// Auto cookie support
|
|
309
310
|
if (options.res) {
|
|
310
|
-
CookieManager.setCookie(options.res, this.
|
|
311
|
+
CookieManager.setCookie(options.res, this.cookieName, token, {
|
|
311
312
|
httpOnly: true,
|
|
312
313
|
secure: options.secure ?? true,
|
|
313
314
|
sameSite: "Strict",
|
|
@@ -325,7 +326,7 @@ class JWTManager {
|
|
|
325
326
|
// If request object provided
|
|
326
327
|
if (typeof input === "object" && input.headers) {
|
|
327
328
|
token =
|
|
328
|
-
CookieManager.getCookie(input, this.
|
|
329
|
+
CookieManager.getCookie(input, this.cookieName) ||
|
|
329
330
|
(input.headers.authorization
|
|
330
331
|
? input.headers.authorization.replace("Bearer ", "")
|
|
331
332
|
: null);
|
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,34 @@
|
|
|
1
|
+
const express = require("express");
|
|
2
|
+
const request = require("supertest");
|
|
3
|
+
const AuthVerify = require("../index");
|
|
4
|
+
|
|
5
|
+
describe("JWT multi-tab verification", () => {
|
|
6
|
+
const auth = new AuthVerify({ jwtSecret: "test_secret", storeTokens: "memory" });
|
|
7
|
+
const app = express();
|
|
8
|
+
|
|
9
|
+
app.get("/login", async (req, res) => {
|
|
10
|
+
const token = await auth.jwt.sign({ userId: 1 }, "5s", { res });
|
|
11
|
+
res.json({ token });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
app.get("/protected", async (req, res) => {
|
|
15
|
+
try {
|
|
16
|
+
const data = await auth.jwt.verify(req);
|
|
17
|
+
res.json({ valid: true, data });
|
|
18
|
+
} catch (err) {
|
|
19
|
+
res.status(401).json({ valid: false, error: err.message });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("should work in second tab (same cookie)", async () => {
|
|
24
|
+
const loginRes = await request(app).get("/login");
|
|
25
|
+
const cookie = loginRes.headers["set-cookie"][0];
|
|
26
|
+
|
|
27
|
+
const protectedRes = await request(app)
|
|
28
|
+
.get("/protected")
|
|
29
|
+
.set("Cookie", cookie);
|
|
30
|
+
|
|
31
|
+
expect(protectedRes.body.valid).toBe(true);
|
|
32
|
+
expect(protectedRes.body.data.userId).toBe(1);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -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
|
+
});
|