mirror of
https://github.com/neynarxyz/create-farcaster-mini-app.git
synced 2025-12-06 17:32:31 -05:00
feat: reapply quickauth changes conditionally
This commit is contained in:
46
src/app/api/auth/validate/route.ts
Normal file
46
src/app/api/auth/validate/route.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { createClient, Errors } from '@farcaster/quick-auth';
|
||||
|
||||
const client = createClient();
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { token } = await request.json();
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json({ error: 'Token is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get domain from environment or request
|
||||
const domain = process.env.NEXT_PUBLIC_URL
|
||||
? new URL(process.env.NEXT_PUBLIC_URL).hostname
|
||||
: request.headers.get('host') || 'localhost';
|
||||
|
||||
try {
|
||||
// Use the official QuickAuth library to verify the JWT
|
||||
const payload = await client.verifyJwt({
|
||||
token,
|
||||
domain,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
user: {
|
||||
fid: payload.sub,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof Errors.InvalidTokenError) {
|
||||
console.info('Invalid token:', e.message);
|
||||
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token validation error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
import { getSession } from "~/auth"
|
||||
import "~/app/globals.css";
|
||||
import { Providers } from "~/app/providers";
|
||||
import { APP_NAME, APP_DESCRIPTION } from "~/lib/constants";
|
||||
@@ -15,7 +14,19 @@ export default async function RootLayout({
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const session = await getSession()
|
||||
// Only get session if sponsored signer is enabled or seed phrase is provided
|
||||
const sponsorSigner = process.env.SPONSOR_SIGNER === 'true';
|
||||
const hasSeedPhrase = !!process.env.SEED_PHRASE;
|
||||
|
||||
let session = null;
|
||||
if (sponsorSigner || hasSeedPhrase) {
|
||||
try {
|
||||
const { getSession } = await import("~/auth");
|
||||
session = await getSession();
|
||||
} catch (error) {
|
||||
console.warn('Failed to get session:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
|
||||
@@ -24,18 +24,36 @@ export function Providers({
|
||||
}) {
|
||||
const solanaEndpoint =
|
||||
process.env.SOLANA_RPC_ENDPOINT || 'https://solana-rpc.publicnode.com';
|
||||
|
||||
// Only wrap with SessionProvider if session is provided
|
||||
if (session) {
|
||||
return (
|
||||
<SessionProvider session={session}>
|
||||
<WagmiProvider>
|
||||
<MiniAppProvider
|
||||
analyticsEnabled={ANALYTICS_ENABLED}
|
||||
backButtonEnabled={true}
|
||||
>
|
||||
<SafeFarcasterSolanaProvider endpoint={solanaEndpoint}>
|
||||
<AuthKitProvider config={{}}>{children}</AuthKitProvider>
|
||||
</SafeFarcasterSolanaProvider>
|
||||
</MiniAppProvider>
|
||||
</WagmiProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// Return without SessionProvider if no session
|
||||
return (
|
||||
<SessionProvider session={session}>
|
||||
<WagmiProvider>
|
||||
<MiniAppProvider
|
||||
analyticsEnabled={ANALYTICS_ENABLED}
|
||||
backButtonEnabled={true}
|
||||
>
|
||||
<SafeFarcasterSolanaProvider endpoint={solanaEndpoint}>
|
||||
<AuthKitProvider config={{}}>{children}</AuthKitProvider>
|
||||
</SafeFarcasterSolanaProvider>
|
||||
</MiniAppProvider>
|
||||
</WagmiProvider>
|
||||
</SessionProvider>
|
||||
<WagmiProvider>
|
||||
<MiniAppProvider
|
||||
analyticsEnabled={ANALYTICS_ENABLED}
|
||||
backButtonEnabled={true}
|
||||
>
|
||||
<SafeFarcasterSolanaProvider endpoint={solanaEndpoint}>
|
||||
<AuthKitProvider config={{}}>{children}</AuthKitProvider>
|
||||
</SafeFarcasterSolanaProvider>
|
||||
</MiniAppProvider>
|
||||
</WagmiProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useState, type ComponentType } from 'react';
|
||||
import { useMiniApp } from '@neynar/react';
|
||||
import { ShareButton } from '../Share';
|
||||
import { Button } from '../Button';
|
||||
import { SignIn } from '../wallet/SignIn';
|
||||
import { type Haptics } from '@farcaster/miniapp-sdk';
|
||||
import { APP_URL } from '~/lib/constants';
|
||||
import { NeynarAuthButton } from '../NeynarAuthButton/index';
|
||||
|
||||
// Optional import for NeynarAuthButton - may not exist in all templates
|
||||
let NeynarAuthButton: ComponentType | null = null;
|
||||
try {
|
||||
const module = require('../NeynarAuthButton/index');
|
||||
NeynarAuthButton = module.NeynarAuthButton;
|
||||
} catch (error) {
|
||||
// Component doesn't exist, that's okay
|
||||
console.log('NeynarAuthButton not available in this template');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ActionsTab component handles mini app actions like sharing, notifications, and haptic feedback.
|
||||
@@ -140,7 +150,7 @@ export function ActionsTab() {
|
||||
<SignIn />
|
||||
|
||||
{/* Neynar Authentication */}
|
||||
<NeynarAuthButton />
|
||||
{NeynarAuthButton && <NeynarAuthButton />}
|
||||
|
||||
{/* Mini app actions */}
|
||||
<Button
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { signIn, signOut, getCsrfToken } from "next-auth/react";
|
||||
import sdk, { SignIn as SignInCore } from "@farcaster/miniapp-sdk";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { Button } from "../Button";
|
||||
import { useCallback, useState } from 'react';
|
||||
import { SignIn as SignInCore } from '@farcaster/miniapp-sdk';
|
||||
import { useQuickAuth } from '~/hooks/useQuickAuth';
|
||||
import { Button } from '../Button';
|
||||
|
||||
/**
|
||||
* SignIn component handles Farcaster authentication using Sign-In with Farcaster (SIWF).
|
||||
* SignIn component handles Farcaster authentication using QuickAuth.
|
||||
*
|
||||
* This component provides a complete authentication flow for Farcaster users:
|
||||
* - Generates nonces for secure authentication
|
||||
* - Handles the SIWF flow using the Farcaster SDK
|
||||
* - Manages NextAuth session state
|
||||
* - Uses the built-in QuickAuth functionality from the Farcaster SDK
|
||||
* - Manages authentication state in memory (no persistence)
|
||||
* - Provides sign-out functionality
|
||||
* - Displays authentication status and results
|
||||
*
|
||||
* The component integrates with both the Farcaster Frame SDK and NextAuth
|
||||
* The component integrates with the Farcaster Frame SDK and QuickAuth
|
||||
* to provide seamless authentication within mini apps.
|
||||
*
|
||||
* @example
|
||||
@@ -36,52 +34,32 @@ export function SignIn() {
|
||||
signingIn: false,
|
||||
signingOut: false,
|
||||
});
|
||||
const [signInResult, setSignInResult] = useState<SignInCore.SignInResult>();
|
||||
const [signInFailure, setSignInFailure] = useState<string>();
|
||||
|
||||
// --- Hooks ---
|
||||
const { data: session, status } = useSession();
|
||||
const { authenticatedUser, status, signIn, signOut } = useQuickAuth();
|
||||
|
||||
// --- Handlers ---
|
||||
/**
|
||||
* Generates a nonce for the sign-in process.
|
||||
* Handles the sign-in process using QuickAuth.
|
||||
*
|
||||
* This function retrieves a CSRF token from NextAuth to use as a nonce
|
||||
* for the SIWF authentication flow. The nonce ensures the authentication
|
||||
* request is fresh and prevents replay attacks.
|
||||
*
|
||||
* @returns Promise<string> - The generated nonce token
|
||||
* @throws Error if unable to generate nonce
|
||||
*/
|
||||
const getNonce = useCallback(async () => {
|
||||
const nonce = await getCsrfToken();
|
||||
if (!nonce) throw new Error('Unable to generate nonce');
|
||||
return nonce;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handles the sign-in process using Farcaster SDK.
|
||||
*
|
||||
* This function orchestrates the complete SIWF flow:
|
||||
* 1. Generates a nonce for security
|
||||
* 2. Calls the Farcaster SDK to initiate sign-in
|
||||
* 3. Submits the result to NextAuth for session management
|
||||
* 4. Handles various error conditions including user rejection
|
||||
* This function uses the built-in QuickAuth functionality:
|
||||
* 1. Gets a token from QuickAuth (handles SIWF flow automatically)
|
||||
* 2. Validates the token with our server
|
||||
* 3. Updates the session state
|
||||
*
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
const handleSignIn = useCallback(async () => {
|
||||
try {
|
||||
setAuthState((prev) => ({ ...prev, signingIn: true }));
|
||||
setAuthState(prev => ({ ...prev, signingIn: true }));
|
||||
setSignInFailure(undefined);
|
||||
const nonce = await getNonce();
|
||||
const result = await sdk.actions.signIn({ nonce });
|
||||
setSignInResult(result);
|
||||
await signIn('farcaster', {
|
||||
message: result.message,
|
||||
signature: result.signature,
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
const success = await signIn();
|
||||
|
||||
if (!success) {
|
||||
setSignInFailure('Authentication failed');
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof SignInCore.RejectedByUser) {
|
||||
setSignInFailure('Rejected by user');
|
||||
@@ -89,52 +67,49 @@ export function SignIn() {
|
||||
}
|
||||
setSignInFailure('Unknown error');
|
||||
} finally {
|
||||
setAuthState((prev) => ({ ...prev, signingIn: false }));
|
||||
setAuthState(prev => ({ ...prev, signingIn: false }));
|
||||
}
|
||||
}, [getNonce]);
|
||||
}, [signIn]);
|
||||
|
||||
/**
|
||||
* Handles the sign-out process.
|
||||
*
|
||||
* This function clears the NextAuth session only if the current session
|
||||
* is using the Farcaster provider, and resets the local sign-in result state.
|
||||
* This function clears the QuickAuth session and resets the local state.
|
||||
*
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
const handleSignOut = useCallback(async () => {
|
||||
try {
|
||||
setAuthState((prev) => ({ ...prev, signingOut: true }));
|
||||
// Only sign out if the current session is from Farcaster provider
|
||||
if (session?.provider === 'farcaster') {
|
||||
await signOut({ redirect: false });
|
||||
}
|
||||
setSignInResult(undefined);
|
||||
setAuthState(prev => ({ ...prev, signingOut: true }));
|
||||
await signOut();
|
||||
} finally {
|
||||
setAuthState((prev) => ({ ...prev, signingOut: false }));
|
||||
setAuthState(prev => ({ ...prev, signingOut: false }));
|
||||
}
|
||||
}, [session]);
|
||||
}, [signOut]);
|
||||
|
||||
// --- Render ---
|
||||
return (
|
||||
<>
|
||||
{/* Authentication Buttons */}
|
||||
{(status !== 'authenticated' || session?.provider !== 'farcaster') && (
|
||||
{status !== 'authenticated' && (
|
||||
<Button onClick={handleSignIn} disabled={authState.signingIn}>
|
||||
Sign In with Farcaster
|
||||
</Button>
|
||||
)}
|
||||
{status === 'authenticated' && session?.provider === 'farcaster' && (
|
||||
{status === 'authenticated' && (
|
||||
<Button onClick={handleSignOut} disabled={authState.signingOut}>
|
||||
Sign out
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Session Information */}
|
||||
{session && (
|
||||
{authenticatedUser && (
|
||||
<div className="my-2 p-2 text-xs overflow-x-scroll bg-gray-100 dark:bg-gray-900 rounded-lg font-mono">
|
||||
<div className="font-semibold text-gray-500 dark:text-gray-300 mb-1">Session</div>
|
||||
<div className="font-semibold text-gray-500 dark:text-gray-300 mb-1">
|
||||
Authenticated User
|
||||
</div>
|
||||
<div className="whitespace-pre text-gray-700 dark:text-gray-200">
|
||||
{JSON.stringify(session, null, 2)}
|
||||
{JSON.stringify(authenticatedUser, null, 2)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -142,20 +117,14 @@ export function SignIn() {
|
||||
{/* Error Display */}
|
||||
{signInFailure && !authState.signingIn && (
|
||||
<div className="my-2 p-2 text-xs overflow-x-scroll bg-gray-100 dark:bg-gray-900 rounded-lg font-mono">
|
||||
<div className="font-semibold text-gray-500 dark:text-gray-300 mb-1">SIWF Result</div>
|
||||
<div className="whitespace-pre text-gray-700 dark:text-gray-200">{signInFailure}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Success Result Display */}
|
||||
{signInResult && !authState.signingIn && (
|
||||
<div className="my-2 p-2 text-xs overflow-x-scroll bg-gray-100 dark:bg-gray-900 rounded-lg font-mono">
|
||||
<div className="font-semibold text-gray-500 dark:text-gray-300 mb-1">SIWF Result</div>
|
||||
<div className="font-semibold text-gray-500 dark:text-gray-300 mb-1">
|
||||
Authentication Error
|
||||
</div>
|
||||
<div className="whitespace-pre text-gray-700 dark:text-gray-200">
|
||||
{JSON.stringify(signInResult, null, 2)}
|
||||
{signInFailure}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
207
src/hooks/useQuickAuth.ts
Normal file
207
src/hooks/useQuickAuth.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { sdk } from '@farcaster/miniapp-sdk';
|
||||
|
||||
/**
|
||||
* Represents the current authenticated user state
|
||||
*/
|
||||
interface AuthenticatedUser {
|
||||
/** The user's Farcaster ID (FID) */
|
||||
fid: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Possible authentication states for QuickAuth
|
||||
*/
|
||||
type QuickAuthStatus = 'loading' | 'authenticated' | 'unauthenticated';
|
||||
|
||||
/**
|
||||
* Return type for the useQuickAuth hook
|
||||
*/
|
||||
interface UseQuickAuthReturn {
|
||||
/** Current authenticated user data, or null if not authenticated */
|
||||
authenticatedUser: AuthenticatedUser | null;
|
||||
/** Current authentication status */
|
||||
status: QuickAuthStatus;
|
||||
/** Function to initiate the sign-in process using QuickAuth */
|
||||
signIn: () => Promise<boolean>;
|
||||
/** Function to sign out and clear the current authentication state */
|
||||
signOut: () => Promise<void>;
|
||||
/** Function to retrieve the current authentication token */
|
||||
getToken: () => Promise<string | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for managing QuickAuth authentication state
|
||||
*
|
||||
* This hook provides a complete authentication flow using Farcaster's QuickAuth:
|
||||
* - Automatically checks for existing authentication on mount
|
||||
* - Validates tokens with the server-side API
|
||||
* - Manages authentication state in memory (no persistence)
|
||||
* - Provides sign-in/sign-out functionality
|
||||
*
|
||||
* QuickAuth tokens are managed in memory only, so signing out of the Farcaster
|
||||
* client will automatically sign the user out of this mini app as well.
|
||||
*
|
||||
* @returns {UseQuickAuthReturn} Object containing user state and authentication methods
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { authenticatedUser, status, signIn, signOut } = useQuickAuth();
|
||||
*
|
||||
* if (status === 'loading') return <div>Loading...</div>;
|
||||
* if (status === 'unauthenticated') return <button onClick={signIn}>Sign In</button>;
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* <p>Welcome, FID: {authenticatedUser?.fid}</p>
|
||||
* <button onClick={signOut}>Sign Out</button>
|
||||
* </div>
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export function useQuickAuth(): UseQuickAuthReturn {
|
||||
// Current authenticated user data
|
||||
const [authenticatedUser, setAuthenticatedUser] =
|
||||
useState<AuthenticatedUser | null>(null);
|
||||
// Current authentication status
|
||||
const [status, setStatus] = useState<QuickAuthStatus>('loading');
|
||||
|
||||
/**
|
||||
* Validates a QuickAuth token with the server-side API
|
||||
*
|
||||
* @param {string} authToken - The JWT token to validate
|
||||
* @returns {Promise<AuthenticatedUser | null>} User data if valid, null otherwise
|
||||
*/
|
||||
const validateTokenWithServer = async (
|
||||
authToken: string,
|
||||
): Promise<AuthenticatedUser | null> => {
|
||||
try {
|
||||
const validationResponse = await fetch('/api/auth/validate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token: authToken }),
|
||||
});
|
||||
|
||||
if (validationResponse.ok) {
|
||||
const responseData = await validationResponse.json();
|
||||
return responseData.user;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Token validation failed:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks for existing authentication token and validates it on component mount
|
||||
* This runs automatically when the hook is first used
|
||||
*/
|
||||
useEffect(() => {
|
||||
const checkExistingAuthentication = async () => {
|
||||
try {
|
||||
// Attempt to retrieve existing token from QuickAuth SDK
|
||||
const { token } = await sdk.quickAuth.getToken();
|
||||
|
||||
if (token) {
|
||||
// Validate the token with our server-side API
|
||||
const validatedUserSession = await validateTokenWithServer(token);
|
||||
|
||||
if (validatedUserSession) {
|
||||
// Token is valid, set authenticated state
|
||||
setAuthenticatedUser(validatedUserSession);
|
||||
setStatus('authenticated');
|
||||
} else {
|
||||
// Token is invalid or expired, clear authentication state
|
||||
setStatus('unauthenticated');
|
||||
}
|
||||
} else {
|
||||
// No existing token found, user is not authenticated
|
||||
setStatus('unauthenticated');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking existing authentication:', error);
|
||||
setStatus('unauthenticated');
|
||||
}
|
||||
};
|
||||
|
||||
checkExistingAuthentication();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Initiates the QuickAuth sign-in process
|
||||
*
|
||||
* Uses sdk.quickAuth.getToken() to get a QuickAuth session token.
|
||||
* If there is already a session token in memory that hasn't expired,
|
||||
* it will be immediately returned, otherwise a fresh one will be acquired.
|
||||
*
|
||||
* @returns {Promise<boolean>} True if sign-in was successful, false otherwise
|
||||
*/
|
||||
const signIn = useCallback(async (): Promise<boolean> => {
|
||||
try {
|
||||
setStatus('loading');
|
||||
|
||||
// Get QuickAuth session token
|
||||
const { token } = await sdk.quickAuth.getToken();
|
||||
|
||||
if (token) {
|
||||
// Validate the token with our server-side API
|
||||
const validatedUserSession = await validateTokenWithServer(token);
|
||||
|
||||
if (validatedUserSession) {
|
||||
// Authentication successful, update user state
|
||||
setAuthenticatedUser(validatedUserSession);
|
||||
setStatus('authenticated');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Authentication failed, clear user state
|
||||
setStatus('unauthenticated');
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Sign-in process failed:', error);
|
||||
setStatus('unauthenticated');
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Signs out the current user and clears the authentication state
|
||||
*
|
||||
* Since QuickAuth tokens are managed in memory only, this simply clears
|
||||
* the local user state. The actual token will be cleared when the
|
||||
* user signs out of their Farcaster client.
|
||||
*/
|
||||
const signOut = useCallback(async (): Promise<void> => {
|
||||
// Clear local user state
|
||||
setAuthenticatedUser(null);
|
||||
setStatus('unauthenticated');
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Retrieves the current authentication token from QuickAuth
|
||||
*
|
||||
* @returns {Promise<string | null>} The current auth token, or null if not authenticated
|
||||
*/
|
||||
const getToken = useCallback(async (): Promise<string | null> => {
|
||||
try {
|
||||
const { token } = await sdk.quickAuth.getToken();
|
||||
return token;
|
||||
} catch (error) {
|
||||
console.error('Failed to retrieve authentication token:', error);
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
authenticatedUser,
|
||||
status,
|
||||
signIn,
|
||||
signOut,
|
||||
getToken,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user