How to Add Support for Passkeys Authentication
Passkeys secure on-chain smart accounts using fingerprint, face recognition, or device PIN codes. Users no longer need to manage complex private keys—they can access their digital wallets using passkey-enabled devices synced via Apple's iCloud Keychain or cross-platform password managers like Proton Pass and Bitwarden.
In Ethereum wallets, Passkeys replace traditional seed phrase backups. Unlike the secp256k1 curve used for Externally Owned Accounts (EOAs), Passkeys generate digital keys using the secp256r1 curve. These keys leverage device secure enclave cryptography, enhancing security and preventing password reuse. Passkeys are built on the WebAuthn standard using public-key cryptography, developed collaboratively by the FIDO Alliance (Apple, Google, Microsoft, and others) in strict adherence to WebAuthn standards.
Safe Passkeys
Safe Passkeys contracts are developed by the Safe Protocol Team. The contracts and audits are available in the Safe-Modules repository. Deployment addresses can be found on our contract deployment page.
Demo
These examples showcase Safe Smart Account deployments utilizing ERC-4337 and Passkeys
-
React Demo:
-
React Native Demo
-
Node.js demo with simulated Passkeys
Quick start
Create Passkeys Account
- npm
- yarn
npm i abstractionkit ox
yarn add abstractionkit ox
Step 1: Create WebAuthn Credentials
Create WebAuthn credentials using the createCredential function from ox/WebAuthnP256. This function simplifies credential creation by handling browser WebAuthn API interactions.
import { createCredential } from 'ox/WebAuthnP256'
const passkeyCredential = await createCredential({
name: 'Safe Wallet',
challenge: crypto.getRandomValues(new Uint8Array(32)),
rp: {
id: window.location.hostname,
name: 'Safe Wallet'
},
authenticatorSelection: {
authenticatorAttachment: 'platform',
residentKey: 'required',
userVerification: 'required',
},
timeout: 60000,
attestation: 'none',
})
Step 2: Extract Public Key
The createCredential function directly returns the public key coordinates, so no extra extraction step is needed.
import { WebauthPublicKey } from "abstractionkit";
const webauthPublicKey: WebauthPublicKey = {
x: passkeyCredential.publicKey.x,
y: passkeyCredential.publicKey.y,
}
Step 3: Initialize Smart Account
Initialize the Safe Smart Account as usual. The SafeAccountV0_3_0 supports Entrypoint v0.7, while SafeAccountV0_2_0 supports Entrypoint v0.6.
import { SafeAccountV0_3_0 as SafeAccount } from "abstractionkit";
const smartAccount = SafeAccount.initializeNewAccount([webauthPublicKey])
Create Passkey UserOp
This step follows the same flow as the standard Safe flow with createUserOperation, with the addition of the expectedSigners override.
let userOperation = await smartAccount.createUserOperation(
[transaction] // construct your MetaTransaction
jsonRpcNodeProvider, //The Node rpc endpoint.
bundlerUrl, //The Bundler rpc endpoint.
{
expectedSigners: [webauthPublicKey]
},
)
Sign with Passkeys
Step 1: Calculate the EIP712 hash
Calculate the Safe EIP712 hash for the UserOp.
const userOpHash = SafeAccount.getUserOperationEip712Hash(
userOperation,
BigInt(chainId),
);
Step 2: Request a WebAuthn Assertion
Use the sign function from ox/WebAuthnP256 to sign the user operation hash.
This function accepts the challenge (user operation hash) and the passkey's credentialId.
import { sign } from 'ox/WebAuthnP256';
import { Hex as OxHex } from 'ox/Hex';
import { Bytes, Hex } from 'ox';
const { metadata, signature } = await sign({
challenge: userOpHash as OxHex,
credentialId: passkeyCredential.id as OxHex,
});
Step 3: Create the WebauthnSignatureData
The sign function returns the metadata and signature needed to construct the WebauthnSignatureData object for abstractionkit.
import { WebauthnSignatureData } from "abstractionkit";
const clientDataMatch = metadata.clientDataJSON.match(
/^\{"type":"webauthn.get","challenge":"[A-Za-z0-9\-_]{43}",(.*)\}$/,
);
if (!clientDataMatch) {
throw new Error('Invalid clientDataJSON format: challenge not found');
}
const [, fields] = clientDataMatch;
const webauthnSignatureData: WebauthnSignatureData = {
authenticatorData: Bytes.fromHex(metadata.authenticatorData).buffer as ArrayBuffer,
clientDataFields: Hex.fromString(fields),
rs: [signature.r, signature.s],
};
const webauthnSignature: string = SafeAccount.createWebAuthnSignature(webauthnSignatureData)
Step 4: Create and Format Signer Signature Pair
Create a SignerSignaturePair containing the webauthPublicKey and webauthSignature, then format it into the expected userOperation signature format.
import { SignerSignaturePair } from "abstractionkit";
const signerSignaturePair: SignerSignaturePair = {
signer: webauthPublicKey,
signature: webauthnSignature,
}
userOperation.signature = SafeAccount.formatSignaturesToUseroperationSignature(
[signerSignaturePair],
{ isInit: userOperation.nonce == 0n },
)
Submit the UserOp onchain
const sendUserOperationResponse = await smartAccount.sendUserOperation(
userOperation,
bundlerUrl,
);
const userOperationReceiptResult = await sendUserOperationResponse.included();
Advanced
Multisig
New Account
To initialize a smart account with multiple signer types, provide both a WebAuthn public key and an EOA public key to the initialization function, with the WebAuthn public key listed first.
To add two Passkey signers, initialize and deploy the account with a single Passkey signer first, then use addOwnerWithThreshold to add the second Passkey signer.
import { SafeAccountV0_3_0 as SafeAccount } from "abstractionkit";
import { Wallet } from 'ethers'
const webauthPublicKey = .. // see above
// EOA Signer
const eoaSigner = Wallet.createRandom();
const eoaPublicKey = eoaSigner.address;
let smartAccount = SafeAccount.initializeNewAccount(
[webauthPublicKey, eoaPublicKey],
{ threshold: 2 }
)
Existing account
- Add a Passkeys owner to an existing account using
createAddOwnerWithThresholdMetaTransactions
import { MetaTransaction } from "abstractionkit"
const addPasskeysOwner: MetaTransaction = await smartAccount.createAddOwnerWithThresholdMetaTransactions(
webauthPublicKey, // the x and y webAuthn publickey
1, // threshold
{ nodeRpcUrl: nodeUrl }
);
- Swap an existing owner to a Passkeys owner using
createSwapOwnerMetaTransactions
import { MetaTransaction } from "abstractionkit"
const swapOwnerWithPasskeys: MetaTransaction = await smartAccount.createSwapOwnerMetaTransactions(
nodeUrl,
webauthPublicKey, // the x and y webAuthn publickey
oldOwnerPublicKey, // the old owner to replace
);
Create UserOp
To obtain accurate gas estimates, pass the expected signers who will sign the UserOperation in the createUserOperation overrides.
let userOperation = await smartAccount.createUserOperation(
[metaTransaction],
jsonRpcNodeProvider,
bundlerUrl,
{
expectedSigners:[webauthPublicKey, eoaPublicKey],
}
)
Signature
To sign a transaction with multiple signers, pass the signer signature pairs to formatSignaturesToUseroperationSignature.
const eoaSignature = eoaSigner.signingKey.sign(userOpHash).serialized;
const eoaSignerSignaturePair: SignerSignaturePair = {
signer: eoaPublicKey,
signature: eoaSignature,
}
userOperation.signature = SafeAccount.formatSignaturesToUseroperationSignature(
[webAuthnSignerSignaturePair, eoaSignerSignaturePair],
{ isInit: userOperation.nonce == 0n }
);
Gas Savings with Precompiles
Leverage Native Passkeys with RIP-7212 when supported for optimal gas efficiency. Import the default precompile address and pass it in the overrides. Verify that your chain has adopted the same precompile address specified in the standard.
New Account
import { SafeAccountV0_3_0 as SafeAccount, DEFAULT_SECP256R1_PRECOMPILE_ADDRESS } from "abstractionkit";
let smartAccount = SafeAccount.initializeNewAccount(
[webauthPublicKey],
{ eip7212WebAuthnPrecompileVerifierForSharedSigner: DEFAULT_SECP256R1_PRECOMPILE_ADDRESS }
)
Create UserOp
let userOperation = await smartAccount.createUserOperation(
[metaTransaction],
nodeRPC,
bundlerURL,
{
expectedSigners:[webauthPublicKey],
eip7212WebAuthnPrecompileVerifier: DEFAULT_SECP256R1_PRECOMPILE_ADDRESS
}
);
Signature
userOperation.signature = SafeAccount.formatSignaturesToUseroperationSignature(
[webauthnSignerSignaturePair],
{
isInit: userOperation.nonce == 0n,
eip7212WebAuthnPrecompileVerifier: DEFAULT_SECP256R1_PRECOMPILE_ADDRESS,
}
);
Verifying a WebAuthn Signature
Validate WebAuthn signatures to verify whether a signature on behalf of a given Safe Account is valid, similar to EOA owner verification.
- Sign a message hash using the standard process:
import { hashMessage } from "ethers";
const messageHashed = hashMessage("Hello World");
const assertion = navigator.credentials.get({
publicKey: {
challenge: ethers.getBytes(messageHashed),
rpId: "candide.dev",
allowCredentials: [
{ type: "public-key", id: new Uint8Array(credential.rawId) },
],
userVerification: UserVerificationRequirement.required,
},
});
const webauthSignatureData: WebauthnSignatureData = {
authenticatorData: assertion.response.authenticatorData,
clientDataFields: extractClientDataFields(assertion.response),
rs: extractSignature(assertion.response),
};
const webauthnSignature: string = SafeAccount.createWebAuthnSignature(webauthSignatureData);
- Validating a signed webAuthn message
verifyWebAuthnSignatureForMessageHashParam
const isSignatureValid: boolean =
await SafeAccount.verifyWebAuthnSignatureForMessageHash(
nodeURL, // node url from a json rpc provider
webauthPublicKey, // the x and y webAuthn publickey
messageHashed,
webauthnSignature
);
For a complete example to sign and verify message, run the repo safe-passkeys-sign-and-verify-message
Additional Notes
WebAuthn / Passkeys API
The WebAuthn API is a web standard that enables passwordless authentication, allowing users to sign in to websites and applications using biometric factors (e.g., fingerprint, face recognition) or security keys. This API is supported by most major browsers, including Google Chrome, Mozilla Firefox, Microsoft Edge, Apple Safari, Brave, and Opera. For more information on the browser support and requirements, Mozilla has created great documentation on WebAuthn.
Web-based:
In this guide, we use the 0x library, which provides a high-level abstraction over the WebAuthn API. This simplifies the integration process by handling the low-level details of the WebAuthn API.
ox/WebAuthnP256: This is the specific module from the0xlibrary used in this guide. It provides functions for creating and using P-256 credentials, which are the standard for passkeys.
React Native:
For React Native applications, teams have used the following libraries to integrate the WebAuthn/Passkeys functionality:
- react-native-passkey: This library provides a React Native wrapper around the platform-specific WebAuthn/Passkeys APIs.
- cbor-web: This library is used in conjunction with
react-native-passkeyto handle the CBOR (Concise Binary Object Representation) data format used by the WebAuthn API.- React Native demo by Adrian, the lead developer from Unit-e, using
abstractionkit,react-native-passkey, andcbor-web.
- React Native demo by Adrian, the lead developer from Unit-e, using
Security Considerations
Consider implementing a multisig setup or the recovery module when using Passkeys. This demo demonstrates a simple setup with a 1/1 Safe using a passkey as the sole signer. While straightforward, this approach is unsuitable for production environments. Passkeys are tied to specific domain names and, in some cases, to particular hardware manufacturers. This dependency introduces vulnerabilities that could make user accounts inaccessible under certain circumstances:
- Device Migration: Users switching between ecosystems (iPhone to Android, Windows to macOS, etc.) may lose access if the Passkey is device or ecosystem-specific.
- Domain Issues: Compromised, unavailable, or unmaintained domains can cause Passkey authentication failures.
Our recommendation: Include at least a second access method for Safe accounts—either a different backup signer (1/2 Safe configuration) or a recovery method via the recovery module.
Saving Public Credentials
Store Passkey public credentials (x, y, and rawId) in a retrievable location. Losing this data prevents users from recovering their accounts with Passkeys. This information isn't sensitive, you can use a simple server or leverage @simplewebauthn/server for storage.
Sync & Recovery
Apple
Passkey recovery on Apple devices uses iCloud Keychain escrow. In case of device loss, users authenticate through their iCloud account using standard procedures, then enter their device passcode. Apple users can also add an account recovery contact for additional support. Learn more about Apple Passkeys security.
Google
Google Password Manager seamlessly syncs passkeys across devices, with plans to extend syncing support to additional operating systems. Learn more about Google Passkeys security.
YubiKey
YubiKey supports passkeys through its authentication protocol implementation. Passkeys can be protected and managed using YubiKey's hardware-based security features. Learn more at Yubico.
Password Managers
Passkey backups extend beyond hardware manufacturers—they're supported across various password managers including Windows Hello, Bitwarden, Proton Pass, 1Password, LastPass, and others.
Device Support
Passkeys are widely available across devices such as:
- Apple Devices: iPhones & iPads (iOS 16+), Mac (macOS 13+)
- Android Devices: Phones and tablets (Android 9+)
- Windows (10/11/+): Supported on Chrome, Brave, Edge, and Firefox browsers
- Linux: Supported on Chrome, Firefox, Edge, and Brave browsers
For a comprehensive list of supported systems, please visit passkeys.dev/device-support