auth-verify 1.5.0 → 1.7.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/authverify.client.js +71 -0
- package/index.js +20 -1
- package/package.json +4 -3
- package/readme.md +397 -6
- package/src/otp/index.js +83 -15
- package/src/passkey/index.js +195 -0
- package/src/totp/index.js +5 -0
- package/tests/passkeymanager.test.js +114 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
window.AuthVerify = class AuthVerify {
|
|
2
|
+
constructor(options = {}){
|
|
3
|
+
this.apiBase = options.apiBase || 'http://localhost:3000';
|
|
4
|
+
this.qrContainer = options.qrEl || null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// Fetch QR code from backend and display
|
|
8
|
+
post(url){
|
|
9
|
+
this.fetchPostUrl = url;
|
|
10
|
+
return this;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get(url){
|
|
14
|
+
this.fetchGetUrl = url;
|
|
15
|
+
return this;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async qr() {
|
|
19
|
+
if (!this.qrContainer) return;
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch(`${this.apiBase}${this.fetchGetUrl}`);
|
|
22
|
+
const data = await res.json();
|
|
23
|
+
if (data.qr) {
|
|
24
|
+
this.qrContainer.src = data.qr;
|
|
25
|
+
} else {
|
|
26
|
+
this.showResponse('No QR received');
|
|
27
|
+
}
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error(err);
|
|
30
|
+
this.showResponse('Error fetching QR');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
showResponse(msg){
|
|
35
|
+
console.log("[AuthVerify]", msg);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async data(payload){
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(`${this.apiBase}${this.fetchPostUrl}`, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
body: JSON.stringify(payload)
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const data = await res.json();
|
|
47
|
+
|
|
48
|
+
// if backend returned jwt we store it but still return whole data
|
|
49
|
+
if (data.token) {
|
|
50
|
+
this.jwt = data.token;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return data;
|
|
54
|
+
|
|
55
|
+
} catch(err){
|
|
56
|
+
console.error(err);
|
|
57
|
+
return { error: true, message: err.message };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
header(){
|
|
62
|
+
if(!this.jwt) return {};
|
|
63
|
+
return {
|
|
64
|
+
Authorization: `Bearer ${this.jwt}`
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async verify(code){
|
|
69
|
+
return this.data({code});
|
|
70
|
+
}
|
|
71
|
+
}
|
package/index.js
CHANGED
|
@@ -3,6 +3,7 @@ const OTPManager = require("./src/otp");
|
|
|
3
3
|
const SessionManager = require("./src/session");
|
|
4
4
|
const OAuthManager = require("./src/oauth");
|
|
5
5
|
const TOTPManager = require("./src/totp");
|
|
6
|
+
const PasskeyManager = require("./src/passkey");
|
|
6
7
|
|
|
7
8
|
class AuthVerify {
|
|
8
9
|
constructor(options = {}) {
|
|
@@ -18,7 +19,10 @@ class AuthVerify {
|
|
|
18
19
|
digits: 6,
|
|
19
20
|
step: 30,
|
|
20
21
|
alg: "SHA1"
|
|
21
|
-
}
|
|
22
|
+
},
|
|
23
|
+
rpName = "auth-verify",
|
|
24
|
+
saveBy = "id",
|
|
25
|
+
passExp = "2m"
|
|
22
26
|
} = options;
|
|
23
27
|
|
|
24
28
|
// ✅ Ensure cookieName and secret always exist
|
|
@@ -44,6 +48,8 @@ class AuthVerify {
|
|
|
44
48
|
this.totp = new TOTPManager(totp);
|
|
45
49
|
|
|
46
50
|
this.senders = new Map();
|
|
51
|
+
|
|
52
|
+
this.passkey = new PasskeyManager({rpName, storeTokens, saveBy, passExp});
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
// --- Session helpers ---
|
|
@@ -59,6 +65,19 @@ class AuthVerify {
|
|
|
59
65
|
return this.session.destroy(sessionId);
|
|
60
66
|
}
|
|
61
67
|
|
|
68
|
+
// --- Passkey helpers ---
|
|
69
|
+
async registerPasskey(user) {
|
|
70
|
+
return this.passkey.register(user);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async finishPasskey(clientResponse) {
|
|
74
|
+
return this.passkey.finish(clientResponse);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async loginPasskey(user) {
|
|
78
|
+
return this.passkey.login(user);
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
// --- Sender registration ---
|
|
63
82
|
register = {
|
|
64
83
|
sender: (name, fn) => {
|
package/package.json
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"dependencies": {
|
|
3
3
|
"axios": "^1.12.2",
|
|
4
|
+
"base64url": "^3.0.1",
|
|
5
|
+
"cbor": "^10.0.11",
|
|
4
6
|
"crypto": "^1.0.1",
|
|
5
7
|
"ioredis": "^5.8.1",
|
|
6
8
|
"jsonwebtoken": "^9.0.2",
|
|
7
9
|
"node-telegram-bot-api": "^0.66.0",
|
|
8
10
|
"nodemailer": "^7.0.6",
|
|
9
11
|
"qrcode": "^1.5.4",
|
|
10
|
-
"redis": "^5.8.3",
|
|
11
12
|
"uuid": "^9.0.1"
|
|
12
13
|
},
|
|
13
14
|
"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 generating TOTP codes and QR codes. And handling JWT with Cookies",
|
|
15
|
+
"version": "1.7.0",
|
|
16
|
+
"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. And also handling passwordless logins with passkeys/webauthn",
|
|
16
17
|
"main": "index.js",
|
|
17
18
|
"scripts": {
|
|
18
19
|
"test": "jest --runInBand"
|
package/readme.md
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
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
8
|
- ✅ Session management (in-memory or Redis).
|
|
9
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(...)
|
|
10
|
+
- ⚙️ Developer extensibility: custom senders via `auth.register.sender()` and chainable sending via `auth.use(name).send(...)`.
|
|
11
|
+
- ✅ Frontend client SDK (`authverify.client.js`) for browser usage: QR display, OTP verification, JWT requests, and auth headers; works without modules, just `<script>`.
|
|
11
12
|
- ✅ Automatic JWT cookie handling for Express apps, supporting secure, HTTP-only cookies and optional auto-verification.
|
|
13
|
+
- ✅ Passwordless login and registration with passkeys and webauthn.
|
|
12
14
|
- ✅ Fully asynchronous/Promise-based API, with callback support where applicable.
|
|
13
15
|
- ✅ Chainable OTP workflow with cooldowns, max attempts, and resend functionality.
|
|
14
16
|
---
|
|
@@ -30,9 +32,10 @@ npm install auth-verify
|
|
|
30
32
|
- `AuthVerify` (entry): constructs and exposes `.jwt`, `.otp`, (optionally) `.session`, `.totp` and `.oauth` managers.
|
|
31
33
|
- `JWTManager`: sign, verify, decode, revoke tokens. Supports `storeTokens: "memory" | "redis" | "none"` and middleware with custom cookie, header, and token extraction.
|
|
32
34
|
- `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
|
|
35
|
+
- `TOTPManager`: generate, verify uri, codes and QR codes.
|
|
34
36
|
- `SessionManager`: simple session creation/verification/destroy with memory or Redis backend.
|
|
35
|
-
- `OAuthManager`: Handle OAuth 2.0 logins for Google, Facebook, GitHub, X, Linkedin, Apple, Discord, Slack, Microsoft, Telegram and WhatsApp
|
|
37
|
+
- `OAuthManager`: Handle OAuth 2.0 logins for Google, Facebook, GitHub, X, Linkedin, Apple, Discord, Slack, Microsoft, Telegram and WhatsApp.
|
|
38
|
+
- `PasskeyManager`: Handle passwordless login and registration using WebAuthn/passkey.
|
|
36
39
|
---
|
|
37
40
|
|
|
38
41
|
## 🚀 Example: Initialize library (CommonJS)
|
|
@@ -53,7 +56,7 @@ const auth = new AuthVerify({
|
|
|
53
56
|
|
|
54
57
|
## 🔐 JWT Usage
|
|
55
58
|
|
|
56
|
-
### JWT Middleware (`protect`) (
|
|
59
|
+
### JWT Middleware (`protect`) (v1.5.0+)
|
|
57
60
|
|
|
58
61
|
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
62
|
|
|
@@ -246,6 +249,15 @@ auth.otp.setSender({
|
|
|
246
249
|
// if smtp service: host, port, secure (boolean)
|
|
247
250
|
});
|
|
248
251
|
|
|
252
|
+
// or you can use sender() method
|
|
253
|
+
// auth.otp.sender({
|
|
254
|
+
// via: 'email',
|
|
255
|
+
// sender: 'your@address.com',
|
|
256
|
+
// pass: 'app-password-or-smtp-pass',
|
|
257
|
+
// service: 'gmail' // or 'smtp'
|
|
258
|
+
// // if smtp service: host, port, secure (boolean)
|
|
259
|
+
// });
|
|
260
|
+
|
|
249
261
|
// sms example (the internal helper expects provider/apiKey or mock flag)
|
|
250
262
|
auth.otp.setSender({
|
|
251
263
|
via: 'sms',
|
|
@@ -282,6 +294,21 @@ auth.otp.setSender({
|
|
|
282
294
|
});
|
|
283
295
|
```
|
|
284
296
|
|
|
297
|
+
### 🛫 Simple and easy sending OTP codes
|
|
298
|
+
|
|
299
|
+
OTP codes can be simply and easily sent by `send()` method.
|
|
300
|
+
|
|
301
|
+
```js
|
|
302
|
+
auth.otp.send('johndoe@mail.com', {otpLen: 5, subject: "Email verification", html: `Your OTP code is ${auth.otp.code}`}, (err)=>{
|
|
303
|
+
if(err) console.log(err)
|
|
304
|
+
console.log('OTP sent!');
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
or you can simple use it like this:
|
|
308
|
+
```js
|
|
309
|
+
auth.otp.send('johndoe@mail.com');
|
|
310
|
+
```
|
|
311
|
+
|
|
285
312
|
### ⛓️ Generate → Save → Send (chainable)
|
|
286
313
|
|
|
287
314
|
OTP generation is chainable: `generate()` returns the OTP manager instance.
|
|
@@ -343,6 +370,13 @@ auth.otp.verify({ check: 'user@example.com', code: '123456' }, (err, isValid)=>{
|
|
|
343
370
|
if(isValid) console.log('Correct code!');
|
|
344
371
|
else console.log('Incorrect code!');
|
|
345
372
|
});
|
|
373
|
+
|
|
374
|
+
// or you can use it like this:
|
|
375
|
+
// auth.otp.verify('user@example.com','123456', (err, isValid)=>{
|
|
376
|
+
// if(err) console.log(err);
|
|
377
|
+
// if(isValid) console.log('Correct code!');
|
|
378
|
+
// else console.log('Incorrect code!');
|
|
379
|
+
// });
|
|
346
380
|
```
|
|
347
381
|
|
|
348
382
|
### Resend and cooldown / max attempts
|
|
@@ -355,6 +389,152 @@ auth.otp.verify({ check: 'user@example.com', code: '123456' }, (err, isValid)=>{
|
|
|
355
389
|
|
|
356
390
|
---
|
|
357
391
|
|
|
392
|
+
## 🗝️ Passkey (WebAuthn) (New in v1.6.1)
|
|
393
|
+
|
|
394
|
+
`AuthVerify` includes a `PasskeyManager` class to handle passwordless login using WebAuthn / passkeys. You can **register** users, **verify login**, and manage **challenges** safely.
|
|
395
|
+
|
|
396
|
+
### Setup
|
|
397
|
+
```js
|
|
398
|
+
const AuthVerify = require("auth-verify");
|
|
399
|
+
|
|
400
|
+
const auth = new AuthVerify({
|
|
401
|
+
passExp: "2m", // passkey challenge TTL
|
|
402
|
+
rpName: "MyApp",
|
|
403
|
+
storeTokens: "memory" // or "redis"
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const user = {
|
|
407
|
+
id: "user1",
|
|
408
|
+
username: "john_doe",
|
|
409
|
+
credentials: [] // will store registered credentials
|
|
410
|
+
};
|
|
411
|
+
```
|
|
412
|
+
### 1️⃣ Register a new Passkey
|
|
413
|
+
The registration process consists of **two steps**:
|
|
414
|
+
**1.** Generate a registration challenge
|
|
415
|
+
**2.** Complete attestation after client responds
|
|
416
|
+
|
|
417
|
+
#### Step 1: Generate challenge
|
|
418
|
+
```js
|
|
419
|
+
// register user
|
|
420
|
+
await auth.passkey.register(user);
|
|
421
|
+
|
|
422
|
+
// get WebAuthn options for the client
|
|
423
|
+
const options = auth.passkey.getOptions();
|
|
424
|
+
console.log(options);
|
|
425
|
+
|
|
426
|
+
/* Example output:
|
|
427
|
+
{
|
|
428
|
+
challenge: "base64url-challenge",
|
|
429
|
+
rp: { name: "MyApp" },
|
|
430
|
+
user: { id: "dXNlcjE", name: "john_doe", displayName: "john_doe" },
|
|
431
|
+
pubKeyCredParams: [{ alg: -7, type: "public-key" }]
|
|
432
|
+
}
|
|
433
|
+
*/
|
|
434
|
+
```
|
|
435
|
+
> Send options to the browser to call:
|
|
436
|
+
> ```js
|
|
437
|
+
> navigator.credentials.create({ publicKey: options })
|
|
438
|
+
> ```
|
|
439
|
+
|
|
440
|
+
#### Step 2: Finish attestation
|
|
441
|
+
Once the client returns the attestation response:
|
|
442
|
+
```js
|
|
443
|
+
const clientResponse = {
|
|
444
|
+
id: "...", // credentialId from browser
|
|
445
|
+
response: {
|
|
446
|
+
clientDataJSON: "...",
|
|
447
|
+
attestationObject: "..."
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const result = await auth.passkey.finish(clientResponse);
|
|
452
|
+
console.log(result);
|
|
453
|
+
/* Example result:
|
|
454
|
+
{
|
|
455
|
+
status: "ok",
|
|
456
|
+
user: {
|
|
457
|
+
id: "user1",
|
|
458
|
+
username: "john_doe",
|
|
459
|
+
credentials: [
|
|
460
|
+
{ id: "credentialId", publicKey: "pem-key" }
|
|
461
|
+
]
|
|
462
|
+
},
|
|
463
|
+
challengeVerified: true,
|
|
464
|
+
rawAuthData: <Buffer ...>
|
|
465
|
+
}
|
|
466
|
+
*/
|
|
467
|
+
```
|
|
468
|
+
> After this, the user now has a **registered passkey**.
|
|
469
|
+
|
|
470
|
+
### 2️⃣ Login with Passkey
|
|
471
|
+
Login also consists of **two steps**:
|
|
472
|
+
**1.** Generate assertion challenge
|
|
473
|
+
**2.** Complete verification
|
|
474
|
+
#### Step 1: Generate login challenge
|
|
475
|
+
```js
|
|
476
|
+
await auth.passkey.login(user);
|
|
477
|
+
|
|
478
|
+
const options = auth.passkey.getOptions();
|
|
479
|
+
console.log(options);
|
|
480
|
+
|
|
481
|
+
/* Example output:
|
|
482
|
+
{
|
|
483
|
+
challenge: "base64url-challenge",
|
|
484
|
+
allowCredentials: [
|
|
485
|
+
{ id: <Buffer...>, type: "public-key" }
|
|
486
|
+
],
|
|
487
|
+
timeout: 60000
|
|
488
|
+
}
|
|
489
|
+
*/
|
|
490
|
+
```
|
|
491
|
+
> Send this `options` to the browser for `navigator.credentials.get({ publicKey: options })`.
|
|
492
|
+
|
|
493
|
+
#### Step 2: Finish login assertion
|
|
494
|
+
```js
|
|
495
|
+
const clientLoginResponse = {
|
|
496
|
+
id: "credentialId",
|
|
497
|
+
response: {
|
|
498
|
+
clientDataJSON: "...",
|
|
499
|
+
authenticatorData: "...",
|
|
500
|
+
signature: "..."
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const loginResult = await auth.passkey.finish(clientLoginResponse);
|
|
505
|
+
console.log(loginResult);
|
|
506
|
+
/* Example output:
|
|
507
|
+
{
|
|
508
|
+
status: "ok",
|
|
509
|
+
user: {
|
|
510
|
+
id: "user1",
|
|
511
|
+
username: "john_doe",
|
|
512
|
+
credentials: [...]
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
*/
|
|
516
|
+
```
|
|
517
|
+
> If `status === "ok"`, the login is successful.
|
|
518
|
+
|
|
519
|
+
### 3️⃣ Notes
|
|
520
|
+
|
|
521
|
+
- `auth.passkey.register()` and `auth.passkey.login()` return this so you can chain:
|
|
522
|
+
```js
|
|
523
|
+
await auth.passkey
|
|
524
|
+
.register(user)
|
|
525
|
+
.getOptions(); // get WebAuthn options
|
|
526
|
+
```
|
|
527
|
+
- `finish()` **must be called after `register()` or `login()`** with the client’s response.
|
|
528
|
+
- TTL (`passExp`) ensures challenges **expire automatically** (memory or Redis store).
|
|
529
|
+
|
|
530
|
+
### 4️⃣ Summary of Methods
|
|
531
|
+
| Method | Purpose | Returns |
|
|
532
|
+
| ------------------------ | ------------------------------- | ------------------ |
|
|
533
|
+
| `register(user)` | Start passkey registration | `this` (chainable) |
|
|
534
|
+
| `login(user)` | Start passkey login | `this` (chainable) |
|
|
535
|
+
| `getOptions()` | Get WebAuthn options for client | Object |
|
|
536
|
+
| `finish(clientResponse)` | Complete attestation/assertion | Result object |
|
|
537
|
+
|
|
358
538
|
## ✅ TOTP (Time-based One Time Passwords) — Google Authenticator support (v1.4.0+)
|
|
359
539
|
```js
|
|
360
540
|
const AuthVerify = require("auth-verify");
|
|
@@ -393,7 +573,7 @@ console.log(uri);
|
|
|
393
573
|
### generate QR code image
|
|
394
574
|
(send this PNG to frontend or show in UI)
|
|
395
575
|
```js
|
|
396
|
-
const qr = await auth.totp.qrcode(uri);
|
|
576
|
+
const qr = await auth.totp.qrcode(uri); // or you can use await auth.totp.qr(uri);
|
|
397
577
|
console.log(qr); // data:image/png;base64,...
|
|
398
578
|
```
|
|
399
579
|
### generate a TOTP code
|
|
@@ -424,8 +604,129 @@ if (auth.totp.verify({ secret, token })) {
|
|
|
424
604
|
}
|
|
425
605
|
```
|
|
426
606
|
---
|
|
607
|
+
|
|
608
|
+
## auth-verify client
|
|
609
|
+
### 1️⃣ Introduction
|
|
610
|
+
|
|
611
|
+
**AuthVerify Client** is a lightweight frontend JavaScript library for TOTP / JWT authentication.
|
|
612
|
+
It works with your backend APIs to:
|
|
613
|
+
- Display QR codes for TOTP enrollment
|
|
614
|
+
- Verify user OTP codes
|
|
615
|
+
- Request JWT tokens from backend
|
|
616
|
+
- Send authenticated requests easily
|
|
617
|
+
|
|
618
|
+
Works like jQuery: just include the script in HTML, no module or bundler needed.
|
|
619
|
+
|
|
620
|
+
## 2️⃣ Installation
|
|
621
|
+
```html
|
|
622
|
+
<script src="https://cdn.jsdelivr.net/gh/jahongir2007/auth-verify/authverify.client.js"></script>
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
### 3️⃣ Initialization
|
|
626
|
+
```js
|
|
627
|
+
const qrImage = document.getElementById('qrImage');
|
|
628
|
+
|
|
629
|
+
const auth = new AuthVerify({
|
|
630
|
+
apiBase: 'http://localhost:3000', // Your backend API base URL
|
|
631
|
+
qrEl: qrImage // Image element to display QR
|
|
632
|
+
});
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
### 4️⃣ Generating QR Code
|
|
636
|
+
```js
|
|
637
|
+
auth.get('/api/qr').qr();
|
|
638
|
+
```
|
|
639
|
+
- Fetches QR code from backend
|
|
640
|
+
- Displays it in the `qrEl` image element
|
|
641
|
+
|
|
642
|
+
### 5️⃣ Sending Data / JWT Requests
|
|
643
|
+
```js
|
|
644
|
+
const payload = { name: 'John', age: 23 };
|
|
645
|
+
|
|
646
|
+
const token = await auth.post('/api/sign-jwt').data(payload);
|
|
647
|
+
console.log('JWT token:', token);
|
|
648
|
+
```
|
|
649
|
+
- `post(url)` sets endpoint
|
|
650
|
+
- `data(payload)` sends JSON payload
|
|
651
|
+
- If backend returns a token, it is stored in `auth.jwt`
|
|
652
|
+
|
|
653
|
+
### 6️⃣ Verifying OTP
|
|
654
|
+
```js
|
|
655
|
+
const result = await auth.post('/api/verify-totp').verify('123456');
|
|
656
|
+
console.log(result); // e.g. { verified: true }
|
|
657
|
+
```
|
|
658
|
+
- Wraps the OTP code in `{ code: '...' }`
|
|
659
|
+
- Sends to backend for verification
|
|
660
|
+
|
|
661
|
+
### 7️⃣ Sending Authenticated Requests
|
|
662
|
+
```js
|
|
663
|
+
const profile = await fetch('http://localhost:3000/api/profile', {
|
|
664
|
+
headers: auth.header()
|
|
665
|
+
}).then(res => res.json());
|
|
666
|
+
|
|
667
|
+
console.log(profile);
|
|
668
|
+
```
|
|
669
|
+
- `auth.header()` returns `{ Authorization: "Bearer <jwt>" }`
|
|
670
|
+
- Easy to attach JWT to any request
|
|
671
|
+
|
|
672
|
+
### 8️⃣ Method Summary
|
|
673
|
+
| Method | Description |
|
|
674
|
+
| --------------- | ----------------------------------------------- |
|
|
675
|
+
| `get(url)` | Set GET endpoint |
|
|
676
|
+
| `post(url)` | Set POST endpoint |
|
|
677
|
+
| `qr()` | Fetch QR from backend and display |
|
|
678
|
+
| `data(payload)` | Send payload to backend; stores JWT if returned |
|
|
679
|
+
| `verify(code)` | Send OTP code to backend |
|
|
680
|
+
| `header()` | Return JWT auth header object |
|
|
681
|
+
|
|
682
|
+
### 9️⃣ Example HTML
|
|
683
|
+
```html
|
|
684
|
+
<img id="qrImage" />
|
|
685
|
+
<div id="response"></div>
|
|
686
|
+
<button id="getQRBtn">Get QR</button>
|
|
687
|
+
<button id="sendBtn">Send Data</button>
|
|
688
|
+
|
|
689
|
+
<script src="authverify.client.js"></script>
|
|
690
|
+
<script>
|
|
691
|
+
const qrImage = document.getElementById('qrImage');
|
|
692
|
+
const responseDiv = document.getElementById('response');
|
|
693
|
+
|
|
694
|
+
const auth = new AuthVerify({ apiBase: 'http://localhost:3000', qrEl: qrImage });
|
|
695
|
+
|
|
696
|
+
document.getElementById('getQRBtn').addEventListener('click', () => auth.get('/api/qr').qr());
|
|
697
|
+
|
|
698
|
+
document.getElementById('sendBtn').addEventListener('click', async () => {
|
|
699
|
+
const payload = { name: 'Jahongir' };
|
|
700
|
+
const result = await auth.post('/api/sign-jwt').data(payload);
|
|
701
|
+
responseDiv.textContent = JSON.stringify(result, null, 2);
|
|
702
|
+
});
|
|
703
|
+
</script>
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
### 10️⃣ Tips for Developers
|
|
707
|
+
- Always call `auth.get('/api/qr').qr()` **after page loads**
|
|
708
|
+
- Use `auth.header()` for any authenticated request
|
|
709
|
+
- Backend must provide endpoints for `/api/qr`, `/api/verify-totp`, `/api/sign-jwt`
|
|
710
|
+
|
|
711
|
+
---
|
|
712
|
+
|
|
427
713
|
## 🌍 OAuth 2.0 Integration (v1.2.0+)
|
|
428
|
-
`auth.oauth` supports login via Google, Facebook, GitHub, X (Twitter) and
|
|
714
|
+
`auth.oauth` supports login via Google, Facebook, GitHub, X (Twitter), Linkedin, Microsoft, Telegram, Slack, WhatsApp, Apple and Discord.
|
|
715
|
+
### Providers & Routes table
|
|
716
|
+
| Provider | Redirect URL | Callback URL | Scopes / Notes |
|
|
717
|
+
| ----------- | ----------------- | -------------------------- | -------------------------------------- |
|
|
718
|
+
| Google | `/auth/google` | `/auth/google/callback` | `openid email profile` |
|
|
719
|
+
| Facebook | `/auth/facebook` | `/auth/facebook/callback` | `email,public_profile` |
|
|
720
|
+
| GitHub | `/auth/github` | `/auth/github/callback` | `user:email` |
|
|
721
|
+
| X (Twitter) | `/auth/x` | `/auth/x/callback` | `tweet.read users.read offline.access` |
|
|
722
|
+
| LinkedIn | `/auth/linkedin` | `/auth/linkedin/callback` | `r_liteprofile r_emailaddress` |
|
|
723
|
+
| Microsoft | `/auth/microsoft` | `/auth/microsoft/callback` | `User.Read` |
|
|
724
|
+
| Telegram | `/auth/telegram` | `/auth/telegram/callback` | Bot deep-link |
|
|
725
|
+
| Slack | `/auth/slack` | `/auth/slack/callback` | `identity.basic identity.email` |
|
|
726
|
+
| WhatsApp | `/auth/whatsapp` | `/auth/whatsapp/callback` | QR / deep-link |
|
|
727
|
+
| Apple | `/auth/apple` | `/auth/apple/callback` | `name email` |
|
|
728
|
+
| Discord | `/auth/discord` | `/auth/discord/callback` | `identify email` |
|
|
729
|
+
|
|
429
730
|
### Example (Google Login with Express)
|
|
430
731
|
```js
|
|
431
732
|
const express = require('express');
|
|
@@ -506,6 +807,79 @@ const linkedin = auth.oauth.linkedin({
|
|
|
506
807
|
redirectUri: "http://localhost:3000/auth/linkedin/callback"
|
|
507
808
|
});
|
|
508
809
|
|
|
810
|
+
// --- MICROSOFT ---
|
|
811
|
+
const microsoft = auth.oauth.microsoft({
|
|
812
|
+
clientId: "YOUR_MICROSOFT_CLIENT_ID",
|
|
813
|
+
clientSecret: "YOUR_MICROSOFT_CLIENT_SECRET",
|
|
814
|
+
redirectUri: "http://localhost:3000/auth/microsoft/callback"
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
app.get("/auth/microsoft", (req, res) => microsoft.redirect(res));
|
|
818
|
+
|
|
819
|
+
app.get("/auth/microsoft/callback", async (req, res) => {
|
|
820
|
+
try {
|
|
821
|
+
const { code } = req.query;
|
|
822
|
+
const user = await microsoft.callback(code);
|
|
823
|
+
res.json({ success: true, provider: "microsoft", user });
|
|
824
|
+
} catch (err) {
|
|
825
|
+
res.status(400).json({ error: err.message });
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// --- TELEGRAM ---
|
|
830
|
+
const telegram = auth.oauth.telegram({
|
|
831
|
+
botId: "YOUR_BOT_ID",
|
|
832
|
+
redirectUri: "http://localhost:3000/auth/telegram/callback"
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
app.get("/auth/telegram", (req, res) => telegram.redirect(res));
|
|
836
|
+
|
|
837
|
+
app.get("/auth/telegram/callback", async (req, res) => {
|
|
838
|
+
try {
|
|
839
|
+
const { code } = req.query;
|
|
840
|
+
const result = await telegram.callback(code);
|
|
841
|
+
res.json({ success: true, provider: "telegram", ...result });
|
|
842
|
+
} catch (err) {
|
|
843
|
+
res.status(400).json({ error: err.message });
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
// --- SLACK ---
|
|
848
|
+
const slack = auth.oauth.slack({
|
|
849
|
+
clientId: "YOUR_SLACK_CLIENT_ID",
|
|
850
|
+
clientSecret: "YOUR_SLACK_CLIENT_SECRET",
|
|
851
|
+
redirectUri: "http://localhost:3000/auth/slack/callback"
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
app.get("/auth/slack", (req, res) => slack.redirect(res));
|
|
855
|
+
|
|
856
|
+
app.get("/auth/slack/callback", async (req, res) => {
|
|
857
|
+
try {
|
|
858
|
+
const { code } = req.query;
|
|
859
|
+
const user = await slack.callback(code);
|
|
860
|
+
res.json({ success: true, provider: "slack", user });
|
|
861
|
+
} catch (err) {
|
|
862
|
+
res.status(400).json({ error: err.message });
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
// --- WHATSAPP ---
|
|
867
|
+
const whatsapp = auth.oauth.whatsapp({
|
|
868
|
+
phoneNumberId: "YOUR_PHONE_ID",
|
|
869
|
+
redirectUri: "http://localhost:3000/auth/whatsapp/callback"
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
app.get("/auth/whatsapp", (req, res) => whatsapp.redirect(res));
|
|
873
|
+
|
|
874
|
+
app.get("/auth/whatsapp/callback", async (req, res) => {
|
|
875
|
+
try {
|
|
876
|
+
const { code } = req.query;
|
|
877
|
+
const result = await whatsapp.callback(code);
|
|
878
|
+
res.json({ success: true, provider: "whatsapp", ...result });
|
|
879
|
+
} catch (err) {
|
|
880
|
+
res.status(400).json({ error: err.message });
|
|
881
|
+
}
|
|
882
|
+
});
|
|
509
883
|
|
|
510
884
|
// ===== FACEBOOK ROUTES =====
|
|
511
885
|
app.get("/auth/facebook", (req, res) => facebook.redirect(res));
|
|
@@ -565,6 +939,21 @@ app.get("/auth/linkedin/callback", async (req, res)=>{
|
|
|
565
939
|
app.listen(PORT, () => console.log(`🚀 Server running at http://localhost:${PORT}`));
|
|
566
940
|
|
|
567
941
|
```
|
|
942
|
+
|
|
943
|
+
### ✅ Notes for Devs
|
|
944
|
+
1. Each provider has **redirect** and **callback** URLs.
|
|
945
|
+
2. Scopes can be customized per provider.
|
|
946
|
+
3. **Telegram & WhatsApp** use deep-link / QR-style flows.
|
|
947
|
+
4. The result of `callback()` is a JSON object containing the user info and `access_token` (except deep-link flows, which return code/messages).
|
|
948
|
+
5. You can **register custom providers** via:
|
|
949
|
+
```js
|
|
950
|
+
auth.oauth.register("myCustom", (options) => {
|
|
951
|
+
return {
|
|
952
|
+
redirect(res) { /* redirect user */ },
|
|
953
|
+
callback: async (code) => { /* handle callback */ }
|
|
954
|
+
};
|
|
955
|
+
});
|
|
956
|
+
```
|
|
568
957
|
---
|
|
569
958
|
|
|
570
959
|
## Telegram integration
|
|
@@ -665,7 +1054,9 @@ auth-verify/
|
|
|
665
1054
|
│ ├─ otpmanager.test.js
|
|
666
1055
|
│ ├─ oauth.test.js
|
|
667
1056
|
│ ├─ totpmanager.test.js
|
|
1057
|
+
│ ├─ passkeymanager.test.js
|
|
668
1058
|
├─ babel.config.js
|
|
1059
|
+
├─ authverify.client.js
|
|
669
1060
|
```
|
|
670
1061
|
|
|
671
1062
|
---
|
package/src/otp/index.js
CHANGED
|
@@ -27,6 +27,16 @@ class OTPManager {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
if(otpOptions.sender){
|
|
31
|
+
this.senderVia = otpOptions.sender.via;
|
|
32
|
+
this.senderService = otpOptions.sender.service;
|
|
33
|
+
this.senderMail = otpOptions.sender.sender;
|
|
34
|
+
this.senderPass = otpOptions.sender.pass;
|
|
35
|
+
this.senderHost = otpOptions.sender.host;
|
|
36
|
+
this.senderPort = otpOptions.sender.port;
|
|
37
|
+
this.senderSecure = otpOptions.sender.secure;
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
// generate(length = 6, callback) {
|
|
@@ -50,6 +60,11 @@ class OTPManager {
|
|
|
50
60
|
this.senderConfig = config;
|
|
51
61
|
}
|
|
52
62
|
|
|
63
|
+
sender(config){
|
|
64
|
+
if(!config.via) throw new Error("⚠️ Sender type { via } is required. It shouldbe 'email' or 'sms' or 'telegram'");
|
|
65
|
+
this.senderConfig = config;
|
|
66
|
+
}
|
|
67
|
+
|
|
53
68
|
async set(receiverEmailorPhone, callback){
|
|
54
69
|
const expiryInSeconds = Math.floor(this.otpExpiry / 1000);
|
|
55
70
|
|
|
@@ -534,33 +549,39 @@ class OTPManager {
|
|
|
534
549
|
// return this._verifyInternal(identifier, code);
|
|
535
550
|
// }
|
|
536
551
|
|
|
537
|
-
async verify(
|
|
552
|
+
async verify(options, code, callback) {
|
|
553
|
+
|
|
538
554
|
const handleError = (err) => {
|
|
539
|
-
|
|
540
|
-
if (err.message
|
|
541
|
-
else if (err.message.includes("Invalid")) err = new Error("Invalid OTP");
|
|
555
|
+
if (err.message?.includes("expired")) return new Error("OTP expired");
|
|
556
|
+
if (err.message?.includes("Invalid")) return new Error("Invalid OTP");
|
|
542
557
|
return err;
|
|
543
558
|
};
|
|
544
559
|
|
|
545
|
-
//
|
|
546
|
-
if (
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
560
|
+
// shape normalize
|
|
561
|
+
if (typeof options === "string" && typeof code === "string") {
|
|
562
|
+
// options as check string
|
|
563
|
+
options = { check: options, code: code };
|
|
564
|
+
code = undefined; // remove
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// callback detect
|
|
568
|
+
if (typeof code === "function") {
|
|
569
|
+
callback = code;
|
|
553
570
|
}
|
|
554
571
|
|
|
555
|
-
// promise style
|
|
556
572
|
try {
|
|
557
|
-
|
|
573
|
+
const res = await this._verifyInternal(options.check, options.code);
|
|
574
|
+
if (callback) return callback(null, res);
|
|
575
|
+
return res;
|
|
558
576
|
} catch (err) {
|
|
559
|
-
|
|
577
|
+
err = handleError(err);
|
|
578
|
+
if (callback) return callback(err);
|
|
579
|
+
throw err;
|
|
560
580
|
}
|
|
561
581
|
}
|
|
562
582
|
|
|
563
583
|
|
|
584
|
+
|
|
564
585
|
// helper used by verify()
|
|
565
586
|
// async _verifyInternal(identifier, code) {
|
|
566
587
|
// // memory
|
|
@@ -1118,7 +1139,54 @@ class OTPManager {
|
|
|
1118
1139
|
console.log("🚀 Telegram verification bot ready!");
|
|
1119
1140
|
}
|
|
1120
1141
|
|
|
1142
|
+
async send(reciever, mailOption , callback){
|
|
1121
1143
|
|
|
1144
|
+
if(typeof mailOption == 'function'){
|
|
1145
|
+
callback = mailOption;
|
|
1146
|
+
mailOption = {}
|
|
1147
|
+
}else if(!mailOption){
|
|
1148
|
+
mailOption = {};
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const sendProcess = async () => {
|
|
1152
|
+
// const otpCode = this.generate(mailOption.otpLen = 6).set(reciever);
|
|
1153
|
+
// console.log(otpCode);
|
|
1154
|
+
if(this.senderConfig.via == 'email'){
|
|
1155
|
+
await this.#sendEmail(reciever, mailOption);
|
|
1156
|
+
}else if(this.senderConfig.via == 'sms'){
|
|
1157
|
+
await this.#sendSMS(reciever, mailOption);
|
|
1158
|
+
}else {
|
|
1159
|
+
throw new Error("senderConfig.via should be 'email' or 'sms'")
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
if(callback) sendProcess().then(result => callback(null, result)).catch(error => callback(error));
|
|
1164
|
+
else return sendProcess();
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
#sendEmail(reciever, mailOption){
|
|
1168
|
+
return this.generate(mailOption.otpLen).set(reciever, (err)=>{
|
|
1169
|
+
if(err) throw err
|
|
1170
|
+
|
|
1171
|
+
this.message({
|
|
1172
|
+
to: reciever,
|
|
1173
|
+
subject: mailOption.subject || "Your OTP code",
|
|
1174
|
+
text: mailOption.text || `Your OTP code is ${this.code}`,
|
|
1175
|
+
html: mailOption.html || `Your OTP code is ${this.code}`
|
|
1176
|
+
});
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
#sendSMS(reciever, code, smsOption){
|
|
1181
|
+
return this.generate(smsOption.otpLen).set(reciever, (err)=>{
|
|
1182
|
+
if(err) throw err;
|
|
1183
|
+
|
|
1184
|
+
this.message({
|
|
1185
|
+
to: reciever,
|
|
1186
|
+
text: smsOption.text || `Your OTP code is ${code}`
|
|
1187
|
+
});
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1122
1190
|
}
|
|
1123
1191
|
|
|
1124
1192
|
module.exports = OTPManager;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
const base64url = require('base64url');
|
|
2
|
+
const crypto = require("crypto");
|
|
3
|
+
const Redis = require('ioredis');
|
|
4
|
+
const cbor = require("cbor");
|
|
5
|
+
|
|
6
|
+
class PasskeyManager {
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.rpName = options.rpName || "auth-verify";
|
|
9
|
+
this.storeType = options.storeTokens || "memory";
|
|
10
|
+
this.saveByToMemory = options.saveBy || "id";
|
|
11
|
+
this.ttl = options.passExp || "2m";
|
|
12
|
+
|
|
13
|
+
if (this.storeType === "memory") {
|
|
14
|
+
this.tokenStore = new Map();
|
|
15
|
+
} else if (this.storeType === 'redis') {
|
|
16
|
+
this.redis = new Redis(options.redisUrl || "redis://localhost:6379");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_generateChallenge() {
|
|
21
|
+
return base64url(crypto.randomBytes(32));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_encode(buffer) {
|
|
25
|
+
return base64url(buffer);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_decode(str) {
|
|
29
|
+
return Buffer.from(base64url.toBuffer(str));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_parseTTL() {
|
|
33
|
+
if (typeof this.ttl !== 'string') return this.ttl;
|
|
34
|
+
const ttlValue = parseInt(this.ttl);
|
|
35
|
+
if (this.ttl.endsWith('m')) return ttlValue * 60 * 1000;
|
|
36
|
+
if (this.ttl.endsWith('s')) return ttlValue * 1000;
|
|
37
|
+
throw new Error("TTL must end with 's' or 'm'");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_setWithTTL(key, challengeValue, ttlMs = 2 * 60 * 1000) {
|
|
41
|
+
this.tokenStore.set(key, {
|
|
42
|
+
value: challengeValue,
|
|
43
|
+
expiresAt: Date.now() + ttlMs
|
|
44
|
+
});
|
|
45
|
+
setTimeout(() => this.tokenStore.delete(key), ttlMs);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_coseToPEM(coseKey) {
|
|
49
|
+
const x = coseKey.get(-2);
|
|
50
|
+
const y = coseKey.get(-3);
|
|
51
|
+
const pubKeyBuffer = Buffer.concat([Buffer.from([0x04]), x, y]);
|
|
52
|
+
const pubKeyDER = Buffer.concat([
|
|
53
|
+
Buffer.from("3059301306072A8648CE3D020106082A8648CE3D030107034200", "hex"),
|
|
54
|
+
pubKeyBuffer
|
|
55
|
+
]);
|
|
56
|
+
return "-----BEGIN PUBLIC KEY-----\n" +
|
|
57
|
+
pubKeyDER.toString("base64").match(/.{1,64}/g).join("\n") +
|
|
58
|
+
"\n-----END PUBLIC KEY-----\n";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async _finishAttestation(clientResponse) {
|
|
62
|
+
const { user, challenge } = this._pending;
|
|
63
|
+
|
|
64
|
+
const clientDataJSON = JSON.parse(
|
|
65
|
+
Buffer.from(clientResponse.response.clientDataJSON, 'base64').toString()
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (clientDataJSON.challenge !== challenge)
|
|
69
|
+
throw new Error("Challenge mismatch");
|
|
70
|
+
|
|
71
|
+
const attestationBuffer = Buffer.from(clientResponse.response.attestationObject, 'base64');
|
|
72
|
+
const attestationStruct = await cbor.decodeFirst(attestationBuffer);
|
|
73
|
+
|
|
74
|
+
// Parse authData
|
|
75
|
+
const authData = attestationStruct.authData;
|
|
76
|
+
const rpIdHash = authData.slice(0, 32);
|
|
77
|
+
const flags = authData[32];
|
|
78
|
+
const signCount = authData.readUInt32BE(33);
|
|
79
|
+
|
|
80
|
+
const credIdLen = authData.readUInt16BE(37);
|
|
81
|
+
const credentialId = authData.slice(39, 39 + credIdLen);
|
|
82
|
+
|
|
83
|
+
const coseKeyBytes = authData.slice(39 + credIdLen);
|
|
84
|
+
const coseKey = await cbor.decodeFirst(coseKeyBytes);
|
|
85
|
+
const publicKeyPEM = this._coseToPEM(coseKey);
|
|
86
|
+
|
|
87
|
+
// Save credential in user object
|
|
88
|
+
user.credentials = user.credentials || [];
|
|
89
|
+
user.credentials.push({
|
|
90
|
+
id: this._encode(credentialId),
|
|
91
|
+
publicKey: publicKeyPEM,
|
|
92
|
+
signCount
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return { status: "ok", user, credentialId: this._encode(credentialId) };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async _finishAssertion(clientResponse) {
|
|
99
|
+
const { user, challenge } = this._pending;
|
|
100
|
+
|
|
101
|
+
const clientDataJSON = JSON.parse(
|
|
102
|
+
Buffer.from(clientResponse.response.clientDataJSON, 'base64').toString()
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (clientDataJSON.challenge !== challenge)
|
|
106
|
+
throw new Error("Challenge mismatch");
|
|
107
|
+
|
|
108
|
+
const credentialId = this._encode(Buffer.from(clientResponse.id, 'base64'));
|
|
109
|
+
const credential = user.credentials?.find(c => c.id === credentialId);
|
|
110
|
+
if (!credential) throw new Error("Unknown credential");
|
|
111
|
+
|
|
112
|
+
const signature = Buffer.from(clientResponse.response.signature, 'base64');
|
|
113
|
+
const authData = Buffer.from(clientResponse.response.authenticatorData, 'base64');
|
|
114
|
+
|
|
115
|
+
const verify = crypto.createVerify('SHA256');
|
|
116
|
+
const clientHash = crypto.createHash('sha256').update(Buffer.from(clientResponse.response.clientDataJSON, 'base64')).digest();
|
|
117
|
+
verify.update(Buffer.concat([authData, clientHash]));
|
|
118
|
+
|
|
119
|
+
const verified = verify.verify(credential.publicKey, signature);
|
|
120
|
+
|
|
121
|
+
return { status: verified ? "ok" : "failed", user };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async register(user) {
|
|
125
|
+
const challenge = this._generateChallenge();
|
|
126
|
+
|
|
127
|
+
if (this.storeType === "memory") {
|
|
128
|
+
this._setWithTTL(user[this.saveByToMemory], challenge, this._parseTTL());
|
|
129
|
+
} else if (this.storeType === "redis") {
|
|
130
|
+
await this.redis.set(user[this.saveByToMemory], challenge, "PX", this._parseTTL());
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this._pending = { type: "register", user, challenge };
|
|
134
|
+
return this;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async login(user) {
|
|
138
|
+
const challenge = this._generateChallenge();
|
|
139
|
+
|
|
140
|
+
if (this.storeType === "memory") {
|
|
141
|
+
this._setWithTTL(user[this.saveByToMemory], challenge, this._parseTTL());
|
|
142
|
+
} else if (this.storeType === "redis") {
|
|
143
|
+
await this.redis.set(user[this.saveByToMemory], challenge, "PX", this._parseTTL());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this._pending = { type: "login", user, challenge };
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getOptions() {
|
|
151
|
+
if (!this._pending) throw new Error("No pending operation");
|
|
152
|
+
|
|
153
|
+
const { type, user, challenge } = this._pending;
|
|
154
|
+
|
|
155
|
+
if (type === "register") {
|
|
156
|
+
return {
|
|
157
|
+
challenge,
|
|
158
|
+
rp: { name: this.rpName },
|
|
159
|
+
user: {
|
|
160
|
+
id: this._encode(Buffer.from(user.id)),
|
|
161
|
+
name: user.username,
|
|
162
|
+
displayName: user.username
|
|
163
|
+
},
|
|
164
|
+
pubKeyCredParams: [{ alg: -7, type: "public-key" }]
|
|
165
|
+
};
|
|
166
|
+
} else if (type === "login") {
|
|
167
|
+
return {
|
|
168
|
+
challenge,
|
|
169
|
+
allowCredentials: user.credentials?.map(c => ({
|
|
170
|
+
id: this._decode(c.id),
|
|
171
|
+
type: "public-key"
|
|
172
|
+
})) || [],
|
|
173
|
+
timeout: 60000
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async finish(clientResponse) {
|
|
179
|
+
if (!this._pending) throw new Error("No pending operation");
|
|
180
|
+
|
|
181
|
+
let result;
|
|
182
|
+
if (this._pending.type === "register") {
|
|
183
|
+
result = await this._finishAttestation(clientResponse);
|
|
184
|
+
} else if (this._pending.type === "login") {
|
|
185
|
+
result = await this._finishAssertion(clientResponse);
|
|
186
|
+
} else {
|
|
187
|
+
throw new Error("Unknown pending operation");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this._pending = null;
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = PasskeyManager;
|
package/src/totp/index.js
CHANGED
|
@@ -43,6 +43,11 @@ class TOTPManager {
|
|
|
43
43
|
return await qrcode.toDataURL(uri);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
async qr(uri) {
|
|
47
|
+
const qrcode = require("qrcode");
|
|
48
|
+
return await qrcode.toDataURL(uri);
|
|
49
|
+
}
|
|
50
|
+
|
|
46
51
|
// internal HOTP algorithm
|
|
47
52
|
_hotp(secret, counter) {
|
|
48
53
|
const key = base32.decode(secret);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const AuthVerify = require("../index");
|
|
2
|
+
const base64url = require("base64url");
|
|
3
|
+
|
|
4
|
+
describe("AuthVerify Passkey Flow (mocked for Jest)", () => {
|
|
5
|
+
let auth;
|
|
6
|
+
const user = { id: "123", username: "testuser", credentials: [] };
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
auth = new AuthVerify({
|
|
10
|
+
storeTokens: "memory",
|
|
11
|
+
passExp: "1m",
|
|
12
|
+
rpName: "TestApp",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Mock _finishAttestation to bypass CBOR decoding
|
|
16
|
+
auth.passkey._finishAttestation = async (clientResponse) => ({
|
|
17
|
+
status: "ok",
|
|
18
|
+
user,
|
|
19
|
+
challengeVerified: true,
|
|
20
|
+
rawAuthData: "FAKE_AUTHDATA",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Mock _finishAssertion to bypass signature verification
|
|
24
|
+
auth.passkey._finishAssertion = async (clientResponse) => {
|
|
25
|
+
const { user, challenge } = auth.passkey._pending;
|
|
26
|
+
const clientDataJSON = JSON.parse(
|
|
27
|
+
Buffer.from(clientResponse.response.clientDataJSON, "base64").toString()
|
|
28
|
+
);
|
|
29
|
+
if (clientDataJSON.challenge !== challenge)
|
|
30
|
+
throw new Error("Challenge mismatch");
|
|
31
|
+
|
|
32
|
+
// Find credential by exact id
|
|
33
|
+
const credentialId = clientResponse.id;
|
|
34
|
+
const credential = user.credentials?.find(c => c.id === credentialId);
|
|
35
|
+
if (!credential) throw new Error("Unknown credential");
|
|
36
|
+
|
|
37
|
+
return { status: "ok", user };
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("register() should store challenge and be chainable", async () => {
|
|
42
|
+
const chainable = await auth.passkey.register(user);
|
|
43
|
+
expect(chainable).toBe(auth.passkey);
|
|
44
|
+
expect(auth.passkey._pending.type).toBe("register");
|
|
45
|
+
expect(auth.passkey._pending.user).toEqual(user);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("getOptions() after register() returns correct options", async () => {
|
|
49
|
+
await auth.passkey.register(user);
|
|
50
|
+
const options = auth.passkey.getOptions();
|
|
51
|
+
expect(options.rp.name).toBe("TestApp");
|
|
52
|
+
expect(options.user.id).toBe(base64url(Buffer.from(user.id)));
|
|
53
|
+
expect(options.pubKeyCredParams[0].type).toBe("public-key");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("finish() after register() verifies attestation", async () => {
|
|
57
|
+
await auth.passkey.register(user);
|
|
58
|
+
const pendingChallenge = auth.passkey._pending.challenge;
|
|
59
|
+
|
|
60
|
+
const clientResponse = {
|
|
61
|
+
response: {
|
|
62
|
+
clientDataJSON: Buffer.from(
|
|
63
|
+
JSON.stringify({ challenge: pendingChallenge })
|
|
64
|
+
).toString("base64"),
|
|
65
|
+
attestationObject: "FAKE_CBOR",
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const result = await auth.passkey.finish(clientResponse);
|
|
70
|
+
expect(result.status).toBe("ok");
|
|
71
|
+
expect(result.challengeVerified).toBe(true);
|
|
72
|
+
expect(result.user).toEqual(user);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("login() should store challenge and allow credentials", async () => {
|
|
76
|
+
// Add a fake credential
|
|
77
|
+
user.credentials.push({
|
|
78
|
+
id: base64url(Buffer.from("cred1")),
|
|
79
|
+
publicKey: "FAKE_KEY",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await auth.passkey.login(user);
|
|
83
|
+
const options = auth.passkey.getOptions();
|
|
84
|
+
|
|
85
|
+
expect(auth.passkey._pending.type).toBe("login");
|
|
86
|
+
expect(options.allowCredentials.length).toBe(1);
|
|
87
|
+
expect(Buffer.isBuffer(options.allowCredentials[0].id)).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("finish() after login() verifies assertion challenge", async () => {
|
|
91
|
+
user.credentials.push({
|
|
92
|
+
id: base64url(Buffer.from("cred1")),
|
|
93
|
+
publicKey: "FAKE_KEY",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await auth.passkey.login(user);
|
|
97
|
+
const challenge = auth.passkey._pending.challenge;
|
|
98
|
+
|
|
99
|
+
const clientResponse = {
|
|
100
|
+
id: user.credentials[0].id,
|
|
101
|
+
response: {
|
|
102
|
+
clientDataJSON: Buffer.from(JSON.stringify({ challenge })).toString(
|
|
103
|
+
"base64"
|
|
104
|
+
),
|
|
105
|
+
authenticatorData: Buffer.from("rawAuthData").toString("base64"),
|
|
106
|
+
signature: Buffer.from("signature").toString("base64"),
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const result = await auth.passkey.finish(clientResponse);
|
|
111
|
+
expect(result.status).toBe("ok"); // mocked result
|
|
112
|
+
expect(result.user).toEqual(user);
|
|
113
|
+
});
|
|
114
|
+
});
|