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.
@@ -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.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",
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`) (New in v1.5.0)
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 Linkedin.
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({ check, code }, callback) {
552
+ async verify(options, code, callback) {
553
+
538
554
  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");
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
- // callback style
546
- if (callback && typeof callback === 'function') {
547
- try {
548
- const res = await this._verifyInternal(check, code);
549
- return callback(null, res);
550
- } catch (err) {
551
- return callback(handleError(err));
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
- return await this._verifyInternal(check, code);
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
- throw handleError(err);
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
+ });