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 +357 -113
- package/dist/create/authorization-controller.d.ts +1 -1
- package/dist/create/authorization-controller.js +1 -0
- package/dist/create/handler.d.ts +37 -29
- package/dist/create/handler.js +194 -142
- package/dist/create/internal-handler.d.ts +34 -0
- package/dist/create/internal-handler.js +154 -0
- package/dist/helpers/index.js +2 -0
- package/dist/helpers/origin.js +1 -1
- package/dist/helpers/validation.d.ts +1 -1
- package/dist/index.d.ts +0 -1
- package/package.json +1 -1
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`
|
|
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
|
|
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 =
|
|
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
|
-
### `
|
|
144
|
+
### `createCredential(publicKeyOptions, additionalOptions)`
|
|
145
|
+
|
|
146
|
+
Creates and registers a new WebAuthn credential using available platform and cross-platform authenticators.
|
|
95
147
|
|
|
96
|
-
|
|
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:
|
|
152
|
+
##### `publicKeyOptions: PublicKeyCredentialCreationOptions`
|
|
101
153
|
|
|
102
|
-
|
|
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
|
-
|
|
156
|
+
##### `additionalOptions: WebauthnCreateRequestOptions`
|
|
105
157
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
- **`
|
|
109
|
-
- **`
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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()
|
|
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 =
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
516
|
-
|
|
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
|
-
- ✅
|
|
532
|
-
- ✅
|
|
533
|
-
- ✅
|
|
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
|
-
- ❌
|
|
542
|
-
- ❌
|
|
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
|
|
|
@@ -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();
|
package/dist/create/handler.d.ts
CHANGED
|
@@ -1,33 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
interface CreateCredentialErrorResult {
|
|
36
|
+
success: false;
|
|
37
|
+
error: "TypeError" | "AbortError" | "NotAllowedError" | "SecurityError" | "InvalidStateError";
|
|
31
38
|
}
|
|
32
|
-
|
|
33
|
-
export
|
|
39
|
+
export type CreateCredentialResult = CreateCredentialSuccessResult | CreateCredentialErrorResult;
|
|
40
|
+
export declare function createCredential(publicKeyOptions: PublicKeyCredentialCreationOptions | undefined, additionalOptions: WebauthnCreateRequestOptions): Promise<CreateCredentialResult>;
|
|
41
|
+
export {};
|
package/dist/create/handler.js
CHANGED
|
@@ -1,154 +1,206 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
108
|
+
return { success: false, error: "TypeError" };
|
|
42
109
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
let userVerificationPreference =
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
146
|
+
userVerificationPreference = "preferred";
|
|
72
147
|
}
|
|
73
148
|
}
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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 };
|
package/dist/helpers/index.js
CHANGED
package/dist/helpers/origin.js
CHANGED
|
@@ -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):
|
|
3
|
+
export declare function isObject(value: unknown): boolean;
|
package/dist/index.d.ts
CHANGED