Blog home

Introducing Farcaster Auth

Justin Hunter

Published on

5 min read

Introducing Farcaster Auth

A powerful way to handle authentication in your Farcaster applications while providing a seamless UX

Today, we’re excited to announce two new ways for developers building in the Farcaster ecosystem to authenticate their users. We previously wrote a blog post that outlined how to use the raw Sign in With Warpcast flow. In that flow, developers have a lot on their plate. Not only do they have to write a significant amount of code to access and utilize this flow, but even after the flow, developers have to write additional code to manage the “signer” that is returned as part of the authentication process. This signer is the private key that allows writes to Farcaster Hubs on behalf of the end user. As you can imagine, the logic associated with managing these and the risk the developer takes are not ideal.

This is where Managed Farcaster Auth comes in. Pinata now handles all of the complex logic and ensures the signing keys don’t have to be handled at the client level. Developers can focus on building their app, not auth. With this release, we are introducing two flows:

  1. Managed Farcaster Auth
  2. Sponsored Farcaster Auth

Managed Farcaster Auth

This flow will look a lot like the blog post linked earlier, but with fewer steps. This improves the developer experience, but it is not quite as smooth as the Sponsored Farcaster Auth Flow that we will outline next. The trade off is that developers get a fully branded experience for auth. When a user logs into the developer’s app using Warpcast, they will see a connect flow that uses the developer’s app name and handle. Here’s a quick code snippet showing how you can use this flow in your app:

const SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN = {
  name: "Farcaster SignedKeyRequestValidator",
  version: "1",
  chainId: 10,
  verifyingContract: "0x00000000fc700472606ed4fa22623acf62c60553",
} as const;

const SIGNED_KEY_REQUEST_TYPE = [
  { name: "requestFid", type: "uint256" },
  { name: "key", type: "bytes" },
  { name: "deadline", type: "uint256" },
] as const;

  try {
    const appFid = process.env.FARCASTER_DEVELOPER_FID;
    const res = await fetch("https://api.pinata.cloud/v3/farcaster/signers", {
      method: "POST",
      headers: {
        'Content-Type': "application/json",
        "Authorization": `Bearer ${process.env.PINATA_JWT}`
      },
      body: JSON.stringify({
        app_fid: parseInt(appFid, 10)
      })
    });

    const signerInfo: any = await res.json();
    
    const { data }: { 
	    data: { 
		    signer_uuid: string, 
		    public_key: string, 
		    signer_approved: string 
				} 
			} = signerInfo;
			
    const account = mnemonicToAccount(
      process.env.FARCASTER_DEVELOPER_MNEMONIC
    );

    const deadline = Math.floor(Date.now() / 1000) + 86400; // signature is valid for 1 day
    const requestFid = parseInt(appFid);

    const signature = await account.signTypedData({
      domain: SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN,
      types: {
        SignedKeyRequest: SIGNED_KEY_REQUEST_TYPE,
      },
      primaryType: "SignedKeyRequest",
      message: {
        requestFid: BigInt(appFid),
        key: `0x${data.public_key}`,
        deadline: BigInt(deadline),
      }
    });

    const registerResponse = await fetch(`https://api.pinata.cloud/v3/farcaster/register_signer_with_warpcast`, {
      method: "POST",
      headers: {
        'Content-Type': "application/json",
        "Authorization": `Bearer ${process.env.PINATA_JWT}`
      },
      body: JSON.stringify({
        signer_id: data.signer_id,
        signature: signature,
        deadline: deadline,
        app_fid: requestFid,
        app_address: account.address
      })
    })

    const warpcastPayload: any = await registerResponse.json()

    const pollResponse: any = await fetch(`https://api.pinata.cloud/v3/farcaster/poll_warpcast_signer?token=${token}`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${process.env.PINATA_JWT}`
      }
    })
   const signerBroadcast = await pollResponse.json()

   console.log(signerBroadcast)
  } catch (error) {
    console.log(error);
  }

When you compare this to the raw ‘Sign In with Warpcast’ flow, documented in the tutorial linked at the beginning of this article, you’ll see that there is less to manage. The only thing the developer needs to concern themselves with is the signer_id. This id maps back to a key that we manage for the developer’s end user. The user has full control and can revoke that key onchain permissionlessly. The app developer can also make a request to our API to revoke the key. This will not revoke it onchain, but it renders the key unusable through our API.

Now, let’s take a look at Sponsored Farcaster Auth.

The Sponsored Farcaster Auth flow is fewer steps than the Managed flow, but it also comes with one additional perk. The developer’s end users do not have to pay for the onchain transaction associated with connecting this signer. Pinata sponsors these fees, which makes for a much smoother onboarding experience. When a developer is trying to onboard new users to their app, asking a user to pay is a UX hurdle that is hard to overcome. Sponsored Farcaster Auth solves this.

The tradeoff for this convenience is that, when a user connects their Warpcast account, Pinata will be displayed as the app they are connecting to instead of the developer’s app. If the authentication flow branding is important, the Managed Farcaster Auth flow is probably the better solution for you. If UX is more important, Sponsored Farcaster Auth is the right choice.

Here’s a simple code snippet showcasing this flow.

const appFid = process.env.FARCASTER_DEVELOPER_FID;
const res = await fetch("https://api.pinata.cloud/v3/farcaster/sponsored_signers", {
  method: "POST",
  headers: {
    'Content-Type': "application/json",
    "Authorization": `Bearer ${process.env.PINATA_JWT}`
  },
  body: JSON.stringify({
    app_fid: parseInt(appFid, 10)
  })
});

const warpcastPayload: any = await res.json()

const pollResponse: any = await fetch(`https://api.pinata.cloud/v3/farcaster/poll_warpcast_signer?token=${warpcastPayload.data.token}`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": `Bearer ${process.env.PINATA_JWT}`
  }
})
const signerBroadcast = await pollResponse.json()

console.log(signerBroadcast)

As you can see, this flow results in considerably less code. All a developer has to manage is the user’s signer_id.

Use It Everywhere

We noticed a trend in web3, and especially in the Farcaster ecosystem, where developer tools are limited to JavaScript. For developers not building in JavaScript, this often means digging through source code to see how the raw API requests work. We wanted to start the other way around.

We built our Farcaster Auth system API-first. We will wrap this up in our Pinata FDK soon, but developers writing code in any programming language can make use of these APIs right now. As the ecosystem matures, this approach provides the most flexibility and (what we think is) the best developer experience.

What’s Next

We have APIs coming that will allow developers to look up a user’s signer_id and thus re-associate it to a signing key, so stay tuned. With this change, you will now be able to send casts much easier through our API. We have a separate tutorial coming for that topic, but the API reference is already updated and documented.

To start using Managed Farcaster Auth and Sponsored Farcaster Auth, you’ll need to be on the Pinata Picnic plan at $20 per month. Happy Casting!

Stay up to date

Join our newsletter for the latest stories & product updates from the Pinata community.

No spam, notifications only about new products, updates and freebies. You can always unsubscribe.