electron-webauthn 0.0.17 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -680
- package/dist/create/authorization-controller.js +10 -3
- package/dist/create/handler.d.ts +3 -1
- package/dist/create/handler.js +22 -4
- package/dist/create/internal-handler.d.ts +2 -1
- package/dist/create/internal-handler.js +71 -19
- package/dist/get/authorization-controller.js +4 -1
- package/dist/get/handler.d.ts +3 -1
- package/dist/get/handler.js +4 -6
- package/dist/get/internal-handler.d.ts +2 -2
- package/dist/get/internal-handler.js +13 -1
- package/dist/helpers/types.d.ts +1 -0
- package/dist/helpers/types.js +1 -0
- package/dist/objc/authentication-services/as-authorization-public-key-credential-parameters.d.ts +8 -0
- package/dist/objc/authentication-services/as-authorization-public-key-credential-parameters.js +6 -0
- package/dist/objc/authentication-services/as-authorization-security-key-public-key-credential-provider.d.ts +3 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -28,11 +28,21 @@ This package provides JavaScript bindings to Apple's AuthenticationServices fram
|
|
|
28
28
|
|
|
29
29
|
## Installation
|
|
30
30
|
|
|
31
|
+
### Prerequisites
|
|
32
|
+
|
|
33
|
+
- Node.js / Bun
|
|
34
|
+
- Xcode Command Line Tools (Run `xcode-select --install` to install)
|
|
35
|
+
- `pkg-config` from Homebrew (Run `brew install pkgconf` to install)
|
|
36
|
+
|
|
37
|
+
### Install using npm, bun, pnpm, or yarn
|
|
38
|
+
|
|
31
39
|
```bash
|
|
32
40
|
npm install electron-webauthn
|
|
33
41
|
# or
|
|
34
42
|
bun add electron-webauthn
|
|
35
43
|
# or
|
|
44
|
+
pnpm add electron-webauthn
|
|
45
|
+
# or
|
|
36
46
|
yarn add electron-webauthn
|
|
37
47
|
```
|
|
38
48
|
|
|
@@ -44,234 +54,15 @@ yarn add electron-webauthn
|
|
|
44
54
|
|
|
45
55
|
## Quick Start
|
|
46
56
|
|
|
47
|
-
|
|
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
|
-
|
|
105
|
-
```typescript
|
|
106
|
-
import { getCredential } from "electron-webauthn";
|
|
107
|
-
import { BrowserWindow } from "electron";
|
|
108
|
-
|
|
109
|
-
// In your Electron main process or preload script
|
|
110
|
-
async function authenticate(window: BrowserWindow, challenge: ArrayBuffer) {
|
|
111
|
-
// Get the native window handle from your BrowserWindow
|
|
112
|
-
const nativeWindowHandle = window.getNativeWindowHandle();
|
|
113
|
-
|
|
114
|
-
// Call getCredential with W3C WebAuthn-compliant options
|
|
115
|
-
// You can plug any publicKeyOptions from navigator.credentials.get() here
|
|
116
|
-
const result = await getCredential(
|
|
117
|
-
{
|
|
118
|
-
challenge: challenge,
|
|
119
|
-
rpId: "example.com",
|
|
120
|
-
timeout: 60000, // Optional: 60 seconds
|
|
121
|
-
userVerification: "preferred", // Optional: "preferred" | "required" | "discouraged"
|
|
122
|
-
allowCredentials: [], // Optional: restrict to specific credentials
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
currentOrigin: "https://example.com",
|
|
126
|
-
topFrameOrigin: "https://example.com", // Use for iframe support
|
|
127
|
-
nativeWindowHandle: nativeWindowHandle,
|
|
128
|
-
}
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
if (result.success) {
|
|
132
|
-
console.log("Authentication successful!");
|
|
133
|
-
console.log("Credential ID:", result.data.credentialId);
|
|
134
|
-
console.log("Signature:", result.data.signature);
|
|
135
|
-
// Result contains base64url-encoded strings ready to send to server
|
|
136
|
-
} else {
|
|
137
|
-
console.error("Authentication failed:", result.error);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
```
|
|
57
|
+
See the [Quick Start Guide](./docs/quick-start.md) for detailed examples on credential creation and authentication.
|
|
141
58
|
|
|
142
59
|
## API Reference
|
|
143
60
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
Creates and registers a new WebAuthn credential using available platform and cross-platform authenticators.
|
|
147
|
-
|
|
148
|
-
**Note:** You can plug any `publicKeyOptions` from the standard `navigator.credentials.create({ publicKey: ... })` directly into this function.
|
|
149
|
-
|
|
150
|
-
#### Parameters
|
|
151
|
-
|
|
152
|
-
##### `publicKeyOptions: PublicKeyCredentialCreationOptions`
|
|
153
|
-
|
|
154
|
-
Standard W3C WebAuthn credential creation options. See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialCreationOptions) for more details.
|
|
155
|
-
|
|
156
|
-
##### `additionalOptions: WebauthnCreateRequestOptions`
|
|
157
|
-
|
|
158
|
-
Additional options specific to the Electron environment:
|
|
159
|
-
|
|
160
|
-
- **`currentOrigin: string`** (required) - The origin of the requesting document (e.g., "https://example.com")
|
|
161
|
-
- **`topFrameOrigin: string | undefined`** - The origin of the top frame (for iframe support). Set to `currentOrigin` if not in an iframe
|
|
162
|
-
- **`nativeWindowHandle: Buffer`** (required) - Native window handle from `BrowserWindow.getNativeWindowHandle()`, or a pointer to a NSView object
|
|
163
|
-
- **`isPublicSuffix?: (domain: string) => boolean`** - Optional function to check if a domain is a public suffix (e.g., "com", "co.uk"). Strongly recommended for security
|
|
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)`
|
|
61
|
+
See the [API Reference](./docs/api-reference.md) for detailed documentation on all available functions and types.
|
|
216
62
|
|
|
217
|
-
|
|
63
|
+
## Advanced Examples
|
|
218
64
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
#### Parameters
|
|
222
|
-
|
|
223
|
-
##### `publicKeyOptions: PublicKeyCredentialRequestOptions`
|
|
224
|
-
|
|
225
|
-
Standard W3C WebAuthn credential request options. See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialRequestOptions) for more details.
|
|
226
|
-
|
|
227
|
-
##### `additionalOptions: WebauthnGetRequestOptions`
|
|
228
|
-
|
|
229
|
-
Additional options specific to the Electron environment:
|
|
230
|
-
|
|
231
|
-
- **`currentOrigin: string`** (required) - The origin of the requesting document (e.g., "https://example.com")
|
|
232
|
-
- **`topFrameOrigin: string | undefined`** - The origin of the top frame (for iframe support). Set to `currentOrigin` if not in an iframe
|
|
233
|
-
- **`nativeWindowHandle: Buffer`** (required) - Native window handle from `BrowserWindow.getNativeWindowHandle()`, or a pointer to a NSView object
|
|
234
|
-
- **`isPublicSuffix?: (domain: string) => boolean`** - Optional function to check if a domain is a public suffix (e.g., "com", "co.uk"). Strongly recommended for security. Use a library like `tldts` for implementation
|
|
235
|
-
|
|
236
|
-
#### Returns
|
|
237
|
-
|
|
238
|
-
`Promise<GetCredentialResult>` - Resolves with the assertion result (that you can transform into a `PublicKeyCredential` object) or error
|
|
239
|
-
|
|
240
|
-
#### Result Types
|
|
241
|
-
|
|
242
|
-
```typescript
|
|
243
|
-
type GetCredentialResult =
|
|
244
|
-
| GetCredentialSuccessResult
|
|
245
|
-
| GetCredentialErrorResult;
|
|
246
|
-
|
|
247
|
-
interface GetCredentialSuccessResult {
|
|
248
|
-
success: true;
|
|
249
|
-
data: {
|
|
250
|
-
credentialId: string; // Base64url-encoded credential ID
|
|
251
|
-
clientDataJSON: string; // Base64url-encoded client data
|
|
252
|
-
authenticatorData: string; // Base64url-encoded authenticator data
|
|
253
|
-
signature: string; // Base64url-encoded signature
|
|
254
|
-
userHandle: string; // Base64url-encoded user handle
|
|
255
|
-
extensions?: {
|
|
256
|
-
prf?: {
|
|
257
|
-
results?: {
|
|
258
|
-
first: string; // Base64url-encoded PRF output
|
|
259
|
-
second?: string; // Base64url-encoded PRF output (if provided)
|
|
260
|
-
};
|
|
261
|
-
};
|
|
262
|
-
largeBlob?: {
|
|
263
|
-
blob?: string; // Base64url-encoded blob data (if read)
|
|
264
|
-
written?: boolean; // True if write succeeded (if write was requested)
|
|
265
|
-
};
|
|
266
|
-
};
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
interface GetCredentialErrorResult {
|
|
271
|
-
success: false;
|
|
272
|
-
error: "TypeError" | "AbortError" | "NotAllowedError" | "SecurityError";
|
|
273
|
-
}
|
|
274
|
-
```
|
|
65
|
+
See the [Advanced Examples](./docs/advanced-examples.md) for detailed examples on how to use the library with more complex use cases.
|
|
275
66
|
|
|
276
67
|
## Architecture
|
|
277
68
|
|
|
@@ -283,417 +74,6 @@ This library implements the W3C WebAuthn standard using Apple's native Authentic
|
|
|
283
74
|
- Properly manages WebAuthn extensions (PRF, Large Blob)
|
|
284
75
|
- Returns base64url-encoded values ready to send to your server for verification
|
|
285
76
|
|
|
286
|
-
## Usage Examples
|
|
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
|
-
|
|
349
|
-
### Basic Authentication
|
|
350
|
-
|
|
351
|
-
```typescript
|
|
352
|
-
import { getCredential } from "electron-webauthn";
|
|
353
|
-
import { app, BrowserWindow } from "electron";
|
|
354
|
-
|
|
355
|
-
let mainWindow: BrowserWindow;
|
|
356
|
-
|
|
357
|
-
app.on("ready", () => {
|
|
358
|
-
mainWindow = new BrowserWindow({
|
|
359
|
-
width: 800,
|
|
360
|
-
height: 600,
|
|
361
|
-
webPreferences: {
|
|
362
|
-
nodeIntegration: false,
|
|
363
|
-
contextIsolation: true,
|
|
364
|
-
preload: "./preload.js",
|
|
365
|
-
},
|
|
366
|
-
});
|
|
367
|
-
mainWindow.loadURL("https://myapp.com");
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
// In your preload script or main process
|
|
371
|
-
export async function authenticateUser(challenge: ArrayBuffer) {
|
|
372
|
-
const nativeHandle = mainWindow.getNativeWindowHandle();
|
|
373
|
-
|
|
374
|
-
const result = await getCredential(
|
|
375
|
-
{
|
|
376
|
-
challenge: challenge,
|
|
377
|
-
rpId: "myapp.com",
|
|
378
|
-
userVerification: "preferred",
|
|
379
|
-
},
|
|
380
|
-
{
|
|
381
|
-
currentOrigin: "https://myapp.com",
|
|
382
|
-
topFrameOrigin: "https://myapp.com",
|
|
383
|
-
nativeWindowHandle: nativeHandle,
|
|
384
|
-
}
|
|
385
|
-
);
|
|
386
|
-
|
|
387
|
-
return result;
|
|
388
|
-
}
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
### Restricting to Specific Credentials
|
|
392
|
-
|
|
393
|
-
```typescript
|
|
394
|
-
// Only allow specific registered credentials
|
|
395
|
-
const result = await getCredential(
|
|
396
|
-
{
|
|
397
|
-
challenge: challenge,
|
|
398
|
-
rpId: "myapp.com",
|
|
399
|
-
allowCredentials: [
|
|
400
|
-
{ type: "public-key", id: credentialId1 },
|
|
401
|
-
{ type: "public-key", id: credentialId2 },
|
|
402
|
-
],
|
|
403
|
-
},
|
|
404
|
-
{
|
|
405
|
-
currentOrigin: "https://myapp.com",
|
|
406
|
-
topFrameOrigin: "https://myapp.com",
|
|
407
|
-
nativeWindowHandle: nativeHandle,
|
|
408
|
-
}
|
|
409
|
-
);
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
### With Required User Verification
|
|
413
|
-
|
|
414
|
-
```typescript
|
|
415
|
-
// Require user verification (e.g., for sensitive operations)
|
|
416
|
-
const result = await getCredential(
|
|
417
|
-
{
|
|
418
|
-
challenge: challenge,
|
|
419
|
-
rpId: "myapp.com",
|
|
420
|
-
userVerification: "required", // Force biometric or PIN
|
|
421
|
-
},
|
|
422
|
-
{
|
|
423
|
-
currentOrigin: "https://myapp.com",
|
|
424
|
-
topFrameOrigin: "https://myapp.com",
|
|
425
|
-
nativeWindowHandle: nativeHandle,
|
|
426
|
-
}
|
|
427
|
-
);
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
### Creating Resident Keys (Discoverable Credentials)
|
|
431
|
-
|
|
432
|
-
```typescript
|
|
433
|
-
// Create a discoverable credential that can be used without specifying allowCredentials
|
|
434
|
-
const result = await createCredential(
|
|
435
|
-
{
|
|
436
|
-
challenge: challenge,
|
|
437
|
-
rp: { name: "My App", id: "myapp.com" },
|
|
438
|
-
user: {
|
|
439
|
-
id: userId,
|
|
440
|
-
name: userName,
|
|
441
|
-
displayName: userDisplayName,
|
|
442
|
-
},
|
|
443
|
-
pubKeyCredParams: [
|
|
444
|
-
{ type: "public-key", alg: -7 },
|
|
445
|
-
{ type: "public-key", alg: -257 },
|
|
446
|
-
],
|
|
447
|
-
authenticatorSelection: {
|
|
448
|
-
residentKey: "required", // Require resident key
|
|
449
|
-
userVerification: "required", // Usually combined with resident keys
|
|
450
|
-
},
|
|
451
|
-
},
|
|
452
|
-
{
|
|
453
|
-
currentOrigin: "https://myapp.com",
|
|
454
|
-
topFrameOrigin: "https://myapp.com",
|
|
455
|
-
nativeWindowHandle: nativeHandle,
|
|
456
|
-
}
|
|
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)
|
|
538
|
-
|
|
539
|
-
The PRF (Pseudo-Random Function) extension allows you to derive cryptographic secrets from credentials:
|
|
540
|
-
|
|
541
|
-
```typescript
|
|
542
|
-
// Generate a random salt for PRF
|
|
543
|
-
const prfSalt = crypto.getRandomValues(new Uint8Array(32));
|
|
544
|
-
|
|
545
|
-
const result = await getCredential(
|
|
546
|
-
{
|
|
547
|
-
challenge: challenge,
|
|
548
|
-
rpId: "myapp.com",
|
|
549
|
-
extensions: {
|
|
550
|
-
prf: {
|
|
551
|
-
eval: {
|
|
552
|
-
first: prfSalt,
|
|
553
|
-
// second: optionalSecondSalt, // Optional second output
|
|
554
|
-
},
|
|
555
|
-
},
|
|
556
|
-
},
|
|
557
|
-
},
|
|
558
|
-
{
|
|
559
|
-
currentOrigin: "https://myapp.com",
|
|
560
|
-
topFrameOrigin: "https://myapp.com",
|
|
561
|
-
nativeWindowHandle: nativeHandle,
|
|
562
|
-
}
|
|
563
|
-
);
|
|
564
|
-
|
|
565
|
-
if (result.success && result.data.extensions?.prf?.results) {
|
|
566
|
-
const prfOutput = result.data.extensions.prf.results.first;
|
|
567
|
-
// Use prfOutput as a cryptographic key for encryption, etc.
|
|
568
|
-
console.log("PRF output:", prfOutput);
|
|
569
|
-
}
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
### Using PRF with Per-Credential Evaluation
|
|
573
|
-
|
|
574
|
-
```typescript
|
|
575
|
-
import { bufferToBase64Url } from "./helpers"; // You'll need to implement this
|
|
576
|
-
|
|
577
|
-
const result = await getCredential(
|
|
578
|
-
{
|
|
579
|
-
challenge: challenge,
|
|
580
|
-
rpId: "myapp.com",
|
|
581
|
-
allowCredentials: [
|
|
582
|
-
{ type: "public-key", id: credentialId1 },
|
|
583
|
-
{ type: "public-key", id: credentialId2 },
|
|
584
|
-
],
|
|
585
|
-
extensions: {
|
|
586
|
-
prf: {
|
|
587
|
-
evalByCredential: {
|
|
588
|
-
[bufferToBase64Url(credentialId1)]: {
|
|
589
|
-
first: new Uint8Array(32), // Different salt for credential 1
|
|
590
|
-
},
|
|
591
|
-
[bufferToBase64Url(credentialId2)]: {
|
|
592
|
-
first: new Uint8Array(32), // Different salt for credential 2
|
|
593
|
-
},
|
|
594
|
-
},
|
|
595
|
-
},
|
|
596
|
-
},
|
|
597
|
-
},
|
|
598
|
-
{
|
|
599
|
-
currentOrigin: "https://myapp.com",
|
|
600
|
-
topFrameOrigin: "https://myapp.com",
|
|
601
|
-
nativeWindowHandle: nativeHandle,
|
|
602
|
-
}
|
|
603
|
-
);
|
|
604
|
-
```
|
|
605
|
-
|
|
606
|
-
### Reading and Writing Large Blobs
|
|
607
|
-
|
|
608
|
-
```typescript
|
|
609
|
-
// Reading a large blob
|
|
610
|
-
const readResult = await getCredential(
|
|
611
|
-
{
|
|
612
|
-
challenge: challenge,
|
|
613
|
-
rpId: "myapp.com",
|
|
614
|
-
extensions: {
|
|
615
|
-
largeBlob: {
|
|
616
|
-
read: true,
|
|
617
|
-
},
|
|
618
|
-
},
|
|
619
|
-
},
|
|
620
|
-
{
|
|
621
|
-
currentOrigin: "https://myapp.com",
|
|
622
|
-
topFrameOrigin: "https://myapp.com",
|
|
623
|
-
nativeWindowHandle: nativeHandle,
|
|
624
|
-
}
|
|
625
|
-
);
|
|
626
|
-
|
|
627
|
-
if (readResult.success && readResult.data.extensions?.largeBlob?.blob) {
|
|
628
|
-
console.log("Large blob data:", readResult.data.extensions.largeBlob.blob);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// Writing a large blob
|
|
632
|
-
const dataToWrite = new TextEncoder().encode("Secret data");
|
|
633
|
-
const writeResult = await getCredential(
|
|
634
|
-
{
|
|
635
|
-
challenge: challenge,
|
|
636
|
-
rpId: "myapp.com",
|
|
637
|
-
extensions: {
|
|
638
|
-
largeBlob: {
|
|
639
|
-
write: dataToWrite,
|
|
640
|
-
},
|
|
641
|
-
},
|
|
642
|
-
},
|
|
643
|
-
{
|
|
644
|
-
currentOrigin: "https://myapp.com",
|
|
645
|
-
topFrameOrigin: "https://myapp.com",
|
|
646
|
-
nativeWindowHandle: nativeHandle,
|
|
647
|
-
}
|
|
648
|
-
);
|
|
649
|
-
|
|
650
|
-
if (writeResult.success && writeResult.data.extensions?.largeBlob?.written) {
|
|
651
|
-
console.log("Large blob written successfully");
|
|
652
|
-
}
|
|
653
|
-
```
|
|
654
|
-
|
|
655
|
-
### With Public Suffix List Validation
|
|
656
|
-
|
|
657
|
-
For enhanced security, use a public suffix list library like `tldts`:
|
|
658
|
-
|
|
659
|
-
```typescript
|
|
660
|
-
import { getPublicSuffix } from "tldts";
|
|
661
|
-
|
|
662
|
-
const result = await getCredential(
|
|
663
|
-
{
|
|
664
|
-
challenge: challenge,
|
|
665
|
-
rpId: "myapp.com",
|
|
666
|
-
},
|
|
667
|
-
{
|
|
668
|
-
currentOrigin: "https://myapp.com",
|
|
669
|
-
topFrameOrigin: "https://myapp.com",
|
|
670
|
-
nativeWindowHandle: nativeHandle,
|
|
671
|
-
isPublicSuffix: (domain) => {
|
|
672
|
-
const suffix = getPublicSuffix(domain);
|
|
673
|
-
return suffix === domain;
|
|
674
|
-
},
|
|
675
|
-
}
|
|
676
|
-
);
|
|
677
|
-
```
|
|
678
|
-
|
|
679
|
-
### Supporting Cross-Origin Iframes
|
|
680
|
-
|
|
681
|
-
If your app loads WebAuthn requests from an iframe:
|
|
682
|
-
|
|
683
|
-
```typescript
|
|
684
|
-
const result = await getCredential(
|
|
685
|
-
{
|
|
686
|
-
challenge: challenge,
|
|
687
|
-
rpId: "myapp.com",
|
|
688
|
-
},
|
|
689
|
-
{
|
|
690
|
-
currentOrigin: "https://auth.myapp.com", // The iframe's origin
|
|
691
|
-
topFrameOrigin: "https://myapp.com", // The parent page's origin
|
|
692
|
-
nativeWindowHandle: nativeHandle,
|
|
693
|
-
}
|
|
694
|
-
);
|
|
695
|
-
```
|
|
696
|
-
|
|
697
77
|
## Error Handling
|
|
698
78
|
|
|
699
79
|
Both `createCredential` and `getCredential` functions return a result object with a `success` field. Always check this field:
|
|
@@ -778,54 +158,9 @@ console.log("Credential ID:", result.data.credentialId);
|
|
|
778
158
|
- ❌ Conditional UI (autofill/conditional mediation)
|
|
779
159
|
- ❌ Other WebAuthn extensions (credProtect, minPinLength, hmac-secret, etc.)
|
|
780
160
|
|
|
781
|
-
## Best Practices
|
|
782
|
-
|
|
783
161
|
### Security Recommendations
|
|
784
162
|
|
|
785
|
-
|
|
786
|
-
2. **Implement public suffix list validation** using `tldts` or similar library
|
|
787
|
-
3. **Validate rpId carefully** - it should match your domain
|
|
788
|
-
4. **Generate strong challenges** - use at least 32 random bytes from a CSPRNG
|
|
789
|
-
5. **Verify assertions on your server** - never trust client-side validation alone
|
|
790
|
-
6. **Store credential public keys securely** - needed for signature verification
|
|
791
|
-
7. **Implement proper timeout handling** - don't leave prompts open indefinitely
|
|
792
|
-
|
|
793
|
-
### Performance Tips
|
|
794
|
-
|
|
795
|
-
1. **Cache native window handles** - no need to get them on every call
|
|
796
|
-
2. **Reuse challenge buffers** when possible
|
|
797
|
-
3. **Set appropriate timeouts** - shorter for better UX, longer for hardware keys
|
|
798
|
-
4. **Handle user cancellation gracefully** - don't retry automatically
|
|
799
|
-
|
|
800
|
-
### TypeScript Types
|
|
801
|
-
|
|
802
|
-
This library exports all necessary TypeScript types. Import them for type safety:
|
|
803
|
-
|
|
804
|
-
```typescript
|
|
805
|
-
import type {
|
|
806
|
-
// Registration types
|
|
807
|
-
CreateCredentialResult,
|
|
808
|
-
CreateCredentialSuccessData,
|
|
809
|
-
PublicKeyCredentialCreationOptions,
|
|
810
|
-
|
|
811
|
-
// Authentication types
|
|
812
|
-
GetCredentialResult,
|
|
813
|
-
GetCredentialSuccessData,
|
|
814
|
-
PublicKeyCredentialRequestOptions,
|
|
815
|
-
|
|
816
|
-
// Shared types
|
|
817
|
-
AuthenticationExtensionsClientInputs,
|
|
818
|
-
PRFInput,
|
|
819
|
-
} from "electron-webauthn";
|
|
820
|
-
```
|
|
821
|
-
|
|
822
|
-
## Debugging
|
|
823
|
-
|
|
824
|
-
Enable detailed logging by checking the console output. The library logs warnings for:
|
|
825
|
-
|
|
826
|
-
- PRF extension enabled but no input values provided
|
|
827
|
-
- Large blob write enabled but no data provided
|
|
828
|
-
- Authorization errors with native error messages
|
|
163
|
+
- **Implement public suffix list validation** using `tldts` or similar library
|
|
829
164
|
|
|
830
165
|
## Known Limitations
|
|
831
166
|
|
|
@@ -4,7 +4,6 @@ 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";
|
|
8
7
|
const createControllerState = new Map();
|
|
9
8
|
function getObjectPointerString(self) {
|
|
10
9
|
return getPointer(self).toBase64();
|
|
@@ -32,7 +31,13 @@ export const WebauthnCreateController = NobjcClass.define({
|
|
|
32
31
|
const context = NobjcClass.super(self, "_requestContextWithRequests$error$", requests, outError);
|
|
33
32
|
const selfPointer = getObjectPointerString(self);
|
|
34
33
|
if (context && createControllerState.has(selfPointer)) {
|
|
35
|
-
|
|
34
|
+
let isSecurityKey = false;
|
|
35
|
+
let registrationOptions = context.platformKeyCredentialCreationOptions();
|
|
36
|
+
if (!registrationOptions) {
|
|
37
|
+
registrationOptions =
|
|
38
|
+
context.securityKeyCredentialCreationOptions();
|
|
39
|
+
isSecurityKey = true;
|
|
40
|
+
}
|
|
36
41
|
const [clientDataHash, pubKeyCredParams, residentKeyRequired, excludeCredentials,] = createControllerState.get(selfPointer);
|
|
37
42
|
registrationOptions.setClientDataHash$(NSDataFromBuffer(clientDataHash));
|
|
38
43
|
registrationOptions.setChallenge$(null);
|
|
@@ -45,7 +50,9 @@ export const WebauthnCreateController = NobjcClass.define({
|
|
|
45
50
|
if (supportedAlgos.length > 0) {
|
|
46
51
|
registrationOptions.setSupportedAlgorithmIdentifiers$(NSArrayFromObjects(supportedAlgos));
|
|
47
52
|
}
|
|
48
|
-
|
|
53
|
+
if (!isSecurityKey) {
|
|
54
|
+
registrationOptions.setShouldRequireResidentKey$(residentKeyRequired);
|
|
55
|
+
}
|
|
49
56
|
const excludeList = [];
|
|
50
57
|
for (const cred of excludeCredentials) {
|
|
51
58
|
const transports = [];
|
package/dist/create/handler.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type CreateCredentialErrorCodes = "TypeError" | "AbortError" | "NotAllowedError" | "SecurityError" | "InvalidStateError";
|
|
1
2
|
export interface CreateCredentialSuccessData {
|
|
2
3
|
credentialId: string;
|
|
3
4
|
clientDataJSON: string;
|
|
@@ -34,7 +35,8 @@ interface CreateCredentialSuccessResult {
|
|
|
34
35
|
}
|
|
35
36
|
interface CreateCredentialErrorResult {
|
|
36
37
|
success: false;
|
|
37
|
-
error:
|
|
38
|
+
error: CreateCredentialErrorCodes;
|
|
39
|
+
errorObject?: Error;
|
|
38
40
|
}
|
|
39
41
|
export type CreateCredentialResult = CreateCredentialSuccessResult | CreateCredentialErrorResult;
|
|
40
42
|
export declare function createCredential(publicKeyOptions: PublicKeyCredentialCreationOptions | undefined, additionalOptions: WebauthnCreateRequestOptions): Promise<CreateCredentialResult>;
|
package/dist/create/handler.js
CHANGED
|
@@ -108,6 +108,16 @@ export async function createCredential(publicKeyOptions, additionalOptions) {
|
|
|
108
108
|
return { success: false, error: "TypeError" };
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
+
if (supportedAlgorithmIdentifiers.length === 0) {
|
|
112
|
+
supportedAlgorithmIdentifiers.push({
|
|
113
|
+
type: "public-key",
|
|
114
|
+
algorithm: -7,
|
|
115
|
+
});
|
|
116
|
+
supportedAlgorithmIdentifiers.push({
|
|
117
|
+
type: "public-key",
|
|
118
|
+
algorithm: -257,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
111
121
|
const excludeCredentials = [];
|
|
112
122
|
if (publicKeyOptions.excludeCredentials &&
|
|
113
123
|
Array.isArray(publicKeyOptions.excludeCredentials)) {
|
|
@@ -128,6 +138,7 @@ export async function createCredential(publicKeyOptions, additionalOptions) {
|
|
|
128
138
|
const { extensions, largeBlobSupport, prf } = getExtensionsConfiguration(publicKeyOptions.extensions);
|
|
129
139
|
let residentKeyRequired = false;
|
|
130
140
|
let userVerificationPreference = "preferred";
|
|
141
|
+
let preferredAuthenticatorAttachment = "all";
|
|
131
142
|
if (publicKeyOptions.authenticatorSelection) {
|
|
132
143
|
if (publicKeyOptions.authenticatorSelection.residentKey === "required") {
|
|
133
144
|
residentKeyRequired = true;
|
|
@@ -145,6 +156,13 @@ export async function createCredential(publicKeyOptions, additionalOptions) {
|
|
|
145
156
|
else {
|
|
146
157
|
userVerificationPreference = "preferred";
|
|
147
158
|
}
|
|
159
|
+
const attachment = publicKeyOptions.authenticatorSelection.authenticatorAttachment;
|
|
160
|
+
if (attachment === "cross-platform") {
|
|
161
|
+
preferredAuthenticatorAttachment = "cross-platform";
|
|
162
|
+
}
|
|
163
|
+
else if (attachment === "platform") {
|
|
164
|
+
preferredAuthenticatorAttachment = "platform";
|
|
165
|
+
}
|
|
148
166
|
}
|
|
149
167
|
const { currentOrigin, topFrameOrigin, isPublicSuffix, nativeWindowHandle } = additionalOptions;
|
|
150
168
|
const isRpIdAllowed = isRpIdAllowedForOrigin(currentOrigin, rpId, {
|
|
@@ -153,13 +171,13 @@ export async function createCredential(publicKeyOptions, additionalOptions) {
|
|
|
153
171
|
if (!isRpIdAllowed.ok) {
|
|
154
172
|
return { success: false, error: "NotAllowedError" };
|
|
155
173
|
}
|
|
156
|
-
|
|
174
|
+
let errorResult = null;
|
|
175
|
+
const result = await createCredentialInternal(rpId, challenge, userName, userID, nativeWindowHandle, currentOrigin, timeout, extensions, attestationPreference, supportedAlgorithmIdentifiers, excludeCredentials, residentKeyRequired, preferredAuthenticatorAttachment, userVerificationPreference, {
|
|
157
176
|
topFrameOrigin,
|
|
158
177
|
largeBlobSupport,
|
|
159
178
|
prf,
|
|
160
179
|
}).catch((error) => {
|
|
161
|
-
|
|
162
|
-
console.log("error.message", error.message);
|
|
180
|
+
errorResult = error;
|
|
163
181
|
if (error.message.includes("(com.apple.AuthenticationServices.AuthorizationError error 1006.)")) {
|
|
164
182
|
return "InvalidStateError";
|
|
165
183
|
}
|
|
@@ -169,7 +187,7 @@ export async function createCredential(publicKeyOptions, additionalOptions) {
|
|
|
169
187
|
return null;
|
|
170
188
|
});
|
|
171
189
|
if (typeof result === "string") {
|
|
172
|
-
return { success: false, error: result };
|
|
190
|
+
return { success: false, error: result, errorObject: errorResult };
|
|
173
191
|
}
|
|
174
192
|
const data = {
|
|
175
193
|
credentialId: bufferToBase64Url(result.credentialId),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type PRFInput } from "../helpers/prf.js";
|
|
2
2
|
import { type PublicKeyCredentialParams } from "./authorization-controller.js";
|
|
3
|
+
export type AuthenticatorAttachmentWithExtra = AuthenticatorAttachment | "all";
|
|
3
4
|
export interface CreateCredentialResult {
|
|
4
5
|
credentialId: Buffer;
|
|
5
6
|
clientDataJSON: Buffer;
|
|
@@ -30,5 +31,5 @@ export interface ExcludeCredential {
|
|
|
30
31
|
id: Buffer;
|
|
31
32
|
transports?: string[];
|
|
32
33
|
}
|
|
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
|
+
declare function createCredentialInternal(rpid: string, challenge: Buffer, username: string, userID: Buffer, nativeWindowHandle: Buffer, origin: string, timeout: number, enabledExtensions: CredentialCreationExtensions[], attestation: CredentialAttestationPreference, supportedAlgorithmIdentifiers: PublicKeyCredentialParams[], excludeCredentials: ExcludeCredential[], residentKeyRequired?: boolean, preferredAuthenticatorAttachment?: AuthenticatorAttachmentWithExtra, userVerification?: CredentialUserVerificationPreference, additionalOptions?: CreateCredentialAdditionalOptions): Promise<CreateCredentialResult>;
|
|
34
35
|
export { createCredentialInternal };
|
|
@@ -17,16 +17,11 @@ import { NSStringFromString } from "../objc/foundation/nsstring.js";
|
|
|
17
17
|
import { removeControllerState, setControllerState, WebauthnCreateController, } from "./authorization-controller.js";
|
|
18
18
|
import { parseAttestationObject } from "@oslojs/webauthn";
|
|
19
19
|
import { ASAuthorizationPublicKeyCredentialAttachment } from "../objc/authentication-services/enums/as-authorization-public-key-credential-attachment.js";
|
|
20
|
+
import { createSecurityKeyPublicKeyCredentialProvider } from "../objc/authentication-services/as-authorization-security-key-public-key-credential-provider.js";
|
|
21
|
+
import { createASAuthorizationPublicKeyCredentialParameters, } from "../objc/authentication-services/as-authorization-public-key-credential-parameters.js";
|
|
20
22
|
const VALID_EXTENSIONS = ["largeBlob", "prf"];
|
|
21
|
-
function
|
|
22
|
-
|
|
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")) {
|
|
23
|
+
function setupPublicKeyCredentialRegistrationRequest(type, keyRequest, attestation, enabledExtensions, userVerification, pubKeyCredParams, additionalOptions) {
|
|
24
|
+
if (type === "platform" && enabledExtensions.includes("largeBlob")) {
|
|
30
25
|
let supportMode;
|
|
31
26
|
const largeBlobSupport = additionalOptions.largeBlobSupport;
|
|
32
27
|
if (largeBlobSupport === "required") {
|
|
@@ -42,11 +37,25 @@ function createCredentialInternal(rpid, challenge, username, userID, nativeWindo
|
|
|
42
37
|
}
|
|
43
38
|
if (supportMode) {
|
|
44
39
|
const largeBlobInput = createASAuthorizationPublicKeyCredentialLargeBlobRegistrationInput(supportMode);
|
|
45
|
-
|
|
40
|
+
keyRequest.setLargeBlob$(largeBlobInput);
|
|
46
41
|
}
|
|
47
42
|
}
|
|
48
43
|
let attestationPreference = ASAuthorizationPublicKeyCredentialAttestationKind.None;
|
|
49
|
-
|
|
44
|
+
if (type === "security-key") {
|
|
45
|
+
if (attestation === "direct") {
|
|
46
|
+
attestationPreference =
|
|
47
|
+
ASAuthorizationPublicKeyCredentialAttestationKind.Direct;
|
|
48
|
+
}
|
|
49
|
+
else if (attestation === "enterprise") {
|
|
50
|
+
attestationPreference =
|
|
51
|
+
ASAuthorizationPublicKeyCredentialAttestationKind.Enterprise;
|
|
52
|
+
}
|
|
53
|
+
else if (attestation === "indirect") {
|
|
54
|
+
attestationPreference =
|
|
55
|
+
ASAuthorizationPublicKeyCredentialAttestationKind.Indirect;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
keyRequest.setAttestationPreference$(NSStringFromString(attestationPreference));
|
|
50
59
|
let userVerificationPreference = ASAuthorizationPublicKeyCredentialUserVerificationPreference.Preferred;
|
|
51
60
|
if (userVerification === "required") {
|
|
52
61
|
userVerificationPreference =
|
|
@@ -56,28 +65,67 @@ function createCredentialInternal(rpid, challenge, username, userID, nativeWindo
|
|
|
56
65
|
userVerificationPreference =
|
|
57
66
|
ASAuthorizationPublicKeyCredentialUserVerificationPreference.Discouraged;
|
|
58
67
|
}
|
|
59
|
-
|
|
60
|
-
if (additionalOptions.userDisplayName) {
|
|
68
|
+
keyRequest.setUserVerificationPreference$(NSStringFromString(userVerificationPreference));
|
|
69
|
+
if (type === "platform" && additionalOptions.userDisplayName) {
|
|
61
70
|
const userDisplayName = NSStringFromString(additionalOptions.userDisplayName);
|
|
62
|
-
|
|
71
|
+
keyRequest.setDisplayName$(userDisplayName);
|
|
63
72
|
}
|
|
64
|
-
if (
|
|
73
|
+
if (type === "security-key") {
|
|
74
|
+
const credentialParameters = [];
|
|
75
|
+
for (const param of pubKeyCredParams) {
|
|
76
|
+
if (param.type === "public-key") {
|
|
77
|
+
credentialParameters.push(createASAuthorizationPublicKeyCredentialParameters(param.algorithm));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const nsCredentialParameters = NSArrayFromObjects(credentialParameters);
|
|
81
|
+
keyRequest.setCredentialParameters$(nsCredentialParameters);
|
|
82
|
+
}
|
|
83
|
+
if (type === "platform" && enabledExtensions.includes("prf")) {
|
|
65
84
|
if (additionalOptions.prf) {
|
|
66
85
|
const inputValues = createPRFInput(additionalOptions.prf);
|
|
67
86
|
const prfInput = createASAuthorizationPublicKeyCredentialPRFRegistrationInput(inputValues);
|
|
68
|
-
|
|
87
|
+
keyRequest.setPrf$(prfInput);
|
|
69
88
|
}
|
|
70
89
|
else {
|
|
71
|
-
|
|
90
|
+
keyRequest.setPrf$(ASAuthorizationPublicKeyCredentialPRFRegistrationInput.checkForSupport());
|
|
72
91
|
}
|
|
73
92
|
}
|
|
74
|
-
|
|
93
|
+
}
|
|
94
|
+
function createCredentialInternal(rpid, challenge, username, userID, nativeWindowHandle, origin, timeout, enabledExtensions, attestation = "none", supportedAlgorithmIdentifiers = [], excludeCredentials, residentKeyRequired = false, preferredAuthenticatorAttachment = "all", userVerification = "preferred", additionalOptions = {}) {
|
|
95
|
+
const { promise, resolve, reject } = PromiseWithResolvers();
|
|
96
|
+
const NS_rpID = NSStringFromString(rpid);
|
|
97
|
+
const NS_challenge = NSDataFromBuffer(challenge);
|
|
98
|
+
const NS_username = NSStringFromString(username);
|
|
99
|
+
const NS_userID = NSDataFromBuffer(userID);
|
|
100
|
+
const requestArrayInput = [];
|
|
101
|
+
if (preferredAuthenticatorAttachment === "all" ||
|
|
102
|
+
preferredAuthenticatorAttachment === "platform") {
|
|
103
|
+
const platformProvider = createPlatformPublicKeyCredentialProvider(NS_rpID);
|
|
104
|
+
const platformKeyRequest = platformProvider.createCredentialRegistrationRequestWithChallenge$name$userID$(NS_challenge, NS_username, NS_userID);
|
|
105
|
+
setupPublicKeyCredentialRegistrationRequest("platform", platformKeyRequest, attestation, enabledExtensions, userVerification, supportedAlgorithmIdentifiers, additionalOptions);
|
|
106
|
+
requestArrayInput.push(platformKeyRequest);
|
|
107
|
+
}
|
|
108
|
+
if (preferredAuthenticatorAttachment === "all" ||
|
|
109
|
+
preferredAuthenticatorAttachment === "cross-platform") {
|
|
110
|
+
const securityKeyProvider = createSecurityKeyPublicKeyCredentialProvider(NS_rpID);
|
|
111
|
+
const securityKeyRequest = securityKeyProvider.createCredentialRegistrationRequestWithChallenge$displayName$name$userID$(NS_challenge, NSStringFromString(additionalOptions.userDisplayName || username), NS_username, NS_userID);
|
|
112
|
+
setupPublicKeyCredentialRegistrationRequest("security-key", securityKeyRequest, attestation, enabledExtensions, userVerification, supportedAlgorithmIdentifiers, additionalOptions);
|
|
113
|
+
requestArrayInput.push(securityKeyRequest);
|
|
114
|
+
}
|
|
115
|
+
const requestsArray = NSArrayFromObjects(requestArrayInput);
|
|
75
116
|
const authController = WebauthnCreateController.alloc().initWithAuthorizationRequests$(requestsArray);
|
|
76
117
|
const clientData = generateWebauthnClientData("webauthn.create", origin, challenge, additionalOptions.topFrameOrigin);
|
|
77
118
|
const { clientDataHash, clientDataBuffer } = generateClientDataInfo(clientData);
|
|
78
119
|
setControllerState(authController, clientDataHash, supportedAlgorithmIdentifiers, residentKeyRequired, excludeCredentials);
|
|
120
|
+
let isFinished = false;
|
|
121
|
+
let timeoutHandlerId = null;
|
|
79
122
|
const finished = (_success) => {
|
|
123
|
+
isFinished = true;
|
|
80
124
|
removeControllerState(authController);
|
|
125
|
+
if (timeoutHandlerId) {
|
|
126
|
+
clearTimeout(timeoutHandlerId);
|
|
127
|
+
timeoutHandlerId = null;
|
|
128
|
+
}
|
|
81
129
|
};
|
|
82
130
|
const delegate = createAuthorizationControllerDelegate({
|
|
83
131
|
didCompleteWithAuthorization: (_, authorization) => {
|
|
@@ -140,7 +188,6 @@ function createCredentialInternal(rpid, challenge, username, userID, nativeWindo
|
|
|
140
188
|
didCompleteWithError: (_, error) => {
|
|
141
189
|
const parsedError = error;
|
|
142
190
|
const errorMessage = parsedError.localizedDescription().UTF8String();
|
|
143
|
-
console.error("Authorization failed:", errorMessage);
|
|
144
191
|
reject(new Error(errorMessage));
|
|
145
192
|
finished(false);
|
|
146
193
|
},
|
|
@@ -149,6 +196,11 @@ function createCredentialInternal(rpid, challenge, username, userID, nativeWindo
|
|
|
149
196
|
const presentationContextProvider = createPresentationContextProviderFromNativeWindowHandle(nativeWindowHandle);
|
|
150
197
|
authController.setPresentationContextProvider$(presentationContextProvider);
|
|
151
198
|
authController.performRequests();
|
|
199
|
+
timeoutHandlerId = setTimeout(() => {
|
|
200
|
+
if (isFinished)
|
|
201
|
+
return;
|
|
202
|
+
authController.cancel();
|
|
203
|
+
}, timeout);
|
|
152
204
|
return promise;
|
|
153
205
|
}
|
|
154
206
|
export { createCredentialInternal };
|
|
@@ -22,7 +22,10 @@ export const WebauthnGetController = NobjcClass.define({
|
|
|
22
22
|
const context = NobjcClass.super(self, "_requestContextWithRequests$error$", requests, outError);
|
|
23
23
|
const selfPointer = getObjectPointerString(self);
|
|
24
24
|
if (getControllerState.has(selfPointer)) {
|
|
25
|
-
|
|
25
|
+
let assertionOptions = context.platformKeyCredentialAssertionOptions();
|
|
26
|
+
if (!assertionOptions) {
|
|
27
|
+
assertionOptions = context.securityKeyCredentialAssertionOptions();
|
|
28
|
+
}
|
|
26
29
|
const clientDataHash = getControllerState.get(selfPointer);
|
|
27
30
|
assertionOptions.setClientDataHash$(NSDataFromBuffer(clientDataHash));
|
|
28
31
|
context.setPlatformKeyCredentialAssertionOptions$(assertionOptions.copyWithZone$(null));
|
package/dist/get/handler.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type GetCredentialErrorCodes = "TypeError" | "AbortError" | "NotAllowedError" | "SecurityError";
|
|
1
2
|
export interface GetCredentialSuccessData {
|
|
2
3
|
credentialId: string;
|
|
3
4
|
clientDataJSON: string;
|
|
@@ -29,7 +30,8 @@ interface GetCredentialSuccessResult {
|
|
|
29
30
|
}
|
|
30
31
|
interface GetCredentialErrorResult {
|
|
31
32
|
success: false;
|
|
32
|
-
error:
|
|
33
|
+
error: GetCredentialErrorCodes;
|
|
34
|
+
errorObject?: Error;
|
|
33
35
|
}
|
|
34
36
|
export type GetCredentialResult = GetCredentialSuccessResult | GetCredentialErrorResult;
|
|
35
37
|
export declare function getCredential(publicKeyOptions: PublicKeyCredentialRequestOptions | undefined, additionalOptions: WebauthnGetRequestOptions): Promise<GetCredentialResult>;
|
package/dist/get/handler.js
CHANGED
|
@@ -95,23 +95,21 @@ export async function getCredential(publicKeyOptions, additionalOptions) {
|
|
|
95
95
|
if (!isRpIdAllowed.ok) {
|
|
96
96
|
return { success: false, error: "NotAllowedError" };
|
|
97
97
|
}
|
|
98
|
-
|
|
98
|
+
let errorResult = null;
|
|
99
|
+
const result = await getCredentialInternal(rpId, challenge, nativeWindowHandle, currentOrigin, timeout, enabledExtensions, allowedCredentialsArray, userVerification, {
|
|
99
100
|
topFrameOrigin,
|
|
100
101
|
largeBlobDataToWrite: largeBlobWriteBuffer,
|
|
101
102
|
prf,
|
|
102
103
|
prfByCredential,
|
|
103
104
|
}).catch((error) => {
|
|
104
|
-
|
|
105
|
+
errorResult = error;
|
|
105
106
|
if (error.message.startsWith("The operation couldn’t be completed.")) {
|
|
106
107
|
return "NotAllowedError";
|
|
107
108
|
}
|
|
108
109
|
return "NotAllowedError";
|
|
109
110
|
});
|
|
110
111
|
if (typeof result === "string") {
|
|
111
|
-
|
|
112
|
-
return { success: false, error: "NotAllowedError" };
|
|
113
|
-
}
|
|
114
|
-
return { success: false, error: "NotAllowedError" };
|
|
112
|
+
return { success: false, error: result, errorObject: errorResult };
|
|
115
113
|
}
|
|
116
114
|
const data = {
|
|
117
115
|
credentialId: bufferToBase64Url(result.id),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type PRFInput } from "../helpers/prf.js";
|
|
2
|
-
type AuthenticatorAttachment
|
|
2
|
+
import type { AuthenticatorAttachment } from "../helpers/types.js";
|
|
3
3
|
export type UserVerificationPreference = "preferred" | "required" | "discouraged";
|
|
4
4
|
declare const VALID_EXTENSIONS: readonly ["largeBlobRead", "largeBlobWrite", "prf"];
|
|
5
5
|
export type CredentialAssertionExtensions = (typeof VALID_EXTENSIONS)[number];
|
|
@@ -20,5 +20,5 @@ export interface GetCredentialAdditionalOptions {
|
|
|
20
20
|
prfByCredential?: Record<string, PRFInput>;
|
|
21
21
|
topFrameOrigin?: string;
|
|
22
22
|
}
|
|
23
|
-
declare function getCredentialInternal(rpid: string, challenge: Buffer, nativeWindowHandle: Buffer, origin: string, enabledExtensions: CredentialAssertionExtensions[], allowedCredentialIds: Buffer[], userVerificationPreference?: UserVerificationPreference, additionalOptions?: GetCredentialAdditionalOptions): Promise<GetCredentialResult>;
|
|
23
|
+
declare function getCredentialInternal(rpid: string, challenge: Buffer, nativeWindowHandle: Buffer, origin: string, timeout: number, enabledExtensions: CredentialAssertionExtensions[], allowedCredentialIds: Buffer[], userVerificationPreference?: UserVerificationPreference, additionalOptions?: GetCredentialAdditionalOptions): Promise<GetCredentialResult>;
|
|
24
24
|
export { getCredentialInternal };
|
|
@@ -76,7 +76,7 @@ function setupPublicKeyCredentialRequest(type, keyRequest, userVerificationPrefe
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
-
function getCredentialInternal(rpid, challenge, nativeWindowHandle, origin, enabledExtensions = [], allowedCredentialIds, userVerificationPreference, additionalOptions = {}) {
|
|
79
|
+
function getCredentialInternal(rpid, challenge, nativeWindowHandle, origin, timeout, enabledExtensions = [], allowedCredentialIds, userVerificationPreference, additionalOptions = {}) {
|
|
80
80
|
const { promise, resolve, reject } = PromiseWithResolvers();
|
|
81
81
|
const NS_rpID = NSStringFromString(rpid);
|
|
82
82
|
const NS_challenge = NSDataFromBuffer(challenge);
|
|
@@ -94,8 +94,15 @@ function getCredentialInternal(rpid, challenge, nativeWindowHandle, origin, enab
|
|
|
94
94
|
const clientData = generateWebauthnClientData("webauthn.get", origin, challenge, additionalOptions.topFrameOrigin);
|
|
95
95
|
const { clientDataHash, clientDataBuffer } = generateClientDataInfo(clientData);
|
|
96
96
|
setClientDataHash(authController, clientDataHash);
|
|
97
|
+
let isFinished = false;
|
|
98
|
+
let timeoutHandlerId = null;
|
|
97
99
|
const finished = (_success) => {
|
|
100
|
+
isFinished = true;
|
|
98
101
|
removeClientDataHash(authController);
|
|
102
|
+
if (timeoutHandlerId) {
|
|
103
|
+
clearTimeout(timeoutHandlerId);
|
|
104
|
+
timeoutHandlerId = null;
|
|
105
|
+
}
|
|
99
106
|
};
|
|
100
107
|
if (allowedCredentialIds.length > 0) {
|
|
101
108
|
const allowedCredentials = NSArrayFromObjects(allowedCredentialIds.map((id) => createPlatformPublicKeyCredentialDescriptor(NSDataFromBuffer(id))));
|
|
@@ -152,6 +159,11 @@ function getCredentialInternal(rpid, challenge, nativeWindowHandle, origin, enab
|
|
|
152
159
|
const presentationContextProvider = createPresentationContextProviderFromNativeWindowHandle(nativeWindowHandle);
|
|
153
160
|
authController.setPresentationContextProvider$(presentationContextProvider);
|
|
154
161
|
authController.performRequests();
|
|
162
|
+
timeoutHandlerId = setTimeout(() => {
|
|
163
|
+
if (isFinished)
|
|
164
|
+
return;
|
|
165
|
+
authController.cancel();
|
|
166
|
+
}, timeout);
|
|
155
167
|
return promise;
|
|
156
168
|
}
|
|
157
169
|
export { getCredentialInternal };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type AuthenticatorAttachment = "platform" | "cross-platform";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/objc/authentication-services/as-authorization-public-key-credential-parameters.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NobjcObject } from "objc-js";
|
|
2
|
+
declare class _ASAuthorizationPublicKeyCredentialParameters extends NobjcObject {
|
|
3
|
+
initWithAlgorithm$(algorithm: number): _ASAuthorizationPublicKeyCredentialParameters;
|
|
4
|
+
algorithm(): number;
|
|
5
|
+
}
|
|
6
|
+
export declare const ASAuthorizationPublicKeyCredentialParameters: typeof _ASAuthorizationPublicKeyCredentialParameters;
|
|
7
|
+
export type { _ASAuthorizationPublicKeyCredentialParameters };
|
|
8
|
+
export declare function createASAuthorizationPublicKeyCredentialParameters(algorithm: number): _ASAuthorizationPublicKeyCredentialParameters;
|
package/dist/objc/authentication-services/as-authorization-public-key-credential-parameters.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { AuthenticationServices } from "./index.js";
|
|
2
|
+
export const ASAuthorizationPublicKeyCredentialParameters = AuthenticationServices.ASAuthorizationPublicKeyCredentialParameters;
|
|
3
|
+
export function createASAuthorizationPublicKeyCredentialParameters(algorithm) {
|
|
4
|
+
const instance = ASAuthorizationPublicKeyCredentialParameters.alloc();
|
|
5
|
+
return instance.initWithAlgorithm$(algorithm);
|
|
6
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import type { _NSData } from "../foundation/nsdata.js";
|
|
2
|
+
import type { _NSString } from "../foundation/nsstring.js";
|
|
1
3
|
import type { NobjcObject } from "objc-js";
|
|
2
4
|
declare class _ASAuthorizationSecurityKeyPublicKeyCredentialProvider extends NobjcObject {
|
|
3
5
|
initWithRelyingPartyIdentifier$(relyingPartyIdentifier: NobjcObject): _ASAuthorizationSecurityKeyPublicKeyCredentialProvider;
|
|
4
|
-
createCredentialRegistrationRequestWithChallenge$name$userID$(challenge:
|
|
6
|
+
createCredentialRegistrationRequestWithChallenge$displayName$name$userID$(challenge: _NSData, displayName: _NSString, name: _NSString, userID: _NSData): NobjcObject;
|
|
5
7
|
createCredentialAssertionRequestWithChallenge$(challenge: NobjcObject): NobjcObject;
|
|
6
8
|
}
|
|
7
9
|
export declare const ASAuthorizationSecurityKeyPublicKeyCredentialProvider: typeof _ASAuthorizationSecurityKeyPublicKeyCredentialProvider;
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "electron-webauthn",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"repository": "https://github.com/iamEvanYT/electron-webauthn.git",
|
|
5
|
+
"homepage": "https://github.com/iamEvanYT/electron-webauthn#readme",
|
|
5
6
|
"description": "Add support for WebAuthn for Electron.",
|
|
6
7
|
"main": "dist/index.js",
|
|
7
8
|
"module": "dist/index.js",
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
],
|
|
19
20
|
"type": "module",
|
|
20
21
|
"scripts": {
|
|
21
|
-
"build": "tsc && rm -rf dist/test",
|
|
22
|
+
"build": "rm -rf dist && tsc && rm -rf dist/test",
|
|
22
23
|
"test": "bun run src/test/index.ts"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
@@ -29,7 +30,7 @@
|
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
31
32
|
"@oslojs/webauthn": "^1.0.0",
|
|
32
|
-
"objc-js": "^0.0
|
|
33
|
+
"objc-js": "^1.0.0"
|
|
33
34
|
},
|
|
34
35
|
"trustedDependencies": [
|
|
35
36
|
"objc-js"
|