[NEW] On-chain Passkeys

This feature is in early access.

The WebAuthn Modular Account enables password-less authentication on-chain using passkeys (via WebAuthn), and is compatible with Alchemy’s Account Kit. This guide demonstrates how to register credentials, authenticate users, and send user operations using the @account-kit/smart-contracts package.

Instead of on-device verification it’s on-chain verification. Devs are responsible for generating the credentials attached to those biometric webauthn compatible passkeys and then using our signer.

Prerequisites

  • A frontend environment (React, Vite, Next.js, etc.)
  • Browser with WebAuthn and Credential Management API support
  • Install Alchemy Account Kit SDK smart contracts package (use the latest version - at least 4.52.1) and Viem
$ yarn add @account-kit/[email protected] @account-kit/[email protected] viem

Example Workflow

  1. User registers a WebAuthn credential
  2. Credential ID and public key are stored locally
  3. On login, navigator.credentials.get() fetches the credential
  4. The app creates a client using the credential
  5. A user operation is signed and sent

Register WebAuthn Credential

You are responsible for retaining your users’ public keys. Public keys generated by the WebAuthn specification are only retrievable once on the initial creation of the credential. As a precaution, we strongly suggest adding a secondary off-chain signer to use for account recovery

import { 
function createWebAuthnCredential(parameters: CreateWebAuthnCredentialParameters): Promise<CreateWebAuthnCredentialReturnType>
createWebAuthnCredential
} from "viem/account-abstraction";
const
const credential: P256Credential
credential
= await
function createWebAuthnCredential(parameters: CreateWebAuthnCredentialParameters): Promise<CreateWebAuthnCredentialReturnType>
createWebAuthnCredential
({
name: string

Name for the credential (user.name).

name
: "Credential Name",
}); // store credential id to public key mapping // NOTE: use of localStorage is for TESTING PURPOSES ONLY, NOT FOR PRODUCTION USE
var localStorage: Storage
localStorage
.
Storage.setItem(key: string, value: string): void

Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously.

Throws a "QuotaExceededError" DOMException exception if the new value couldn't be set. (Setting could fail if, e.g., the user has disabled storage for the site, or if the quota has been exceeded.)

Dispatches a storage event on Window objects holding an equivalent Storage object.

MDN Reference

setItem
(
const credential: P256Credential
credential
.
id: string
id
,
const credential: P256Credential
credential
.
publicKey: `0x${string}`
publicKey
); //credentialIdAsBase64Encoded -> publicKeyHex

Login With Credential

import { 
function createModularAccountV2Client<TChain extends Chain = Chain, TSigner extends SmartAccountSigner = SmartAccountSigner<any>>(args: CreateModularAccountV2AlchemyClientParams<AlchemyTransport, TChain, TSigner>): Promise<ModularAccountV2Client<TSigner, TChain, AlchemyTransport>> (+2 overloads)
createModularAccountV2Client
} from "@account-kit/smart-contracts";
import {
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.

alchemy
,
const arbitrumSepolia: Chain
arbitrumSepolia
} from "@account-kit/infra";
const
const publicKeyRequest: PublicKeyCredentialRequestOptions
publicKeyRequest
: PublicKeyCredentialRequestOptions = {
PublicKeyCredentialRequestOptions.challenge: BufferSource
challenge
:
var Uint8Array: Uint8ArrayConstructor

A typed array of 8-bit unsigned integer values. The contents are initialized to 0. If the requested number of bytes could not be allocated an exception is raised.

Uint8Array
.
any
fromHex
("0x"), // Generate a random challenge
PublicKeyCredentialRequestOptions.rpId?: string | undefined
rpId
: "localhost", // should match your dApp domain
}; // retrieve available passkeys for the provided domain const
const publicKeyCredential: Credential | null
publicKeyCredential
= await
var navigator: Navigator
navigator
.
Navigator.credentials: CredentialsContainer

Available only in secure contexts.

MDN Reference

credentials
.
CredentialsContainer.get(options?: CredentialRequestOptions): Promise<Credential | null>
get
({
publicKeyRequest: PublicKeyCredentialRequestOptions
publicKeyRequest
,
}); if (
const publicKeyCredential: Credential | null
publicKeyCredential
) {
// verify that passkey with corresponding id exists on dApp const
const publicKeyHex: string | null
publicKeyHex
=
var localStorage: Storage
localStorage
.
Storage.getItem(key: string): string | null

Returns the current value associated with the given key, or null if the given key does not exist.

MDN Reference

getItem
(
const publicKeyCredential: Credential
publicKeyCredential
.
Credential.id: string
id
);
if (!
const publicKeyHex: string | null
publicKeyHex
) throw new
var Error: ErrorConstructor new (message?: string) => Error
Error
("Account does not exist");
// create client to send transactions on behalf of verified user const
const accountClient: { [x: string]: unknown; account: ModularAccountV2<SmartAccountSigner<any>>; batch?: { multicall?: boolean | Prettify<MulticallBatchOptions> | undefined; } | undefined; cacheTime: number; ccipRead?: false | { request?: (parameters: CcipRequestParameters) => Promise<CcipRequestReturnType>; } | undefined; chain: Chain; experimental_blockTag?: BlockTag | undefined; ... 84 more ...; extend: <const client extends { ...; } & ExactPartial<...>>(fn: (client: Client<...>) => client) => Client<...>; }
accountClient
= await
createModularAccountV2Client<Chain, SmartAccountSigner<any>>(args: CreateModularAccountV2AlchemyClientParams<AlchemyTransport, Chain, SmartAccountSigner<any>>): Promise<{ [x: string]: unknown; account: ModularAccountV2<SmartAccountSigner<any>>; batch?: { multicall?: boolean | Prettify<MulticallBatchOptions> | undefined; } | undefined; cacheTime: number; ... 87 more ...; extend: <client>(fn: (client: Client<...>) => client) => Client<...>; }> (+2 overloads)
createModularAccountV2Client
({
policyId?: string | string[] | undefined
policyId
: "YOUR_POLICY_ID",
mode?: "default" | "7702" | undefined
mode
: "webauthn",
credential: { id: string; publicKey: string; }
credential
: {
id: string
id
:
const publicKeyCredential: Credential
publicKeyCredential
.
Credential.id: string
id
,
publicKey: string
publicKey
:
const publicKeyHex: string
publicKeyHex
,
},
rpId: string
rpId
: "localhost",
chain: { blockExplorers?: { [key: string]: ChainBlockExplorer; default: ChainBlockExplorer; } | undefined; blockTime?: number | undefined; contracts?: Prettify<{ [key: string]: ChainContract | { [sourceId: number]: ChainContract | undefined; } | undefined; } & { ensRegistry?: ChainContract | undefined; ensUniversalResolver?: ChainContract | undefined; multicall3?: ChainContract | undefined; erc6492Verifier?: ChainContract | undefined; }> | undefined; ... 7 more ...; testnet?: boolean | undefined; } & ChainConfig<...>

Chain for the client.

chain
:
const arbitrumSepolia: Chain
arbitrumSepolia
,
transport: AlchemyTransport

The RPC transport

transport
:
function alchemy(config: AlchemyTransportConfig): AlchemyTransport

Creates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.

alchemy
({
apiKey: string
apiKey
: "YOUR_ALCHEMY_API_KEY" }),
}); }
ParameterTypeDescription
policyIdstringYour account policy UUID from the Alchemy dashboard
mode"webauthn"Specifies credential mode
credentialobject{ id: string, publicKey: Address }
rpIdstringRelying Party ID (e.g., localhost or your domain)
chainChainNetwork config (e.g., arbitrumSepolia)
transportTransportAlchemy or custom RPC transport

Send User Operation

1const operation = await accountClient.sendUserOperation({
2 uo: {
3 target: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", // Example: Vitalik's address
4 data: "0x", // No calldata
5 value: parseEther("0"),
6 },
7});

React Native Integration

Get your React Native environment set up by following these docs. Once you’ve completed this setup, you can use the webauthn signer as detailed above!

localStorage is not available in React Native. Please use an alternative storage method.

What’s Next

This is only the initial SDK release for on-chain passkeys. We are actively working on the DevX so your feedback will be greatly appreciated. If you have any questions or are interested in learning more, please reach out!