electron-webauthn 0.0.15 → 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.
package/README.md CHANGED
@@ -4,9 +4,9 @@ Add native WebAuthn/FIDO2 support to Electron on macOS using its AuthenticationS
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 using W3C WebAuthn-compliant APIs.
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
 
@@ -14,7 +14,9 @@ This package provides JavaScript bindings to Apple's AuthenticationServices fram
14
14
  - Support for platform authenticators (Touch ID, Face ID)
15
15
  - Support for cross-platform authenticators (external security keys)
16
16
  - TypeScript first with complete type definitions
17
- - W3C WebAuthn-compliant API
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
18
20
  - Seamless integration with Electron's native window system
19
21
  - PRF (Pseudo-Random Function) extension support
20
22
  - Large Blob extension support for reading/writing credential-specific data
@@ -22,6 +24,7 @@ This package provides JavaScript bindings to Apple's AuthenticationServices fram
22
24
  - Proper origin validation with public suffix list support
23
25
  - Cross-origin iframe support with `topFrameOrigin`
24
26
  - Per-credential PRF evaluation with `evalByCredential`
27
+ - Resident key (discoverable credential) support
25
28
 
26
29
  ## Installation
27
30
 
@@ -41,17 +44,75 @@ yarn add electron-webauthn
41
44
 
42
45
  ## Quick Start
43
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
+
51
+ ```typescript
52
+ import { createCredential } from "electron-webauthn";
53
+ import { BrowserWindow } from "electron";
54
+
55
+ // In your Electron main process or preload script
56
+ async function register(window: BrowserWindow, challenge: ArrayBuffer) {
57
+ // Get the native window handle from your BrowserWindow
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
+ }
90
+ );
91
+
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
+
44
105
  ```typescript
45
106
  import { getCredential } from "electron-webauthn";
46
- import { getPointer } from "objc-js";
47
107
  import { BrowserWindow } from "electron";
48
108
 
49
109
  // In your Electron main process or preload script
50
110
  async function authenticate(window: BrowserWindow, challenge: ArrayBuffer) {
51
111
  // Get the native window handle from your BrowserWindow
52
- const nativeWindowHandle = getPointer(window.getNativeWindowHandle());
112
+ const nativeWindowHandle = window.getNativeWindowHandle();
53
113
 
54
114
  // Call getCredential with W3C WebAuthn-compliant options
115
+ // You can plug any publicKeyOptions from navigator.credentials.get() here
55
116
  const result = await getCredential(
56
117
  {
57
118
  challenge: challenge,
@@ -59,17 +120,6 @@ async function authenticate(window: BrowserWindow, challenge: ArrayBuffer) {
59
120
  timeout: 60000, // Optional: 60 seconds
60
121
  userVerification: "preferred", // Optional: "preferred" | "required" | "discouraged"
61
122
  allowCredentials: [], // Optional: restrict to specific credentials
62
- extensions: {
63
- // Optional extensions
64
- prf: {
65
- eval: {
66
- first: new Uint8Array(32), // 32 bytes
67
- },
68
- },
69
- largeBlob: {
70
- read: true,
71
- },
72
- },
73
123
  },
74
124
  {
75
125
  currentOrigin: "https://example.com",
@@ -91,55 +141,88 @@ async function authenticate(window: BrowserWindow, challenge: ArrayBuffer) {
91
141
 
92
142
  ## API Reference
93
143
 
94
- ### `getCredential(publicKeyOptions, additionalOptions)`
144
+ ### `createCredential(publicKeyOptions, additionalOptions)`
145
+
146
+ Creates and registers a new WebAuthn credential using available platform and cross-platform authenticators.
95
147
 
96
- Performs a WebAuthn assertion (authentication) using available platform and cross-platform authenticators. This function follows the W3C WebAuthn specification.
148
+ **Note:** You can plug any `publicKeyOptions` from the standard `navigator.credentials.create({ publicKey: ... })` directly into this function.
97
149
 
98
150
  #### Parameters
99
151
 
100
- ##### `publicKeyOptions: PublicKeyCredentialRequestOptions`
152
+ ##### `publicKeyOptions: PublicKeyCredentialCreationOptions`
101
153
 
102
- You can plug the `publicKeyOptions` from `navigator.credentials.get()` directly onto this function.
154
+ Standard W3C WebAuthn credential creation options. See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialCreationOptions) for more details.
103
155
 
104
- Standard W3C WebAuthn credential request options:
156
+ ##### `additionalOptions: WebauthnCreateRequestOptions`
105
157
 
106
- - **`challenge: BufferSource`** (required) - The challenge from your server (32+ bytes recommended)
107
- - **`rpId: string`** (required) - The Relying Party ID (typically your domain, e.g., "example.com")
108
- - **`timeout?: number`** - Timeout in milliseconds (default: 10 minutes, max: 1 hour)
109
- - **`userVerification?: string`** - User verification preference:
110
- - `"preferred"` - User verification is preferred but not required (default)
111
- - `"required"` - User verification is required (e.g., biometric or PIN)
112
- - `"discouraged"` - User verification should be discouraged
113
- - **`allowCredentials?: PublicKeyCredentialDescriptor[]`** - Optional array to restrict allowed credentials:
114
- ```typescript
115
- {
116
- type: "public-key",
117
- id: BufferSource, // Credential ID from registration
118
- }
119
- ```
120
- - **`extensions?: AuthenticationExtensionsClientInputs`** - Optional WebAuthn extensions:
121
- - **`prf`** - Pseudo-Random Function extension:
122
- ```typescript
123
- {
124
- eval?: {
125
- first: BufferSource, // 32+ bytes
126
- second?: BufferSource // 32+ bytes (optional)
127
- },
128
- evalByCredential?: {
129
- [base64UrlCredentialId: string]: {
130
- first: BufferSource,
131
- second?: BufferSource
132
- }
133
- }
134
- }
135
- ```
136
- - **`largeBlob`** - Large Blob extension:
137
- ```typescript
138
- {
139
- read?: boolean, // Read existing blob
140
- write?: BufferSource // Write new blob data
141
- }
142
- ```
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
164
+
165
+ #### Returns
166
+
167
+ `Promise<CreateCredentialResult>` - Resolves with the registration result (that you can transform into a `PublicKeyCredential` object) or error
168
+
169
+ #### Result Types
170
+
171
+ ```typescript
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";
212
+ }
213
+ ```
214
+
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.
143
226
 
144
227
  ##### `additionalOptions: WebauthnGetRequestOptions`
145
228
 
@@ -147,12 +230,12 @@ Additional options specific to the Electron environment:
147
230
 
148
231
  - **`currentOrigin: string`** (required) - The origin of the requesting document (e.g., "https://example.com")
149
232
  - **`topFrameOrigin: string | undefined`** - The origin of the top frame (for iframe support). Set to `currentOrigin` if not in an iframe
150
- - **`nativeWindowHandle: Buffer`** (required) - Native window handle from `BrowserWindow.getNativeWindowHandle()` wrapped with `getPointer()` from `objc-js`
233
+ - **`nativeWindowHandle: Buffer`** (required) - Native window handle from `BrowserWindow.getNativeWindowHandle()`, or a pointer to a NSView object
151
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
152
235
 
153
236
  #### Returns
154
237
 
155
- `Promise<GetCredentialResult>` - Resolves with the assertion result or error
238
+ `Promise<GetCredentialResult>` - Resolves with the assertion result (that you can transform into a `PublicKeyCredential` object) or error
156
239
 
157
240
  #### Result Types
158
241
 
@@ -202,11 +285,71 @@ This library implements the W3C WebAuthn standard using Apple's native Authentic
202
285
 
203
286
  ## Usage Examples
204
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
+
205
349
  ### Basic Authentication
206
350
 
207
351
  ```typescript
208
352
  import { getCredential } from "electron-webauthn";
209
- import { getPointer } from "objc-js";
210
353
  import { app, BrowserWindow } from "electron";
211
354
 
212
355
  let mainWindow: BrowserWindow;
@@ -226,7 +369,7 @@ app.on("ready", () => {
226
369
 
227
370
  // In your preload script or main process
228
371
  export async function authenticateUser(challenge: ArrayBuffer) {
229
- const nativeHandle = getPointer(mainWindow.getNativeWindowHandle());
372
+ const nativeHandle = mainWindow.getNativeWindowHandle();
230
373
 
231
374
  const result = await getCredential(
232
375
  {
@@ -284,7 +427,114 @@ const result = await getCredential(
284
427
  );
285
428
  ```
286
429
 
287
- ### Using PRF Extension
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
+ }
457
+ );
458
+ ```
459
+
460
+ ### Preventing Duplicate Registrations
461
+
462
+ ```typescript
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)
288
538
 
289
539
  The PRF (Pseudo-Random Function) extension allows you to derive cryptographic secrets from credentials:
290
540
 
@@ -444,47 +694,13 @@ const result = await getCredential(
444
694
  );
445
695
  ```
446
696
 
447
- ### Server-Side Verification
448
-
449
- After getting the assertion result, verify it on your server using any WebAuthn library:
450
-
451
- ```typescript
452
- // Example with @simplewebauthn/server (Node.js)
453
- import { verifyAuthenticationResponse } from "@simplewebauthn/server";
454
-
455
- // The result.data object contains base64url-encoded values
456
- const verification = await verifyAuthenticationResponse({
457
- response: {
458
- id: result.data.credentialId,
459
- rawId: result.data.credentialId,
460
- response: {
461
- clientDataJSON: result.data.clientDataJSON,
462
- authenticatorData: result.data.authenticatorData,
463
- signature: result.data.signature,
464
- userHandle: result.data.userHandle,
465
- },
466
- type: "public-key",
467
- },
468
- expectedChallenge: "expected-challenge-from-session",
469
- expectedOrigin: "https://myapp.com",
470
- expectedRPID: "myapp.com",
471
- authenticator: {
472
- credentialID: savedCredentialId,
473
- credentialPublicKey: savedPublicKey,
474
- counter: savedCounter,
475
- },
476
- });
477
-
478
- if (verification.verified) {
479
- console.log("Authentication successful!");
480
- }
481
- ```
482
-
483
697
  ## Error Handling
484
698
 
485
- The `getCredential` function returns a result object with a `success` field. Always check this field:
699
+ Both `createCredential` and `getCredential` functions return a result object with a `success` field. Always check this field:
486
700
 
487
701
  ```typescript
702
+ const result = await createCredential(publicKeyOptions, additionalOptions);
703
+ // or
488
704
  const result = await getCredential(publicKeyOptions, additionalOptions);
489
705
 
490
706
  if (!result.success) {
@@ -494,7 +710,7 @@ if (!result.success) {
494
710
  console.error("Invalid parameters provided");
495
711
  break;
496
712
  case "NotAllowedError":
497
- console.error("User cancelled or no credentials available");
713
+ console.error("User cancelled or operation not allowed");
498
714
  break;
499
715
  case "SecurityError":
500
716
  console.error("Origin or rpId validation failed");
@@ -502,6 +718,11 @@ if (!result.success) {
502
718
  case "AbortError":
503
719
  console.error("Operation was aborted");
504
720
  break;
721
+ case "InvalidStateError":
722
+ console.error(
723
+ "Authenticator is in invalid state (e.g., credential already registered)"
724
+ );
725
+ break;
505
726
  }
506
727
  return;
507
728
  }
@@ -512,25 +733,41 @@ console.log("Credential ID:", result.data.credentialId);
512
733
 
513
734
  ### Common Error Scenarios
514
735
 
515
- - **NotAllowedError**: User cancelled the prompt, no valid credentials available, or the authenticator failed
516
- - **SecurityError**: Origin doesn't match rpId, invalid origin format, or rpId is a public suffix
736
+ #### For Both Registration and Authentication
737
+
517
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
518
741
  - **AbortError**: Operation timeout or explicitly aborted
519
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
+
520
751
  ## Feature Support
521
752
 
522
753
  **Currently Supported:**
523
754
 
755
+ - ✅ WebAuthn credential creation (registration/attestation)
524
756
  - ✅ WebAuthn assertions (authentication with existing credentials)
525
757
  - ✅ Cross-platform authenticators (external security keys like YubiKey)
526
758
  - ✅ Platform authenticators (Touch ID, Face ID)
759
+ - ✅ Discoverable credentials (resident keys)
760
+ - ✅ Attestation formats (none, indirect, direct)
761
+ - ✅ Duplicate credential prevention with `excludeCredentials`
527
762
  - ✅ PRF (Pseudo-Random Function) extension
528
- - ✅ Global evaluation (`eval`)
529
- - ✅ Per-credential evaluation (`evalByCredential`)
763
+ - ✅ Global evaluation (`eval`) for both registration and authentication
764
+ - ✅ Per-credential evaluation (`evalByCredential`) for authentication
530
765
  - ✅ Large Blob extension
531
- - ✅ Reading blobs
532
- - ✅ Writing blobs
533
- - ✅ User verification preferences
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)
534
771
  - ✅ Credential filtering with `allowCredentials`
535
772
  - ✅ Proper origin and rpId validation
536
773
  - ✅ Cross-origin iframe support
@@ -538,10 +775,8 @@ console.log("Credential ID:", result.data.credentialId);
538
775
 
539
776
  **Not Yet Supported:**
540
777
 
541
- - ❌ Credential registration (attestation) - coming soon
542
- - ❌ Discoverable credentials (resident keys)
543
- - ❌ Conditional UI
544
- - ❌ Other WebAuthn extensions (credProtect, minPinLength, etc.)
778
+ - ❌ Conditional UI (autofill/conditional mediation)
779
+ - ❌ Other WebAuthn extensions (credProtect, minPinLength, hmac-secret, etc.)
545
780
 
546
781
  ## Best Practices
547
782
 
@@ -568,9 +803,17 @@ This library exports all necessary TypeScript types. Import them for type safety
568
803
 
569
804
  ```typescript
570
805
  import type {
806
+ // Registration types
807
+ CreateCredentialResult,
808
+ CreateCredentialSuccessData,
809
+ PublicKeyCredentialCreationOptions,
810
+
811
+ // Authentication types
571
812
  GetCredentialResult,
572
813
  GetCredentialSuccessData,
573
814
  PublicKeyCredentialRequestOptions,
815
+
816
+ // Shared types
574
817
  AuthenticationExtensionsClientInputs,
575
818
  PRFInput,
576
819
  } from "electron-webauthn";
@@ -586,7 +829,8 @@ Enable detailed logging by checking the console output. The library logs warning
586
829
 
587
830
  ## Known Limitations
588
831
 
589
- - **PRF and Large Blob**: Only supported on platform authenticators (Touch ID/Face ID), not security keys on macOS
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
590
834
 
591
835
  ## License
592
836
 
@@ -1,5 +1,5 @@
1
1
  import { NobjcObject } from "objc-js";
2
- import type { ExcludeCredential } from "./handler.js";
2
+ import type { ExcludeCredential } from "./internal-handler.js";
3
3
  export interface PublicKeyCredentialParams {
4
4
  type: "public-key";
5
5
  algorithm: number;
@@ -4,6 +4,7 @@ import { NSArrayFromObjects } from "../objc/foundation/nsarray.js";
4
4
  import { NSStringFromString } from "../objc/foundation/nsstring.js";
5
5
  import { createASCPublicKeyCredentialDescriptor } from "../objc/authentication-services/as-authorization-c-public-key-credential-descriptor.js";
6
6
  import { NSNumberFromInteger } from "../objc/foundation/nsinteger.js";
7
+ import { isNumber, isObject } from "../helpers/validation.js";
7
8
  const createControllerState = new Map();
8
9
  function getObjectPointerString(self) {
9
10
  return getPointer(self).toBase64();
@@ -1,33 +1,41 @@
1
- import { type PRFInput } from "../helpers/prf.js";
2
- import { type PublicKeyCredentialParams } from "./authorization-controller.js";
3
- export interface CreateCredentialResult {
4
- credentialId: Buffer;
5
- clientDataJSON: Buffer;
6
- attestationObject: Buffer;
7
- authenticatorData: Buffer;
8
- attachment: AuthenticatorAttachment;
9
- transports: string[];
10
- isResidentKey: boolean;
1
+ export interface CreateCredentialSuccessData {
2
+ credentialId: string;
3
+ clientDataJSON: string;
4
+ attestationObject: string;
5
+ authData: string;
6
+ publicKey: string;
11
7
  publicKeyAlgorithm: number;
12
- publicKey: Buffer;
13
- isLargeBlobSupported: boolean | null;
14
- isPRFSupported: boolean | null;
15
- prfFirst: Buffer | null;
16
- prfSecond: Buffer | null;
8
+ transports: string[];
9
+ extensions: {
10
+ credProps?: {
11
+ rk: boolean;
12
+ };
13
+ prf?: {
14
+ enabled?: boolean;
15
+ results: {
16
+ first?: string;
17
+ second?: string;
18
+ };
19
+ };
20
+ largeBlob?: {
21
+ supported?: boolean;
22
+ };
23
+ };
24
+ }
25
+ interface WebauthnCreateRequestOptions {
26
+ currentOrigin: string;
27
+ topFrameOrigin: string | undefined;
28
+ isPublicSuffix?: (domain: string) => boolean;
29
+ nativeWindowHandle: Buffer;
17
30
  }
18
- type CredentialUserVerificationPreference = "required" | "preferred" | "discouraged";
19
- type CredentialAttestationPreference = "direct" | "enterprise" | "indirect" | "none";
20
- declare const VALID_EXTENSIONS: readonly ["largeBlob", "prf"];
21
- export type CredentialCreationExtensions = (typeof VALID_EXTENSIONS)[number];
22
- interface CreateCredentialAdditionalOptions {
23
- topFrameOrigin?: string;
24
- userDisplayName?: string;
25
- largeBlobSupport?: "required" | "preferred" | "unspecified";
26
- prf?: PRFInput;
31
+ interface CreateCredentialSuccessResult {
32
+ success: true;
33
+ data: CreateCredentialSuccessData;
27
34
  }
28
- export interface ExcludeCredential {
29
- id: Buffer;
30
- transports?: string[];
35
+ interface CreateCredentialErrorResult {
36
+ success: false;
37
+ error: "TypeError" | "AbortError" | "NotAllowedError" | "SecurityError" | "InvalidStateError";
31
38
  }
32
- declare function createCredential(rpid: string, challenge: Buffer, username: string, userID: Buffer, nativeWindowHandle: Buffer, origin: string, enabledExtensions: CredentialCreationExtensions[], attestation: CredentialAttestationPreference, supportedAlgorithmIdentifiers: PublicKeyCredentialParams[], excludeCredentials: ExcludeCredential[], residentKeyRequired?: boolean, userVerification?: CredentialUserVerificationPreference, additionalOptions?: CreateCredentialAdditionalOptions): Promise<CreateCredentialResult>;
33
- export { createCredential };
39
+ export type CreateCredentialResult = CreateCredentialSuccessResult | CreateCredentialErrorResult;
40
+ export declare function createCredential(publicKeyOptions: PublicKeyCredentialCreationOptions | undefined, additionalOptions: WebauthnCreateRequestOptions): Promise<CreateCredentialResult>;
41
+ export {};
@@ -1,154 +1,206 @@
1
- import { generateClientDataInfo } from "../helpers/client-data.js";
2
- import { generateWebauthnClientData } from "../helpers/client-data.js";
3
- import { PromiseWithResolvers } from "../helpers/index.js";
4
- import { encodeEC2PublicKeyToSPKI } from "../helpers/public-key.js";
5
- import { createPresentationContextProviderFromNativeWindowHandle } from "../helpers/presentation.js";
6
- import { createPRFInput } from "../helpers/prf.js";
7
- import { createAuthorizationControllerDelegate } from "../objc/authentication-services/as-authorization-controller-delegate.js";
8
- import { createPlatformPublicKeyCredentialProvider } from "../objc/authentication-services/as-authorization-platform-public-key-credential-provider.js";
9
- import { createASAuthorizationPublicKeyCredentialLargeBlobRegistrationInput } from "../objc/authentication-services/as-authorization-public-key-credential-large-blob-registration-input.js";
10
- import { ASAuthorizationPublicKeyCredentialPRFRegistrationInput, createASAuthorizationPublicKeyCredentialPRFRegistrationInput, } from "../objc/authentication-services/as-authorization-public-key-credential-prf-registration-input.js";
11
- import { ASAuthorizationPublicKeyCredentialAttestationKind } from "../objc/authentication-services/enums/as-authorization-public-key-credential-attestation-kind.js";
12
- import { ASAuthorizationPublicKeyCredentialLargeBlobSupportRequirement } from "../objc/authentication-services/enums/as-authorization-public-key-credential-large-blob-support-requirement.js";
13
- import { ASAuthorizationPublicKeyCredentialUserVerificationPreference } from "../objc/authentication-services/enums/as-authorization-public-key-credential-user-verification-preference.js";
14
- import { NSArrayFromObjects } from "../objc/foundation/nsarray.js";
15
- import { bufferFromNSDataDirect, NSDataFromBuffer, } from "../objc/foundation/nsdata.js";
16
- import { NSStringFromString } from "../objc/foundation/nsstring.js";
17
- import { removeControllerState, setControllerState, WebauthnCreateController, } from "./authorization-controller.js";
18
- import { parseAttestationObject } from "@oslojs/webauthn";
19
- import { ASAuthorizationPublicKeyCredentialAttachment } from "../objc/authentication-services/enums/as-authorization-public-key-credential-attachment.js";
20
- const VALID_EXTENSIONS = ["largeBlob", "prf"];
21
- function createCredential(rpid, challenge, username, userID, nativeWindowHandle, origin, enabledExtensions, attestation = "none", supportedAlgorithmIdentifiers, excludeCredentials, residentKeyRequired = false, userVerification = "preferred", additionalOptions = {}) {
22
- const { promise, resolve, reject } = PromiseWithResolvers();
23
- const NS_rpID = NSStringFromString(rpid);
24
- const NS_challenge = NSDataFromBuffer(challenge);
25
- const NS_username = NSStringFromString(username);
26
- const NS_userID = NSDataFromBuffer(userID);
27
- const platformProvider = createPlatformPublicKeyCredentialProvider(NS_rpID);
28
- const platformKeyRequest = platformProvider.createCredentialRegistrationRequestWithChallenge$name$userID$(NS_challenge, NS_username, NS_userID);
29
- if (enabledExtensions.includes("largeBlob")) {
30
- let supportMode;
31
- const largeBlobSupport = additionalOptions.largeBlobSupport;
32
- if (largeBlobSupport === "required") {
33
- supportMode =
34
- ASAuthorizationPublicKeyCredentialLargeBlobSupportRequirement.Required;
1
+ import { bufferSourceToBuffer, bufferToBase64Url } from "../helpers/index.js";
2
+ import { isRpIdAllowedForOrigin } from "../helpers/rpid.js";
3
+ import { isNumber, isObject, isString } from "../helpers/validation.js";
4
+ import { createCredentialInternal, } from "./internal-handler.js";
5
+ function getExtensionsConfiguration(extensionsData) {
6
+ if (!(extensionsData && typeof extensionsData === "object")) {
7
+ return {
8
+ extensions: [],
9
+ };
10
+ }
11
+ const extensions = [];
12
+ let largeBlobSupport;
13
+ if (isObject(extensionsData.largeBlob)) {
14
+ extensions.push("largeBlob");
15
+ const largeBlobConfig = extensionsData.largeBlob;
16
+ if (largeBlobConfig.support === "required") {
17
+ largeBlobSupport = "required";
18
+ }
19
+ else if (largeBlobConfig.support === "preferred") {
20
+ largeBlobSupport = "preferred";
21
+ }
22
+ }
23
+ let prf;
24
+ if (isObject(extensionsData.prf)) {
25
+ const prfEval = extensionsData.prf.eval;
26
+ if (prfEval) {
27
+ const first = bufferSourceToBuffer(prfEval.first);
28
+ const second = bufferSourceToBuffer(prfEval.second);
29
+ if (first) {
30
+ prf = {
31
+ first: first ? first : null,
32
+ second: second ? second : undefined,
33
+ };
34
+ }
35
+ else {
36
+ console.warn("[electron-webauthn] prf is enabled but prf.first is not valid, skipping PRF evaluation");
37
+ }
35
38
  }
36
- else if (largeBlobSupport === "preferred") {
37
- supportMode =
38
- ASAuthorizationPublicKeyCredentialLargeBlobSupportRequirement.Preferred;
39
+ }
40
+ return {
41
+ extensions,
42
+ largeBlobSupport,
43
+ prf,
44
+ };
45
+ }
46
+ export async function createCredential(publicKeyOptions, additionalOptions) {
47
+ if (!publicKeyOptions) {
48
+ return null;
49
+ }
50
+ const rpInfo = publicKeyOptions.rp;
51
+ if (!isObject(rpInfo)) {
52
+ return { success: false, error: "TypeError" };
53
+ }
54
+ let rpId = rpInfo.id;
55
+ if (!rpId) {
56
+ try {
57
+ const url = new URL(additionalOptions.currentOrigin);
58
+ rpId = url.hostname;
59
+ }
60
+ catch { }
61
+ }
62
+ if (!isString(rpId)) {
63
+ return { success: false, error: "TypeError" };
64
+ }
65
+ let timeout = publicKeyOptions.timeout;
66
+ if (!isNumber(timeout) || timeout <= 0) {
67
+ timeout = 10 * 60 * 1000;
68
+ }
69
+ else if (timeout > 60 * 60 * 1000) {
70
+ timeout = 60 * 60 * 1000;
71
+ }
72
+ const challenge = bufferSourceToBuffer(publicKeyOptions.challenge);
73
+ if (!challenge) {
74
+ return { success: false, error: "TypeError" };
75
+ }
76
+ if (!isObject(publicKeyOptions.user)) {
77
+ return { success: false, error: "TypeError" };
78
+ }
79
+ const userName = publicKeyOptions.user.name;
80
+ const userDisplayName = publicKeyOptions.user.displayName;
81
+ if (!isString(userName) || !isString(userDisplayName)) {
82
+ return { success: false, error: "TypeError" };
83
+ }
84
+ const userID = bufferSourceToBuffer(publicKeyOptions.user.id);
85
+ if (!userID) {
86
+ return { success: false, error: "TypeError" };
87
+ }
88
+ const attestationPreference = publicKeyOptions.attestation;
89
+ if (attestationPreference && !isString(attestationPreference)) {
90
+ return { success: false, error: "TypeError" };
91
+ }
92
+ const pubKeyCredParams = publicKeyOptions.pubKeyCredParams;
93
+ const supportedAlgorithmIdentifiers = [];
94
+ if (pubKeyCredParams) {
95
+ if (Array.isArray(pubKeyCredParams)) {
96
+ for (const param of pubKeyCredParams) {
97
+ if (!isObject(param))
98
+ continue;
99
+ if (!isNumber(param.alg))
100
+ continue;
101
+ supportedAlgorithmIdentifiers.push({
102
+ type: "public-key",
103
+ algorithm: param.alg,
104
+ });
105
+ }
39
106
  }
40
107
  else {
41
- console.warn("[electron-webauthn] largeBlobSupport is enabled but largeBlobSupport is not provided, skipping large blob support");
108
+ return { success: false, error: "TypeError" };
42
109
  }
43
- if (supportMode) {
44
- const largeBlobInput = createASAuthorizationPublicKeyCredentialLargeBlobRegistrationInput(supportMode);
45
- platformKeyRequest.setLargeBlob$(largeBlobInput);
110
+ }
111
+ const excludeCredentials = [];
112
+ if (publicKeyOptions.excludeCredentials &&
113
+ Array.isArray(publicKeyOptions.excludeCredentials)) {
114
+ for (const excludeCredential of publicKeyOptions.excludeCredentials) {
115
+ if (!isObject(excludeCredential))
116
+ continue;
117
+ if (excludeCredential.type !== "public-key")
118
+ continue;
119
+ const idBuffer = bufferSourceToBuffer(excludeCredential.id);
120
+ if (!idBuffer)
121
+ continue;
122
+ excludeCredentials.push({
123
+ id: idBuffer,
124
+ transports: excludeCredential.transports,
125
+ });
46
126
  }
47
127
  }
48
- let attestationPreference = ASAuthorizationPublicKeyCredentialAttestationKind.None;
49
- platformKeyRequest.setAttestationPreference$(NSStringFromString(attestationPreference));
50
- let userVerificationPreference = ASAuthorizationPublicKeyCredentialUserVerificationPreference.Preferred;
51
- if (userVerification === "required") {
52
- userVerificationPreference =
53
- ASAuthorizationPublicKeyCredentialUserVerificationPreference.Required;
54
- }
55
- else if (userVerification === "discouraged") {
56
- userVerificationPreference =
57
- ASAuthorizationPublicKeyCredentialUserVerificationPreference.Discouraged;
58
- }
59
- platformKeyRequest.setUserVerificationPreference$(NSStringFromString(userVerificationPreference));
60
- if (additionalOptions.userDisplayName) {
61
- const userDisplayName = NSStringFromString(additionalOptions.userDisplayName);
62
- platformKeyRequest.setDisplayName$(userDisplayName);
63
- }
64
- if (enabledExtensions.includes("prf")) {
65
- if (additionalOptions.prf) {
66
- const inputValues = createPRFInput(additionalOptions.prf);
67
- const prfInput = createASAuthorizationPublicKeyCredentialPRFRegistrationInput(inputValues);
68
- platformKeyRequest.setPrf$(prfInput);
128
+ const { extensions, largeBlobSupport, prf } = getExtensionsConfiguration(publicKeyOptions.extensions);
129
+ let residentKeyRequired = false;
130
+ let userVerificationPreference = "preferred";
131
+ if (publicKeyOptions.authenticatorSelection) {
132
+ if (publicKeyOptions.authenticatorSelection.residentKey === "required") {
133
+ residentKeyRequired = true;
134
+ }
135
+ else if (publicKeyOptions.authenticatorSelection.requireResidentKey) {
136
+ residentKeyRequired = true;
137
+ }
138
+ const userVerifyParam = publicKeyOptions.authenticatorSelection.userVerification;
139
+ if (userVerifyParam === "required") {
140
+ userVerificationPreference = "required";
141
+ }
142
+ else if (userVerifyParam === "discouraged") {
143
+ userVerificationPreference = "discouraged";
69
144
  }
70
145
  else {
71
- platformKeyRequest.setPrf$(ASAuthorizationPublicKeyCredentialPRFRegistrationInput.checkForSupport());
146
+ userVerificationPreference = "preferred";
72
147
  }
73
148
  }
74
- const requestsArray = NSArrayFromObjects([platformKeyRequest]);
75
- const authController = WebauthnCreateController.alloc().initWithAuthorizationRequests$(requestsArray);
76
- const clientData = generateWebauthnClientData("webauthn.create", origin, challenge, additionalOptions.topFrameOrigin);
77
- const { clientDataHash, clientDataBuffer } = generateClientDataInfo(clientData);
78
- setControllerState(authController, clientDataHash, supportedAlgorithmIdentifiers, residentKeyRequired, excludeCredentials);
79
- const finished = (_success) => {
80
- removeControllerState(authController);
81
- };
82
- const delegate = createAuthorizationControllerDelegate({
83
- didCompleteWithAuthorization: (_, authorization) => {
84
- const credential = authorization.credential();
85
- console.log("Authorization succeeded:", credential);
86
- const credentialIdBuffer = bufferFromNSDataDirect(credential.credentialID());
87
- const attestationObjectBuffer = bufferFromNSDataDirect(credential.rawAttestationObject());
88
- const attestation = parseAttestationObject(attestationObjectBuffer);
89
- const publicKey = attestation.authenticatorData.credential.publicKey;
90
- const ec2Key = publicKey.ec2();
91
- const publicKeySPKI = encodeEC2PublicKeyToSPKI(ec2Key.x, ec2Key.y);
92
- const authenticatorData = Buffer.from(JSON.stringify(attestation.authenticatorData));
93
- let authenticatorAttachment = "platform";
94
- if (credential.attachment() ===
95
- ASAuthorizationPublicKeyCredentialAttachment.ASAuthorizationPublicKeyCredentialAttachmentCrossPlatform) {
96
- authenticatorAttachment = "cross-platform";
97
- }
98
- let isLargeBlobSupported = null;
99
- if (enabledExtensions.includes("largeBlob")) {
100
- const largeBlobOutput = credential.largeBlob();
101
- if (largeBlobOutput) {
102
- isLargeBlobSupported = largeBlobOutput.isSupported();
103
- }
104
- }
105
- let prfFirst = null;
106
- let prfSecond = null;
107
- let isPRFSupported = null;
108
- if (enabledExtensions.includes("prf")) {
109
- const prfOutput = credential.prf();
110
- if (prfOutput) {
111
- const prfFirstData = prfOutput.first();
112
- const prfSecondData = prfOutput.second();
113
- if (prfFirstData) {
114
- prfFirst = bufferFromNSDataDirect(prfFirstData);
115
- }
116
- if (prfSecondData) {
117
- prfSecond = bufferFromNSDataDirect(prfSecondData);
118
- }
119
- isPRFSupported = prfOutput.isSupported();
120
- }
121
- }
122
- const data = {
123
- credentialId: credentialIdBuffer,
124
- clientDataJSON: clientDataBuffer,
125
- attestationObject: attestationObjectBuffer,
126
- authenticatorData,
127
- attachment: authenticatorAttachment,
128
- transports: ["hybrid", "internal"],
129
- isResidentKey: true,
130
- publicKeyAlgorithm: publicKey.algorithm(),
131
- publicKey: publicKeySPKI,
132
- isLargeBlobSupported,
133
- isPRFSupported,
134
- prfFirst,
135
- prfSecond,
136
- };
137
- resolve(data);
138
- finished(true);
139
- },
140
- didCompleteWithError: (_, error) => {
141
- const parsedError = error;
142
- const errorMessage = parsedError.localizedDescription().UTF8String();
143
- console.error("Authorization failed:", errorMessage);
144
- reject(new Error(errorMessage));
145
- finished(false);
146
- },
149
+ const { currentOrigin, topFrameOrigin, isPublicSuffix, nativeWindowHandle } = additionalOptions;
150
+ const isRpIdAllowed = isRpIdAllowedForOrigin(currentOrigin, rpId, {
151
+ isPublicSuffix,
147
152
  });
148
- authController.setDelegate$(delegate);
149
- const presentationContextProvider = createPresentationContextProviderFromNativeWindowHandle(nativeWindowHandle);
150
- authController.setPresentationContextProvider$(presentationContextProvider);
151
- authController.performRequests();
152
- return promise;
153
+ if (!isRpIdAllowed.ok) {
154
+ return { success: false, error: "NotAllowedError" };
155
+ }
156
+ const result = await createCredentialInternal(rpId, challenge, userName, userID, nativeWindowHandle, currentOrigin, extensions, attestationPreference, supportedAlgorithmIdentifiers, excludeCredentials, residentKeyRequired, userVerificationPreference, {
157
+ topFrameOrigin,
158
+ largeBlobSupport,
159
+ prf,
160
+ }).catch((error) => {
161
+ console.error("Error creating credential", error);
162
+ console.log("error.message", error.message);
163
+ if (error.message.includes("(com.apple.AuthenticationServices.AuthorizationError error 1006.)")) {
164
+ return "InvalidStateError";
165
+ }
166
+ if (error.message.startsWith("The operation couldn’t be completed.")) {
167
+ return "NotAllowedError";
168
+ }
169
+ return null;
170
+ });
171
+ if (typeof result === "string") {
172
+ return { success: false, error: result };
173
+ }
174
+ const data = {
175
+ credentialId: bufferToBase64Url(result.credentialId),
176
+ clientDataJSON: bufferToBase64Url(result.clientDataJSON),
177
+ attestationObject: bufferToBase64Url(result.attestationObject),
178
+ authData: bufferToBase64Url(result.authenticatorData),
179
+ publicKey: bufferToBase64Url(result.publicKey),
180
+ publicKeyAlgorithm: result.publicKeyAlgorithm,
181
+ transports: result.transports,
182
+ extensions: {},
183
+ };
184
+ if (publicKeyOptions.extensions?.credProps) {
185
+ data.extensions.credProps = {
186
+ rk: result.isResidentKey,
187
+ };
188
+ }
189
+ if (result.isLargeBlobSupported !== null) {
190
+ data.extensions.largeBlob = {
191
+ supported: result.isLargeBlobSupported,
192
+ };
193
+ }
194
+ if (result.isPRFSupported !== null) {
195
+ const prfFirst = result.prfFirst;
196
+ const prfSecond = result.prfSecond;
197
+ data.extensions.prf = {
198
+ enabled: result.isPRFSupported,
199
+ results: {
200
+ first: prfFirst ? bufferToBase64Url(prfFirst) : undefined,
201
+ second: prfSecond ? bufferToBase64Url(prfSecond) : undefined,
202
+ },
203
+ };
204
+ }
205
+ return { success: true, data };
153
206
  }
154
- export { createCredential };
@@ -0,0 +1,34 @@
1
+ import { type PRFInput } from "../helpers/prf.js";
2
+ import { type PublicKeyCredentialParams } from "./authorization-controller.js";
3
+ export interface CreateCredentialResult {
4
+ credentialId: Buffer;
5
+ clientDataJSON: Buffer;
6
+ attestationObject: Buffer;
7
+ authenticatorData: Buffer;
8
+ attachment: AuthenticatorAttachment;
9
+ transports: string[];
10
+ isResidentKey: boolean;
11
+ publicKeyAlgorithm: number;
12
+ publicKey: Buffer;
13
+ isLargeBlobSupported: boolean | null;
14
+ isPRFSupported: boolean | null;
15
+ prfFirst: Buffer | null;
16
+ prfSecond: Buffer | null;
17
+ }
18
+ type CredentialUserVerificationPreference = "required" | "preferred" | "discouraged";
19
+ type CredentialAttestationPreference = "direct" | "enterprise" | "indirect" | "none";
20
+ declare const VALID_EXTENSIONS: readonly ["largeBlob", "prf"];
21
+ export type CredentialCreationExtensions = (typeof VALID_EXTENSIONS)[number];
22
+ export type LargeBlobSupport = "required" | "preferred" | "unspecified";
23
+ interface CreateCredentialAdditionalOptions {
24
+ topFrameOrigin?: string;
25
+ userDisplayName?: string;
26
+ largeBlobSupport?: LargeBlobSupport;
27
+ prf?: PRFInput;
28
+ }
29
+ export interface ExcludeCredential {
30
+ id: Buffer;
31
+ transports?: string[];
32
+ }
33
+ declare function createCredentialInternal(rpid: string, challenge: Buffer, username: string, userID: Buffer, nativeWindowHandle: Buffer, origin: string, enabledExtensions: CredentialCreationExtensions[], attestation: CredentialAttestationPreference, supportedAlgorithmIdentifiers: PublicKeyCredentialParams[], excludeCredentials: ExcludeCredential[], residentKeyRequired?: boolean, userVerification?: CredentialUserVerificationPreference, additionalOptions?: CreateCredentialAdditionalOptions): Promise<CreateCredentialResult>;
34
+ export { createCredentialInternal };
@@ -0,0 +1,154 @@
1
+ import { generateClientDataInfo } from "../helpers/client-data.js";
2
+ import { generateWebauthnClientData } from "../helpers/client-data.js";
3
+ import { PromiseWithResolvers } from "../helpers/index.js";
4
+ import { encodeEC2PublicKeyToSPKI } from "../helpers/public-key.js";
5
+ import { createPresentationContextProviderFromNativeWindowHandle } from "../helpers/presentation.js";
6
+ import { createPRFInput } from "../helpers/prf.js";
7
+ import { createAuthorizationControllerDelegate } from "../objc/authentication-services/as-authorization-controller-delegate.js";
8
+ import { createPlatformPublicKeyCredentialProvider } from "../objc/authentication-services/as-authorization-platform-public-key-credential-provider.js";
9
+ import { createASAuthorizationPublicKeyCredentialLargeBlobRegistrationInput } from "../objc/authentication-services/as-authorization-public-key-credential-large-blob-registration-input.js";
10
+ import { ASAuthorizationPublicKeyCredentialPRFRegistrationInput, createASAuthorizationPublicKeyCredentialPRFRegistrationInput, } from "../objc/authentication-services/as-authorization-public-key-credential-prf-registration-input.js";
11
+ import { ASAuthorizationPublicKeyCredentialAttestationKind } from "../objc/authentication-services/enums/as-authorization-public-key-credential-attestation-kind.js";
12
+ import { ASAuthorizationPublicKeyCredentialLargeBlobSupportRequirement } from "../objc/authentication-services/enums/as-authorization-public-key-credential-large-blob-support-requirement.js";
13
+ import { ASAuthorizationPublicKeyCredentialUserVerificationPreference } from "../objc/authentication-services/enums/as-authorization-public-key-credential-user-verification-preference.js";
14
+ import { NSArrayFromObjects } from "../objc/foundation/nsarray.js";
15
+ import { bufferFromNSDataDirect, NSDataFromBuffer, } from "../objc/foundation/nsdata.js";
16
+ import { NSStringFromString } from "../objc/foundation/nsstring.js";
17
+ import { removeControllerState, setControllerState, WebauthnCreateController, } from "./authorization-controller.js";
18
+ import { parseAttestationObject } from "@oslojs/webauthn";
19
+ import { ASAuthorizationPublicKeyCredentialAttachment } from "../objc/authentication-services/enums/as-authorization-public-key-credential-attachment.js";
20
+ const VALID_EXTENSIONS = ["largeBlob", "prf"];
21
+ function createCredentialInternal(rpid, challenge, username, userID, nativeWindowHandle, origin, enabledExtensions, attestation = "none", supportedAlgorithmIdentifiers = [], excludeCredentials, residentKeyRequired = false, userVerification = "preferred", additionalOptions = {}) {
22
+ const { promise, resolve, reject } = PromiseWithResolvers();
23
+ const NS_rpID = NSStringFromString(rpid);
24
+ const NS_challenge = NSDataFromBuffer(challenge);
25
+ const NS_username = NSStringFromString(username);
26
+ const NS_userID = NSDataFromBuffer(userID);
27
+ const platformProvider = createPlatformPublicKeyCredentialProvider(NS_rpID);
28
+ const platformKeyRequest = platformProvider.createCredentialRegistrationRequestWithChallenge$name$userID$(NS_challenge, NS_username, NS_userID);
29
+ if (enabledExtensions.includes("largeBlob")) {
30
+ let supportMode;
31
+ const largeBlobSupport = additionalOptions.largeBlobSupport;
32
+ if (largeBlobSupport === "required") {
33
+ supportMode =
34
+ ASAuthorizationPublicKeyCredentialLargeBlobSupportRequirement.Required;
35
+ }
36
+ else if (largeBlobSupport === "preferred") {
37
+ supportMode =
38
+ ASAuthorizationPublicKeyCredentialLargeBlobSupportRequirement.Preferred;
39
+ }
40
+ else {
41
+ console.warn("[electron-webauthn] largeBlobSupport is enabled but largeBlobSupport is not provided, skipping large blob support");
42
+ }
43
+ if (supportMode) {
44
+ const largeBlobInput = createASAuthorizationPublicKeyCredentialLargeBlobRegistrationInput(supportMode);
45
+ platformKeyRequest.setLargeBlob$(largeBlobInput);
46
+ }
47
+ }
48
+ let attestationPreference = ASAuthorizationPublicKeyCredentialAttestationKind.None;
49
+ platformKeyRequest.setAttestationPreference$(NSStringFromString(attestationPreference));
50
+ let userVerificationPreference = ASAuthorizationPublicKeyCredentialUserVerificationPreference.Preferred;
51
+ if (userVerification === "required") {
52
+ userVerificationPreference =
53
+ ASAuthorizationPublicKeyCredentialUserVerificationPreference.Required;
54
+ }
55
+ else if (userVerification === "discouraged") {
56
+ userVerificationPreference =
57
+ ASAuthorizationPublicKeyCredentialUserVerificationPreference.Discouraged;
58
+ }
59
+ platformKeyRequest.setUserVerificationPreference$(NSStringFromString(userVerificationPreference));
60
+ if (additionalOptions.userDisplayName) {
61
+ const userDisplayName = NSStringFromString(additionalOptions.userDisplayName);
62
+ platformKeyRequest.setDisplayName$(userDisplayName);
63
+ }
64
+ if (enabledExtensions.includes("prf")) {
65
+ if (additionalOptions.prf) {
66
+ const inputValues = createPRFInput(additionalOptions.prf);
67
+ const prfInput = createASAuthorizationPublicKeyCredentialPRFRegistrationInput(inputValues);
68
+ platformKeyRequest.setPrf$(prfInput);
69
+ }
70
+ else {
71
+ platformKeyRequest.setPrf$(ASAuthorizationPublicKeyCredentialPRFRegistrationInput.checkForSupport());
72
+ }
73
+ }
74
+ const requestsArray = NSArrayFromObjects([platformKeyRequest]);
75
+ const authController = WebauthnCreateController.alloc().initWithAuthorizationRequests$(requestsArray);
76
+ const clientData = generateWebauthnClientData("webauthn.create", origin, challenge, additionalOptions.topFrameOrigin);
77
+ const { clientDataHash, clientDataBuffer } = generateClientDataInfo(clientData);
78
+ setControllerState(authController, clientDataHash, supportedAlgorithmIdentifiers, residentKeyRequired, excludeCredentials);
79
+ const finished = (_success) => {
80
+ removeControllerState(authController);
81
+ };
82
+ const delegate = createAuthorizationControllerDelegate({
83
+ didCompleteWithAuthorization: (_, authorization) => {
84
+ const credential = authorization.credential();
85
+ console.log("Authorization succeeded:", credential);
86
+ const credentialIdBuffer = bufferFromNSDataDirect(credential.credentialID());
87
+ const attestationObjectBuffer = bufferFromNSDataDirect(credential.rawAttestationObject());
88
+ const attestation = parseAttestationObject(attestationObjectBuffer);
89
+ const publicKey = attestation.authenticatorData.credential.publicKey;
90
+ const ec2Key = publicKey.ec2();
91
+ const publicKeySPKI = encodeEC2PublicKeyToSPKI(ec2Key.x, ec2Key.y);
92
+ const authenticatorData = Buffer.from(JSON.stringify(attestation.authenticatorData));
93
+ let authenticatorAttachment = "platform";
94
+ if (credential.attachment() ===
95
+ ASAuthorizationPublicKeyCredentialAttachment.ASAuthorizationPublicKeyCredentialAttachmentCrossPlatform) {
96
+ authenticatorAttachment = "cross-platform";
97
+ }
98
+ let isLargeBlobSupported = null;
99
+ if (enabledExtensions.includes("largeBlob")) {
100
+ const largeBlobOutput = credential.largeBlob();
101
+ if (largeBlobOutput) {
102
+ isLargeBlobSupported = largeBlobOutput.isSupported();
103
+ }
104
+ }
105
+ let prfFirst = null;
106
+ let prfSecond = null;
107
+ let isPRFSupported = null;
108
+ if (enabledExtensions.includes("prf")) {
109
+ const prfOutput = credential.prf();
110
+ if (prfOutput) {
111
+ const prfFirstData = prfOutput.first();
112
+ const prfSecondData = prfOutput.second();
113
+ if (prfFirstData) {
114
+ prfFirst = bufferFromNSDataDirect(prfFirstData);
115
+ }
116
+ if (prfSecondData) {
117
+ prfSecond = bufferFromNSDataDirect(prfSecondData);
118
+ }
119
+ isPRFSupported = prfOutput.isSupported();
120
+ }
121
+ }
122
+ const data = {
123
+ credentialId: credentialIdBuffer,
124
+ clientDataJSON: clientDataBuffer,
125
+ attestationObject: attestationObjectBuffer,
126
+ authenticatorData,
127
+ attachment: authenticatorAttachment,
128
+ transports: ["hybrid", "internal"],
129
+ isResidentKey: true,
130
+ publicKeyAlgorithm: publicKey.algorithm(),
131
+ publicKey: publicKeySPKI,
132
+ isLargeBlobSupported,
133
+ isPRFSupported,
134
+ prfFirst,
135
+ prfSecond,
136
+ };
137
+ resolve(data);
138
+ finished(true);
139
+ },
140
+ didCompleteWithError: (_, error) => {
141
+ const parsedError = error;
142
+ const errorMessage = parsedError.localizedDescription().UTF8String();
143
+ console.error("Authorization failed:", errorMessage);
144
+ reject(new Error(errorMessage));
145
+ finished(false);
146
+ },
147
+ });
148
+ authController.setDelegate$(delegate);
149
+ const presentationContextProvider = createPresentationContextProviderFromNativeWindowHandle(nativeWindowHandle);
150
+ authController.setPresentationContextProvider$(presentationContextProvider);
151
+ authController.performRequests();
152
+ return promise;
153
+ }
154
+ export { createCredentialInternal };
@@ -32,6 +32,8 @@ export function base64UrlToBuffer(b64url) {
32
32
  return Buffer.from(b64, "base64");
33
33
  }
34
34
  export function bufferSourceToBuffer(src) {
35
+ if (!src)
36
+ return null;
35
37
  if (Buffer.isBuffer(src))
36
38
  return src;
37
39
  if (src instanceof ArrayBuffer ||
@@ -101,6 +101,6 @@ function originString(x) {
101
101
  }
102
102
  const o = computeOrigin(url);
103
103
  if (o.type === "opaque")
104
- return "null";
104
+ return null;
105
105
  return `${o.scheme}://${o.host}${o.port == null ? "" : `:${o.port}`}`;
106
106
  }
@@ -1,3 +1,3 @@
1
1
  export declare function isString(value: unknown): value is string;
2
2
  export declare function isNumber(value: unknown): value is number;
3
- export declare function isObject(value: unknown): value is Record<string, unknown>;
3
+ export declare function isObject(value: unknown): boolean;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,2 @@
1
1
  export * from "./get/handler.js";
2
2
  export * from "./create/handler.js";
3
- export type { PRFInput } from "./helpers/prf.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electron-webauthn",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
4
4
  "repository": "https://github.com/iamEvanYT/electron-webauthn",
5
5
  "description": "Add support for WebAuthn for Electron.",
6
6
  "main": "dist/index.js",