How to use a Passkey (WebAuthn) server | Pimlico Docs
Skip to content

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
        }))
    }
}