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