auth-verify 1.5.0 → 1.6.1

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 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,17 +1,18 @@
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
+ "version": "1.6.1",
15
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",
16
17
  "main": "index.js",
17
18
  "scripts": {
package/readme.md CHANGED
@@ -7,7 +7,7 @@
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
11
  - ✅ Automatic JWT cookie handling for Express apps, supporting secure, HTTP-only cookies and optional auto-verification.
12
12
  - ✅ Fully asynchronous/Promise-based API, with callback support where applicable.
13
13
  - ✅ Chainable OTP workflow with cooldowns, max attempts, and resend functionality.
@@ -30,9 +30,10 @@ npm install auth-verify
30
30
  - `AuthVerify` (entry): constructs and exposes `.jwt`, `.otp`, (optionally) `.session`, `.totp` and `.oauth` managers.
31
31
  - `JWTManager`: sign, verify, decode, revoke tokens. Supports `storeTokens: "memory" | "redis" | "none"` and middleware with custom cookie, header, and token extraction.
32
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
33
+ - `TOTPManager`: generate, verify uri, codes and QR codes.
34
34
  - `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
35
+ - `OAuthManager`: Handle OAuth 2.0 logins for Google, Facebook, GitHub, X, Linkedin, Apple, Discord, Slack, Microsoft, Telegram and WhatsApp.
36
+ - `PasskeyManager`: Handle passwordless login and registration using WebAuthn/passkey.
36
37
  ---
37
38
 
38
39
  ## 🚀 Example: Initialize library (CommonJS)
@@ -53,7 +54,7 @@ const auth = new AuthVerify({
53
54
 
54
55
  ## 🔐 JWT Usage
55
56
 
56
- ### JWT Middleware (`protect`) (New in v1.5.0)
57
+ ### JWT Middleware (`protect`) (v1.5.0+)
57
58
 
58
59
  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
 
@@ -355,6 +356,152 @@ auth.otp.verify({ check: 'user@example.com', code: '123456' }, (err, isValid)=>{
355
356
 
356
357
  ---
357
358
 
359
+ ## 🗝️ Passkey (WebAuthn) (New in v1.6.1)
360
+
361
+ `AuthVerify` includes a `PasskeyManager` class to handle passwordless login using WebAuthn / passkeys. You can **register** users, **verify login**, and manage **challenges** safely.
362
+
363
+ ### Setup
364
+ ```js
365
+ const AuthVerify = require("auth-verify");
366
+
367
+ const auth = new AuthVerify({
368
+ passExp: "2m", // passkey challenge TTL
369
+ rpName: "MyApp",
370
+ storeTokens: "memory" // or "redis"
371
+ });
372
+
373
+ const user = {
374
+ id: "user1",
375
+ username: "john_doe",
376
+ credentials: [] // will store registered credentials
377
+ };
378
+ ```
379
+ ### 1️⃣ Register a new Passkey
380
+ The registration process consists of **two steps**:
381
+ **1.** Generate a registration challenge
382
+ **2.** Complete attestation after client responds
383
+
384
+ #### Step 1: Generate challenge
385
+ ```js
386
+ // register user
387
+ await auth.passkey.register(user);
388
+
389
+ // get WebAuthn options for the client
390
+ const options = auth.passkey.getOptions();
391
+ console.log(options);
392
+
393
+ /* Example output:
394
+ {
395
+ challenge: "base64url-challenge",
396
+ rp: { name: "MyApp" },
397
+ user: { id: "dXNlcjE", name: "john_doe", displayName: "john_doe" },
398
+ pubKeyCredParams: [{ alg: -7, type: "public-key" }]
399
+ }
400
+ */
401
+ ```
402
+ > Send options to the browser to call:
403
+ > ```js
404
+ > navigator.credentials.create({ publicKey: options })
405
+ > ```
406
+
407
+ #### Step 2: Finish attestation
408
+ Once the client returns the attestation response:
409
+ ```js
410
+ const clientResponse = {
411
+ id: "...", // credentialId from browser
412
+ response: {
413
+ clientDataJSON: "...",
414
+ attestationObject: "..."
415
+ }
416
+ };
417
+
418
+ const result = await auth.passkey.finish(clientResponse);
419
+ console.log(result);
420
+ /* Example result:
421
+ {
422
+ status: "ok",
423
+ user: {
424
+ id: "user1",
425
+ username: "john_doe",
426
+ credentials: [
427
+ { id: "credentialId", publicKey: "pem-key" }
428
+ ]
429
+ },
430
+ challengeVerified: true,
431
+ rawAuthData: <Buffer ...>
432
+ }
433
+ */
434
+ ```
435
+ > After this, the user now has a **registered passkey**.
436
+
437
+ ### 2️⃣ Login with Passkey
438
+ Login also consists of **two steps**:
439
+ **1.** Generate assertion challenge
440
+ **2.** Complete verification
441
+ #### Step 1: Generate login challenge
442
+ ```js
443
+ await auth.passkey.login(user);
444
+
445
+ const options = auth.passkey.getOptions();
446
+ console.log(options);
447
+
448
+ /* Example output:
449
+ {
450
+ challenge: "base64url-challenge",
451
+ allowCredentials: [
452
+ { id: <Buffer...>, type: "public-key" }
453
+ ],
454
+ timeout: 60000
455
+ }
456
+ */
457
+ ```
458
+ > Send this `options` to the browser for `navigator.credentials.get({ publicKey: options })`.
459
+
460
+ #### Step 2: Finish login assertion
461
+ ```js
462
+ const clientLoginResponse = {
463
+ id: "credentialId",
464
+ response: {
465
+ clientDataJSON: "...",
466
+ authenticatorData: "...",
467
+ signature: "..."
468
+ }
469
+ };
470
+
471
+ const loginResult = await auth.passkey.finish(clientLoginResponse);
472
+ console.log(loginResult);
473
+ /* Example output:
474
+ {
475
+ status: "ok",
476
+ user: {
477
+ id: "user1",
478
+ username: "john_doe",
479
+ credentials: [...]
480
+ }
481
+ }
482
+ */
483
+ ```
484
+ > If `status === "ok"`, the login is successful.
485
+
486
+ ### 3️⃣ Notes
487
+
488
+ - `auth.passkey.register()` and `auth.passkey.login()` return this so you can chain:
489
+ ```js
490
+ await auth.passkey
491
+ .register(user)
492
+ .getOptions(); // get WebAuthn options
493
+ ```
494
+ - `finish()` **must be called after `register()` or `login()`** with the client’s response.
495
+ - TTL (`passExp`) ensures challenges **expire automatically** (memory or Redis store).
496
+
497
+ ### 4️⃣ Summary of Methods
498
+ | Method | Purpose | Returns |
499
+ | ------------------------ | ------------------------------- | ------------------ |
500
+ | `register(user)` | Start passkey registration | `this` (chainable) |
501
+ | `login(user)` | Start passkey login | `this` (chainable) |
502
+ | `getOptions()` | Get WebAuthn options for client | Object |
503
+ | `finish(clientResponse)` | Complete attestation/assertion | Result object |
504
+
358
505
  ## ✅ TOTP (Time-based One Time Passwords) — Google Authenticator support (v1.4.0+)
359
506
  ```js
360
507
  const AuthVerify = require("auth-verify");
@@ -425,7 +572,22 @@ if (auth.totp.verify({ secret, token })) {
425
572
  ```
426
573
  ---
427
574
  ## 🌍 OAuth 2.0 Integration (v1.2.0+)
428
- `auth.oauth` supports login via Google, Facebook, GitHub, X (Twitter) and Linkedin.
575
+ `auth.oauth` supports login via Google, Facebook, GitHub, X (Twitter), Linkedin, Microsoft, Telegram, Slack, WhatsApp, Apple and Discord.
576
+ ### Providers & Routes table
577
+ | Provider | Redirect URL | Callback URL | Scopes / Notes |
578
+ | ----------- | ----------------- | -------------------------- | -------------------------------------- |
579
+ | Google | `/auth/google` | `/auth/google/callback` | `openid email profile` |
580
+ | Facebook | `/auth/facebook` | `/auth/facebook/callback` | `email,public_profile` |
581
+ | GitHub | `/auth/github` | `/auth/github/callback` | `user:email` |
582
+ | X (Twitter) | `/auth/x` | `/auth/x/callback` | `tweet.read users.read offline.access` |
583
+ | LinkedIn | `/auth/linkedin` | `/auth/linkedin/callback` | `r_liteprofile r_emailaddress` |
584
+ | Microsoft | `/auth/microsoft` | `/auth/microsoft/callback` | `User.Read` |
585
+ | Telegram | `/auth/telegram` | `/auth/telegram/callback` | Bot deep-link |
586
+ | Slack | `/auth/slack` | `/auth/slack/callback` | `identity.basic identity.email` |
587
+ | WhatsApp | `/auth/whatsapp` | `/auth/whatsapp/callback` | QR / deep-link |
588
+ | Apple | `/auth/apple` | `/auth/apple/callback` | `name email` |
589
+ | Discord | `/auth/discord` | `/auth/discord/callback` | `identify email` |
590
+
429
591
  ### Example (Google Login with Express)
430
592
  ```js
431
593
  const express = require('express');
@@ -506,6 +668,79 @@ const linkedin = auth.oauth.linkedin({
506
668
  redirectUri: "http://localhost:3000/auth/linkedin/callback"
507
669
  });
508
670
 
671
+ // --- MICROSOFT ---
672
+ const microsoft = auth.oauth.microsoft({
673
+ clientId: "YOUR_MICROSOFT_CLIENT_ID",
674
+ clientSecret: "YOUR_MICROSOFT_CLIENT_SECRET",
675
+ redirectUri: "http://localhost:3000/auth/microsoft/callback"
676
+ });
677
+
678
+ app.get("/auth/microsoft", (req, res) => microsoft.redirect(res));
679
+
680
+ app.get("/auth/microsoft/callback", async (req, res) => {
681
+ try {
682
+ const { code } = req.query;
683
+ const user = await microsoft.callback(code);
684
+ res.json({ success: true, provider: "microsoft", user });
685
+ } catch (err) {
686
+ res.status(400).json({ error: err.message });
687
+ }
688
+ });
689
+
690
+ // --- TELEGRAM ---
691
+ const telegram = auth.oauth.telegram({
692
+ botId: "YOUR_BOT_ID",
693
+ redirectUri: "http://localhost:3000/auth/telegram/callback"
694
+ });
695
+
696
+ app.get("/auth/telegram", (req, res) => telegram.redirect(res));
697
+
698
+ app.get("/auth/telegram/callback", async (req, res) => {
699
+ try {
700
+ const { code } = req.query;
701
+ const result = await telegram.callback(code);
702
+ res.json({ success: true, provider: "telegram", ...result });
703
+ } catch (err) {
704
+ res.status(400).json({ error: err.message });
705
+ }
706
+ });
707
+
708
+ // --- SLACK ---
709
+ const slack = auth.oauth.slack({
710
+ clientId: "YOUR_SLACK_CLIENT_ID",
711
+ clientSecret: "YOUR_SLACK_CLIENT_SECRET",
712
+ redirectUri: "http://localhost:3000/auth/slack/callback"
713
+ });
714
+
715
+ app.get("/auth/slack", (req, res) => slack.redirect(res));
716
+
717
+ app.get("/auth/slack/callback", async (req, res) => {
718
+ try {
719
+ const { code } = req.query;
720
+ const user = await slack.callback(code);
721
+ res.json({ success: true, provider: "slack", user });
722
+ } catch (err) {
723
+ res.status(400).json({ error: err.message });
724
+ }
725
+ });
726
+
727
+ // --- WHATSAPP ---
728
+ const whatsapp = auth.oauth.whatsapp({
729
+ phoneNumberId: "YOUR_PHONE_ID",
730
+ redirectUri: "http://localhost:3000/auth/whatsapp/callback"
731
+ });
732
+
733
+ app.get("/auth/whatsapp", (req, res) => whatsapp.redirect(res));
734
+
735
+ app.get("/auth/whatsapp/callback", async (req, res) => {
736
+ try {
737
+ const { code } = req.query;
738
+ const result = await whatsapp.callback(code);
739
+ res.json({ success: true, provider: "whatsapp", ...result });
740
+ } catch (err) {
741
+ res.status(400).json({ error: err.message });
742
+ }
743
+ });
509
744
 
510
745
  // ===== FACEBOOK ROUTES =====
511
746
  app.get("/auth/facebook", (req, res) => facebook.redirect(res));
@@ -565,6 +800,21 @@ app.get("/auth/linkedin/callback", async (req, res)=>{
565
800
  app.listen(PORT, () => console.log(`🚀 Server running at http://localhost:${PORT}`));
566
801
 
567
802
  ```
803
+
804
+ ### ✅ Notes for Devs
805
+ 1. Each provider has **redirect** and **callback** URLs.
806
+ 2. Scopes can be customized per provider.
807
+ 3. **Telegram & WhatsApp** use deep-link / QR-style flows.
808
+ 4. The result of `callback()` is a JSON object containing the user info and `access_token` (except deep-link flows, which return code/messages).
809
+ 5. You can **register custom providers** via:
810
+ ```js
811
+ auth.oauth.register("myCustom", (options) => {
812
+ return {
813
+ redirect(res) { /* redirect user */ },
814
+ callback: async (code) => { /* handle callback */ }
815
+ };
816
+ });
817
+ ```
568
818
  ---
569
819
 
570
820
  ## Telegram integration
@@ -665,6 +915,7 @@ auth-verify/
665
915
  │ ├─ otpmanager.test.js
666
916
  │ ├─ oauth.test.js
667
917
  │ ├─ totpmanager.test.js
918
+ │ ├─ passkeymanager.test.js
668
919
  ├─ babel.config.js
669
920
  ```
670
921
 
@@ -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;
@@ -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
+ });