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
Endpoint | Method | Purpose | Step |
---|
/api/auth/nonce | GET | Generate authentication nonce | Step 1 |
/api/auth/signers | GET | Fetch user signers | Step 5 |
/api/auth/session-signers | GET | Fetch signers with user data | Step 5 (Backend) |
/api/auth/signer | POST | Create new signer | Step 7 |
/api/auth/signer | GET | Check signer status | Step 9 |
/api/auth/signer/signed_key | POST | Register signed key | Step 8 |
/api/auth/[...nextauth] | GET/POST | NextAuth handlers | Backend 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:
- Secure Authentication: Using SIWF protocol with cryptographic nonces
- Signer Management: Creating and approving signers for Farcaster actions
- Multi-Platform Support: Works in web browsers and Farcaster mobile clients
- 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! 🎉