electron-webauthn-mac-test
v1.0.6
Published
A native WebAuthn addon for Electron on macOS
Readme
electron-webauthn-mac
Native WebAuthn/Passkey support for Electron on macOS using Apple's AuthenticationServices framework.
Why This Addon?
The Web Authentication API (navigator.credentials) is the standard way to implement passkey authentication in web applications. However, in Electron applications on macOS, this API is currently broken and non-functional due to platform-specific limitations (see electron/electron#24573).
This addon serves as a native implementation and polyfill for macOS, providing direct access to Apple's AuthenticationServices framework. It allows Electron applications to use passkey authentication on macOS while maintaining the option to use the standard Web Authentication API on other platforms.
Cross-Platform Implementation Pattern
async function createPasskey(userId, userName, rpId) {
if (process.platform === 'darwin') {
// Use native addon on macOS
const webauthn = require('electron-webauthn-mac');
return await webauthn.createCredential({
rpId,
userId,
name: userName,
displayName: userName
});
} else {
// Use standard Web Authentication API on other platforms
return await navigator.credentials.create({ publicKey: { /* ... */ } });
}
}Features
- Platform authenticators — Touch ID, iCloud Keychain, QR code pairing (cross-device)
- Security keys — External FIDO2 devices via USB, NFC, or BLE
- PRF extension — Derive symmetric keys from passkeys (platform authenticators only)
- LargeBlob extension — Store/retrieve arbitrary data on authenticator (platform authenticators only)
- TypeScript support — Full type definitions included
- Credential algorithm — ES256 (ECDSA P-256 with SHA-256), the only algorithm supported by Apple's AuthenticationServices
Platform vs Security Key authenticators: In Apple's AuthenticationServices API, "platform" authenticators are built-in or tightly integrated with the device — Touch ID, Face ID, iCloud Keychain passkeys, and cross-device authentication via QR code (where another Apple device acts as the authenticator). "Security key" authenticators are external FIDO2 hardware tokens connected via USB, NFC, or Bluetooth. PRF and LargeBlob extensions are only available for platform authenticators.
Requirements
| Feature | Minimum Version | |---------|-----------------| | Basic passkey operations | macOS 13.0+ | | Attachment type, credential transports | macOS 13.5+ | | LargeBlob extension | macOS 14.0+ | | Security key transports, appID | macOS 14.5+ | | PRF extension | macOS 15.0+ |
Build requirements:
- Xcode 15+ with Command Line Tools
- Node.js 18+ with node-gyp
- Apple Developer account (for code signing)
Installation
npm install electron-webauthn-macTypeScript Support
TypeScript definitions are included. Import types directly:
import type {
CreateCredentialOptions,
GetCredentialOptions,
RegistrationCredential,
AssertionCredential
} from 'electron-webauthn-mac';Basic Example
const webauthn = require('electron-webauthn-mac');
// Create a new passkey
async function registerUser() {
try {
const credential = await webauthn.createCredential({
rpId: 'example.com',
userId: 'user123',
name: 'John Doe',
displayName: 'John Doe'
});
console.log('Created credential:', credential);
} catch (error) {
console.error('Registration failed:', error);
}
}
// Authenticate with an existing passkey
async function authenticateUser() {
try {
const assertion = await webauthn.getCredential({
rpId: 'example.com'
});
console.log('Authentication successful:', assertion);
} catch (error) {
console.error('Authentication failed:', error);
}
}API Reference
Note: All credentials use ES256 algorithm (ECDSA P-256 with SHA-256) — the only algorithm supported by Apple's AuthenticationServices.
createCredential(options)
Creates a new passkey credential using Touch ID, iCloud Keychain, or an external security key.
Options:
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| rpId | string | ✅ | Relying Party identifier (your domain, e.g., "example.com") |
| userId | string | ✅ | Stable user identifier (max 64 bytes recommended) |
| name | string | ✅ | User's name (used for both platform and security key authentication) |
| displayName | string | ✅ | User's display name (used for security key only) |
| authenticators | string[] | | Which authenticator types to offer: ['platform', 'securityKey'] (default: both) |
| excludeCredentials | object[] | | Existing credentials to prevent re-registration |
| userVerification | string | | 'required', 'preferred' (default), or 'discouraged' |
| attestation | string | | 'none' (default), 'indirect', 'direct', or 'enterprise' |
| largeBlobRequired | boolean | | Require largeBlob support (macOS 14.0+, platform keys only) |
| prf | object | | PRF extension request (macOS 15.0+, platform keys only) |
Returns: Promise<RegistrationCredential>
Platform credential response (Touch ID / iCloud Keychain):
{
type: "platform",
credentialID: string, // Base64-encoded credential ID
attestationObject: string, // Base64-encoded CBOR attestation
clientDataJSON: string, // Base64-encoded client data
attachment?: string, // "platform" or "crossPlatform" (macOS 13.5+)
largeBlobSupported?: boolean, // Whether largeBlob is supported (macOS 14.0+)
prfEnabled?: boolean, // Whether PRF extension is supported (macOS 15.0+)
prfFirst?: string, // Base64-encoded first PRF output (if requested)
prfSecond?: string // Base64-encoded second PRF output (if requested)
}Security key credential response (external FIDO2 key):
{
type: "securityKey",
credentialID: string, // Base64-encoded credential ID
attestationObject: string, // Base64-encoded CBOR attestation
clientDataJSON: string, // Base64-encoded client data
transports?: string[] // ["usb", "nfc", "ble", "internal", "hybrid"] (macOS 14.5+)
}getCredential(options)
Authenticates a user using an existing passkey.
Options:
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| rpId | string | ✅ | Relying Party identifier (your domain) |
| authenticators | string[] | | Which authenticator types to offer: ['platform', 'securityKey'] (default: both) |
| allowCredentials | object[] | | Specific credentials to allow (if not set, discovers available) |
| userVerification | string | | 'required', 'preferred' (default), or 'discouraged' |
| largeBlobOperation | object | | { read: true } or { write: "base64data" } (macOS 14.0+, platform keys only) |
| prf | object | | { eval: { first: "base64", second?: "base64" } } (macOS 15.0+, platform keys only) |
Returns: Promise<AssertionCredential>
Platform credential response (Touch ID / iCloud Keychain):
{
type: "platform",
userID: string, // Base64-encoded user handle
credentialID: string, // Base64-encoded credential ID
authenticatorData: string, // Base64-encoded authenticator data
clientDataJSON: string, // Base64-encoded client data
signature: string, // Base64-encoded signature
attachment?: string, // "platform" or "crossPlatform" (macOS 13.5+)
largeBlobResult?: object, // { type: 'read', data } or { type: 'write', success } (macOS 14.0+)
prfEnabled?: boolean, // Whether PRF extension was used (macOS 15.0+)
prfFirst?: string, // Base64-encoded first PRF output
prfSecond?: string // Base64-encoded second PRF output
}Security key credential response (external FIDO2 key):
{
type: "securityKey",
userID: string, // Base64-encoded user handle
credentialID: string, // Base64-encoded credential ID
authenticatorData: string, // Base64-encoded authenticator data
clientDataJSON: string, // Base64-encoded client data
signature: string, // Base64-encoded signature
appID?: boolean // Whether legacy FIDO U2F appID was used (macOS 14.5+)
}managePasswords()
Opens the macOS system password manager (Settings > Passwords).
Parameters: None
Returns: void
Differences from Browser WebAuthn
This addon differs from the standard browser-based Web Authentication API (navigator.credentials). These differences stem from Apple's AuthenticationServices framework, which does not expose certain WebAuthn parameters.
challenge
In browser-based WebAuthn, the server generates a cryptographic challenge and sends it to the client, which includes it in the credential request. The server then verifies the signed challenge in the response.
This addon intentionally generates challenges internally to simplify the API — consumers don't need to handle challenge generation themselves. Challenges are created using SecRandomCopyBytes with 32 bytes of cryptographically secure random data:
- Registration:
PasskeyManager.swift, Line 204 - Authentication:
PasskeyManager.swift, Line 343
The generated challenge can be retrieved from the returned clientDataJSON field (base64-encoded JSON containing the challenge, origin, and type). This design is suitable for Electron applications where traditional server-side challenge verification may not be required.
rpName
In browser-based WebAuthn, rp.name specifies a human-readable name for the relying party (e.g., "My Company") that is displayed to users during authentication prompts.
Apple's ASAuthorizationPlatformPublicKeyCredentialProvider constructor only accepts relyingPartyIdentifier (the domain string). There is no API to specify a separate display name. The framework shows the rpId domain value to users instead.
timeout
In browser-based WebAuthn, the timeout parameter specifies how long (in milliseconds) the user has to complete the authentication gesture before the operation fails.
Apple's ASAuthorizationPublicKeyCredentialRegistrationRequest and ASAuthorizationPublicKeyCredentialAssertionRequest do not provide properties to configure operation timeout. The system manages timeouts internally based on platform policies, and this behavior cannot be overridden.
Domain Association (Required for rpId)
For WebAuthn to work with your domain (rpId), you must establish an association between your app and the domain. This is done by hosting an apple-app-site-association file on your server. See Apple's Associated Domains documentation for details.
1. Create the Association File
Create a file at https://your-domain.com/.well-known/apple-app-site-association with the following content:
{
"webcredentials": {
"apps": [
"TEAM_ID.BUNDLE_ID"
]
}
}Example:
{
"webcredentials": {
"apps": [
"A1B2C3D4E5.com.example.myapp"
]
}
}2. Find Your Team ID and Bundle ID
- Team ID: Found in Apple Developer Portal → Membership Details
- Bundle ID: Your app's bundle identifier (e.g.,
com.yourcompany.yourapp)
3. Server Requirements
The file must be:
- Served over HTTPS (valid SSL certificate required)
- Content-Type:
application/json - Accessible without redirects at the exact path
/.well-known/apple-app-site-association - No
.jsonextension in the URL
4. Add Associated Domains Entitlement
In your Electron app's entitlements file, add:
<key>com.apple.developer.associated-domains</key>
<array>
<string>webcredentials:your-domain.com</string>
</array>⚠️ Note: The domain in
rpIdmust exactly match the domain in your associated domains entitlement and the domain hosting theapple-app-site-associationfile.
Verification
After deployment, you can verify your association file:
- Visit
https://your-domain.com/.well-known/apple-app-site-associationin a browser - Use Apple's Associated Domains Validator
Troubleshooting
"The calling process does not have an application identifier. Make sure it is properly configured."
Cause: The app is not running as a signed .app bundle.
Solution: Build the app with npm run build:mac and run from dist/mac-arm64/YourApp.app. Running with npm start or electron . will not work.
"Application is not associated with domain" or "No credentials available"
Cause: The rpId domain is not associated with your app. macOS requires a verified link between your app's bundle identifier and the domain used as rpId.
Solution: Set up domain association as described in Domain Association (Required for rpId):
- Host an
apple-app-site-associationfile athttps://your-domain.com/.well-known/apple-app-site-association - Add the
com.apple.developer.associated-domainsentitlement to your app - Ensure Team ID and Bundle ID match exactly
PRF or LargeBlob not working
Cause: These extensions are only supported for platform authenticators (Touch ID / iCloud Keychain), not security keys.
Solution: Use authenticators: ['platform'] to restrict to platform keys when using these extensions.
How It Works
The addon provides native WebAuthn/Passkey functionality using:
- Swift: Core passkey logic using AuthenticationServices framework (
shared/PasskeyManager.swift) - Objective-C: Bridge between Swift and C++
- C++: N-API bindings for Node.js integration
- JavaScript/TypeScript: User-friendly wrapper API with full type definitions
The example application demonstrates proper IPC setup, showing how to expose the addon from the main process to the renderer thread using Electron's contextBridge and ipcMain/ipcRenderer.
Example Electron App
The repository includes an example Electron application (example-electron-app/) demonstrating the addon usage.
⚠️ The example app must be built as a
.appbundle and run fromdist/. Running withnpm startorelectron .will fail due to macOS security requirements.
cd example-electron-app
npm install
npm run build:mac
open dist/mac-arm64/FortressDemo.appThe example demonstrates creating passkeys, authenticating, and opening the system password manager.
Development
Prerequisites
- macOS 13.0+ (Ventura or later)
- Xcode 15+ with Command Line Tools (
xcode-select --install) - Node.js 18+ with npm
- Apple Developer account (for code signing)
Project Structure
electron-webauthn-mac/
├── electron-webauthn-mac/ # Native addon package
│ ├── src/ # C++/Objective-C bridge code
│ ├── js/ # JavaScript wrapper + TypeScript definitions
│ ├── binding.gyp # node-gyp build configuration
│ └── native/ # Prebuilt .node binary (generated)
├── shared/ # Shared Swift code
│ └── PasskeyManager.swift # Core WebAuthn logic (used by addon and test app)
├── example-electron-app/ # Example Electron application
└── test-mac-app/ # Native macOS test app (Xcode project)Building the Addon
cd electron-webauthn-mac
npm install
npm run buildThis compiles the Swift code, builds the native addon, and copies the .node binary to native/.
Test macOS App (Development Playground)
For faster iteration during development, use the native macOS test app:
open test-mac-app/TestMacWebauthn.xcodeproj
# Build and run in Xcode (⌘R)Why a separate native app?
Building through Electron requires code signing and bundling — which is slow. The native app provides:
- Instant iteration — build and run directly from Xcode in seconds
- Full debugger access — set breakpoints in Swift code, inspect variables
- API playground — quickly test new AuthenticationServices features
- Shared codebase — both projects use the same
PasskeyManager.swiftfromshared/
The test app includes buttons for all WebAuthn operations: registration, authentication, PRF encryption/decryption, and largeBlob read/write.
💡 Tip: When developing new features, prototype them in the test app first, then integrate into the Electron addon once verified.
Publishing
cd electron-webauthn-mac
npm run build # Builds and copies .node to native/
npm publish # prepublishOnly runs build automaticallyLicense
MIT License
Copyright (c) 2025 Vault12 Inc.
See LICENSE file for full details.
