Let's Buidl - WebAuthn Account Abstraction

Their are many ways to skin a cat as they say, and in the world of blockchain, there are many ways to abstract accounts. In this blog post, we will explore a novel approach to account abstraction using Web Credentials API (WebAuthn), session tokens, and Account Kit.

Having authored this implementation, I will share my experience and insights on how this approach can be used to enhance user experience and security in decentralized applications.

With the upcoming and in review EIP-3074 in mind, this implementation does not factor in AUTH and AUTHCALL opcode usage and implications.

Prerequisites

  • Familiarity with EIP-4337 (Account Abstraction)
  • A basic understanding of session authentication (e.g, Auth0)

High-Level Overview

The Web Credentials API (WebAuthn) is a W3C standard that allows users to authenticate without passwords using public-key cryptography. This standard is widely supported by modern browsers and is inherently a multi-factor authentication method.

Alchemy's Account Kit is a fully featured tech stack for building ERC-4337 compliant applications. It provides everything from the smart contract accounts to bundlers for sending transactions and it's modularity allows for easy integration with other services.

We will use Auth0 for session management, WebAuthn for multi-factor authentication, and Account Kit for account abstraction. With these we'll be able to generate a cryptographically secure and deterministic EOA (Externally Owned Account) that will own the smart contract account allowing for seamless user onboarding and account management.

Step 1: User Registration and Authentication

First we need our user to authenticate through Auth0 giving us some user metadata which we can use to create the passkey credential with, specifically a name and display name which is part of the WebCredential API spec.

Let's assume we have Supabase as our database and Auth0 with GitHub as our authentication provider, we have logged in with GitHub and have the user's metadata. We can now create a passkey credential for the user.

As recommened by the WebCredential docs, it is best to introduce an additional authentication step prior to revealing the user's credential ID which could be used to identify the user in some cases.

const passkey = await navigator.credentials.create({
  publicKey: {
    rp: {
      name: "Keyrxng's Blog",
      id: "keyrxng.xyz"
    },
    user: {
      id: new Uint8Array(16),
      name: "Keyrxng",
      displayName: "Keyrxng"
    },
    challenge: new Uint8Array(32),
    pubKeyCredParams: [
      {
        type: "public-key",
        alg: -7
      }
    ],
    timeout: 60000,
    excludeCredentials: [],
    authenticatorSelection: {
      authenticatorAttachment: "platform",
      requireResidentKey: false,
      userVerification: "required"
    },
    attestation: "none"
    }
    });

Before we proceed, let's take a moment to understand what's happening here. We are creating a new credential for the user using the WebAuthn API. The

publicKey
object contains the following properties:

  • rp: The relying party (RP) name and ID.
  • user: The user ID, name, and display name.
  • challenge: A random challenge value (crypto.randomBytes(32))
  • pubKeyCredParams: The public key credential parameters. We are using the ES256 algorithm.
  • timeout: The timeout value in milliseconds.
  • excludeCredentials: An array of excluded credentials.
  • authenticatorSelection: The authenticator selection criteria. We are using the platform authenticator attachment, no resident key, and user verification required.
  • attestation: The attestation type.

So now we have a passkey credential for the user, but to be sure everything went smoothly, let's try to request the passkey that we just created.

const passkey = await navigator.credentials.get({
  publicKey: {
    challenge: new Uint8Array(32),
    timeout: 60000,
    rpId: "keyrxng.xyz",
    allowCredentials: [
      {
        type: "public-key",
        id: new Uint8Array(16),
        transports: ["internal"]
      }
    ],
    userVerification: "required"
  }
});

if we can successfully retrieve the passkey, we can now proceed to the next step.

Step 2: Account Creation

Now that we have all of the data points we need to create the EOA, we will use a combination of libsodium and node:crypto to generate a deterministic EOA. Remembering that it is critical to use a secure and adequate amount of entropy to generate the EOA to ensure it is cryptographically secure but also ensuring that the EOA is deterministic to allow for account recovery and management from your dapp.

To make sure it meets the criteria above, we'll consume user metadata, passkey credential, session-auth and a secure salt to generate the EOA. For example, we can use the following code snippet to generate the EOA:

import { LocalAccountSigner } from "@alchemy/aa-core";
import { createHash } from "crypto";
import { wordlists } from "ethers";
import { BytesLike, entropyToMnemonic, isValidMnemonic } from "ethers/lib/utils";
import { crypto_generichash, crypto_generichash_BYTES } from "libsodium-wrappers";
import { Hex, keccak256 } from "viem";

// the salt could be a user password, pin, etc...
function generateEOA(userMetadata, passkey, sessionAuth, salt) {
    const { name, displayName } = userMetadata;
    const { id } = passkey;
    const { auth_account_created_at } = sessionAuth;

    // Concatenate the data into a buffer then hash it
    const concData = keccak256(
    Buffer.concat([Buffer.from(name), Buffer.from(displayName), Buffer.from(id), Buffer.from(auth_account_created_at), Buffer.from(salt, "hex")])
  );

    // hash the concatenated data which has already been hashed once
    const hash = crypto_generichash(crypto_generichash_BYTES, concData);

    // create a private key from the hash of the twice hashed data
    const privateKey = "0x" + createHash("sha256").update(hash).digest().toString("hex")

    // generate a mnemonic from the private key using ethers
    const mnemonic = generateMnemonic(privateKey);

    // create an account signer using alchemy from the mnemonic
    const accSigner = LocalAccountSigner.mnemonicToAccountSigner(mnemonic);

    // get the public key from the account signer
    const publicKey = await accSigner.getAddress();

    return { mnemonic, publicKey, privateKey };
}

function generateMnemonic(pk: BytesLike) {
  const mnemonic = entropyToMnemonic(pk, wordlists["en"]);

  if (isValidMnemonic(mnemonic)) {
    return mnemonic;
  } else {
    throw new Error("Invalid mnemonic generated");
  }
}

You probably noticed that I have used three different hashing functions one from the crypto module, one from libsodium and one from viem. Different libraries tend to use similar hashing functions but with different implementations, plus hashing the data multiple times is a good practice to ensure the EOA is cryptographically secure.

The security of the EOA is only as strong as the weakest point of entropy.

Step 3: Account Management

Now that we have the EOA, we can use it to interact with the smart contract account. We can use the Account Kit to manage the account and interact with the blockchain. The delightful part now is that we already know ahead of time where the smart contract account will be deployed to so at this stage we can choose to perform some action as the smart contract account through the Account Kit SDK which will make it's way through the Alchemy relayers and be executed on the blockchain, which deploys the smart contract account in the same transaction.

Ideally you'd pre-fund or deploy their accounts immediately after account creation, but you can also choose to do this at a later time. For the sake of this example, we'll deploy the smart contract account immediately after account creation under the assumption that you (the developer) have already funded the new user's EOA with just enough gas to send one empty transaction as soon as you were aware of the EOA address in the previous step.

import { createLightAccountAlchemyClient } from "@alchemy/aa-alchemy";
import { LocalAccountSigner, SmartAccountClient } from "@alchemy/aa-core";

async function deploySmartAccount(privateKey: Hex) {
    const signer = LocalAccountSigner.privateKeyToAccountSigner(privateKey);

    const provider: SmartAccountClient = await createLightAccountAlchemyClient({
      apiKey: process.env.ALCHEMY_API_KEY,
      chain,
      signer,
    });

    const isDeployed = await provider.account?.isAccountDeployed();
    const smaAddress = provider.account?.address;

    if (!isDeployed) {
      // deploy the smart account
      const { hash } = await provider.sendUserOperation({
        account,
        uo: {
          target: smaAddress,
          data: "0x",
          value: BigInt(0),
        },
      });

      const txHash = await provider.waitForUserOperationTransaction({ hash });

      if (!txHash) {
        console.error("Failed to deploy smart account");
        return null;
      }

      return txHash;
    }
}

Even our empty transaction will be enough to deploy the account, it is important to note that the user will need to have some gas in their EOA or smart contract account to pay for the gas fees. Depending on your use-case you may choose to gas-sponsor your users, have them pay for their own gas fees upfront via a registration fee or have it subsidized by a third party or your protocols revenue.

Step 4: Account Recovery

In the event that the user loses their EOA, they can recover their account using the mnemonic generated in the account creation step. This is a simple process that involves generating the EOA from the mnemonic and then using the EOA to interact with the smart contract account.

In step 2, we generated the EOA from the user's metadata, passkey, session-auth, and salt. To recover the account, we simply need to re-execute this process using the same data points. The mnemonic generated from the private key will be the same as the one generated in the account creation step, allowing the user to recover their account.

Some wallet providers only reveal this mnemonic once, some none at all, so it is important to store this mnemonic securely and not expose it to the user unless absolutely necessary if that is the type of account abstraction you are going for.

Additional Considerations

  • Security: It is important to ensure that the EOA is generated securely and that the points of entropy are kept secure.

  • User Experience: It is important to ensure that the process is seamless and intuitive for the user without compromising security, abstracting the complexities of blockchain away completely.

  • Account Recovery: It is important to provide users with a way to recover their account in the event that they lose their EOA unless you intend on being a custodial service.

  • Gas Fees: This depends on how you intend to run your shop, if opting for full gas sponsorship, Alchemy Account Kit provides tools to help you manage this unless you have an in-house solution like a gas faucet.

Advantages

  • Multi-factor: The use of WebAuthn for multi-factor authentication and the generation of a deterministic EOA provides a high level of security for the user's account.
  • User Experience: The use of WebAuthn and session tokens provides a seamless and intuitive user experience for account creation and management.
  • Account Recovery: The use of a mnemonic for account recovery is standard and provides a simple and secure way for users to recover their account in the event of loss.

Disadvantages

  • Complexity: The implementation of account abstraction using WebAuthn, session tokens, and Account Kit can be complex and require a deep understanding of these technologies.
  • Security Risks: The use of centralized data points such as user metadata, passkey, and session-auth introduces security risks that must be carefully managed if those are core entropy points.
  • Novelty: This approach is pretty novel and there are more out-of-the-box solutions available that integrate Passkey-based authentication in Account Kit modules.

Conclusion

So after all is said and done, we have completed EIP-4337 and implemented a truly novel and custom account abstraction solution using WebAuthn, session tokens, and Account Kit. This approach provides a high level of security, a seamless user experience, and a simple account recovery process.

I learned a lot from this implementation and I hope you did too, I hope you can take this knowledge and build something amazing with it. Reach out if you have any questions or need help with your implementation.

Further Reading

About the Author

Keyrxng - Full Stack Web3 Developer.

Experimenting with writing blog posts and sharing what little knowledge I have accumulated.
Come roast me on LinkedIn or Twitter.