auth-verify 1.3.0 → 1.5.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 +8 -1
- package/package.json +7 -4
- package/readme.md +228 -15
- package/src/jwt/index.js +70 -0
- package/src/oauth/index.js +178 -0
- package/src/totp/base32.js +46 -0
- package/src/totp/index.js +66 -0
- package/tests/jwt.middleware.test.js +89 -0
- package/tests/oauth.test.js +77 -0
- package/tests/totpmanager.test.js +54 -0
package/index.js
CHANGED
|
@@ -2,6 +2,7 @@ const JWTManager = require("./src/jwt");
|
|
|
2
2
|
const OTPManager = require("./src/otp");
|
|
3
3
|
const SessionManager = require("./src/session");
|
|
4
4
|
const OAuthManager = require("./src/oauth");
|
|
5
|
+
const TOTPManager = require("./src/totp");
|
|
5
6
|
|
|
6
7
|
class AuthVerify {
|
|
7
8
|
constructor(options = {}) {
|
|
@@ -12,7 +13,12 @@ class AuthVerify {
|
|
|
12
13
|
storeTokens = "memory",
|
|
13
14
|
otpHash = "sha256",
|
|
14
15
|
redisUrl,
|
|
15
|
-
useAlg
|
|
16
|
+
useAlg,
|
|
17
|
+
totp = {
|
|
18
|
+
digits: 6,
|
|
19
|
+
step: 30,
|
|
20
|
+
alg: "SHA1"
|
|
21
|
+
}
|
|
16
22
|
} = options;
|
|
17
23
|
|
|
18
24
|
// ✅ Ensure cookieName and secret always exist
|
|
@@ -35,6 +41,7 @@ class AuthVerify {
|
|
|
35
41
|
|
|
36
42
|
this.session = new SessionManager({ storeTokens, redisUrl });
|
|
37
43
|
this.oauth = new OAuthManager();
|
|
44
|
+
this.totp = new TOTPManager(totp);
|
|
38
45
|
|
|
39
46
|
this.senders = new Map();
|
|
40
47
|
}
|
package/package.json
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
"jsonwebtoken": "^9.0.2",
|
|
7
7
|
"node-telegram-bot-api": "^0.66.0",
|
|
8
8
|
"nodemailer": "^7.0.6",
|
|
9
|
+
"qrcode": "^1.5.4",
|
|
9
10
|
"redis": "^5.8.3",
|
|
10
|
-
"twilio": "^5.10.3",
|
|
11
11
|
"uuid": "^9.0.1"
|
|
12
12
|
},
|
|
13
13
|
"name": "auth-verify",
|
|
14
|
-
"version": "1.
|
|
15
|
-
"description": "A simple Node.js library for sending and verifying OTP via email, SMS and Telegram bot. And handling JWT with Cookies",
|
|
14
|
+
"version": "1.5.0",
|
|
15
|
+
"description": "A simple Node.js library for sending and verifying OTP via email, SMS and Telegram bot. And generating TOTP codes and QR codes. And handling JWT with Cookies",
|
|
16
16
|
"main": "index.js",
|
|
17
17
|
"scripts": {
|
|
18
18
|
"test": "jest --runInBand"
|
|
@@ -41,7 +41,10 @@
|
|
|
41
41
|
"redis",
|
|
42
42
|
"cookie",
|
|
43
43
|
"jwa",
|
|
44
|
-
"jsonwebtoken"
|
|
44
|
+
"jsonwebtoken",
|
|
45
|
+
"totp",
|
|
46
|
+
"google-authenticator",
|
|
47
|
+
"signin"
|
|
45
48
|
],
|
|
46
49
|
"author": "Jahongir Sobirov",
|
|
47
50
|
"license": "MIT",
|
package/readme.md
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# auth-verify
|
|
2
2
|
|
|
3
3
|
**auth-verify** is a Node.js authentication utility that provides:
|
|
4
|
-
- ✅ Secure OTP (one-time password) generation and verification
|
|
5
|
-
- ✅ Sending OTPs via Email, SMS (pluggable helpers), and Telegram bot
|
|
6
|
-
- ✅
|
|
7
|
-
- ✅
|
|
8
|
-
- ✅
|
|
9
|
-
-
|
|
4
|
+
- ✅ Secure OTP (one-time password) generation and verification.
|
|
5
|
+
- ✅ Sending OTPs via Email, SMS (pluggable helpers), and Telegram bot.
|
|
6
|
+
- ✅ TOTP (Time-based One Time Passwords) generation, QR code generation, and verification (Google Authenticator support).
|
|
7
|
+
- ✅ JWT creation, verification, optional token revocation with memory/Redis storage, and advanced middleware for protecting routes, custom cookie/header handling, role-based guards, and token extraction from custom sources.
|
|
8
|
+
- ✅ Session management (in-memory or Redis).
|
|
9
|
+
- ✅ OAuth 2.0 integration for Google, Facebook, GitHub, X (Twitter), Linkedin, and additional providers like Apple, Discord, Slack, Microsoft, Telegram,and WhatsApp.
|
|
10
|
+
- ⚙️ Developer extensibility: custom senders via auth.register.sender() and chainable sending via auth.use(name).send(...).
|
|
11
|
+
- ✅ Automatic JWT cookie handling for Express apps, supporting secure, HTTP-only cookies and optional auto-verification.
|
|
12
|
+
- ✅ Fully asynchronous/Promise-based API, with callback support where applicable.
|
|
13
|
+
- ✅ Chainable OTP workflow with cooldowns, max attempts, and resend functionality.
|
|
10
14
|
---
|
|
11
15
|
|
|
12
16
|
## 🧩 Installation
|
|
@@ -23,11 +27,12 @@ npm install auth-verify
|
|
|
23
27
|
|
|
24
28
|
## ⚙️ Quick overview
|
|
25
29
|
|
|
26
|
-
- `AuthVerify` (entry): constructs and exposes `.jwt`, `.otp`, (optionally) `.session` and `.oauth` managers.
|
|
27
|
-
- `JWTManager`: sign, verify, decode, revoke tokens. Supports `storeTokens: "memory" | "redis" | "none"
|
|
30
|
+
- `AuthVerify` (entry): constructs and exposes `.jwt`, `.otp`, (optionally) `.session`, `.totp` and `.oauth` managers.
|
|
31
|
+
- `JWTManager`: sign, verify, decode, revoke tokens. Supports `storeTokens: "memory" | "redis" | "none"` and middleware with custom cookie, header, and token extraction.
|
|
28
32
|
- `OTPManager`: generate, store, send, verify, resend OTPs. Supports `storeTokens: "memory" | "redis" | "none"`. Supports email, SMS helper, Telegram bot, and custom dev senders.
|
|
33
|
+
- `TOTPManager`: generate, verify uri, codes and QR codes
|
|
29
34
|
- `SessionManager`: simple session creation/verification/destroy with memory or Redis backend.
|
|
30
|
-
- `OAuthManager`: Handle OAuth 2.0 logins for Google, Facebook, GitHub, X and
|
|
35
|
+
- `OAuthManager`: Handle OAuth 2.0 logins for Google, Facebook, GitHub, X, Linkedin, Apple, Discord, Slack, Microsoft, Telegram and WhatsApp
|
|
31
36
|
---
|
|
32
37
|
|
|
33
38
|
## 🚀 Example: Initialize library (CommonJS)
|
|
@@ -48,7 +53,109 @@ const auth = new AuthVerify({
|
|
|
48
53
|
|
|
49
54
|
## 🔐 JWT Usage
|
|
50
55
|
|
|
51
|
-
###
|
|
56
|
+
### JWT Middleware (`protect`) (New in v1.5.0)
|
|
57
|
+
|
|
58
|
+
auth-verify comes with a fully customizable JWT middleware, making it easy to **protect routes**, **attach decoded data to the request**, and **check user roles**.
|
|
59
|
+
|
|
60
|
+
#### ⚙️ `protect` Method Overview
|
|
61
|
+
|
|
62
|
+
Function signature:
|
|
63
|
+
```js
|
|
64
|
+
protect(options = {})
|
|
65
|
+
```
|
|
66
|
+
**Description**:
|
|
67
|
+
- Returns an Express-style middleware.
|
|
68
|
+
- Automatically reads JWT from cookie, header, or custom extractor.
|
|
69
|
+
- Verifies the token and optionally checks for roles.
|
|
70
|
+
- Attaches the decoded payload to req (default property: req.user).
|
|
71
|
+
|
|
72
|
+
| Option | Type | Default | Description |
|
|
73
|
+
| ---------------- | -------- | ----------------- | -------------------------------------------------------------------------------------- |
|
|
74
|
+
| `onError` | Function | `null` | Custom error handler. `(err, req, res)` |
|
|
75
|
+
| `attachProperty` | String | `"user"` | Where to attach decoded token payload on `req` |
|
|
76
|
+
| `requiredRole` | String | `null` | Optional role check. Throws error if decoded role does not match |
|
|
77
|
+
| `cookieName` | String | `this.cookieName` | Name of the cookie to read JWT from |
|
|
78
|
+
| `headerName` | String | `"authorization"` | Header name to read JWT from. `"authorization"` splits `Bearer TOKEN` automatically |
|
|
79
|
+
| `extractor` | Function | `null` | Custom function to extract token. Receives `req` as argument and must return the token |
|
|
80
|
+
|
|
81
|
+
#### Middleware Behavior
|
|
82
|
+
|
|
83
|
+
1. **Token extraction order:**
|
|
84
|
+
- First: `extractor(req)` if provided
|
|
85
|
+
- Second: `req.headers[headerName]` (for `authorization`, splits `Bearer TOKEN`)
|
|
86
|
+
- Third: `cookieName` from request cookies
|
|
87
|
+
2. **Verification**:
|
|
88
|
+
- Calls `this.verify(token)`
|
|
89
|
+
- Throws `NO_TOKEN` if no token is found
|
|
90
|
+
- Throws `ROLE_NOT_ALLOWED` if `requiredRole`is provided but decoded role does not match
|
|
91
|
+
3. **Attachment**:
|
|
92
|
+
- Decoded token is attached to `req[attachProperty]`
|
|
93
|
+
4. **Error handling**:
|
|
94
|
+
- Default: responds with `401` and JSON `{ success: false, error: err.message }`
|
|
95
|
+
- Custom: if `onError` is provided, it is called instead of default behavior
|
|
96
|
+
|
|
97
|
+
#### Example Usage
|
|
98
|
+
##### Basic Usage
|
|
99
|
+
```js
|
|
100
|
+
const express = require("express");
|
|
101
|
+
const AuthVerify = require("auth-verify");
|
|
102
|
+
const app = express();
|
|
103
|
+
|
|
104
|
+
const auth = new AuthVerify({ jwtSecret: "supersecret" });
|
|
105
|
+
|
|
106
|
+
// Protect route
|
|
107
|
+
app.get("/dashboard", auth.jwt.protect(), (req, res) => {
|
|
108
|
+
// req.user contains decoded JWT payload
|
|
109
|
+
res.json({ message: `Welcome, ${req.user.userId}` });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
app.listen(3000, () => console.log("Server running on port 3000"));
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
##### Custom Cookie & Header
|
|
116
|
+
```js
|
|
117
|
+
app.get("/profile", auth.jwt.protect({
|
|
118
|
+
cookieName: "myToken",
|
|
119
|
+
headerName: "x-access-token"
|
|
120
|
+
}), (req, res) => {
|
|
121
|
+
res.json({ user: req.user });
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
- JWT will be read from the cookie named `"myToken"` or from the header `"x-access-token"`.
|
|
125
|
+
|
|
126
|
+
#### Role-based Guard
|
|
127
|
+
```js
|
|
128
|
+
app.get("/admin", auth.jwt.protect({
|
|
129
|
+
requiredRole: "admin"
|
|
130
|
+
}), (req, res) => {
|
|
131
|
+
res.json({ message: "Welcome Admin" });
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
- Throws error if decoded token does not have role: `"admin"`.
|
|
135
|
+
|
|
136
|
+
#### Custom Token Extractor
|
|
137
|
+
```js
|
|
138
|
+
app.get("/custom", auth.jwt.protect({
|
|
139
|
+
extractor: (req) => req.query.token
|
|
140
|
+
}), (req, res) => {
|
|
141
|
+
res.json({ user: req.user });
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
- Allows you to read token from any custom location (e.g., query params).
|
|
145
|
+
|
|
146
|
+
#### Custom Error Handler
|
|
147
|
+
```js
|
|
148
|
+
app.get("/custom-error", auth.jwt.protect({
|
|
149
|
+
onError: (err, req, res) => {
|
|
150
|
+
res.status(403).json({ error: "Access denied", details: err.message });
|
|
151
|
+
}
|
|
152
|
+
}), (req, res) => {
|
|
153
|
+
res.json({ user: req.user });
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
- Overrides default `401` response with custom logic.
|
|
157
|
+
|
|
158
|
+
### JWA Handling (v1.3.0+)
|
|
52
159
|
|
|
53
160
|
You can choose json web algorithm for signing jwt
|
|
54
161
|
```js
|
|
@@ -143,8 +250,26 @@ auth.otp.setSender({
|
|
|
143
250
|
auth.otp.setSender({
|
|
144
251
|
via: 'sms',
|
|
145
252
|
provider: 'infobip',
|
|
146
|
-
apiKey: '
|
|
147
|
-
apiSecret: '
|
|
253
|
+
apiKey: 'API_KEY',
|
|
254
|
+
apiSecret: 'API_SECRET',
|
|
255
|
+
sender: 'SENDER_NAME',
|
|
256
|
+
mock: true // in dev prints message instead of sending
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
auth.otp.setSender({
|
|
260
|
+
via: 'sms',
|
|
261
|
+
provider: 'twilio',
|
|
262
|
+
apiKey: 'ACCOUNT_SID',
|
|
263
|
+
apiSecret: 'AUTH_TOKEN',
|
|
264
|
+
sender: 'SENDER_NAME',
|
|
265
|
+
mock: true // in dev prints message instead of sending
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
auth.otp.setSender({
|
|
269
|
+
via: 'sms',
|
|
270
|
+
provider: 'vonage',
|
|
271
|
+
apiKey: 'API_KEY',
|
|
272
|
+
apiSecret: 'API_SECRET',
|
|
148
273
|
sender: 'SENDER_NAME',
|
|
149
274
|
mock: true // in dev prints message instead of sending
|
|
150
275
|
});
|
|
@@ -174,7 +299,20 @@ auth.otp.generate(6).set('user@example.com', (err) => {
|
|
|
174
299
|
else console.log('sent', info && info.messageId);
|
|
175
300
|
});
|
|
176
301
|
});
|
|
302
|
+
|
|
303
|
+
// Sending OTP with SMS
|
|
304
|
+
auth.otp.generate(6).set('+1234567890', (err) => {
|
|
305
|
+
if (err) throw err;
|
|
306
|
+
auth.otp.message({
|
|
307
|
+
to: '+1234567890',
|
|
308
|
+
text: `Your code: <b>${auth.otp.code}</b>`
|
|
309
|
+
}, (err, info) => {
|
|
310
|
+
if (err) console.error('send error', err);
|
|
311
|
+
else console.log('sent', info && info.messageId);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
177
314
|
```
|
|
315
|
+
`+1234567890` is reciever number
|
|
178
316
|
|
|
179
317
|
Async/await style:
|
|
180
318
|
|
|
@@ -215,6 +353,76 @@ auth.otp.verify({ check: 'user@example.com', code: '123456' }, (err, isValid)=>{
|
|
|
215
353
|
|
|
216
354
|
`resend` returns the new code (promise style) or calls callback.
|
|
217
355
|
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## ✅ TOTP (Time-based One Time Passwords) — Google Authenticator support (v1.4.0+)
|
|
359
|
+
```js
|
|
360
|
+
const AuthVerify = require("auth-verify");
|
|
361
|
+
const auth = new AuthVerify();
|
|
362
|
+
// Optionally:
|
|
363
|
+
/*
|
|
364
|
+
const AuthVerify = require("auth-verify");
|
|
365
|
+
const auth = new AuthVerify({
|
|
366
|
+
totp: {
|
|
367
|
+
digits: 6 (default)
|
|
368
|
+
step: 30 (default)
|
|
369
|
+
alg: "SHA1" (default)
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
*/
|
|
373
|
+
```
|
|
374
|
+
You can change `digits`, `step`, `alg`.
|
|
375
|
+
- `digits`: how many digits your one-time password has **(Google Authenticator default = 6 digits)**
|
|
376
|
+
- `step`: how long each TOTP code lives in seconds **(Google Authenticator default = 30 seconds)**
|
|
377
|
+
- `alg`: the hashing algorithm used to generate the OTP **(Google Authenticator default = SHA1)**
|
|
378
|
+
### Generate secret
|
|
379
|
+
```js
|
|
380
|
+
const secret = auth.totp.secret();
|
|
381
|
+
console.log(secret); //base 32
|
|
382
|
+
```
|
|
383
|
+
### generate otpauth URI
|
|
384
|
+
```js
|
|
385
|
+
const uri = auth.totp.uri({
|
|
386
|
+
label: "user@example.com",
|
|
387
|
+
issuer: "AuthVerify",
|
|
388
|
+
secret
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
console.log(uri);
|
|
392
|
+
```
|
|
393
|
+
### generate QR code image
|
|
394
|
+
(send this PNG to frontend or show in UI)
|
|
395
|
+
```js
|
|
396
|
+
const qr = await auth.totp.qrcode(uri);
|
|
397
|
+
console.log(qr); // data:image/png;base64,...
|
|
398
|
+
```
|
|
399
|
+
### generate a TOTP code
|
|
400
|
+
```js
|
|
401
|
+
const token = auth.totp.generate(secret);
|
|
402
|
+
console.log("TOTP:", token);
|
|
403
|
+
```
|
|
404
|
+
### verify a code entered by user
|
|
405
|
+
```js
|
|
406
|
+
const ok = auth.totp.verify({ secret, token });
|
|
407
|
+
console.log(ok); // true or false
|
|
408
|
+
```
|
|
409
|
+
### example real flow
|
|
410
|
+
```js
|
|
411
|
+
// Register UI
|
|
412
|
+
const secret = auth.totp.secret();
|
|
413
|
+
const uri = auth.totp.uri({ label: "john@example.com", issuer: "AuthVerify", secret });
|
|
414
|
+
const qr = await auth.totp.qrcode(uri);
|
|
415
|
+
// show qr to user
|
|
416
|
+
|
|
417
|
+
// Then user scans QR with Google Authenticator
|
|
418
|
+
// Then user enters 6-digit code
|
|
419
|
+
const token = req.body.code;
|
|
420
|
+
|
|
421
|
+
// Verify
|
|
422
|
+
if (auth.totp.verify({ secret, token })) {
|
|
423
|
+
// enable 2FA
|
|
424
|
+
}
|
|
425
|
+
```
|
|
218
426
|
---
|
|
219
427
|
## 🌍 OAuth 2.0 Integration (v1.2.0+)
|
|
220
428
|
`auth.oauth` supports login via Google, Facebook, GitHub, X (Twitter) and Linkedin.
|
|
@@ -347,7 +555,7 @@ app.get("/auth/linkedin/callback", async (req, res)=>{
|
|
|
347
555
|
try{
|
|
348
556
|
const { code } = req.query;
|
|
349
557
|
const user = await linkedin.callback(code);
|
|
350
|
-
res.json({ success: true, provider: "
|
|
558
|
+
res.json({ success: true, provider: "linkedin", user });
|
|
351
559
|
}catch(err){
|
|
352
560
|
res.status(400).json({ error: err.message });
|
|
353
561
|
}
|
|
@@ -438,20 +646,25 @@ Notes:
|
|
|
438
646
|
auth-verify/
|
|
439
647
|
├─ README.md
|
|
440
648
|
├─ package.json
|
|
649
|
+
├─ index.js // exports AuthVerify
|
|
441
650
|
├─ src/
|
|
442
|
-
│ ├─ index.js // exports AuthVerify
|
|
443
651
|
│ ├─ jwt/
|
|
444
652
|
| | ├─ index.js
|
|
445
653
|
| | ├─ cookie/index.js
|
|
446
654
|
│ ├─ /otp/index.js
|
|
655
|
+
│ ├─ totp/
|
|
656
|
+
| | ├─ index.js
|
|
657
|
+
| | ├─ base32.js
|
|
447
658
|
│ ├─ /session/index.js
|
|
448
659
|
| ├─ /oauth/index.js
|
|
449
660
|
│ └─ helpers/helper.js
|
|
450
|
-
├─
|
|
661
|
+
├─ tests/
|
|
451
662
|
│ ├─ jwa.test.js
|
|
452
663
|
│ ├─ jwtmanager.multitab.test.js
|
|
453
664
|
│ ├─ jwtmanager.test.js
|
|
454
665
|
│ ├─ otpmanager.test.js
|
|
666
|
+
│ ├─ oauth.test.js
|
|
667
|
+
│ ├─ totpmanager.test.js
|
|
455
668
|
├─ babel.config.js
|
|
456
669
|
```
|
|
457
670
|
|
package/src/jwt/index.js
CHANGED
|
@@ -391,6 +391,76 @@ class JWTManager {
|
|
|
391
391
|
await this.redis.set(token, JSON.stringify(data));
|
|
392
392
|
}
|
|
393
393
|
}
|
|
394
|
+
|
|
395
|
+
// JWT middleware part
|
|
396
|
+
readCookie(req, name) {
|
|
397
|
+
const cookieHeader = req.headers.cookie;
|
|
398
|
+
if (!cookieHeader) return null;
|
|
399
|
+
|
|
400
|
+
const cookies = cookieHeader.split(';').map(v => v.trim());
|
|
401
|
+
for (const c of cookies) {
|
|
402
|
+
const [key, val] = c.split('=');
|
|
403
|
+
if (key === name) return val;
|
|
404
|
+
}
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
protect(options = {}) {
|
|
409
|
+
// customizable messages / behavior
|
|
410
|
+
const {
|
|
411
|
+
onError,
|
|
412
|
+
attachProperty = "user", // where to put decoded data on req
|
|
413
|
+
requiredRole = null, // optional: role check
|
|
414
|
+
cookieName = this.cookieName,
|
|
415
|
+
headerName = "authorization",
|
|
416
|
+
extractor = null
|
|
417
|
+
} = options;
|
|
418
|
+
|
|
419
|
+
return async (req, res, next) => {
|
|
420
|
+
try {
|
|
421
|
+
// read token (cookie → header)
|
|
422
|
+
let token = null;
|
|
423
|
+
|
|
424
|
+
if (extractor && typeof extractor === "function") {
|
|
425
|
+
token = extractor(req);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if(!token && req.headers[headerName]){
|
|
429
|
+
if(headerName == "authorization"){
|
|
430
|
+
token = req.headers.authorization.split(" ")[1];
|
|
431
|
+
}else{
|
|
432
|
+
token = req.headers[headerName]; // custom header — return raw
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (!token) {
|
|
437
|
+
token = this.readCookie(req, cookieName);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (!token) throw new Error("NO_TOKEN");
|
|
441
|
+
|
|
442
|
+
const decoded = await this.verify(token);
|
|
443
|
+
|
|
444
|
+
// role guard?
|
|
445
|
+
if (requiredRole && decoded.role !== requiredRole) {
|
|
446
|
+
throw new Error("ROLE_NOT_ALLOWED");
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// attach user
|
|
450
|
+
req[attachProperty] = decoded;
|
|
451
|
+
|
|
452
|
+
next();
|
|
453
|
+
} catch (err) {
|
|
454
|
+
if (onError) return onError(err, req, res);
|
|
455
|
+
|
|
456
|
+
return res.status(401).json({
|
|
457
|
+
success: false,
|
|
458
|
+
error: err.message
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
394
464
|
}
|
|
395
465
|
|
|
396
466
|
module.exports = JWTManager;
|
package/src/oauth/index.js
CHANGED
|
@@ -235,6 +235,184 @@ class OAuthManager {
|
|
|
235
235
|
};
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
// --- APPLE LOGIN ---
|
|
239
|
+
apple({ clientId, clientSecret, redirectUri }) {
|
|
240
|
+
return {
|
|
241
|
+
redirect: (res) => {
|
|
242
|
+
const url =
|
|
243
|
+
"https://appleid.apple.com/auth/authorize?" +
|
|
244
|
+
new URLSearchParams({
|
|
245
|
+
response_type: "code",
|
|
246
|
+
client_id: clientId,
|
|
247
|
+
redirect_uri: redirectUri,
|
|
248
|
+
scope: "name email",
|
|
249
|
+
response_mode: "form_post",
|
|
250
|
+
});
|
|
251
|
+
res.redirect(url);
|
|
252
|
+
},
|
|
253
|
+
callback: async (code) => {
|
|
254
|
+
const tokenRes = await fetch("https://appleid.apple.com/auth/token", {
|
|
255
|
+
method: "POST",
|
|
256
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
257
|
+
body: new URLSearchParams({
|
|
258
|
+
grant_type: "authorization_code",
|
|
259
|
+
code,
|
|
260
|
+
client_id: clientId,
|
|
261
|
+
client_secret: clientSecret,
|
|
262
|
+
redirect_uri: redirectUri,
|
|
263
|
+
}),
|
|
264
|
+
});
|
|
265
|
+
const tokenData = await tokenRes.json();
|
|
266
|
+
if (tokenData.error) throw new Error("OAuth Error: " + tokenData.error);
|
|
267
|
+
return tokenData;
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// --- DISCORD LOGIN ---
|
|
273
|
+
discord({ clientId, clientSecret, redirectUri }) {
|
|
274
|
+
return {
|
|
275
|
+
redirect: (res) => {
|
|
276
|
+
const url =
|
|
277
|
+
"https://discord.com/api/oauth2/authorize?" +
|
|
278
|
+
new URLSearchParams({
|
|
279
|
+
client_id: clientId,
|
|
280
|
+
redirect_uri: redirectUri,
|
|
281
|
+
response_type: "code",
|
|
282
|
+
scope: "identify email",
|
|
283
|
+
});
|
|
284
|
+
res.redirect(url);
|
|
285
|
+
},
|
|
286
|
+
callback: async (code) => {
|
|
287
|
+
const tokenRes = await fetch("https://discord.com/api/oauth2/token", {
|
|
288
|
+
method: "POST",
|
|
289
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
290
|
+
body: new URLSearchParams({
|
|
291
|
+
client_id: clientId,
|
|
292
|
+
client_secret: clientSecret,
|
|
293
|
+
code,
|
|
294
|
+
grant_type: "authorization_code",
|
|
295
|
+
redirect_uri: redirectUri,
|
|
296
|
+
}),
|
|
297
|
+
});
|
|
298
|
+
const tokenData = await tokenRes.json();
|
|
299
|
+
if (tokenData.error) throw new Error("OAuth Error: " + tokenData.error);
|
|
300
|
+
const userRes = await fetch("https://discord.com/api/users/@me", {
|
|
301
|
+
headers: { Authorization: `Bearer ${tokenData.access_token}` },
|
|
302
|
+
});
|
|
303
|
+
const user = await userRes.json();
|
|
304
|
+
return { ...user, access_token: tokenData.access_token };
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// --- SLACK LOGIN ---
|
|
310
|
+
slack({ clientId, clientSecret, redirectUri }) {
|
|
311
|
+
return {
|
|
312
|
+
redirect: (res) => {
|
|
313
|
+
const url =
|
|
314
|
+
"https://slack.com/oauth/v2/authorize?" +
|
|
315
|
+
new URLSearchParams({
|
|
316
|
+
client_id: clientId,
|
|
317
|
+
redirect_uri: redirectUri,
|
|
318
|
+
scope: "identity.basic identity.email",
|
|
319
|
+
});
|
|
320
|
+
res.redirect(url);
|
|
321
|
+
},
|
|
322
|
+
callback: async (code) => {
|
|
323
|
+
const tokenRes = await fetch("https://slack.com/api/oauth.v2.access?" +
|
|
324
|
+
new URLSearchParams({
|
|
325
|
+
client_id: clientId,
|
|
326
|
+
client_secret: clientSecret,
|
|
327
|
+
code,
|
|
328
|
+
redirect_uri: redirectUri,
|
|
329
|
+
}));
|
|
330
|
+
const tokenData = await tokenRes.json();
|
|
331
|
+
if (!tokenData.ok) throw new Error("OAuth Error: " + tokenData.error);
|
|
332
|
+
return { ...tokenData, access_token: tokenData.access_token };
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// --- MICROSOFT LOGIN ---
|
|
338
|
+
microsoft({ clientId, clientSecret, redirectUri }) {
|
|
339
|
+
return {
|
|
340
|
+
redirect: (res) => {
|
|
341
|
+
const url =
|
|
342
|
+
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize?" +
|
|
343
|
+
new URLSearchParams({
|
|
344
|
+
client_id: clientId,
|
|
345
|
+
redirect_uri: redirectUri,
|
|
346
|
+
response_type: "code",
|
|
347
|
+
scope: "User.Read",
|
|
348
|
+
});
|
|
349
|
+
res.redirect(url);
|
|
350
|
+
},
|
|
351
|
+
callback: async (code) => {
|
|
352
|
+
const tokenRes = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
|
353
|
+
method: "POST",
|
|
354
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
355
|
+
body: new URLSearchParams({
|
|
356
|
+
client_id: clientId,
|
|
357
|
+
client_secret: clientSecret,
|
|
358
|
+
code,
|
|
359
|
+
redirect_uri: redirectUri,
|
|
360
|
+
grant_type: "authorization_code",
|
|
361
|
+
}),
|
|
362
|
+
});
|
|
363
|
+
const tokenData = await tokenRes.json();
|
|
364
|
+
if (tokenData.error) throw new Error("OAuth Error: " + tokenData.error);
|
|
365
|
+
return tokenData;
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// --- TELEGRAM LOGIN ---
|
|
371
|
+
telegram({ botId, redirectUri }) {
|
|
372
|
+
return {
|
|
373
|
+
redirect: (res) => {
|
|
374
|
+
const url =
|
|
375
|
+
"https://t.me/" + botId + "?start=auth&redirect_uri=" + encodeURIComponent(redirectUri);
|
|
376
|
+
res.redirect(url);
|
|
377
|
+
},
|
|
378
|
+
callback: async (code) => {
|
|
379
|
+
// Telegram uses bot login; typically handled via deep links
|
|
380
|
+
return { code, message: "Telegram login uses deep link auth" };
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// --- WHATSAPP LOGIN ---
|
|
386
|
+
whatsapp({ phoneNumberId, redirectUri }) {
|
|
387
|
+
return {
|
|
388
|
+
redirect: (res) => {
|
|
389
|
+
const url =
|
|
390
|
+
"https://api.whatsapp.com/send?" +
|
|
391
|
+
new URLSearchParams({
|
|
392
|
+
phone: phoneNumberId,
|
|
393
|
+
text: "Please authorize: " + redirectUri,
|
|
394
|
+
});
|
|
395
|
+
res.redirect(url);
|
|
396
|
+
},
|
|
397
|
+
callback: async (code) => {
|
|
398
|
+
// WhatsApp login usually handled via QR / deep link
|
|
399
|
+
return { code, message: "WhatsApp login uses QR/deep link auth" };
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// --- CUSTOM PROVIDER ---
|
|
405
|
+
register(name, fn) {
|
|
406
|
+
if (!name || typeof fn !== "function") throw new Error("Provider registration requires a name and function");
|
|
407
|
+
this.providers[name] = fn;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
use(name, options) {
|
|
411
|
+
const provider = this.providers[name];
|
|
412
|
+
if (!provider) throw new Error(`Provider "${name}" not found`);
|
|
413
|
+
return provider(options);
|
|
414
|
+
}
|
|
415
|
+
|
|
238
416
|
}
|
|
239
417
|
|
|
240
418
|
module.exports = OAuthManager;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// base32.js
|
|
2
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
3
|
+
|
|
4
|
+
function encode(buffer) {
|
|
5
|
+
let bits = 0;
|
|
6
|
+
let value = 0;
|
|
7
|
+
let output = "";
|
|
8
|
+
|
|
9
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
10
|
+
value = (value << 8) | buffer[i];
|
|
11
|
+
bits += 8;
|
|
12
|
+
|
|
13
|
+
while (bits >= 5) {
|
|
14
|
+
output += alphabet[(value >>> (bits - 5)) & 31];
|
|
15
|
+
bits -= 5;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (bits > 0) {
|
|
20
|
+
output += alphabet[(value << (5 - bits)) & 31];
|
|
21
|
+
}
|
|
22
|
+
return output;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function decode(str) {
|
|
26
|
+
let bits = 0;
|
|
27
|
+
let value = 0;
|
|
28
|
+
const output = [];
|
|
29
|
+
|
|
30
|
+
for (const char of str.toUpperCase()) {
|
|
31
|
+
if (char === "=") break;
|
|
32
|
+
const idx = alphabet.indexOf(char);
|
|
33
|
+
if (idx === -1) continue;
|
|
34
|
+
|
|
35
|
+
value = (value << 5) | idx;
|
|
36
|
+
bits += 5;
|
|
37
|
+
|
|
38
|
+
if (bits >= 8) {
|
|
39
|
+
output.push((value >>> (bits - 8)) & 0xff);
|
|
40
|
+
bits -= 8;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return Buffer.from(output);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { encode, decode };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// totp.js
|
|
2
|
+
const crypto = require("crypto");
|
|
3
|
+
const base32 = require("./base32");
|
|
4
|
+
|
|
5
|
+
class TOTPManager {
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
this.step = options.step || 30;
|
|
8
|
+
this.digits = options.digits || 6;
|
|
9
|
+
this.algorithm = (options.alg || "SHA1").toUpperCase();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// generate random Base32 secret
|
|
13
|
+
secret(length = 20) {
|
|
14
|
+
const buf = crypto.randomBytes(length);
|
|
15
|
+
return base32.encode(buf);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// generate TOTP code
|
|
19
|
+
generate(secret) {
|
|
20
|
+
const time = Math.floor(Date.now() / 1000 / this.step);
|
|
21
|
+
return this._hotp(secret, time);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// verify code from user
|
|
25
|
+
verify(secret, code, window = 1) {
|
|
26
|
+
// check ± 1 window to allow delay
|
|
27
|
+
const time = Math.floor(Date.now() / 1000 / this.step);
|
|
28
|
+
for (let i = -window; i <= window; i++) {
|
|
29
|
+
const otp = this._hotp(secret, time + i);
|
|
30
|
+
if (otp === code) return true;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// build key URI for authenticator apps
|
|
36
|
+
uri({ label, secret, issuer }) {
|
|
37
|
+
return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(label)}?secret=${secret}&issuer=${encodeURIComponent(issuer)}&algorithm=${this.algorithm}&digits=${this.digits}&period=${this.step}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// optional QR for CLI users (we can integrate QR lib later)
|
|
41
|
+
async qrcode(uri) {
|
|
42
|
+
const qrcode = require("qrcode");
|
|
43
|
+
return await qrcode.toDataURL(uri);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// internal HOTP algorithm
|
|
47
|
+
_hotp(secret, counter) {
|
|
48
|
+
const key = base32.decode(secret);
|
|
49
|
+
|
|
50
|
+
const buf = Buffer.alloc(8);
|
|
51
|
+
buf.writeUInt32BE(Math.floor(counter / Math.pow(2, 32)), 0);
|
|
52
|
+
buf.writeUInt32BE(counter & 0xffffffff, 4);
|
|
53
|
+
|
|
54
|
+
const hmac = crypto.createHmac(this.algorithm, key).update(buf).digest();
|
|
55
|
+
const offset = hmac[hmac.length - 1] & 0xf;
|
|
56
|
+
|
|
57
|
+
const code = ((hmac[offset] & 0x7f) << 24) |
|
|
58
|
+
((hmac[offset + 1] & 0xff) << 16) |
|
|
59
|
+
((hmac[offset + 2] & 0xff) << 8) |
|
|
60
|
+
(hmac[offset + 3] & 0xff);
|
|
61
|
+
|
|
62
|
+
return (code % 10 ** this.digits).toString().padStart(this.digits, "0");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = TOTPManager;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const request = require("supertest");
|
|
2
|
+
const express = require("express");
|
|
3
|
+
const AuthVerify = require("../index"); // path to your AuthVerify file
|
|
4
|
+
|
|
5
|
+
describe("AuthVerify protect() with custom options", () => {
|
|
6
|
+
let app;
|
|
7
|
+
let auth;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
auth = new AuthVerify({ jwtSecret: "test_secret" });
|
|
11
|
+
|
|
12
|
+
app = express();
|
|
13
|
+
|
|
14
|
+
// route that signs a token + sets cookie
|
|
15
|
+
app.get("/login", async (req, res) => {
|
|
16
|
+
const token = await auth.jwt.sign({ id: 123 }, "30m", { res });
|
|
17
|
+
res.json({ token });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// standard protected route
|
|
21
|
+
app.get("/profile", auth.jwt.protect(), (req, res) => {
|
|
22
|
+
res.json({ ok: true, user: req.user });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// route with custom cookie
|
|
26
|
+
app.get(
|
|
27
|
+
"/cookie",
|
|
28
|
+
auth.jwt.protect({ cookieName: "my_cookie" }),
|
|
29
|
+
(req, res) => {
|
|
30
|
+
res.json({ ok: true, user: req.user });
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// route with custom header
|
|
35
|
+
app.get(
|
|
36
|
+
"/header",
|
|
37
|
+
auth.jwt.protect({ headerName: "x-access-token" }),
|
|
38
|
+
(req, res) => {
|
|
39
|
+
res.json({ ok: true, user: req.user });
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// route with custom extractor
|
|
44
|
+
app.get(
|
|
45
|
+
"/query",
|
|
46
|
+
auth.jwt.protect({
|
|
47
|
+
extractor: (req) => req.query.token
|
|
48
|
+
}),
|
|
49
|
+
(req, res) => {
|
|
50
|
+
res.json({ ok: true, user: req.user });
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("custom cookie name works", async () => {
|
|
56
|
+
const token = await auth.jwt.sign({ id: 555 });
|
|
57
|
+
const res = await request(app)
|
|
58
|
+
.get("/cookie")
|
|
59
|
+
.set("Cookie", `my_cookie=${token}`);
|
|
60
|
+
expect(res.status).toBe(200);
|
|
61
|
+
expect(res.body.user.id).toBe(555);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("custom header name works", async () => {
|
|
65
|
+
const token = await auth.jwt.sign({ id: 777 });
|
|
66
|
+
const res = await request(app)
|
|
67
|
+
.get("/header")
|
|
68
|
+
.set("x-access-token", token);
|
|
69
|
+
expect(res.status).toBe(200);
|
|
70
|
+
expect(res.body.user.id).toBe(777);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("custom extractor works (query param)", async () => {
|
|
74
|
+
const token = await auth.jwt.sign({ id: 999 });
|
|
75
|
+
const res = await request(app)
|
|
76
|
+
.get(`/query?token=${token}`);
|
|
77
|
+
expect(res.status).toBe(200);
|
|
78
|
+
expect(res.body.user.id).toBe(999);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("default fallback still works", async () => {
|
|
82
|
+
const token = await auth.jwt.sign({ id: 123 });
|
|
83
|
+
const res = await request(app)
|
|
84
|
+
.get("/profile")
|
|
85
|
+
.set("Authorization", `Bearer ${token}`);
|
|
86
|
+
expect(res.status).toBe(200);
|
|
87
|
+
expect(res.body.user.id).toBe(123);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// __tests__/authVerify.oauth.direct.test.js
|
|
2
|
+
const AuthVerify = require("../index"); // your wrapper
|
|
3
|
+
|
|
4
|
+
describe("AuthVerify OAuth - Direct Provider Tests", () => {
|
|
5
|
+
let auth;
|
|
6
|
+
let res;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
auth = new AuthVerify();
|
|
10
|
+
res = { redirect: jest.fn() }; // mock Express response
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
global.fetch = jest.fn(); // mock fetch
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
jest.clearAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("Apple redirect and callback", async () => {
|
|
22
|
+
const apple = auth.oauth.apple({
|
|
23
|
+
clientId: "apple_id",
|
|
24
|
+
clientSecret: "apple_secret",
|
|
25
|
+
redirectUri: "http://localhost/callback",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
apple.redirect(res);
|
|
29
|
+
expect(res.redirect).toHaveBeenCalled();
|
|
30
|
+
expect(res.redirect.mock.calls[0][0]).toContain("https://appleid.apple.com/auth/authorize");
|
|
31
|
+
|
|
32
|
+
global.fetch.mockResolvedValueOnce({ json: async () => ({ access_token: "apple_token" }) });
|
|
33
|
+
const tokenData = await apple.callback("code123");
|
|
34
|
+
expect(tokenData.access_token).toBe("apple_token");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("Discord redirect and callback", async () => {
|
|
38
|
+
const discord = auth.oauth.discord({
|
|
39
|
+
clientId: "discord_id",
|
|
40
|
+
clientSecret: "discord_secret",
|
|
41
|
+
redirectUri: "http://localhost/callback",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
discord.redirect(res);
|
|
45
|
+
expect(res.redirect).toHaveBeenCalled();
|
|
46
|
+
|
|
47
|
+
global.fetch
|
|
48
|
+
.mockResolvedValueOnce({ json: async () => ({ access_token: "discord_token" }) })
|
|
49
|
+
.mockResolvedValueOnce({ json: async () => ({ id: "discord_123", username: "user" }) });
|
|
50
|
+
|
|
51
|
+
const userData = await discord.callback("code123");
|
|
52
|
+
expect(userData.access_token).toBe("discord_token");
|
|
53
|
+
expect(userData.id).toBe("discord_123");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("Telegram callback returns message", async () => {
|
|
57
|
+
const telegram = auth.oauth.telegram({ botId: "bot123", redirectUri: "http://localhost/callback" });
|
|
58
|
+
const tResult = await telegram.callback("code123");
|
|
59
|
+
expect(tResult.message).toContain("Telegram login");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("WhatsApp callback returns message", async () => {
|
|
63
|
+
const whatsapp = auth.oauth.whatsapp({ phoneNumberId: "12345", redirectUri: "http://localhost/callback" });
|
|
64
|
+
const wResult = await whatsapp.callback("code456");
|
|
65
|
+
expect(wResult.message).toContain("WhatsApp login");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("Slack and Microsoft redirect URLs", () => {
|
|
69
|
+
const slack = auth.oauth.slack({ clientId: "slack_id", redirectUri: "http://localhost/callback" });
|
|
70
|
+
slack.redirect(res);
|
|
71
|
+
expect(res.redirect).toHaveBeenCalled();
|
|
72
|
+
|
|
73
|
+
const ms = auth.oauth.microsoft({ clientId: "ms_id", redirectUri: "http://localhost/callback" });
|
|
74
|
+
ms.redirect(res);
|
|
75
|
+
expect(res.redirect).toHaveBeenCalledTimes(2);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const AuthVerify = require('../index');
|
|
2
|
+
|
|
3
|
+
describe("TOTP Manager", () => {
|
|
4
|
+
let auth;
|
|
5
|
+
let secret;
|
|
6
|
+
|
|
7
|
+
beforeAll(() => {
|
|
8
|
+
auth = new AuthVerify();
|
|
9
|
+
secret = auth.totp.secret();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("should generate base32 secret", () => {
|
|
13
|
+
expect(typeof secret).toBe("string");
|
|
14
|
+
expect(secret.length).toBeGreaterThan(10);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("should generate 6 digit code", () => {
|
|
18
|
+
const code = auth.totp.generate(secret);
|
|
19
|
+
expect(code).toMatch(/^[0-9]{6}$/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("should verify valid totp", () => {
|
|
23
|
+
const code = auth.totp.generate(secret);
|
|
24
|
+
expect(auth.totp.verify(secret, code)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("should reject invalid totp", () => {
|
|
28
|
+
const fake = "000000";
|
|
29
|
+
expect(auth.totp.verify(secret, fake)).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("should generate valid otpauth:// URL", () => {
|
|
33
|
+
const uri = auth.totp.uri({
|
|
34
|
+
label: "test@example.com",
|
|
35
|
+
issuer: "AuthVerify",
|
|
36
|
+
secret,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(uri.startsWith("otpauth://totp/")).toBe(true);
|
|
40
|
+
expect(uri.includes("secret=")).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("should generate QR DataURL", async () => {
|
|
44
|
+
const uri = auth.totp.uri({
|
|
45
|
+
label: "qrtest@example.com",
|
|
46
|
+
issuer: "AuthVerify",
|
|
47
|
+
secret,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const url = await auth.totp.qrcode(uri);
|
|
51
|
+
|
|
52
|
+
expect(url.startsWith("data:image/png;base64,")).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
});
|