How to use a Passkey (WebAuthn) server
This how-to guide will walk you through the steps to integrate Passkey (WebAuthn) server with a smart account whose user operations are relayed and sponsored by Pimlico.
Steps
Install the required packages
npm install viem ox permissionless
Create a passkeys client
You will need to create a passkeys client to interact with the passkeys server. Learn how to create your own or use passkeys server at How to create a Passkey (WebAuthn) server.
If you want to use Pimlico's passkeys server, visit Dashboard and configure your passkeys server.
You will then create a passkeys client with the following code:
const passkeyServerClient = createPasskeyServerClient({
chain,
transport: http(
`https://api.pimlico.io/v2/${chain.id}/rpc?apikey=${pimlicoApiKey}`
)
})
Create credentials
import { useState } from "react"
import {
createWebAuthnCredential,
} from "viem/account-abstraction"
export function PasskeysDemo() {
const [credential, setCredential] = useState<P256Credential>()
const createCredential = async () => {
const credential = await createWebAuthnCredential(
// Start the registration process
await passkeyServerClient.startRegistration({
context: {
// userName that will be shown to the user when creating the passkey
userName: "plusminushalf"
}
})
)
// Verify the registration
const verifiedCredential = await passkeyServerClient.verifyRegistration(
{
credential,
context: {
// userName that will be shown to the user when creating the passkey
userName: "plusminushalf"
}
}
)
setCredential(verifiedCredential)
}
if (!credential)
return (
<button type="button" onClick={createCredential}>
Create credential
</button>
)
return (
<div>
<p>Credential: {credential.id}</p>
</div>
)
}
Use an existing credential
If you already have a credential, you can use it with the following code:
const loginCredential = async () => {
const credentials = await passkeyServerClient.getCredentials({
context: {
userName: "plusminushalf"
}
})
// credentials is an array of credentials that match the userName
setCredential(credentials[0])
}
Create Kernel Smart Account
import {
type SmartAccountClient,
createSmartAccountClient
} from "permissionless"
import {
type ToKernelSmartAccountReturnType,
toKernelSmartAccount
} from "permissionless/accounts"
import {
entryPoint07Address,
toWebAuthnAccount
} from "viem/account-abstraction"
const pimlicoUrl = `https://api.pimlico.io/v2/${chain.id}/rpc?apikey=${pimlicoApiKey}`
const pimlicoClient = createPimlicoClient({
chain,
transport: http(pimlicoUrl)
})
export function PasskeysDemo() {
const [smartAccountClient, setSmartAccountClient] =
React.useState<
SmartAccountClient<
Transport,
Chain,
ToKernelSmartAccountReturnType<"0.7">
>
>()
...
React.useEffect(() => {
if (!credential) return
toKernelSmartAccount({
client: publicClient,
version: "0.3.1",
owners: [toWebAuthnAccount({ credential })],
entryPoint: {
address: entryPoint07Address,
version: "0.7"
}
}).then((account: ToKernelSmartAccountReturnType<"0.7">) => {
setSmartAccountClient(
createSmartAccountClient({
account,
paymaster: pimlicoClient,
chain,
userOperation: {
estimateFeesPerGas: async () =>
(await pimlicoClient.getUserOperationGasPrice())
.fast
},
bundlerTransport: http(pimlicoUrl)
})
)
})
}, [credential])
...
}
Send a transaction
export function PasskeysDemo() {
...
const [txHash, setTxHash] = React.useState<Hex>()
const sendUserOperation = async (
event: React.FormEvent<HTMLFormElement>
) => {
event.preventDefault()
if (!smartAccountClient) return
const formData = new FormData(event.currentTarget)
const to = formData.get("to") as `0x${string}`
const value = formData.get("value") as string
const txHash = await smartAccountClient.sendTransaction({
calls: [
{
to,
value: parseEther(value)
}
],
})
setTxHash(txHash)
}
return (
<>
<h2>Account</h2>
<p>Address: {smartAccountClient?.account?.address}</p>
<h2>Send User Operation</h2>
<form onSubmit={sendUserOperation}>
<input name="to" placeholder="Address" />
<input name="value" placeholder="Amount (ETH)" />
<button type="submit">Send</button>
{txHash && <p>Transaction Hash: {txHash}</p>}
</form>
</>
)
}
Sign & verify a message
export function PasskeysDemo() {
const [signature, setSignature] = React.useState<Hex>()
const [isVerified, setIsVerified] = React.useState<boolean>()
...
const signAndVerifyMessage = async () => {
if (!smartAccountClient) return
const signature = await smartAccountClient.signTypedData(typedData)
const isVerified = await publicClient.verifyTypedData({
...typedData,
address: smartAccountClient.account.address,
signature
})
setIsVerified(isVerified)
setSignature(signature)
}
return (
<>
<h2>Account</h2>
<p>Address: {smartAccountClient?.account?.address}</p>
<h2>Sign typed data</h2>
<button type="button" onClick={signAndVerifyMessage}>
Sign typed data Test
</button>
{signature && (
<p>
Signature: <pre>{signature}</pre>
</p>
)}
{isVerified !== undefined && (
<p>Verified: {isVerified.toString()}</p>
)}
</>
)
}
How to create a Passkey (WebAuthn) server
This guide will walk you through the steps to create your own Passkey (WebAuthn) server.
Install the required packages
npm install @simplewebauthn/server @levischuck/tiny-cbor fastify @fastify/cors zod
Create a server
import Fastify, { FastifyReply, FastifyRequest } from "fastify"
import cors from "@fastify/cors"
import { z } from "zod"
const app = Fastify()
// Optional: Enable CORS
app.register(cors, {
origin: "*"
})
// Define the schema for the request
export const pksStartRegistrationRequestSchema = z.object({
method: z.literal("pks_startRegistration"),
params: z.tuple([
z.object({
userName: z.string()
})
]),
jsonrpc: z.literal("2.0"),
id: z.number()
})
export const pksVerifyRegistrationRequestSchema = z.object({
method: z.literal("pks_verifyRegistration"),
params: z.tuple([
z.object({
id: z.string(),
rawId: z.string(),
response: z.object({
clientDataJSON: z.string(),
attestationObject: z.string(),
authenticatorData: z.string().optional(),
transports: z
.array(
z.enum([
"ble",
"cable",
"hybrid",
"internal",
"nfc",
"smart-card",
"usb"
])
)
.optional(),
publicKeyAlgorithm: z.number().optional(),
publicKeyType: z.string().optional()
}),
authenticatorAttachment: z.enum(["cross-platform", "platform"]),
clientExtensionResults: z.object({
appid: z.boolean().optional(),
credProps: z
.object({
rk: z.boolean().optional()
})
.optional(),
hmacCreateSecret: z.boolean().optional()
}),
type: z.literal("public-key")
}),
z.object({
userName: z.string()
})
]),
jsonrpc: z.literal("2.0"),
id: z.number()
})
export const pksGetCredentialsRequestSchema = z.object({
method: z.literal("pks_getCredentials"),
params: z.tuple([
z.object({
userName: z.string()
})
]),
jsonrpc: z.literal("2.0"),
id: z.number()
})
export const bodySchema = z.discriminatedUnion("method", [
pksStartRegistrationRequestSchema,
pksVerifyRegistrationRequestSchema,
pksGetCredentialsRequestSchema
])
app.get("/rpc", async (request: FastifyRequest, reply: FastifyReply) => {
const body = request.query as any
const result = bodySchema.safeParse(body)
if (!result.success) {
// Invalid request
return reply.status(400).send({
jsonrpc: "2.0",
id: body.id || 0,
error: {
code: -32600,
message: "Invalid request"
}
})
}
if (result.data.method === "pks_startRegistration") {
// Start registration
return pksStartRegistrationHandler(request, reply)
} else if (result.data.method === "pks_verifyRegistration") {
// Verify registration
return pksVerifyRegistrationHandler(request, reply)
} else if (result.data.method === "pks_getCredentials") {
// Get credentials
return pksGetCredentialsHandler(request, reply)
}
})
Start registration
import { generateRegistrationOptions } from "@simplewebauthn/server"
const pksStartRegistrationHandler = async (
body: z.infer<typeof pksStartRegistrationRequestSchema>
) => {
const {
params: [{ userName }]
} = body
// Validate userName
if (!userName) {
throw new Error("Invalid userName")
}
// Start registration
const options = await generateRegistrationOptions({
rpName: rpName,
rpID: rpID,
userName: userName,
attestationType: "none",
authenticatorSelection: {
residentKey: "preferred",
userVerification: "preferred",
authenticatorAttachment: "platform"
}
})
// Save options somewhere as we will need it for verification
// ....
// ....
return {
jsonrpc: "2.0",
id: body.id,
result: options
}
}
Verify registration
import { verifyRegistrationResponse } from "@simplewebauthn/server"
import { decodePartialCBOR } from "@levischuck/tiny-cbor"
import { PublicKey } from "ox"
const pksVerifyRegistrationHandler = async (
body: z.infer<typeof pksVerifyRegistrationRequestSchema>
) => {
const {
params: [passkeysResponse, { userName }]
} = body
// Validate userName
if (!userName) {
throw new Error("Invalid userName")
}
// Fetch the saved options for the userName
// ....
// ....
// Verify registration
const verification = await verifyRegistrationResponse({
response: passkeysResponse,
expectedChallenge: savedOption.challenge,
expectedOrigin: expectedOrigin,
expectedRPID: savedOption.domain
})
// You can now delete the saved options
// ....
// ....
if (!(verification.verified && verification.registrationInfo)) {
return {
jsonrpc: "2.0",
id: body.id,
error: {
message: "Passkey verification failed"
}
}
}
const {
registrationInfo: {
credential,
credentialDeviceType,
credentialBackedUp
}
} = verification
const [first] = decodePartialCBOR(credential.publicKey, 0) as [
Map<number, Uint8Array> | undefined,
number
]
if (!first) {
return {
jsonrpc: "2.0",
id: body.id,
error: {
message: "Invalid public key"
}
}
}
const publicKey = {
prefix: 4,
x: first.get(-2),
y: first.get(-3)
}
if (!(publicKey.x && publicKey.y)) {
return {
jsonrpc: "2.0",
id: body.id,
error: {
message: "Invalid public key"
}
}
}
const newPasskey = {
userName: userName,
webAuthnUserID: savedOption.user.id,
id: credential.id,
publicKey: PublicKey.toHex(
PublicKey.from(
new Uint8Array([0x04, ...publicKey.x, ...publicKey.y])
)
),
counter: credential.counter,
transports: credential.transports?.join(","),
deviceType: credentialDeviceType,
backedUp: credentialBackedUp
}
// Save the new passkey
// ....
// ....
return {
jsonrpc: "2.0",
id: body.id,
result: {
success: verification.verified,
id: newPasskey.id,
publicKey: newPasskey.publicKey
}
}
}
Get credentials
const pksGetCredentialsHandler = (
body: z.infer<typeof pksGetCredentialsRequestSchema>
) => {
const {
params: [{ userName }]
} = body
// Get saved passkeys for the userName
// ....
// ....
return {
jsonrpc: "2.0",
id: body.id,
result: savedPasskeys.map((passkey) => ({
id: passkey.id,
publicKey: passkey.publicKey
}))
}
}