electron-webauthn 0.0.14 → 0.0.15
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 +470 -109
- package/dist/create/authorization-controller.d.ts +9 -0
- package/dist/create/authorization-controller.js +69 -0
- package/dist/create/handler.d.ts +30 -1
- package/dist/create/handler.js +149 -1
- package/dist/get/handler.d.ts +34 -22
- package/dist/get/handler.js +126 -166
- package/dist/get/internal-handler.d.ts +24 -0
- package/dist/get/internal-handler.js +157 -0
- package/dist/get/updated-handler.d.ts +1 -0
- package/dist/get/updated-handler.js +1 -0
- package/dist/helpers/client-data.d.ts +14 -0
- package/dist/helpers/client-data.js +36 -0
- package/dist/helpers/index.d.ts +1 -1
- package/dist/helpers/index.js +13 -10
- package/dist/helpers/presentation.d.ts +1 -0
- package/dist/helpers/presentation.js +12 -0
- package/dist/helpers/public-key.d.ts +1 -0
- package/dist/helpers/public-key.js +49 -0
- package/dist/helpers/rpid.d.ts +13 -0
- package/dist/helpers/rpid.js +59 -0
- package/dist/helpers/validation.d.ts +3 -0
- package/dist/helpers/validation.js +9 -0
- package/dist/objc/authentication-services/as-authorization-c-public-key-credential-descriptor.d.ts +9 -0
- package/dist/objc/authentication-services/as-authorization-c-public-key-credential-descriptor.js +6 -0
- package/dist/objc/authentication-services/as-authorization-platform-public-key-credential-provider.d.ts +3 -1
- package/dist/objc/authentication-services/as-authorization-platform-public-key-credential-registration.d.ts +15 -0
- package/dist/objc/authentication-services/as-authorization-platform-public-key-credential-registration.js +2 -0
- package/dist/objc/authentication-services/as-authorization-public-key-credential-large-blob-registration-input.d.ts +7 -0
- package/dist/objc/authentication-services/as-authorization-public-key-credential-large-blob-registration-input.js +6 -0
- package/dist/objc/authentication-services/as-authorization-public-key-credential-large-blob-registration-output.d.ts +5 -0
- package/dist/objc/authentication-services/as-authorization-public-key-credential-large-blob-registration-output.js +2 -0
- package/dist/objc/authentication-services/as-authorization-public-key-credential-prf-registration-input.d.ts +9 -0
- package/dist/objc/authentication-services/as-authorization-public-key-credential-prf-registration-input.js +6 -0
- package/dist/objc/authentication-services/as-authorization-public-key-credential-prf-registration-output.d.ts +9 -0
- package/dist/objc/authentication-services/as-authorization-public-key-credential-prf-registration-output.js +2 -0
- package/dist/objc/authentication-services/as-authorization-public-key-credential-registration.d.ts +14 -0
- package/dist/objc/authentication-services/as-authorization-public-key-credential-registration.js +2 -0
- package/dist/objc/authentication-services/enums/as-authorization-public-key-credential-attestation-kind.d.ts +6 -0
- package/dist/objc/authentication-services/enums/as-authorization-public-key-credential-attestation-kind.js +7 -0
- package/dist/objc/authentication-services/enums/as-authorization-public-key-credential-large-blob-support-requirement.d.ts +4 -0
- package/dist/objc/authentication-services/enums/as-authorization-public-key-credential-large-blob-support-requirement.js +5 -0
- package/dist/objc/authentication-services/enums/as-authorization-public-key-credential-user-verification-preference.d.ts +5 -0
- package/dist/objc/authentication-services/enums/as-authorization-public-key-credential-user-verification-preference.js +6 -0
- package/dist/test.d.ts +1 -0
- package/dist/test.js +5 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
# electron-webauthn
|
|
2
2
|
|
|
3
|
-
Add native WebAuthn/FIDO2 support to Electron
|
|
3
|
+
Add native WebAuthn/FIDO2 support to Electron on macOS using its AuthenticationServices framework.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
7
|
`electron-webauthn` is a TypeScript library that bridges Electron with the native macOS AuthenticationServices framework, enabling WebAuthn/FIDO2 authentication directly through native platform authenticators (Touch ID, Face ID, hardware security keys, etc.).
|
|
8
8
|
|
|
9
|
-
This package provides JavaScript bindings to Apple's AuthenticationServices framework, allowing you to perform WebAuthn assertions (authentication/signing with existing credentials) in your Electron applications.
|
|
9
|
+
This package provides JavaScript bindings to Apple's AuthenticationServices framework, allowing you to perform WebAuthn assertions (authentication/signing with existing credentials) in your Electron applications using W3C WebAuthn-compliant APIs.
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
13
|
+
- Native WebAuthn support for Electron on macOS
|
|
14
|
+
- Support for platform authenticators (Touch ID, Face ID)
|
|
15
|
+
- Support for cross-platform authenticators (external security keys)
|
|
16
|
+
- TypeScript first with complete type definitions
|
|
17
|
+
- W3C WebAuthn-compliant API
|
|
18
|
+
- Seamless integration with Electron's native window system
|
|
19
|
+
- PRF (Pseudo-Random Function) extension support
|
|
20
|
+
- Large Blob extension support for reading/writing credential-specific data
|
|
21
|
+
- User verification preference configuration (preferred, required, discouraged)
|
|
22
|
+
- Proper origin validation with public suffix list support
|
|
23
|
+
- Cross-origin iframe support with `topFrameOrigin`
|
|
24
|
+
- Per-credential PRF evaluation with `evalByCredential`
|
|
21
25
|
|
|
22
26
|
## Installation
|
|
23
27
|
|
|
@@ -40,81 +44,161 @@ yarn add electron-webauthn
|
|
|
40
44
|
```typescript
|
|
41
45
|
import { getCredential } from "electron-webauthn";
|
|
42
46
|
import { getPointer } from "objc-js";
|
|
47
|
+
import { BrowserWindow } from "electron";
|
|
43
48
|
|
|
44
49
|
// In your Electron main process or preload script
|
|
45
|
-
async function authenticate() {
|
|
50
|
+
async function authenticate(window: BrowserWindow, challenge: ArrayBuffer) {
|
|
46
51
|
// Get the native window handle from your BrowserWindow
|
|
47
|
-
const nativeWindowHandle = getPointer(
|
|
48
|
-
yourElectronWindow.getNativeWindowHandle()
|
|
49
|
-
);
|
|
52
|
+
const nativeWindowHandle = getPointer(window.getNativeWindowHandle());
|
|
50
53
|
|
|
51
|
-
// Call getCredential
|
|
54
|
+
// Call getCredential with W3C WebAuthn-compliant options
|
|
52
55
|
const result = await getCredential(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
{
|
|
57
|
+
challenge: challenge,
|
|
58
|
+
rpId: "example.com",
|
|
59
|
+
timeout: 60000, // Optional: 60 seconds
|
|
60
|
+
userVerification: "preferred", // Optional: "preferred" | "required" | "discouraged"
|
|
61
|
+
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
|
+
},
|
|
74
|
+
{
|
|
75
|
+
currentOrigin: "https://example.com",
|
|
76
|
+
topFrameOrigin: "https://example.com", // Use for iframe support
|
|
77
|
+
nativeWindowHandle: nativeWindowHandle,
|
|
78
|
+
}
|
|
58
79
|
);
|
|
59
80
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// - prf: [Buffer | null, Buffer | null] - PRF output (if supported)
|
|
69
|
-
// - largeBlob: Buffer | null - Large blob data (if supported)
|
|
81
|
+
if (result.success) {
|
|
82
|
+
console.log("Authentication successful!");
|
|
83
|
+
console.log("Credential ID:", result.data.credentialId);
|
|
84
|
+
console.log("Signature:", result.data.signature);
|
|
85
|
+
// Result contains base64url-encoded strings ready to send to server
|
|
86
|
+
} else {
|
|
87
|
+
console.error("Authentication failed:", result.error);
|
|
88
|
+
}
|
|
70
89
|
}
|
|
71
90
|
```
|
|
72
91
|
|
|
73
92
|
## API Reference
|
|
74
93
|
|
|
75
|
-
### `getCredential(
|
|
94
|
+
### `getCredential(publicKeyOptions, additionalOptions)`
|
|
76
95
|
|
|
77
|
-
Performs a WebAuthn assertion (authentication) using available platform and cross-platform authenticators.
|
|
96
|
+
Performs a WebAuthn assertion (authentication) using available platform and cross-platform authenticators. This function follows the W3C WebAuthn specification.
|
|
78
97
|
|
|
79
98
|
#### Parameters
|
|
80
99
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
100
|
+
##### `publicKeyOptions: PublicKeyCredentialRequestOptions`
|
|
101
|
+
|
|
102
|
+
You can plug the `publicKeyOptions` from `navigator.credentials.get()` directly onto this function.
|
|
103
|
+
|
|
104
|
+
Standard W3C WebAuthn credential request options:
|
|
105
|
+
|
|
106
|
+
- **`challenge: BufferSource`** (required) - The challenge from your server (32+ bytes recommended)
|
|
107
|
+
- **`rpId: string`** (required) - The Relying Party ID (typically your domain, e.g., "example.com")
|
|
108
|
+
- **`timeout?: number`** - Timeout in milliseconds (default: 10 minutes, max: 1 hour)
|
|
109
|
+
- **`userVerification?: string`** - User verification preference:
|
|
86
110
|
- `"preferred"` - User verification is preferred but not required (default)
|
|
87
|
-
- `"required"` - User verification is required
|
|
111
|
+
- `"required"` - User verification is required (e.g., biometric or PIN)
|
|
88
112
|
- `"discouraged"` - User verification should be discouraged
|
|
113
|
+
- **`allowCredentials?: PublicKeyCredentialDescriptor[]`** - Optional array to restrict allowed credentials:
|
|
114
|
+
```typescript
|
|
115
|
+
{
|
|
116
|
+
type: "public-key",
|
|
117
|
+
id: BufferSource, // Credential ID from registration
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
- **`extensions?: AuthenticationExtensionsClientInputs`** - Optional WebAuthn extensions:
|
|
121
|
+
- **`prf`** - Pseudo-Random Function extension:
|
|
122
|
+
```typescript
|
|
123
|
+
{
|
|
124
|
+
eval?: {
|
|
125
|
+
first: BufferSource, // 32+ bytes
|
|
126
|
+
second?: BufferSource // 32+ bytes (optional)
|
|
127
|
+
},
|
|
128
|
+
evalByCredential?: {
|
|
129
|
+
[base64UrlCredentialId: string]: {
|
|
130
|
+
first: BufferSource,
|
|
131
|
+
second?: BufferSource
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
- **`largeBlob`** - Large Blob extension:
|
|
137
|
+
```typescript
|
|
138
|
+
{
|
|
139
|
+
read?: boolean, // Read existing blob
|
|
140
|
+
write?: BufferSource // Write new blob data
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
##### `additionalOptions: WebauthnGetRequestOptions`
|
|
145
|
+
|
|
146
|
+
Additional options specific to the Electron environment:
|
|
147
|
+
|
|
148
|
+
- **`currentOrigin: string`** (required) - The origin of the requesting document (e.g., "https://example.com")
|
|
149
|
+
- **`topFrameOrigin: string | undefined`** - The origin of the top frame (for iframe support). Set to `currentOrigin` if not in an iframe
|
|
150
|
+
- **`nativeWindowHandle: Buffer`** (required) - Native window handle from `BrowserWindow.getNativeWindowHandle()` wrapped with `getPointer()` from `objc-js`
|
|
151
|
+
- **`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
|
|
89
152
|
|
|
90
153
|
#### Returns
|
|
91
154
|
|
|
92
|
-
`Promise<GetCredentialResult>` - Resolves with the assertion result
|
|
155
|
+
`Promise<GetCredentialResult>` - Resolves with the assertion result or error
|
|
93
156
|
|
|
94
|
-
#### Result
|
|
157
|
+
#### Result Types
|
|
95
158
|
|
|
96
159
|
```typescript
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
160
|
+
type GetCredentialResult =
|
|
161
|
+
| GetCredentialSuccessResult
|
|
162
|
+
| GetCredentialErrorResult;
|
|
163
|
+
|
|
164
|
+
interface GetCredentialSuccessResult {
|
|
165
|
+
success: true;
|
|
166
|
+
data: {
|
|
167
|
+
credentialId: string; // Base64url-encoded credential ID
|
|
168
|
+
clientDataJSON: string; // Base64url-encoded client data
|
|
169
|
+
authenticatorData: string; // Base64url-encoded authenticator data
|
|
170
|
+
signature: string; // Base64url-encoded signature
|
|
171
|
+
userHandle: string; // Base64url-encoded user handle
|
|
172
|
+
extensions?: {
|
|
173
|
+
prf?: {
|
|
174
|
+
results?: {
|
|
175
|
+
first: string; // Base64url-encoded PRF output
|
|
176
|
+
second?: string; // Base64url-encoded PRF output (if provided)
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
largeBlob?: {
|
|
180
|
+
blob?: string; // Base64url-encoded blob data (if read)
|
|
181
|
+
written?: boolean; // True if write succeeded (if write was requested)
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
};
|
|
106
185
|
}
|
|
107
|
-
```
|
|
108
186
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
187
|
+
interface GetCredentialErrorResult {
|
|
188
|
+
success: false;
|
|
189
|
+
error: "TypeError" | "AbortError" | "NotAllowedError" | "SecurityError";
|
|
190
|
+
}
|
|
113
191
|
```
|
|
114
192
|
|
|
115
193
|
## Architecture
|
|
116
194
|
|
|
117
|
-
This library implements the WebAuthn standard using native
|
|
195
|
+
This library implements the W3C WebAuthn standard using Apple's native AuthenticationServices framework. It provides a standards-compliant API that works seamlessly with existing WebAuthn servers and libraries. Under the hood, it:
|
|
196
|
+
|
|
197
|
+
- Validates origins and Relying Party IDs according to the WebAuthn specification
|
|
198
|
+
- Generates proper client data with `crossOrigin` field support (fixing Apple's default behavior)
|
|
199
|
+
- Handles both platform (Touch ID/Face ID) and cross-platform (security keys) authenticators simultaneously
|
|
200
|
+
- Properly manages WebAuthn extensions (PRF, Large Blob)
|
|
201
|
+
- Returns base64url-encoded values ready to send to your server for verification
|
|
118
202
|
|
|
119
203
|
## Usage Examples
|
|
120
204
|
|
|
@@ -129,14 +213,35 @@ let mainWindow: BrowserWindow;
|
|
|
129
213
|
|
|
130
214
|
app.on("ready", () => {
|
|
131
215
|
mainWindow = new BrowserWindow({
|
|
132
|
-
|
|
216
|
+
width: 800,
|
|
217
|
+
height: 600,
|
|
218
|
+
webPreferences: {
|
|
219
|
+
nodeIntegration: false,
|
|
220
|
+
contextIsolation: true,
|
|
221
|
+
preload: "./preload.js",
|
|
222
|
+
},
|
|
133
223
|
});
|
|
224
|
+
mainWindow.loadURL("https://myapp.com");
|
|
134
225
|
});
|
|
135
226
|
|
|
136
227
|
// In your preload script or main process
|
|
137
|
-
export async function authenticateUser(challenge:
|
|
228
|
+
export async function authenticateUser(challenge: ArrayBuffer) {
|
|
138
229
|
const nativeHandle = getPointer(mainWindow.getNativeWindowHandle());
|
|
139
|
-
|
|
230
|
+
|
|
231
|
+
const result = await getCredential(
|
|
232
|
+
{
|
|
233
|
+
challenge: challenge,
|
|
234
|
+
rpId: "myapp.com",
|
|
235
|
+
userVerification: "preferred",
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
currentOrigin: "https://myapp.com",
|
|
239
|
+
topFrameOrigin: "https://myapp.com",
|
|
240
|
+
nativeWindowHandle: nativeHandle,
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
return result;
|
|
140
245
|
}
|
|
141
246
|
```
|
|
142
247
|
|
|
@@ -145,99 +250,355 @@ export async function authenticateUser(challenge: Buffer) {
|
|
|
145
250
|
```typescript
|
|
146
251
|
// Only allow specific registered credentials
|
|
147
252
|
const result = await getCredential(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
253
|
+
{
|
|
254
|
+
challenge: challenge,
|
|
255
|
+
rpId: "myapp.com",
|
|
256
|
+
allowCredentials: [
|
|
257
|
+
{ type: "public-key", id: credentialId1 },
|
|
258
|
+
{ type: "public-key", id: credentialId2 },
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
currentOrigin: "https://myapp.com",
|
|
263
|
+
topFrameOrigin: "https://myapp.com",
|
|
264
|
+
nativeWindowHandle: nativeHandle,
|
|
265
|
+
}
|
|
152
266
|
);
|
|
153
267
|
```
|
|
154
268
|
|
|
155
|
-
### With User Verification
|
|
269
|
+
### With Required User Verification
|
|
156
270
|
|
|
157
271
|
```typescript
|
|
158
272
|
// Require user verification (e.g., for sensitive operations)
|
|
159
273
|
const result = await getCredential(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
274
|
+
{
|
|
275
|
+
challenge: challenge,
|
|
276
|
+
rpId: "myapp.com",
|
|
277
|
+
userVerification: "required", // Force biometric or PIN
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
currentOrigin: "https://myapp.com",
|
|
281
|
+
topFrameOrigin: "https://myapp.com",
|
|
282
|
+
nativeWindowHandle: nativeHandle,
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Using PRF Extension
|
|
288
|
+
|
|
289
|
+
The PRF (Pseudo-Random Function) extension allows you to derive cryptographic secrets from credentials:
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
// Generate a random salt for PRF
|
|
293
|
+
const prfSalt = crypto.getRandomValues(new Uint8Array(32));
|
|
294
|
+
|
|
295
|
+
const result = await getCredential(
|
|
296
|
+
{
|
|
297
|
+
challenge: challenge,
|
|
298
|
+
rpId: "myapp.com",
|
|
299
|
+
extensions: {
|
|
300
|
+
prf: {
|
|
301
|
+
eval: {
|
|
302
|
+
first: prfSalt,
|
|
303
|
+
// second: optionalSecondSalt, // Optional second output
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
currentOrigin: "https://myapp.com",
|
|
310
|
+
topFrameOrigin: "https://myapp.com",
|
|
311
|
+
nativeWindowHandle: nativeHandle,
|
|
312
|
+
}
|
|
165
313
|
);
|
|
314
|
+
|
|
315
|
+
if (result.success && result.data.extensions?.prf?.results) {
|
|
316
|
+
const prfOutput = result.data.extensions.prf.results.first;
|
|
317
|
+
// Use prfOutput as a cryptographic key for encryption, etc.
|
|
318
|
+
console.log("PRF output:", prfOutput);
|
|
319
|
+
}
|
|
166
320
|
```
|
|
167
321
|
|
|
168
|
-
### Using PRF
|
|
322
|
+
### Using PRF with Per-Credential Evaluation
|
|
169
323
|
|
|
170
324
|
```typescript
|
|
171
|
-
//
|
|
325
|
+
import { bufferToBase64Url } from "./helpers"; // You'll need to implement this
|
|
326
|
+
|
|
172
327
|
const result = await getCredential(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
328
|
+
{
|
|
329
|
+
challenge: challenge,
|
|
330
|
+
rpId: "myapp.com",
|
|
331
|
+
allowCredentials: [
|
|
332
|
+
{ type: "public-key", id: credentialId1 },
|
|
333
|
+
{ type: "public-key", id: credentialId2 },
|
|
334
|
+
],
|
|
335
|
+
extensions: {
|
|
336
|
+
prf: {
|
|
337
|
+
evalByCredential: {
|
|
338
|
+
[bufferToBase64Url(credentialId1)]: {
|
|
339
|
+
first: new Uint8Array(32), // Different salt for credential 1
|
|
340
|
+
},
|
|
341
|
+
[bufferToBase64Url(credentialId2)]: {
|
|
342
|
+
first: new Uint8Array(32), // Different salt for credential 2
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
currentOrigin: "https://myapp.com",
|
|
350
|
+
topFrameOrigin: "https://myapp.com",
|
|
351
|
+
nativeWindowHandle: nativeHandle,
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Reading and Writing Large Blobs
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
// Reading a large blob
|
|
360
|
+
const readResult = await getCredential(
|
|
361
|
+
{
|
|
362
|
+
challenge: challenge,
|
|
363
|
+
rpId: "myapp.com",
|
|
364
|
+
extensions: {
|
|
365
|
+
largeBlob: {
|
|
366
|
+
read: true,
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
currentOrigin: "https://myapp.com",
|
|
372
|
+
topFrameOrigin: "https://myapp.com",
|
|
373
|
+
nativeWindowHandle: nativeHandle,
|
|
374
|
+
}
|
|
177
375
|
);
|
|
178
376
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
377
|
+
if (readResult.success && readResult.data.extensions?.largeBlob?.blob) {
|
|
378
|
+
console.log("Large blob data:", readResult.data.extensions.largeBlob.blob);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Writing a large blob
|
|
382
|
+
const dataToWrite = new TextEncoder().encode("Secret data");
|
|
383
|
+
const writeResult = await getCredential(
|
|
384
|
+
{
|
|
385
|
+
challenge: challenge,
|
|
386
|
+
rpId: "myapp.com",
|
|
387
|
+
extensions: {
|
|
388
|
+
largeBlob: {
|
|
389
|
+
write: dataToWrite,
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
currentOrigin: "https://myapp.com",
|
|
395
|
+
topFrameOrigin: "https://myapp.com",
|
|
396
|
+
nativeWindowHandle: nativeHandle,
|
|
397
|
+
}
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
if (writeResult.success && writeResult.data.extensions?.largeBlob?.written) {
|
|
401
|
+
console.log("Large blob written successfully");
|
|
184
402
|
}
|
|
185
403
|
```
|
|
186
404
|
|
|
405
|
+
### With Public Suffix List Validation
|
|
406
|
+
|
|
407
|
+
For enhanced security, use a public suffix list library like `tldts`:
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
import { getPublicSuffix } from "tldts";
|
|
411
|
+
|
|
412
|
+
const result = await getCredential(
|
|
413
|
+
{
|
|
414
|
+
challenge: challenge,
|
|
415
|
+
rpId: "myapp.com",
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
currentOrigin: "https://myapp.com",
|
|
419
|
+
topFrameOrigin: "https://myapp.com",
|
|
420
|
+
nativeWindowHandle: nativeHandle,
|
|
421
|
+
isPublicSuffix: (domain) => {
|
|
422
|
+
const suffix = getPublicSuffix(domain);
|
|
423
|
+
return suffix === domain;
|
|
424
|
+
},
|
|
425
|
+
}
|
|
426
|
+
);
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Supporting Cross-Origin Iframes
|
|
430
|
+
|
|
431
|
+
If your app loads WebAuthn requests from an iframe:
|
|
432
|
+
|
|
433
|
+
```typescript
|
|
434
|
+
const result = await getCredential(
|
|
435
|
+
{
|
|
436
|
+
challenge: challenge,
|
|
437
|
+
rpId: "myapp.com",
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
currentOrigin: "https://auth.myapp.com", // The iframe's origin
|
|
441
|
+
topFrameOrigin: "https://myapp.com", // The parent page's origin
|
|
442
|
+
nativeWindowHandle: nativeHandle,
|
|
443
|
+
}
|
|
444
|
+
);
|
|
445
|
+
```
|
|
446
|
+
|
|
187
447
|
### Server-Side Verification
|
|
188
448
|
|
|
189
|
-
After getting the assertion result, verify it on your server:
|
|
449
|
+
After getting the assertion result, verify it on your server using any WebAuthn library:
|
|
190
450
|
|
|
191
451
|
```typescript
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
//
|
|
196
|
-
|
|
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
|
+
}
|
|
197
481
|
```
|
|
198
482
|
|
|
199
483
|
## Error Handling
|
|
200
484
|
|
|
201
|
-
The `getCredential`
|
|
202
|
-
|
|
203
|
-
- The user cancels the authentication prompt
|
|
204
|
-
- No valid credentials are available
|
|
205
|
-
- The authenticator fails
|
|
206
|
-
- The native window is invalid
|
|
485
|
+
The `getCredential` function returns a result object with a `success` field. Always check this field:
|
|
207
486
|
|
|
208
487
|
```typescript
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
488
|
+
const result = await getCredential(publicKeyOptions, additionalOptions);
|
|
489
|
+
|
|
490
|
+
if (!result.success) {
|
|
491
|
+
// Handle specific error types
|
|
492
|
+
switch (result.error) {
|
|
493
|
+
case "TypeError":
|
|
494
|
+
console.error("Invalid parameters provided");
|
|
495
|
+
break;
|
|
496
|
+
case "NotAllowedError":
|
|
497
|
+
console.error("User cancelled or no credentials available");
|
|
498
|
+
break;
|
|
499
|
+
case "SecurityError":
|
|
500
|
+
console.error("Origin or rpId validation failed");
|
|
501
|
+
break;
|
|
502
|
+
case "AbortError":
|
|
503
|
+
console.error("Operation was aborted");
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
219
507
|
}
|
|
508
|
+
|
|
509
|
+
// Success - process the credential
|
|
510
|
+
console.log("Credential ID:", result.data.credentialId);
|
|
220
511
|
```
|
|
221
512
|
|
|
222
|
-
|
|
513
|
+
### Common Error Scenarios
|
|
514
|
+
|
|
515
|
+
- **NotAllowedError**: User cancelled the prompt, no valid credentials available, or the authenticator failed
|
|
516
|
+
- **SecurityError**: Origin doesn't match rpId, invalid origin format, or rpId is a public suffix
|
|
517
|
+
- **TypeError**: Invalid parameter types (missing required fields, wrong data types)
|
|
518
|
+
- **AbortError**: Operation timeout or explicitly aborted
|
|
519
|
+
|
|
520
|
+
## Feature Support
|
|
521
|
+
|
|
522
|
+
**Currently Supported:**
|
|
223
523
|
|
|
224
524
|
- ✅ WebAuthn assertions (authentication with existing credentials)
|
|
225
|
-
- ✅ Cross-platform authenticators (external security keys)
|
|
525
|
+
- ✅ Cross-platform authenticators (external security keys like YubiKey)
|
|
226
526
|
- ✅ Platform authenticators (Touch ID, Face ID)
|
|
227
|
-
- ✅ PRF (Pseudo-Random Function)
|
|
228
|
-
- ✅
|
|
527
|
+
- ✅ PRF (Pseudo-Random Function) extension
|
|
528
|
+
- ✅ Global evaluation (`eval`)
|
|
529
|
+
- ✅ Per-credential evaluation (`evalByCredential`)
|
|
530
|
+
- ✅ Large Blob extension
|
|
531
|
+
- ✅ Reading blobs
|
|
532
|
+
- ✅ Writing blobs
|
|
533
|
+
- ✅ User verification preferences
|
|
534
|
+
- ✅ Credential filtering with `allowCredentials`
|
|
535
|
+
- ✅ Proper origin and rpId validation
|
|
536
|
+
- ✅ Cross-origin iframe support
|
|
537
|
+
- ✅ W3C WebAuthn-compliant client data generation
|
|
538
|
+
|
|
539
|
+
**Not Yet Supported:**
|
|
540
|
+
|
|
541
|
+
- ❌ Credential registration (attestation) - coming soon
|
|
542
|
+
- ❌ Discoverable credentials (resident keys)
|
|
543
|
+
- ❌ Conditional UI
|
|
544
|
+
- ❌ Other WebAuthn extensions (credProtect, minPinLength, etc.)
|
|
545
|
+
|
|
546
|
+
## Best Practices
|
|
547
|
+
|
|
548
|
+
### Security Recommendations
|
|
549
|
+
|
|
550
|
+
1. **Always use HTTPS origins** in production (unless testing on `localhost`)
|
|
551
|
+
2. **Implement public suffix list validation** using `tldts` or similar library
|
|
552
|
+
3. **Validate rpId carefully** - it should match your domain
|
|
553
|
+
4. **Generate strong challenges** - use at least 32 random bytes from a CSPRNG
|
|
554
|
+
5. **Verify assertions on your server** - never trust client-side validation alone
|
|
555
|
+
6. **Store credential public keys securely** - needed for signature verification
|
|
556
|
+
7. **Implement proper timeout handling** - don't leave prompts open indefinitely
|
|
557
|
+
|
|
558
|
+
### Performance Tips
|
|
559
|
+
|
|
560
|
+
1. **Cache native window handles** - no need to get them on every call
|
|
561
|
+
2. **Reuse challenge buffers** when possible
|
|
562
|
+
3. **Set appropriate timeouts** - shorter for better UX, longer for hardware keys
|
|
563
|
+
4. **Handle user cancellation gracefully** - don't retry automatically
|
|
564
|
+
|
|
565
|
+
### TypeScript Types
|
|
229
566
|
|
|
230
|
-
|
|
567
|
+
This library exports all necessary TypeScript types. Import them for type safety:
|
|
231
568
|
|
|
232
|
-
|
|
233
|
-
|
|
569
|
+
```typescript
|
|
570
|
+
import type {
|
|
571
|
+
GetCredentialResult,
|
|
572
|
+
GetCredentialSuccessData,
|
|
573
|
+
PublicKeyCredentialRequestOptions,
|
|
574
|
+
AuthenticationExtensionsClientInputs,
|
|
575
|
+
PRFInput,
|
|
576
|
+
} from "electron-webauthn";
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
## Debugging
|
|
580
|
+
|
|
581
|
+
Enable detailed logging by checking the console output. The library logs warnings for:
|
|
582
|
+
|
|
583
|
+
- PRF extension enabled but no input values provided
|
|
584
|
+
- Large blob write enabled but no data provided
|
|
585
|
+
- Authorization errors with native error messages
|
|
586
|
+
|
|
587
|
+
## Known Limitations
|
|
588
|
+
|
|
589
|
+
- **PRF and Large Blob**: Only supported on platform authenticators (Touch ID/Face ID), not security keys on macOS
|
|
234
590
|
|
|
235
591
|
## License
|
|
236
592
|
|
|
237
593
|
See [LICENSE](./LICENSE) file for details.
|
|
238
594
|
|
|
595
|
+
## Contributing
|
|
596
|
+
|
|
597
|
+
Contributions are welcome! Please feel free to submit issues and pull requests.
|
|
598
|
+
|
|
239
599
|
## Resources
|
|
240
600
|
|
|
241
|
-
- [WebAuthn Specification](https://www.w3.org/TR/webauthn-
|
|
601
|
+
- [W3C WebAuthn Level 3 Specification](https://www.w3.org/TR/webauthn-3/)
|
|
602
|
+
- [WebAuthn Guide](https://webauthn.guide/)
|
|
242
603
|
- [Electron Documentation](https://www.electronjs.org/docs)
|
|
243
604
|
- [objc-js Library](https://github.com/iamEvanYT/objc-js)
|