[NEW] On-chain Passkeys
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
Example Workflow
- User registers a WebAuthn credential
- Credential ID and public key are stored locally
- On login,
navigator.credentials.get()
fetches the credential - The app creates a client using the credential
- 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: P256Credentialcredential = await function createWebAuthnCredential(parameters: CreateWebAuthnCredentialParameters): Promise<CreateWebAuthnCredentialReturnType>createWebAuthnCredential({
name: stringName 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
anylocalStorage.anysetItem(const credential: P256Credentialcredential.id: stringid, const credential: P256Credentialcredential.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>> (+1 overload)createModularAccountV2Client } from "@account-kit/smart-contracts";
import { function alchemy(config: AlchemyTransportConfig): AlchemyTransportCreates 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: ChainarbitrumSepolia } from "@account-kit/infra";
const const publicKeyRequest: PublicKeyCredentialRequestOptionspublicKeyRequest: type PublicKeyCredentialRequestOptions = /*unresolved*/ anyPublicKeyCredentialRequestOptions = {
challenge: anychallenge: var Uint8Array: Uint8ArrayConstructorA 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.anyfromHex("0x"), // Generate a random challenge
rpId: stringrpId: "localhost", // should match your dApp domain
};
// retrieve available passkeys for the provided domain
const const publicKeyCredential: anypublicKeyCredential = await anynavigator.anycredentials.anyget({
publicKeyRequest: PublicKeyCredentialRequestOptionspublicKeyRequest,
});
if (const publicKeyCredential: anypublicKeyCredential) {
// verify that passkey with corresponding id exists on dApp
const const publicKeyHex: anypublicKeyHex = anylocalStorage.anygetItem(const publicKeyCredential: anypublicKeyCredential.anyid);
if (!const publicKeyHex: anypublicKeyHex) throw new var Error: ErrorConstructor
new (message?: string) => ErrorError("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;
... 83 more ...;
extend: <const client extends {
...;
} & ExactPartial<...>>(fn: (client: Client<...>) => client) => Client<...>;
}accountClient = await createModularAccountV2Client<Chain, SmartAccountSigner<any>>(args: CreateModularAccountV2AlchemyClientParams<AlchemyTransport, Chain, SmartAccountSigner<any>>): Promise<...> (+1 overload)createModularAccountV2Client({
policyId?: string | string[] | undefinedpolicyId: "YOUR_POLICY_ID",
mode?: "default" | "7702" | undefinedmode: "webauthn",
credential: {
id: any;
publicKey: any;
}credential: {
id: anyid: const publicKeyCredential: anypublicKeyCredential.anyid,
publicKey: anypublicKey: const publicKeyHex: anypublicKeyHex,
},
rpId: stringrpId: "localhost",
chain: {
blockExplorers?: {
[key: string]: ChainBlockExplorer;
default: ChainBlockExplorer;
} | undefined;
... 7 more ...;
testnet?: boolean | undefined;
} & ChainConfig<...>Chain for the client.
chain: const arbitrumSepolia: ChainarbitrumSepolia,
transport: AlchemyTransportThe RPC transport
transport: function alchemy(config: AlchemyTransportConfig): AlchemyTransportCreates 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: stringapiKey: "YOUR_ALCHEMY_API_KEY" }),
});
}
createModularAccountV2Client
Parameters
createModularAccountV2Client
Parameters
Send User Operation
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!