electron-webauthn 0.0.14 → 0.0.16

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.
Files changed (51) hide show
  1. package/README.md +718 -113
  2. package/dist/create/authorization-controller.d.ts +9 -0
  3. package/dist/create/authorization-controller.js +70 -0
  4. package/dist/create/handler.d.ts +40 -3
  5. package/dist/create/handler.js +205 -5
  6. package/dist/create/internal-handler.d.ts +34 -0
  7. package/dist/create/internal-handler.js +154 -0
  8. package/dist/get/handler.d.ts +34 -22
  9. package/dist/get/handler.js +126 -166
  10. package/dist/get/internal-handler.d.ts +24 -0
  11. package/dist/get/internal-handler.js +157 -0
  12. package/dist/get/updated-handler.d.ts +1 -0
  13. package/dist/get/updated-handler.js +1 -0
  14. package/dist/helpers/client-data.d.ts +14 -0
  15. package/dist/helpers/client-data.js +36 -0
  16. package/dist/helpers/index.d.ts +1 -1
  17. package/dist/helpers/index.js +15 -10
  18. package/dist/helpers/origin.js +1 -1
  19. package/dist/helpers/presentation.d.ts +1 -0
  20. package/dist/helpers/presentation.js +12 -0
  21. package/dist/helpers/public-key.d.ts +1 -0
  22. package/dist/helpers/public-key.js +49 -0
  23. package/dist/helpers/rpid.d.ts +13 -0
  24. package/dist/helpers/rpid.js +59 -0
  25. package/dist/helpers/validation.d.ts +3 -0
  26. package/dist/helpers/validation.js +9 -0
  27. package/dist/index.d.ts +0 -1
  28. package/dist/objc/authentication-services/as-authorization-c-public-key-credential-descriptor.d.ts +9 -0
  29. package/dist/objc/authentication-services/as-authorization-c-public-key-credential-descriptor.js +6 -0
  30. package/dist/objc/authentication-services/as-authorization-platform-public-key-credential-provider.d.ts +3 -1
  31. package/dist/objc/authentication-services/as-authorization-platform-public-key-credential-registration.d.ts +15 -0
  32. package/dist/objc/authentication-services/as-authorization-platform-public-key-credential-registration.js +2 -0
  33. package/dist/objc/authentication-services/as-authorization-public-key-credential-large-blob-registration-input.d.ts +7 -0
  34. package/dist/objc/authentication-services/as-authorization-public-key-credential-large-blob-registration-input.js +6 -0
  35. package/dist/objc/authentication-services/as-authorization-public-key-credential-large-blob-registration-output.d.ts +5 -0
  36. package/dist/objc/authentication-services/as-authorization-public-key-credential-large-blob-registration-output.js +2 -0
  37. package/dist/objc/authentication-services/as-authorization-public-key-credential-prf-registration-input.d.ts +9 -0
  38. package/dist/objc/authentication-services/as-authorization-public-key-credential-prf-registration-input.js +6 -0
  39. package/dist/objc/authentication-services/as-authorization-public-key-credential-prf-registration-output.d.ts +9 -0
  40. package/dist/objc/authentication-services/as-authorization-public-key-credential-prf-registration-output.js +2 -0
  41. package/dist/objc/authentication-services/as-authorization-public-key-credential-registration.d.ts +14 -0
  42. package/dist/objc/authentication-services/as-authorization-public-key-credential-registration.js +2 -0
  43. package/dist/objc/authentication-services/enums/as-authorization-public-key-credential-attestation-kind.d.ts +6 -0
  44. package/dist/objc/authentication-services/enums/as-authorization-public-key-credential-attestation-kind.js +7 -0
  45. package/dist/objc/authentication-services/enums/as-authorization-public-key-credential-large-blob-support-requirement.d.ts +4 -0
  46. package/dist/objc/authentication-services/enums/as-authorization-public-key-credential-large-blob-support-requirement.js +5 -0
  47. package/dist/objc/authentication-services/enums/as-authorization-public-key-credential-user-verification-preference.d.ts +5 -0
  48. package/dist/objc/authentication-services/enums/as-authorization-public-key-credential-user-verification-preference.js +6 -0
  49. package/dist/test.d.ts +1 -0
  50. package/dist/test.js +5 -0
  51. package/package.json +2 -1
package/README.md CHANGED
@@ -1,23 +1,30 @@
1
1
  # electron-webauthn
2
2
 
3
- Add native WebAuthn/FIDO2 support to Electron using macOS AuthenticationServices framework.
3
+ Add native WebAuthn/FIDO2 support to Electron on macOS using its AuthenticationServices framework.
4
4
 
5
5
  ## Overview
6
6
 
7
- `electron-webauthn` is a TypeScript library that bridges Electron with the native macOS AuthenticationServices framework, enabling WebAuthn/FIDO2 authentication directly through native platform authenticators (Touch ID, Face ID, hardware security keys, etc.).
7
+ `electron-webauthn` allows you to process WebAuthn requests on macOS Electron. Simply plug any `publicKeyOptions` from the standard `navigator.credentials.get()` or `navigator.credentials.create()` directly into this library's functions and they'll work with the native macOS authenticators.
8
8
 
9
- This package provides JavaScript bindings to Apple's AuthenticationServices framework, allowing you to perform WebAuthn assertions (authentication/signing with existing credentials) in your Electron applications.
9
+ This package provides JavaScript bindings to Apple's AuthenticationServices framework, allowing you to perform WebAuthn credential creation (registration) and assertions (authentication/signing) in your Electron applications using W3C WebAuthn-compliant APIs.
10
10
 
11
11
  ## Features
12
12
 
13
- - 🔐 Native WebAuthn support for Electron on macOS
14
- - 🎯 Support for platform authenticators (Touch ID, Face ID)
15
- - 🔑 Support for cross-platform authenticators (external security keys)
16
- - 📦 TypeScript first with complete type definitions
17
- - 🎨 Seamless integration with Electron's native window system
18
- - 🔐 PRF (Pseudo-Random Function) support for credential assertions
19
- - 💾 Large Blob support for storing credential-specific data
20
- - ⚙️ User verification preference configuration (preferred, required, discouraged)
13
+ - Native WebAuthn support for Electron on macOS
14
+ - Support for platform authenticators (Touch ID, Face ID)
15
+ - Support for cross-platform authenticators (external security keys)
16
+ - TypeScript first with complete type definitions
17
+ - **W3C WebAuthn-compliant API** - drop in any standard `publicKeyOptions` and it just works
18
+ - Credential creation (registration) with attestation
19
+ - Credential authentication (assertions) with existing credentials
20
+ - Seamless integration with Electron's native window system
21
+ - PRF (Pseudo-Random Function) extension support
22
+ - Large Blob extension support for reading/writing credential-specific data
23
+ - User verification preference configuration (preferred, required, discouraged)
24
+ - Proper origin validation with public suffix list support
25
+ - Cross-origin iframe support with `topFrameOrigin`
26
+ - Per-credential PRF evaluation with `evalByCredential`
27
+ - Resident key (discoverable credential) support
21
28
 
22
29
  ## Installation
23
30
 
@@ -37,106 +44,347 @@ yarn add electron-webauthn
37
44
 
38
45
  ## Quick Start
39
46
 
47
+ > **💡 Drop-in Compatibility:** Simply plug any `publicKeyOptions` from the standard `navigator.credentials.create()` or `navigator.credentials.get()` APIs directly into these functions. They follow the W3C WebAuthn specification exactly.
48
+
49
+ ### Creating a Credential (Registration)
50
+
40
51
  ```typescript
41
- import { getCredential } from "electron-webauthn";
42
- import { getPointer } from "objc-js";
52
+ import { createCredential } from "electron-webauthn";
53
+ import { BrowserWindow } from "electron";
43
54
 
44
55
  // In your Electron main process or preload script
45
- async function authenticate() {
56
+ async function register(window: BrowserWindow, challenge: ArrayBuffer) {
46
57
  // Get the native window handle from your BrowserWindow
47
- const nativeWindowHandle = getPointer(
48
- yourElectronWindow.getNativeWindowHandle()
58
+ const nativeWindowHandle = window.getNativeWindowHandle();
59
+
60
+ // Call createCredential with W3C WebAuthn-compliant options
61
+ // You can plug any publicKeyOptions from navigator.credentials.create() here
62
+ const result = await createCredential(
63
+ {
64
+ challenge: challenge,
65
+ rp: {
66
+ name: "Example App",
67
+ id: "example.com",
68
+ },
69
+ user: {
70
+ id: new Uint8Array(16), // Random user ID
71
+ name: "user@example.com",
72
+ displayName: "User Name",
73
+ },
74
+ pubKeyCredParams: [
75
+ { type: "public-key", alg: -7 }, // ES256
76
+ { type: "public-key", alg: -257 }, // RS256
77
+ ],
78
+ timeout: 60000, // Optional: 60 seconds
79
+ attestation: "none", // Optional: "none" | "indirect" | "direct"
80
+ authenticatorSelection: {
81
+ userVerification: "preferred", // Optional: "preferred" | "required" | "discouraged"
82
+ residentKey: "preferred", // Optional: "discouraged" | "preferred" | "required"
83
+ },
84
+ },
85
+ {
86
+ currentOrigin: "https://example.com",
87
+ topFrameOrigin: "https://example.com",
88
+ nativeWindowHandle: nativeWindowHandle,
89
+ }
49
90
  );
50
91
 
51
- // Call getCredential to prompt the user for authentication
92
+ if (result.success) {
93
+ console.log("Registration successful!");
94
+ console.log("Credential ID:", result.data.credentialId);
95
+ console.log("Public Key:", result.data.publicKey);
96
+ // Result contains base64url-encoded strings ready to send to server
97
+ } else {
98
+ console.error("Registration failed:", result.error);
99
+ }
100
+ }
101
+ ```
102
+
103
+ ### Authenticating with a Credential
104
+
105
+ ```typescript
106
+ import { getCredential } from "electron-webauthn";
107
+ import { BrowserWindow } from "electron";
108
+
109
+ // In your Electron main process or preload script
110
+ async function authenticate(window: BrowserWindow, challenge: ArrayBuffer) {
111
+ // Get the native window handle from your BrowserWindow
112
+ const nativeWindowHandle = window.getNativeWindowHandle();
113
+
114
+ // Call getCredential with W3C WebAuthn-compliant options
115
+ // You can plug any publicKeyOptions from navigator.credentials.get() here
52
116
  const result = await getCredential(
53
- "example.com", // Relying Party ID
54
- Buffer.from("your-challenge"), // Challenge from server
55
- "https://example.com", // Origin
56
- [], // Optional: allowed credential IDs
57
- "preferred" // Optional: user verification preference
117
+ {
118
+ challenge: challenge,
119
+ rpId: "example.com",
120
+ timeout: 60000, // Optional: 60 seconds
121
+ userVerification: "preferred", // Optional: "preferred" | "required" | "discouraged"
122
+ allowCredentials: [], // Optional: restrict to specific credentials
123
+ },
124
+ {
125
+ currentOrigin: "https://example.com",
126
+ topFrameOrigin: "https://example.com", // Use for iframe support
127
+ nativeWindowHandle: nativeWindowHandle,
128
+ }
58
129
  );
59
130
 
60
- console.log(result);
61
- // Result contains:
62
- // - id: Credential ID (Buffer)
63
- // - authenticatorAttachment: 'platform' | 'cross-platform'
64
- // - clientDataJSON: Raw client data (Buffer)
65
- // - authenticatorData: Authenticator data (Buffer)
66
- // - signature: Assertion signature (Buffer)
67
- // - userHandle: User handle (Buffer)
68
- // - prf: [Buffer | null, Buffer | null] - PRF output (if supported)
69
- // - largeBlob: Buffer | null - Large blob data (if supported)
131
+ if (result.success) {
132
+ console.log("Authentication successful!");
133
+ console.log("Credential ID:", result.data.credentialId);
134
+ console.log("Signature:", result.data.signature);
135
+ // Result contains base64url-encoded strings ready to send to server
136
+ } else {
137
+ console.error("Authentication failed:", result.error);
138
+ }
70
139
  }
71
140
  ```
72
141
 
73
142
  ## API Reference
74
143
 
75
- ### `getCredential(rpid, challenge, origin, allowedCredentialIds, userVerificationPreference)`
144
+ ### `createCredential(publicKeyOptions, additionalOptions)`
76
145
 
77
- Performs a WebAuthn assertion (authentication) using available platform and cross-platform authenticators.
146
+ Creates and registers a new WebAuthn credential using available platform and cross-platform authenticators.
147
+
148
+ **Note:** You can plug any `publicKeyOptions` from the standard `navigator.credentials.create({ publicKey: ... })` directly into this function.
78
149
 
79
150
  #### Parameters
80
151
 
81
- - **`rpid: string`** - The Relying Party ID (typically your domain)
82
- - **`challenge: Buffer`** - The challenge from your server (32+ bytes recommended)
83
- - **`origin: string`** - The origin where the credential assertion will be used (e.g., "https://example.com")
84
- - **`allowedCredentialIds: Buffer[]`** - Optional array of credential IDs the user can use (empty = all registered credentials)
85
- - **`userVerificationPreference?: UserVerificationPreference`** - Optional preference for user verification. Can be:
86
- - `"preferred"` - User verification is preferred but not required (default)
87
- - `"required"` - User verification is required
88
- - `"discouraged"` - User verification should be discouraged
152
+ ##### `publicKeyOptions: PublicKeyCredentialCreationOptions`
153
+
154
+ Standard W3C WebAuthn credential creation options. See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialCreationOptions) for more details.
155
+
156
+ ##### `additionalOptions: WebauthnCreateRequestOptions`
157
+
158
+ Additional options specific to the Electron environment:
159
+
160
+ - **`currentOrigin: string`** (required) - The origin of the requesting document (e.g., "https://example.com")
161
+ - **`topFrameOrigin: string | undefined`** - The origin of the top frame (for iframe support). Set to `currentOrigin` if not in an iframe
162
+ - **`nativeWindowHandle: Buffer`** (required) - Native window handle from `BrowserWindow.getNativeWindowHandle()`, or a pointer to a NSView object
163
+ - **`isPublicSuffix?: (domain: string) => boolean`** - Optional function to check if a domain is a public suffix (e.g., "com", "co.uk"). Strongly recommended for security
89
164
 
90
165
  #### Returns
91
166
 
92
- `Promise<GetCredentialResult>` - Resolves with the assertion result
167
+ `Promise<CreateCredentialResult>` - Resolves with the registration result (that you can transform into a `PublicKeyCredential` object) or error
93
168
 
94
- #### Result Type
169
+ #### Result Types
95
170
 
96
171
  ```typescript
97
- interface GetCredentialResult {
98
- id: Buffer; // Credential ID
99
- authenticatorAttachment: "platform" | "cross-platform"; // Type of authenticator used
100
- clientDataJSON: Buffer; // Raw client data JSON (encoded as UTF-8 bytes)
101
- authenticatorData: Buffer; // Authenticator data from the device
102
- signature: Buffer; // Digital signature from the authenticator
103
- userHandle: Buffer; // User handle from the credential
104
- prf: [Buffer | null, Buffer | null]; // PRF output (if supported by authenticator)
105
- largeBlob: Buffer | null; // Large blob data (if supported by authenticator)
172
+ type CreateCredentialResult =
173
+ | CreateCredentialSuccessResult
174
+ | CreateCredentialErrorResult;
175
+
176
+ interface CreateCredentialSuccessResult {
177
+ success: true;
178
+ data: {
179
+ credentialId: string; // Base64url-encoded credential ID
180
+ clientDataJSON: string; // Base64url-encoded client data
181
+ attestationObject: string; // Base64url-encoded attestation object
182
+ authData: string; // Base64url-encoded authenticator data
183
+ publicKey: string; // Base64url-encoded public key (COSE format)
184
+ publicKeyAlgorithm: number; // COSE algorithm identifier (e.g., -7 for ES256)
185
+ transports: string[]; // Available transports (e.g., ["internal", "usb"])
186
+ extensions: {
187
+ credProps?: {
188
+ rk: boolean; // True if credential is a resident key
189
+ };
190
+ prf?: {
191
+ enabled?: boolean; // True if PRF is supported
192
+ results?: {
193
+ first?: string; // Base64url-encoded PRF output
194
+ second?: string; // Base64url-encoded PRF output (if provided)
195
+ };
196
+ };
197
+ largeBlob?: {
198
+ supported?: boolean; // True if large blob is supported
199
+ };
200
+ };
201
+ };
202
+ }
203
+
204
+ interface CreateCredentialErrorResult {
205
+ success: false;
206
+ error:
207
+ | "TypeError"
208
+ | "AbortError"
209
+ | "NotAllowedError"
210
+ | "SecurityError"
211
+ | "InvalidStateError";
106
212
  }
107
213
  ```
108
214
 
109
- #### User Verification Preference Type
215
+ ### `getCredential(publicKeyOptions, additionalOptions)`
216
+
217
+ Performs a WebAuthn assertion (authentication) using available platform and cross-platform authenticators.
218
+
219
+ **Note:** You can plug any `publicKeyOptions` from the standard `navigator.credentials.get({ publicKey: ... })` directly into this function.
220
+
221
+ #### Parameters
222
+
223
+ ##### `publicKeyOptions: PublicKeyCredentialRequestOptions`
224
+
225
+ Standard W3C WebAuthn credential request options. See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialRequestOptions) for more details.
226
+
227
+ ##### `additionalOptions: WebauthnGetRequestOptions`
228
+
229
+ Additional options specific to the Electron environment:
230
+
231
+ - **`currentOrigin: string`** (required) - The origin of the requesting document (e.g., "https://example.com")
232
+ - **`topFrameOrigin: string | undefined`** - The origin of the top frame (for iframe support). Set to `currentOrigin` if not in an iframe
233
+ - **`nativeWindowHandle: Buffer`** (required) - Native window handle from `BrowserWindow.getNativeWindowHandle()`, or a pointer to a NSView object
234
+ - **`isPublicSuffix?: (domain: string) => boolean`** - Optional function to check if a domain is a public suffix (e.g., "com", "co.uk"). Strongly recommended for security. Use a library like `tldts` for implementation
235
+
236
+ #### Returns
237
+
238
+ `Promise<GetCredentialResult>` - Resolves with the assertion result (that you can transform into a `PublicKeyCredential` object) or error
239
+
240
+ #### Result Types
110
241
 
111
242
  ```typescript
112
- type UserVerificationPreference = "preferred" | "required" | "discouraged";
243
+ type GetCredentialResult =
244
+ | GetCredentialSuccessResult
245
+ | GetCredentialErrorResult;
246
+
247
+ interface GetCredentialSuccessResult {
248
+ success: true;
249
+ data: {
250
+ credentialId: string; // Base64url-encoded credential ID
251
+ clientDataJSON: string; // Base64url-encoded client data
252
+ authenticatorData: string; // Base64url-encoded authenticator data
253
+ signature: string; // Base64url-encoded signature
254
+ userHandle: string; // Base64url-encoded user handle
255
+ extensions?: {
256
+ prf?: {
257
+ results?: {
258
+ first: string; // Base64url-encoded PRF output
259
+ second?: string; // Base64url-encoded PRF output (if provided)
260
+ };
261
+ };
262
+ largeBlob?: {
263
+ blob?: string; // Base64url-encoded blob data (if read)
264
+ written?: boolean; // True if write succeeded (if write was requested)
265
+ };
266
+ };
267
+ };
268
+ }
269
+
270
+ interface GetCredentialErrorResult {
271
+ success: false;
272
+ error: "TypeError" | "AbortError" | "NotAllowedError" | "SecurityError";
273
+ }
113
274
  ```
114
275
 
115
276
  ## Architecture
116
277
 
117
- This library implements the WebAuthn standard using native APIs. Under the hood it handles all the complexity of macOS's authentication system, so you just call `getCredential()` with your challenge and get back the signed assertion.
278
+ This library implements the W3C WebAuthn standard using Apple's native AuthenticationServices framework. It provides a standards-compliant API that works seamlessly with existing WebAuthn servers and libraries. Under the hood, it:
279
+
280
+ - Validates origins and Relying Party IDs according to the WebAuthn specification
281
+ - Generates proper client data with `crossOrigin` field support (fixing Apple's default behavior)
282
+ - Handles both platform (Touch ID/Face ID) and cross-platform (security keys) authenticators simultaneously
283
+ - Properly manages WebAuthn extensions (PRF, Large Blob)
284
+ - Returns base64url-encoded values ready to send to your server for verification
118
285
 
119
286
  ## Usage Examples
120
287
 
288
+ ### Basic Registration
289
+
290
+ ```typescript
291
+ import { createCredential } from "electron-webauthn";
292
+ import { app, BrowserWindow } from "electron";
293
+
294
+ let mainWindow: BrowserWindow;
295
+
296
+ app.on("ready", () => {
297
+ mainWindow = new BrowserWindow({
298
+ width: 800,
299
+ height: 600,
300
+ webPreferences: {
301
+ nodeIntegration: false,
302
+ contextIsolation: true,
303
+ preload: "./preload.js",
304
+ },
305
+ });
306
+ mainWindow.loadURL("https://myapp.com");
307
+ });
308
+
309
+ // In your preload script or main process
310
+ export async function registerUser(
311
+ challenge: ArrayBuffer,
312
+ userId: ArrayBuffer,
313
+ userName: string,
314
+ userDisplayName: string
315
+ ) {
316
+ const nativeHandle = mainWindow.getNativeWindowHandle();
317
+
318
+ const result = await createCredential(
319
+ {
320
+ challenge: challenge,
321
+ rp: {
322
+ name: "My App",
323
+ id: "myapp.com",
324
+ },
325
+ user: {
326
+ id: userId,
327
+ name: userName,
328
+ displayName: userDisplayName,
329
+ },
330
+ pubKeyCredParams: [
331
+ { type: "public-key", alg: -7 }, // ES256
332
+ { type: "public-key", alg: -257 }, // RS256
333
+ ],
334
+ authenticatorSelection: {
335
+ userVerification: "preferred",
336
+ },
337
+ },
338
+ {
339
+ currentOrigin: "https://myapp.com",
340
+ topFrameOrigin: "https://myapp.com",
341
+ nativeWindowHandle: nativeHandle,
342
+ }
343
+ );
344
+
345
+ return result;
346
+ }
347
+ ```
348
+
121
349
  ### Basic Authentication
122
350
 
123
351
  ```typescript
124
352
  import { getCredential } from "electron-webauthn";
125
- import { getPointer } from "objc-js";
126
353
  import { app, BrowserWindow } from "electron";
127
354
 
128
355
  let mainWindow: BrowserWindow;
129
356
 
130
357
  app.on("ready", () => {
131
358
  mainWindow = new BrowserWindow({
132
- webPreferences: { preload: "./preload.js" },
359
+ width: 800,
360
+ height: 600,
361
+ webPreferences: {
362
+ nodeIntegration: false,
363
+ contextIsolation: true,
364
+ preload: "./preload.js",
365
+ },
133
366
  });
367
+ mainWindow.loadURL("https://myapp.com");
134
368
  });
135
369
 
136
370
  // In your preload script or main process
137
- export async function authenticateUser(challenge: Buffer) {
138
- const nativeHandle = getPointer(mainWindow.getNativeWindowHandle());
139
- return getCredential("myapp.com", challenge, "https://myapp.com", []);
371
+ export async function authenticateUser(challenge: ArrayBuffer) {
372
+ const nativeHandle = mainWindow.getNativeWindowHandle();
373
+
374
+ const result = await getCredential(
375
+ {
376
+ challenge: challenge,
377
+ rpId: "myapp.com",
378
+ userVerification: "preferred",
379
+ },
380
+ {
381
+ currentOrigin: "https://myapp.com",
382
+ topFrameOrigin: "https://myapp.com",
383
+ nativeWindowHandle: nativeHandle,
384
+ }
385
+ );
386
+
387
+ return result;
140
388
  }
141
389
  ```
142
390
 
@@ -145,99 +393,456 @@ export async function authenticateUser(challenge: Buffer) {
145
393
  ```typescript
146
394
  // Only allow specific registered credentials
147
395
  const result = await getCredential(
148
- "myapp.com",
149
- challenge,
150
- "https://myapp.com",
151
- [credentialId1, credentialId2] // User can only use these credentials
396
+ {
397
+ challenge: challenge,
398
+ rpId: "myapp.com",
399
+ allowCredentials: [
400
+ { type: "public-key", id: credentialId1 },
401
+ { type: "public-key", id: credentialId2 },
402
+ ],
403
+ },
404
+ {
405
+ currentOrigin: "https://myapp.com",
406
+ topFrameOrigin: "https://myapp.com",
407
+ nativeWindowHandle: nativeHandle,
408
+ }
152
409
  );
153
410
  ```
154
411
 
155
- ### With User Verification Requirement
412
+ ### With Required User Verification
156
413
 
157
414
  ```typescript
158
415
  // Require user verification (e.g., for sensitive operations)
159
416
  const result = await getCredential(
160
- "myapp.com",
161
- challenge,
162
- "https://myapp.com",
163
- [], // No credential restrictions
164
- "required" // Require user verification (biometric or PIN)
417
+ {
418
+ challenge: challenge,
419
+ rpId: "myapp.com",
420
+ userVerification: "required", // Force biometric or PIN
421
+ },
422
+ {
423
+ currentOrigin: "https://myapp.com",
424
+ topFrameOrigin: "https://myapp.com",
425
+ nativeWindowHandle: nativeHandle,
426
+ }
427
+ );
428
+ ```
429
+
430
+ ### Creating Resident Keys (Discoverable Credentials)
431
+
432
+ ```typescript
433
+ // Create a discoverable credential that can be used without specifying allowCredentials
434
+ const result = await createCredential(
435
+ {
436
+ challenge: challenge,
437
+ rp: { name: "My App", id: "myapp.com" },
438
+ user: {
439
+ id: userId,
440
+ name: userName,
441
+ displayName: userDisplayName,
442
+ },
443
+ pubKeyCredParams: [
444
+ { type: "public-key", alg: -7 },
445
+ { type: "public-key", alg: -257 },
446
+ ],
447
+ authenticatorSelection: {
448
+ residentKey: "required", // Require resident key
449
+ userVerification: "required", // Usually combined with resident keys
450
+ },
451
+ },
452
+ {
453
+ currentOrigin: "https://myapp.com",
454
+ topFrameOrigin: "https://myapp.com",
455
+ nativeWindowHandle: nativeHandle,
456
+ }
165
457
  );
166
458
  ```
167
459
 
168
- ### Using PRF Output
460
+ ### Preventing Duplicate Registrations
169
461
 
170
462
  ```typescript
171
- // Get credential with PRF support
463
+ // Prevent user from registering the same authenticator multiple times
464
+ const result = await createCredential(
465
+ {
466
+ challenge: challenge,
467
+ rp: { name: "My App", id: "myapp.com" },
468
+ user: {
469
+ id: userId,
470
+ name: userName,
471
+ displayName: userDisplayName,
472
+ },
473
+ pubKeyCredParams: [
474
+ { type: "public-key", alg: -7 },
475
+ { type: "public-key", alg: -257 },
476
+ ],
477
+ excludeCredentials: [
478
+ // List of credentials already registered for this user
479
+ { type: "public-key", id: existingCredentialId1 },
480
+ { type: "public-key", id: existingCredentialId2 },
481
+ ],
482
+ },
483
+ {
484
+ currentOrigin: "https://myapp.com",
485
+ topFrameOrigin: "https://myapp.com",
486
+ nativeWindowHandle: nativeHandle,
487
+ }
488
+ );
489
+
490
+ // If user tries to use an excluded authenticator, you'll get:
491
+ // { success: false, error: "InvalidStateError" }
492
+ ```
493
+
494
+ ### Creating Credentials with PRF Extension
495
+
496
+ ```typescript
497
+ // Register a credential with PRF support and immediately evaluate it
498
+ const prfSalt = crypto.getRandomValues(new Uint8Array(32));
499
+
500
+ const result = await createCredential(
501
+ {
502
+ challenge: challenge,
503
+ rp: { name: "My App", id: "myapp.com" },
504
+ user: {
505
+ id: userId,
506
+ name: userName,
507
+ displayName: userDisplayName,
508
+ },
509
+ pubKeyCredParams: [
510
+ { type: "public-key", alg: -7 },
511
+ { type: "public-key", alg: -257 },
512
+ ],
513
+ extensions: {
514
+ prf: {
515
+ eval: {
516
+ first: prfSalt,
517
+ },
518
+ },
519
+ },
520
+ },
521
+ {
522
+ currentOrigin: "https://myapp.com",
523
+ topFrameOrigin: "https://myapp.com",
524
+ nativeWindowHandle: nativeHandle,
525
+ }
526
+ );
527
+
528
+ if (result.success && result.data.extensions?.prf?.results?.first) {
529
+ console.log(
530
+ "PRF is supported and evaluated:",
531
+ result.data.extensions.prf.results.first
532
+ );
533
+ // Use this PRF output as an encryption key
534
+ }
535
+ ```
536
+
537
+ ### Using PRF Extension (Authentication)
538
+
539
+ The PRF (Pseudo-Random Function) extension allows you to derive cryptographic secrets from credentials:
540
+
541
+ ```typescript
542
+ // Generate a random salt for PRF
543
+ const prfSalt = crypto.getRandomValues(new Uint8Array(32));
544
+
172
545
  const result = await getCredential(
173
- "myapp.com",
174
- challenge,
175
- "https://myapp.com",
176
- []
546
+ {
547
+ challenge: challenge,
548
+ rpId: "myapp.com",
549
+ extensions: {
550
+ prf: {
551
+ eval: {
552
+ first: prfSalt,
553
+ // second: optionalSecondSalt, // Optional second output
554
+ },
555
+ },
556
+ },
557
+ },
558
+ {
559
+ currentOrigin: "https://myapp.com",
560
+ topFrameOrigin: "https://myapp.com",
561
+ nativeWindowHandle: nativeHandle,
562
+ }
177
563
  );
178
564
 
179
- // PRF output is available in the result
180
- const [prfFirst, prfSecond] = result.prf;
181
- if (prfFirst) {
182
- // Use PRF output for additional cryptographic operations
183
- console.log("PRF first output:", prfFirst);
565
+ if (result.success && result.data.extensions?.prf?.results) {
566
+ const prfOutput = result.data.extensions.prf.results.first;
567
+ // Use prfOutput as a cryptographic key for encryption, etc.
568
+ console.log("PRF output:", prfOutput);
184
569
  }
185
570
  ```
186
571
 
187
- ### Server-Side Verification
572
+ ### Using PRF with Per-Credential Evaluation
188
573
 
189
- After getting the assertion result, verify it on your server:
574
+ ```typescript
575
+ import { bufferToBase64Url } from "./helpers"; // You'll need to implement this
576
+
577
+ const result = await getCredential(
578
+ {
579
+ challenge: challenge,
580
+ rpId: "myapp.com",
581
+ allowCredentials: [
582
+ { type: "public-key", id: credentialId1 },
583
+ { type: "public-key", id: credentialId2 },
584
+ ],
585
+ extensions: {
586
+ prf: {
587
+ evalByCredential: {
588
+ [bufferToBase64Url(credentialId1)]: {
589
+ first: new Uint8Array(32), // Different salt for credential 1
590
+ },
591
+ [bufferToBase64Url(credentialId2)]: {
592
+ first: new Uint8Array(32), // Different salt for credential 2
593
+ },
594
+ },
595
+ },
596
+ },
597
+ },
598
+ {
599
+ currentOrigin: "https://myapp.com",
600
+ topFrameOrigin: "https://myapp.com",
601
+ nativeWindowHandle: nativeHandle,
602
+ }
603
+ );
604
+ ```
605
+
606
+ ### Reading and Writing Large Blobs
190
607
 
191
608
  ```typescript
192
- // Server-side (Node.js, Python, etc.)
193
- // 1. Verify the signature using the public key from the credential registration
194
- // 2. Check that the challenge matches what you sent
195
- // 3. Verify the authenticator data flags
196
- // 4. Check the user handle matches the authenticated user
609
+ // Reading a large blob
610
+ const readResult = await getCredential(
611
+ {
612
+ challenge: challenge,
613
+ rpId: "myapp.com",
614
+ extensions: {
615
+ largeBlob: {
616
+ read: true,
617
+ },
618
+ },
619
+ },
620
+ {
621
+ currentOrigin: "https://myapp.com",
622
+ topFrameOrigin: "https://myapp.com",
623
+ nativeWindowHandle: nativeHandle,
624
+ }
625
+ );
626
+
627
+ if (readResult.success && readResult.data.extensions?.largeBlob?.blob) {
628
+ console.log("Large blob data:", readResult.data.extensions.largeBlob.blob);
629
+ }
630
+
631
+ // Writing a large blob
632
+ const dataToWrite = new TextEncoder().encode("Secret data");
633
+ const writeResult = await getCredential(
634
+ {
635
+ challenge: challenge,
636
+ rpId: "myapp.com",
637
+ extensions: {
638
+ largeBlob: {
639
+ write: dataToWrite,
640
+ },
641
+ },
642
+ },
643
+ {
644
+ currentOrigin: "https://myapp.com",
645
+ topFrameOrigin: "https://myapp.com",
646
+ nativeWindowHandle: nativeHandle,
647
+ }
648
+ );
649
+
650
+ if (writeResult.success && writeResult.data.extensions?.largeBlob?.written) {
651
+ console.log("Large blob written successfully");
652
+ }
197
653
  ```
198
654
 
199
- ## Error Handling
655
+ ### With Public Suffix List Validation
656
+
657
+ For enhanced security, use a public suffix list library like `tldts`:
658
+
659
+ ```typescript
660
+ import { getPublicSuffix } from "tldts";
661
+
662
+ const result = await getCredential(
663
+ {
664
+ challenge: challenge,
665
+ rpId: "myapp.com",
666
+ },
667
+ {
668
+ currentOrigin: "https://myapp.com",
669
+ topFrameOrigin: "https://myapp.com",
670
+ nativeWindowHandle: nativeHandle,
671
+ isPublicSuffix: (domain) => {
672
+ const suffix = getPublicSuffix(domain);
673
+ return suffix === domain;
674
+ },
675
+ }
676
+ );
677
+ ```
200
678
 
201
- The `getCredential` promise will reject if:
679
+ ### Supporting Cross-Origin Iframes
202
680
 
203
- - The user cancels the authentication prompt
204
- - No valid credentials are available
205
- - The authenticator fails
206
- - The native window is invalid
681
+ If your app loads WebAuthn requests from an iframe:
207
682
 
208
683
  ```typescript
209
- try {
210
- const result = await getCredential(
211
- "example.com",
212
- challenge,
213
- "https://example.com",
214
- []
215
- );
216
- } catch (error) {
217
- console.error("Authentication failed:", error.message);
218
- // Handle error appropriately
684
+ const result = await getCredential(
685
+ {
686
+ challenge: challenge,
687
+ rpId: "myapp.com",
688
+ },
689
+ {
690
+ currentOrigin: "https://auth.myapp.com", // The iframe's origin
691
+ topFrameOrigin: "https://myapp.com", // The parent page's origin
692
+ nativeWindowHandle: nativeHandle,
693
+ }
694
+ );
695
+ ```
696
+
697
+ ## Error Handling
698
+
699
+ Both `createCredential` and `getCredential` functions return a result object with a `success` field. Always check this field:
700
+
701
+ ```typescript
702
+ const result = await createCredential(publicKeyOptions, additionalOptions);
703
+ // or
704
+ const result = await getCredential(publicKeyOptions, additionalOptions);
705
+
706
+ if (!result.success) {
707
+ // Handle specific error types
708
+ switch (result.error) {
709
+ case "TypeError":
710
+ console.error("Invalid parameters provided");
711
+ break;
712
+ case "NotAllowedError":
713
+ console.error("User cancelled or operation not allowed");
714
+ break;
715
+ case "SecurityError":
716
+ console.error("Origin or rpId validation failed");
717
+ break;
718
+ case "AbortError":
719
+ console.error("Operation was aborted");
720
+ break;
721
+ case "InvalidStateError":
722
+ console.error(
723
+ "Authenticator is in invalid state (e.g., credential already registered)"
724
+ );
725
+ break;
726
+ }
727
+ return;
219
728
  }
729
+
730
+ // Success - process the credential
731
+ console.log("Credential ID:", result.data.credentialId);
220
732
  ```
221
733
 
222
- **Supported:**
734
+ ### Common Error Scenarios
223
735
 
736
+ #### For Both Registration and Authentication
737
+
738
+ - **TypeError**: Invalid parameter types (missing required fields, wrong data types)
739
+ - **NotAllowedError**: User cancelled the prompt or the authenticator failed
740
+ - **SecurityError**: Origin doesn't match rpId, invalid origin format, or rpId is a public suffix
741
+ - **AbortError**: Operation timeout or explicitly aborted
742
+
743
+ #### Registration-Specific (`createCredential`)
744
+
745
+ - **InvalidStateError**: The authenticator attempted to register a credential that matches one in the `excludeCredentials` list (prevents duplicate registrations)
746
+
747
+ #### Authentication-Specific (`getCredential`)
748
+
749
+ - **NotAllowedError**: No valid credentials available for the specified rpId
750
+
751
+ ## Feature Support
752
+
753
+ **Currently Supported:**
754
+
755
+ - ✅ WebAuthn credential creation (registration/attestation)
224
756
  - ✅ WebAuthn assertions (authentication with existing credentials)
225
- - ✅ Cross-platform authenticators (external security keys)
757
+ - ✅ Cross-platform authenticators (external security keys like YubiKey)
226
758
  - ✅ Platform authenticators (Touch ID, Face ID)
227
- - ✅ PRF (Pseudo-Random Function) output
228
- - ✅ Large Blob support
759
+ - ✅ Discoverable credentials (resident keys)
760
+ - ✅ Attestation formats (none, indirect, direct)
761
+ - ✅ Duplicate credential prevention with `excludeCredentials`
762
+ - ✅ PRF (Pseudo-Random Function) extension
763
+ - ✅ Global evaluation (`eval`) for both registration and authentication
764
+ - ✅ Per-credential evaluation (`evalByCredential`) for authentication
765
+ - ✅ Large Blob extension
766
+ - ✅ Support indication during registration
767
+ - ✅ Reading blobs during authentication
768
+ - ✅ Writing blobs during authentication
769
+ - ✅ credProps extension (credential properties)
770
+ - ✅ User verification preferences (required, preferred, discouraged)
771
+ - ✅ Credential filtering with `allowCredentials`
772
+ - ✅ Proper origin and rpId validation
773
+ - ✅ Cross-origin iframe support
774
+ - ✅ W3C WebAuthn-compliant client data generation
775
+
776
+ **Not Yet Supported:**
777
+
778
+ - ❌ Conditional UI (autofill/conditional mediation)
779
+ - ❌ Other WebAuthn extensions (credProtect, minPinLength, hmac-secret, etc.)
780
+
781
+ ## Best Practices
782
+
783
+ ### Security Recommendations
784
+
785
+ 1. **Always use HTTPS origins** in production (unless testing on `localhost`)
786
+ 2. **Implement public suffix list validation** using `tldts` or similar library
787
+ 3. **Validate rpId carefully** - it should match your domain
788
+ 4. **Generate strong challenges** - use at least 32 random bytes from a CSPRNG
789
+ 5. **Verify assertions on your server** - never trust client-side validation alone
790
+ 6. **Store credential public keys securely** - needed for signature verification
791
+ 7. **Implement proper timeout handling** - don't leave prompts open indefinitely
792
+
793
+ ### Performance Tips
794
+
795
+ 1. **Cache native window handles** - no need to get them on every call
796
+ 2. **Reuse challenge buffers** when possible
797
+ 3. **Set appropriate timeouts** - shorter for better UX, longer for hardware keys
798
+ 4. **Handle user cancellation gracefully** - don't retry automatically
799
+
800
+ ### TypeScript Types
801
+
802
+ This library exports all necessary TypeScript types. Import them for type safety:
229
803
 
230
- **Not Supported:**
804
+ ```typescript
805
+ import type {
806
+ // Registration types
807
+ CreateCredentialResult,
808
+ CreateCredentialSuccessData,
809
+ PublicKeyCredentialCreationOptions,
810
+
811
+ // Authentication types
812
+ GetCredentialResult,
813
+ GetCredentialSuccessData,
814
+ PublicKeyCredentialRequestOptions,
815
+
816
+ // Shared types
817
+ AuthenticationExtensionsClientInputs,
818
+ PRFInput,
819
+ } from "electron-webauthn";
820
+ ```
821
+
822
+ ## Debugging
823
+
824
+ Enable detailed logging by checking the console output. The library logs warnings for:
231
825
 
232
- - Credential registration (attestation)
233
- - Discoverable credentials
826
+ - PRF extension enabled but no input values provided
827
+ - Large blob write enabled but no data provided
828
+ - Authorization errors with native error messages
829
+
830
+ ## Known Limitations
831
+
832
+ - **PRF and Large Blob extensions**: Only supported on platform authenticators (Touch ID/Face ID), not security keys on macOS
833
+ - **Attestation formats**: macOS typically returns "none" attestation even when "direct" or "indirect" is requested, unless the authenticator specifically supports it
234
834
 
235
835
  ## License
236
836
 
237
837
  See [LICENSE](./LICENSE) file for details.
238
838
 
839
+ ## Contributing
840
+
841
+ Contributions are welcome! Please feel free to submit issues and pull requests.
842
+
239
843
  ## Resources
240
844
 
241
- - [WebAuthn Specification](https://www.w3.org/TR/webauthn-2/)
845
+ - [W3C WebAuthn Level 3 Specification](https://www.w3.org/TR/webauthn-3/)
846
+ - [WebAuthn Guide](https://webauthn.guide/)
242
847
  - [Electron Documentation](https://www.electronjs.org/docs)
243
848
  - [objc-js Library](https://github.com/iamEvanYT/objc-js)