# 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. Instead, they access their digital wallets using passkey-enabled devices synced via Apple's **iCloud Keychain** or cross-platform password managers like **Proton Pass** and **Bitwarden**.

Passkeys replace traditional seed phrase backups in Ethereum wallets. Unlike the secp256k1 curve used for Externally Owned Accounts (EOAs), Passkeys use the **secp256r1** curve. These keys leverage device secure enclave cryptography, built on the WebAuthn standard using public-key cryptography developed by the FIDO Alliance (Apple, Google, Microsoft, and others).

Safe Passkeys contracts are developed by the Safe Protocol Team. The contracts and audits are available in the [Safe-Modules repository](https://github.com/safe-global/safe-modules/tree/main/modules/passkey). Deployment addresses can be found on our [contract deployment](https://docs.candide.dev/wallet/technical-reference/deployments/.md#safe-passkeys) page.

#### What You'll Build[​](#what-youll-build "Direct link to What You'll Build")

By the end of this guide, you'll have:

* Created WebAuthn passkey credentials for biometric authentication
* Initialized a Safe Smart Account with a passkey signer
* Signed and submitted a UserOperation using passkeys

#### Prerequisites[​](#prerequisites "Direct link to Prerequisites")

Before starting, make sure you have:

* **Node.js 18+** and npm or yarn
* **Basic TypeScript knowledge**
* **Familiarity with Safe Smart Accounts** and UserOperations. See the [Getting Started guide](https://docs.candide.dev/wallet/guides/getting-started.md) if you're new

Security: Do Not Ship a 1/1 Passkey-Only Safe to Production

This guide demonstrates a 1/1 Safe with a single passkey signer. For production use, consider adding a second access method since passkeys can be tied to specific domains or device ecosystems. A [1/2 multisig setup](#multisig) or the [recovery module](https://docs.candide.dev/wallet/plugins/recovery-with-guardians.md) gives users a backup path if they switch devices or lose access to their passkey

## Quick start[​](#quick-start "Direct link to Quick start")

[YouTube video player](https://www.youtube-nocookie.com/embed/Xh7ZJ3oeOu8?si=UUARp_pt_cjebiop)

## Demo[​](#demo "Direct link to Demo")

These examples showcase Safe Smart Account deployments using ERC-4337 and Passkeys:

* **React Demo**: Full browser-based passkeys flow with account creation and transaction signing.
  <!-- -->
  * [Live Demo](https://passkeys.candide.dev/) | [Source Code](https://github.com/candidelabs/safe-passkeys-react-example)
* **React Native Demo**: Mobile passkeys integration using `react-native-passkey` and `cbor-web`.
  <!-- -->
  * [Source Code](https://github.com/candidelabs/passkeys-react-native-demo)
* **Node.js Demo**: Client side example with simulated passkeys for testing and CI environments.
  <!-- -->
  * [Source Code](https://github.com/candidelabs/abstractionkit-examples/tree/main/passkeys)

## Create a Passkey-Authenticated Smart Account[​](#create-a-passkey-authenticated-smart-account "Direct link to Create a Passkey-Authenticated Smart Account")

### Step 1: Install Dependencies[​](#step-1-install-dependencies "Direct link to Step 1: Install Dependencies")

Install `abstractionkit` for Safe account tooling and `ox` for WebAuthn interactions.

* npm
* yarn

terminal

```
npm i abstractionkit ox
```

terminal

```
yarn add abstractionkit ox
```

**Why ox?**:

The `ox` library provides a high-level abstraction over the browser's WebAuthn API. The `ox/WebAuthnP256` module handles credential creation and signing with P-256 keys, removing the need to work with raw WebAuthn responses.

### Step 2: Create WebAuthn Credentials[​](#step-2-create-webauthn-credentials "Direct link to Step 2: Create WebAuthn Credentials")

Call `createCredential` from `ox/WebAuthnP256` to trigger the browser's passkey prompt. This returns a credential object containing the public key and credential ID.

createPasskey.ts

```
import { createCredential } from 'ox/WebAuthnP256'

const passkeyCredential = await createCredential({
    name: 'Safe Wallet',
    // Random challenge to prevent replay attacks
    challenge: crypto.getRandomValues(new Uint8Array(32)),
    rp: {
      // Ties the credential to the current domain
      id: window.location.hostname,
      name: 'Safe Wallet'
    },
    authenticatorSelection: {
      // Use device biometrics (Touch ID, Face ID, Windows Hello)
      authenticatorAttachment: 'platform',
      residentKey: 'required',
      userVerification: 'required',
    },
    timeout: 60000,
    attestation: 'none',
})
```

### Step 3: Extract Public Key[​](#step-3-extract-public-key "Direct link to Step 3: Extract Public Key")

The `createCredential` function returns the public key coordinates directly. Wrap them in a `WebauthPublicKey` object for use with `abstractionkit`.

extractPublicKey.ts

```
import { WebauthPublicKey } from "abstractionkit";

const webauthPublicKey: WebauthPublicKey = {
    x: passkeyCredential.publicKey.x,
    y: passkeyCredential.publicKey.y,
}
```

**Save Public Credentials**:

Store the passkey's public credentials (`x`, `y`, and `passkeyCredential.id`) in a retrievable location before the smart account is deployed. Losing this data prevents users from using their accounts if the account has not been. This information is not sensitive. You can use a simple backend server or leverage [@simplewebauthn/server](https://simplewebauthn.dev/docs/packages/server) for storage.

### Step 4: Initialize Smart Account[​](#step-4-initialize-smart-account "Direct link to Step 4: Initialize Smart Account")

Initialize the Safe Smart Account with the passkey as the signer. `SafeAccountV0_3_0` supports Entrypoint v0.7, while `SafeAccountV0_2_0` supports Entrypoint v0.6.

initAccount.ts

```
import { SafeAccountV0_3_0 as SafeAccount } from "abstractionkit";

const smartAccount = SafeAccount.initializeNewAccount([webauthPublicKey])
```

## Create and Sign a UserOperation[​](#create-and-sign-a-useroperation "Direct link to Create and Sign a UserOperation")

### Step 5: Create UserOperation[​](#step-5-create-useroperation "Direct link to Step 5: Create UserOperation")

Create a UserOperation following the standard Safe flow with [createUserOperation](https://docs.candide.dev/wallet/abstractionkit/safe-account-v3/.md#createuseroperation). Pass `expectedSigners` so that gas estimation accounts for the passkey signature format.

createUserOp.ts

```
let userOperation = await smartAccount.createUserOperation(
    [transaction], // your MetaTransaction (to, value, data)
    jsonRpcNodeProvider, // JSON-RPC node endpoint
    bundlerUrl, // Bundler RPC endpoint
    {
        expectedSigners: [webauthPublicKey]
    },
)
```

### Step 6: Sign with Passkeys[​](#step-6-sign-with-passkeys "Direct link to Step 6: Sign with Passkeys")

Signing a UserOperation with passkeys involves four substeps: hashing, requesting a WebAuthn assertion, building the signature data, and formatting it.

#### Step 6a: Calculate the EIP-712 Hash[​](#step-6a-calculate-the-eip-712-hash "Direct link to Step 6a: Calculate the EIP-712 Hash")

Compute the Safe EIP-712 hash for the UserOperation. This is the challenge that the passkey will sign.

signUserOp.ts

```
const userOpHash = SafeAccount.getUserOperationEip712Hash(
    userOperation,
    BigInt(chainId),
);
```

#### Step 6b: Request a WebAuthn Assertion[​](#step-6b-request-a-webauthn-assertion "Direct link to Step 6b: Request a WebAuthn Assertion")

Use the `sign` function from `ox/WebAuthnP256` to prompt the user for biometric authentication. Pass the `userOpHash` as the challenge and the credential ID from Step 2.

signUserOp.ts

```
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 6c: Build the WebauthnSignatureData[​](#step-6c-build-the-webauthnsignaturedata "Direct link to Step 6c: Build the WebauthnSignatureData")

Extract the `clientDataFields` from the WebAuthn response metadata and construct the `WebauthnSignatureData` object that `abstractionkit` expects.

signUserOp.ts

```
import { WebauthnSignatureData } from "abstractionkit";

// Extract the fields portion of clientDataJSON (everything after the challenge)
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 6d: Format the Signer Signature Pair[​](#step-6d-format-the-signer-signature-pair "Direct link to Step 6d: Format the Signer Signature Pair")

Create a `SignerSignaturePair` linking the public key to its signature, then format it into the UserOperation's expected signature field.

signUserOp.ts

```
import { SignerSignaturePair } from "abstractionkit";

const signerSignaturePair: SignerSignaturePair = {
    signer: webauthPublicKey,
    signature: webauthnSignature,
}

userOperation.signature = SafeAccount.formatSignaturesToUseroperationSignature(
    [signerSignaturePair],
    { isInit: userOperation.nonce == 0n },
)
```

### Step 7: Submit Onchain[​](#step-7-submit-onchain "Direct link to Step 7: Submit Onchain")

Send the signed UserOperation to the bundler and wait for it to be included onchain.

submitUserOp.ts

```
const sendUserOperationResponse = await smartAccount.sendUserOperation(
    userOperation,
    bundlerUrl,
);

// Wait for the transaction to be included in a block
const userOperationReceiptResult = await sendUserOperationResponse.included();
```

## Troubleshooting[​](#troubleshooting "Direct link to Troubleshooting")

Here are common errors you may encounter when integrating passkeys:

* **"The operation either timed out or was not allowed"**: The user cancelled the WebAuthn prompt, or the browser does not support passkeys. Verify that you are serving over HTTPS (required for WebAuthn) and that the user's browser supports the Web Authentication API.

* **"Invalid signature" or gas estimation failure**: This typically means a mismatch between the `expectedSigners` passed to `createUserOperation` and the actual signing key. Ensure you are using the same `webauthPublicKey` in both account initialization and UserOperation creation.

* **Domain mismatch**: The `rp.id` value passed during credential creation must match the domain where the signing occurs. If you created credentials on `localhost` but are signing on a deployed domain (or vice versa), the browser will reject the assertion.

* **Passkey prompt does not appear**: Ensure `authenticatorAttachment` is set to `'platform'` for built-in biometrics, or `'cross-platform'` for security keys. Some browsers require a user gesture (like a button click) before the WebAuthn prompt can appear.

#### Getting Help[​](#getting-help "Direct link to Getting Help")

If you're still stuck:

* Ask questions in abstractionkit's [GitHub Discussions](https://github.com/candidelabs/abstractionkit/discussions)
* Join our [Discord](https://discord.gg/KJSzy2Rqtg)

## Advanced[​](#advanced "Direct link to Advanced")

### Multisig[​](#multisig "Direct link to Multisig")

#### New Account[​](#new-account "Direct link to 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`](https://docs.candide.dev/wallet/abstractionkit/safe-account-v3/.md#createaddownerwiththresholdmetatransactions) to add the second Passkey signer.

multisigInit.ts

```
import { SafeAccountV0_3_0 as SafeAccount } from "abstractionkit";
import { Wallet } from 'ethers'

const webauthPublicKey = .. // see Step 3

// EOA Signer
const eoaSigner = Wallet.createRandom();
const eoaPublicKey = eoaSigner.address;

let smartAccount = SafeAccount.initializeNewAccount(
    [webauthPublicKey, eoaPublicKey],
    { threshold: 2 }
)
```

#### Existing account[​](#existing-account "Direct link to Existing account")

* Add a Passkeys owner to an existing account using [`createAddOwnerWithThresholdMetaTransactions`](https://docs.candide.dev/wallet/abstractionkit/safe-account-v3/.md#createaddownerwiththresholdmetatransactions)

addPasskeyOwner.ts

```
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`](https://docs.candide.dev/wallet/abstractionkit/safe-account-v3/.md#createswapownermetatransactions)

swapToPasskeyOwner.ts

```
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[​](#create-userop "Direct link to Create UserOp")

To obtain accurate gas estimates, pass the expected signers who will sign the UserOperation in the `createUserOperation` overrides.

multisigUserOp.ts

```
let userOperation = await smartAccount.createUserOperation(
    [metaTransaction],
    jsonRpcNodeProvider,
    bundlerUrl,
    {
       expectedSigners:[webauthPublicKey, eoaPublicKey],
    }
)
```

#### Signature[​](#signature "Direct link to Signature")

To sign a transaction with multiple signers, pass the signer signature pairs to `formatSignaturesToUseroperationSignature`.

multisigSignature.ts

```
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[​](#gas-savings-with-precompiles "Direct link to Gas Savings with Precompiles")

Leverage Native Passkeys with [RIP-7212](https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md) 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.

**Save Gas with RIP-7212**:

Chains that support the RIP-7212 precompile for secp256r1 verification can reduce passkey signature validation costs significantly. Check your target chain's documentation to confirm support before enabling.

#### New Account[​](#new-account-1 "Direct link to New Account")

precompileInit.ts

```
import { SafeAccountV0_3_0 as SafeAccount, DEFAULT_SECP256R1_PRECOMPILE_ADDRESS } from "abstractionkit";

let smartAccount = SafeAccount.initializeNewAccount(
    [webauthPublicKey],
    { eip7212WebAuthnPrecompileVerifierForSharedSigner: DEFAULT_SECP256R1_PRECOMPILE_ADDRESS }
)
```

#### Create UserOp[​](#create-userop-1 "Direct link to Create UserOp")

precompileUserOp.ts

```
let userOperation = await smartAccount.createUserOperation(
    [metaTransaction],
    nodeRPC,
    bundlerURL,
    {
        expectedSigners:[webauthPublicKey],
        eip7212WebAuthnPrecompileVerifier: DEFAULT_SECP256R1_PRECOMPILE_ADDRESS
    }
);
```

#### Signature[​](#signature-1 "Direct link to Signature")

precompileSignature.ts

```
userOperation.signature = SafeAccount.formatSignaturesToUseroperationSignature(
    [webauthnSignerSignaturePair],
    {
        isInit: userOperation.nonce == 0n,
        eip7212WebAuthnPrecompileVerifier: DEFAULT_SECP256R1_PRECOMPILE_ADDRESS,
    }
);
```

### Verifying a WebAuthn Signature[​](#verifying-a-webauthn-signature "Direct link to 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:

signMessage.ts

```
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`](https://docs.candide.dev/wallet/abstractionkit/safe-account-v3/.md#verifywebauthnsignatureformessagehash)

verifyMessage.ts

```
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](https://github.com/Sednaoui/safe-passkeys-sign-and-verify-message)

## Additional Notes[​](#additional-notes "Direct link to Additional Notes")

### WebAuthn / Passkeys API[​](#webauthn--passkeys-api "Direct link to 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 browser support, Mozilla has created great [documentation on WebAuthn](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API).

In this guide, we use the `ox` library, which provides a high-level abstraction over the WebAuthn API via the `ox/WebAuthnP256` module.

React Native Integration

For React Native applications, teams have used the following libraries to integrate WebAuthn/Passkeys:

* [**react-native-passkey**](https://www.npmjs.com/package/react-native-passkey): A React Native wrapper around the platform-specific WebAuthn/Passkeys APIs.
* [**cbor-web**](https://www.npmjs.com/package/cbor-web): Used in conjunction with `react-native-passkey` to handle the CBOR (Concise Binary Object Representation) data format used by the WebAuthn API.
* [**React Native demo**](https://github.com/candidelabs/passkeys-react-native-demo) by Adrian, the lead developer from Unit-e, using `abstractionkit`, `react-native-passkey`, and `cbor-web`.

### Sync & Recovery[​](#sync--recovery "Direct link to Sync & Recovery")

#### Apple[​](#apple "Direct link to 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](https://support.apple.com/en-us/102195).

#### Google[​](#google "Direct link to 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](https://developers.google.com/identity/passkeys).

#### YubiKey[​](#yubikey "Direct link to 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](https://www.yubico.com/blog/a-yubico-faq-about-passkeys/).

#### Password Managers[​](#password-managers "Direct link to Password Managers")

Passkey backups extend beyond hardware manufacturers. They are supported across various password managers including [Windows Hello](https://support.microsoft.com/en-us/windows/passkeys-in-windows-301c8944-5ea2-452b-9886-97e4d2ef4422), [Bitwarden](https://bitwarden.com/passwordless-passkeys/), [Proton Pass](https://proton.me/blog/proton-pass-passkeys), [1Password](https://1password.com/product/passkeys), [LastPass](https://www.lastpass.com/features/passwordless-authentication), and others.

### Device Support[​](#device-support "Direct link to 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](https://passkeys.dev/device-support/)

## What's Next?[​](#whats-next "Direct link to What's Next?")

Now that you've integrated passkeys authentication, explore these features to harden your setup:

#### Recommended Next Steps[​](#recommended-next-steps "Direct link to Recommended Next Steps")

1. **[Recovery Module](https://docs.candide.dev/wallet/plugins/recovery-with-guardians.md)**: Protect passkey-only accounts with social recovery guardians
2. **[Gas Sponsorship](https://docs.candide.dev/wallet/guides/getting-started.md)**: Sponsor gas fees for your users with a Paymaster
3. **[Multisig Setup](#multisig)**: Add a second signer for production-grade security (recommended for any 1/1 passkey account)
4. **[Pay Gas in ERC-20](https://docs.candide.dev/wallet/guides/pay-gas-in-erc20.md)**: Let users pay transaction fees with stablecoins or other tokens
