Authentication & Signer Management

This document provides a comprehensive overview of the authentication system and signer creation process in your Farcaster mini app. The system uses Sign in with Farcaster (SIWF) protocol to authenticate users and create signers for Farcaster protocol interactions.

This example is not only limited to mini apps. It can be used in any application that requires Farcaster authentication and signer management.

Overview

The authentication system is built around signers - cryptographic keys that allow your application to act on behalf of a user within the Farcaster protocol.

Full code for this flow can be found in the Neynar Mini App Starter Kit

Architecture Components

The system involves four main components:

API Endpoints

EndpointMethodPurposeStep
/api/auth/nonceGETGenerate authentication nonceStep 1
/api/auth/signersGETFetch user signersStep 5
/api/auth/session-signersGETFetch signers with user dataStep 5 (Backend)
/api/auth/signerPOSTCreate new signerStep 7
/api/auth/signerGETCheck signer statusStep 9
/api/auth/signer/signed_keyPOSTRegister signed keyStep 8
/api/auth/[...nextauth]GET/POSTNextAuth handlersBackend Flow

Complete Authentication Flow

Step 1: Get the Nonce

The authentication process begins by fetching a cryptographic nonce from the Neynar server.

Mini App Client → Mini App Server:

const generateNonce = async () => {
  const response = await fetch('/api/auth/nonce');
  const data = await response.json();
  setNonce(data.nonce);
};

Mini App Server → Neynar Server:

// /api/auth/nonce/route.ts
export async function GET() {
  try {
    const client = getNeynarClient();
    const response = await client.fetchNonce();
    return NextResponse.json(response);
  } catch (error) {
    console.error('Error fetching nonce:', error);
    return NextResponse.json(
      { error: 'Failed to fetch nonce' },
      { status: 500 }
    );
  }
}

Step 2: Inject Nonce in Sign in with Farcaster

The nonce is used to create a Sign in with Farcaster message.

// Frontend Flow using Farcaster Auth Kit
const { signIn, connect, data } = useSignIn({
  nonce: nonce || undefined,
  onSuccess: onSuccessCallback,
  onError: onErrorCallback,
});

// Backend Flow using Farcaster SDK
const handleBackendSignIn = async () => {
  const result = await sdk.actions.signIn({ nonce });
  // result contains message and signature
};

Step 3: Ask User for the Signature

The user is prompted to sign the SIWF message through their Farcaster client.

Frontend Flow:

useEffect(() => {
  if (nonce && !useBackendFlow) {
    connect(); // Triggers signing flow
  }
}, [nonce, connect, useBackendFlow]);

Backend Flow:

// User signs through Farcaster mobile app
const signInResult = await sdk.actions.signIn({ nonce });
const { message, signature } = signInResult;

Step 4: Receive Message and Signature

Once the user signs the message, the client receives the signature.

const onSuccessCallback = useCallback(
  async (res: UseSignInData) => {
    console.log('✅ Authentication successful:', res);
    setMessage(res.message);
    setSignature(res.signature);
  },
  [useBackendFlow, fetchUserData]
);

Step 5: Send to /api/auth/signers to Fetch Signers

With the signed message and signature, fetch existing signers for the user.

Mini App Client → Mini App Server:

const fetchAllSigners = async (message: string, signature: string) => {
  const endpoint = useBackendFlow
    ? `/api/auth/session-signers?message=${encodeURIComponent(
        message
      )}&signature=${signature}`
    : `/api/auth/signers?message=${encodeURIComponent(
        message
      )}&signature=${signature}`;

  const response = await fetch(endpoint);
  const signerData = await response.json();
  return signerData;
};

Mini App Server → Neynar Server:

// /api/auth/signers/route.ts
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const message = searchParams.get('message');
  const signature = searchParams.get('signature');

  if (!message || !signature) {
    return NextResponse.json(
      { error: 'Message and signature are required' },
      { status: 400 }
    );
  }

  try {
    const client = getNeynarClient();
    const data = await client.fetchSigners({ message, signature });
    return NextResponse.json({
      signers: data.signers,
    });
  } catch (error) {
    console.error('Error fetching signers:', error);
    return NextResponse.json(
      { error: 'Failed to fetch signers' },
      { status: 500 }
    );
  }
}

For Backend Flow with User Data:

// /api/auth/session-signers/route.ts
export async function GET(request: Request) {
  try {
    const { searchParams } = new URL(request.url);
    const message = searchParams.get('message');
    const signature = searchParams.get('signature');

    const client = getNeynarClient();
    const data = await client.fetchSigners({ message, signature });
    const signers = data.signers;

    // Fetch user data if signers exist
    let user = null;
    if (signers && signers.length > 0 && signers[0].fid) {
      const {
        users: [fetchedUser],
      } = await client.fetchBulkUsers({
        fids: [signers[0].fid],
      });
      user = fetchedUser;
    }

    return NextResponse.json({
      signers,
      user,
    });
  } catch (error) {
    console.error('Error in session-signers API:', error);
    return NextResponse.json(
      { error: 'Failed to fetch signers' },
      { status: 500 }
    );
  }
}

Step 6: Check if Signers are Present

Determine if the user has existing approved signers.

const hasApprovedSigners = signerData?.signers?.some(
  (signer: any) => signer.status === 'approved'
);

if (hasApprovedSigners) {
  // User has signers, proceed to store them
  proceedToStorage(signerData);
} else {
  // No signers, need to create new ones
  startSignerCreationFlow();
}

Step 7: Create a Signer

If no signers exist, create a new signer.

Mini App Client → Mini App Server:

const createSigner = async () => {
  const response = await fetch('/api/auth/signer', {
    method: 'POST',
  });

  if (!response.ok) {
    throw new Error('Failed to create signer');
  }

  return await response.json();
};

Mini App Server → Neynar Server:

// /api/auth/signer/route.ts
export async function POST() {
  try {
    const neynarClient = getNeynarClient();
    const signer = await neynarClient.createSigner();
    return NextResponse.json(signer);
  } catch (error) {
    console.error('Error creating signer:', error);
    return NextResponse.json(
      { error: 'Failed to create signer' },
      { status: 500 }
    );
  }
}

Step 8: Register a Signed Key

Register the signer’s public key with the Farcaster protocol.

Mini App Client → Mini App Server:

const generateSignedKeyRequest = async (
  signerUuid: string,
  publicKey: string
) => {
  const response = await fetch('/api/auth/signer/signed_key', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      signerUuid,
      publicKey,
      redirectUrl: window.location.origin, // Optional redirect after approval
    }),
  });

  if (!response.ok) {
    throw new Error('Failed to register signed key');
  }

  return await response.json();
};

Mini App Server → Neynar Server:

// /api/auth/signer/signed_key/route.ts
export async function POST(request: Request) {
  const body = await request.json();
  const { signerUuid, publicKey, redirectUrl } = body;

  // Validate required fields
  if (!signerUuid || !publicKey) {
    return NextResponse.json(
      { error: 'signerUuid and publicKey are required' },
      { status: 400 }
    );
  }

  try {
    // Get the app's account from seed phrase
    const seedPhrase = process.env.SEED_PHRASE;
    const shouldSponsor = process.env.SPONSOR_SIGNER === 'true';

    if (!seedPhrase) {
      return NextResponse.json(
        { error: 'App configuration missing (SEED_PHRASE)' },
        { status: 500 }
      );
    }

    const neynarClient = getNeynarClient();
    const account = mnemonicToAccount(seedPhrase);

    // Get app FID from custody address
    const {
      user: { fid },
    } = await neynarClient.lookupUserByCustodyAddress({
      custodyAddress: account.address,
    });

    const appFid = fid;

    // Generate deadline (24 hours from now)
    const deadline = Math.floor(Date.now() / 1000) + 86400;

    // Generate EIP-712 signature
    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: publicKey,
        deadline: BigInt(deadline),
      },
    });

    const signer = await neynarClient.registerSignedKey({
      appFid,
      deadline,
      signature,
      signerUuid,
      ...(redirectUrl && { redirectUrl }),
      ...(shouldSponsor && { sponsor: { sponsored_by_neynar: true } }),
    });

    return NextResponse.json(signer);
  } catch (error) {
    console.error('Error registering signed key:', error);
    return NextResponse.json(
      { error: 'Failed to register signed key' },
      { status: 500 }
    );
  }
}

Step 9: Start Polling

Begin polling the signer status to detect when it’s approved.

const startPolling = (
  signerUuid: string,
  message: string,
  signature: string
) => {
  const interval = setInterval(async () => {
    try {
      const response = await fetch(`/api/auth/signer?signerUuid=${signerUuid}`);
      const signerData = await response.json();

      if (signerData.status === 'approved') {
        clearInterval(interval);
        console.log('✅ Signer approved!');
        // Refetch all signers
        await fetchAllSigners(message, signature);
      }
    } catch (error) {
      console.error('Error polling signer:', error);
    }
  }, 2000); // Poll every 2 seconds

  return interval;
};

Polling API Implementation:

// /api/auth/signer/route.ts (GET method)
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const signerUuid = searchParams.get('signerUuid');

  if (!signerUuid) {
    return NextResponse.json(
      { error: 'signerUuid is required' },
      { status: 400 }
    );
  }

  try {
    const neynarClient = getNeynarClient();
    const signer = await neynarClient.lookupSigner({
      signerUuid,
    });
    return NextResponse.json(signer);
  } catch (error) {
    console.error('Error fetching signer status:', error);
    return NextResponse.json(
      { error: 'Failed to fetch signer status' },
      { status: 500 }
    );
  }
}

Step 10: Show Signer Approval URL

Display QR code for desktop users or deep link for mobile users.

const handleSignerApproval = (approvalUrl: string) => {
  const isMobile =
    /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
      navigator.userAgent
    );

  if (isMobile && context?.client) {
    // Mobile: Deep link to Farcaster app
    const farcasterUrl = approvalUrl.replace(
      'https://client.farcaster.xyz/deeplinks/signed-key-request',
      'https://farcaster.xyz/~/connect'
    );

    // Use SDK to open URL in Farcaster app
    sdk.actions.openUrl(farcasterUrl);
  } else {
    // Desktop: Show QR code
    setSignerApprovalUrl(approvalUrl);
    setDialogStep('access');
    setShowDialog(true);
  }
};

Step 11: Store Signers

Once approved, store the signers in appropriate storage.

Frontend Flow (LocalStorage):

const storeInLocalStorage = (user: any, signers: any[]) => {
  const authState = {
    isAuthenticated: true,
    user,
    signers,
  };

  setItem(STORAGE_KEY, authState);
  setStoredAuth(authState);
};

Backend Flow (NextAuth Session):

const updateSessionWithSigners = async (signers: any[], user: any) => {
  if (useBackendFlow && message && signature && nonce) {
    const signInData = {
      message,
      signature,
      nonce,
      fid: user?.fid?.toString(),
      signers: JSON.stringify(signers),
      user: JSON.stringify(user),
    };

    const result = await backendSignIn('neynar', {
      ...signInData,
      redirect: false,
    });

    if (result?.ok) {
      console.log('✅ Session updated with signers');
    }
  }
};

State Management & Storage

Frontend Flow State (LocalStorage)

interface StoredAuthState {
  isAuthenticated: boolean;
  user: {
    fid: number;
    username: string;
    display_name: string;
    pfp_url: string;
    custody_address: string;
    profile: {
      bio: { text: string };
      location?: any;
    };
    follower_count: number;
    following_count: number;
    verifications: string[];
    verified_addresses: {
      eth_addresses: string[];
      sol_addresses: string[];
      primary: {
        eth_address: string;
        sol_address: string;
      };
    };
    power_badge: boolean;
    score: number;
  } | null;
  signers: {
    object: 'signer';
    signer_uuid: string;
    public_key: string;
    status: 'approved';
    fid: number;
  }[];
}

// Stored in localStorage with key 'neynar_authenticated_user'
const STORAGE_KEY = 'neynar_authenticated_user';

Backend Flow State (NextAuth Session)

interface Session {
  provider: 'neynar';
  user: {
    fid: number;
    object: 'user';
    username: string;
    display_name: string;
    pfp_url: string;
    custody_address: string;
    profile: {
      bio: { text: string };
      location?: any;
    };
    follower_count: number;
    following_count: number;
    verifications: string[];
    verified_addresses: {
      eth_addresses: string[];
      sol_addresses: string[];
      primary: {
        eth_address: string;
        sol_address: string;
      };
    };
    power_badge: boolean;
    score: number;
  };
  signers: {
    object: 'signer';
    signer_uuid: string;
    public_key: string;
    status: 'approved';
    fid: number;
  }[];
}

Security & Configuration

EIP-712 Signature Validation

The system uses EIP-712 typed data signing for secure signer registration:

// From /lib/constants.ts
export const SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN = {
  name: 'Farcaster SignedKeyRequestValidator',
  version: '1',
  chainId: 10,
  verifyingContract:
    '0x00000000fc700472606ed4fa22623acf62c60553' as `0x${string}`,
};

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

Required Environment Variables

# Neynar API configuration
NEYNAR_API_KEY=your_neynar_api_key
NEYNAR_CLIENT_ID=your_neynar_client_id

# App signing key for signer registration
SEED_PHRASE=your_twelve_word_mnemonic_phrase

# Optional: Sponsor signer creation costs (recommended for production)
SPONSOR_SIGNER=true

Flow Detection

// Determine which flow to use
const { context } = useMiniApp();
const useBackendFlow = context !== undefined;

// Frontend flow uses localStorage + Farcaster Auth Kit
// Backend flow uses NextAuth sessions + Farcaster SDK

Integration Examples

Checking Authentication Status

// Frontend Flow
const isAuthenticated =
  storedAuth?.isAuthenticated &&
  storedAuth?.signers?.some((s) => s.status === 'approved');

// Backend Flow
const isAuthenticated =
  session?.provider === 'neynar' &&
  session?.user?.fid &&
  session?.signers?.some((s) => s.status === 'approved');

Using Signers for Farcaster Actions

// Get signer
const signer = (useBackendFlow ? session?.signers : storedAuth?.signers)[0];

if (signer) {
  // Use signer for publishing casts
  const client = getNeynarClient();
  await client.publishCast({
    signerUuid: signer.signer_uuid,
    text: 'Hello from my mini app!',
  });
}

Summary

Authentication flow provides a comprehensive system for:

  1. Secure Authentication: Using SIWF protocol with cryptographic nonces
  2. Signer Management: Creating and approving signers for Farcaster actions
  3. Multi-Platform Support: Works in web browsers and Farcaster mobile clients
  4. State Persistence: Maintains authentication across sessions

The system abstracts the complexity of Farcaster protocol interactions while providing a seamless user experience for both web and mobile environments.

Feel Free to reach out with any questions or for further assistance in integrating this authentication flow into your Farcaster mini app!

Happy coding! 🎉