Skip to main content

Authentication & Signer Management

Although titled “Mini app authentication”, this can also be used in web apps if you’d like.

Overview

The authorization 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 This authentication system is designed to work both in a regular web browser and inside a miniapp. In other words, it supports authentication when the miniapp context is not present (web browser) as well as when the app is running inside a miniapp. If you only need authentication for a web application, follow the Webapp flow; if you only need authentication inside a miniapp, follow the Miniapp flow.

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 (Miniapp)
/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 handlersMiniapp 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.
// Webapp flow using Farcaster Auth Kit
const { signIn, connect, data } = useSignIn({
  nonce: nonce || undefined,
  onSuccess: onSuccessCallback,
  onError: onErrorCallback,
});

// Miniapp flow using Farcaster SDK
const handleMiniappSignIn = 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. Webapp flow:
useEffect(() => {
  if (nonce && !useMiniappFlow) {
    connect(); // Triggers signing flow
  }
}, [nonce, connect, useMiniappFlow]);
Miniapp 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);
  },
  [useMiniappFlow, 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 = useMiniappFlow
    ? `/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 Miniapp 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. Webapp flow (LocalStorage):
const storeInLocalStorage = (user: any, signers: any[]) => {
  const authState = {
    isAuthenticated: true,
    user,
    signers,
  };

  setItem(STORAGE_KEY, authState);
  setStoredAuth(authState);
};
Miniapp flow (NextAuth Session):
const updateSessionWithSigners = async (signers: any[], user: any) => {
  if (useMiniappFlow && 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

Webapp 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';

Miniapp 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

// 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 useMiniappFlow = context !== undefined;

// Webapp flow uses localStorage + Farcaster Auth Kit
// Miniapp flow uses NextAuth sessions + Farcaster SDK

Integration Examples

Checking Authentication Status

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

// Miniapp 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 = (useMiniappFlow ? 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! 🎉

I