mirror of
https://github.com/neynarxyz/create-farcaster-mini-app.git
synced 2025-11-16 08:08:56 -05:00
Seperate SIWN and SIWF
This commit is contained in:
parent
6a7d1424e9
commit
96bb45ede0
118
src/auth.ts
118
src/auth.ts
@ -6,8 +6,15 @@ declare module 'next-auth' {
|
|||||||
interface Session {
|
interface Session {
|
||||||
user: {
|
user: {
|
||||||
fid: number;
|
fid: number;
|
||||||
|
provider?: string;
|
||||||
|
username?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
provider?: string;
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDomainFromUrl(urlString: string | undefined): string {
|
function getDomainFromUrl(urlString: string | undefined): string {
|
||||||
@ -29,6 +36,7 @@ export const authOptions: AuthOptions = {
|
|||||||
// Configure one or more authentication providers
|
// Configure one or more authentication providers
|
||||||
providers: [
|
providers: [
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
|
id: 'farcaster',
|
||||||
name: 'Sign in with Farcaster',
|
name: 'Sign in with Farcaster',
|
||||||
credentials: {
|
credentials: {
|
||||||
message: {
|
message: {
|
||||||
@ -62,9 +70,7 @@ export const authOptions: AuthOptions = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
async authorize(credentials, req) {
|
async authorize(credentials, req) {
|
||||||
const csrfToken = req?.body?.csrfToken;
|
const nonce = req?.body?.csrfToken;
|
||||||
|
|
||||||
const nonce = credentials?.nonce || csrfToken;
|
|
||||||
|
|
||||||
if (!nonce) {
|
if (!nonce) {
|
||||||
console.error('No nonce or CSRF token provided');
|
console.error('No nonce or CSRF token provided');
|
||||||
@ -91,17 +97,123 @@ export const authOptions: AuthOptions = {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: fid.toString(),
|
id: fid.toString(),
|
||||||
|
name: credentials?.name || `User ${fid}`,
|
||||||
|
image: credentials?.pfp || null,
|
||||||
|
provider: 'farcaster',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
CredentialsProvider({
|
||||||
|
id: 'neynar',
|
||||||
|
name: 'Sign in with Neynar',
|
||||||
|
credentials: {
|
||||||
|
message: {
|
||||||
|
label: 'Message',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: '0x0',
|
||||||
|
},
|
||||||
|
signature: {
|
||||||
|
label: 'Signature',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: '0x0',
|
||||||
|
},
|
||||||
|
nonce: {
|
||||||
|
label: 'Nonce',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'Custom nonce (optional)',
|
||||||
|
},
|
||||||
|
fid: {
|
||||||
|
label: 'FID',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: '0',
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
label: 'Username',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'username',
|
||||||
|
},
|
||||||
|
displayName: {
|
||||||
|
label: 'Display Name',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'Display Name',
|
||||||
|
},
|
||||||
|
pfpUrl: {
|
||||||
|
label: 'Profile Picture URL',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'https://...',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async authorize(credentials) {
|
||||||
|
const nonce = credentials?.nonce;
|
||||||
|
|
||||||
|
if (!nonce) {
|
||||||
|
console.error('No nonce or CSRF token provided for Neynar auth');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Neynar, we can use a different validation approach
|
||||||
|
// This could involve validating against Neynar's API or using their SDK
|
||||||
|
try {
|
||||||
|
// Validate the signature using Farcaster's auth client (same as Farcaster provider)
|
||||||
|
const appClient = createAppClient({
|
||||||
|
ethereum: viemConnector(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const domain = getDomainFromUrl(process.env.NEXTAUTH_URL);
|
||||||
|
|
||||||
|
const verifyResponse = await appClient.verifySignInMessage({
|
||||||
|
message: credentials?.message as string,
|
||||||
|
signature: credentials?.signature as `0x${string}`,
|
||||||
|
domain,
|
||||||
|
nonce,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { success, fid } = verifyResponse;
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that the provided FID matches the verified FID
|
||||||
|
if (credentials?.fid && parseInt(credentials.fid) !== fid) {
|
||||||
|
console.error('FID mismatch in Neynar auth');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: fid.toString(),
|
||||||
|
name:
|
||||||
|
credentials?.displayName ||
|
||||||
|
credentials?.username ||
|
||||||
|
`User ${fid}`,
|
||||||
|
image: credentials?.pfpUrl || null,
|
||||||
|
provider: 'neynar',
|
||||||
|
username: credentials?.username || undefined,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in Neynar auth:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
callbacks: {
|
callbacks: {
|
||||||
session: async ({ session, token }) => {
|
session: async ({ session, token }) => {
|
||||||
if (session?.user) {
|
if (session?.user) {
|
||||||
session.user.fid = parseInt(token.sub ?? '');
|
session.user.fid = parseInt(token.sub ?? '');
|
||||||
|
// Add provider information to session
|
||||||
|
session.user.provider = token.provider as string;
|
||||||
|
session.user.username = token.username as string;
|
||||||
}
|
}
|
||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
|
jwt: async ({ token, user }) => {
|
||||||
|
if (user) {
|
||||||
|
token.provider = user.provider;
|
||||||
|
token.username = user.username;
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
cookies: {
|
cookies: {
|
||||||
sessionToken: {
|
sessionToken: {
|
||||||
|
|||||||
@ -13,9 +13,18 @@ import { useMiniApp } from '@neynar/react';
|
|||||||
import {
|
import {
|
||||||
signIn as backendSignIn,
|
signIn as backendSignIn,
|
||||||
signOut as backendSignOut,
|
signOut as backendSignOut,
|
||||||
|
useSession,
|
||||||
} from 'next-auth/react';
|
} from 'next-auth/react';
|
||||||
import sdk, { SignIn as SignInCore } from '@farcaster/frame-sdk';
|
import sdk, { SignIn as SignInCore } from '@farcaster/frame-sdk';
|
||||||
|
|
||||||
|
type User = {
|
||||||
|
fid: number;
|
||||||
|
username: string;
|
||||||
|
display_name: string;
|
||||||
|
pfp_url: string;
|
||||||
|
// Add other user properties as needed
|
||||||
|
};
|
||||||
|
|
||||||
const STORAGE_KEY = 'neynar_authenticated_user';
|
const STORAGE_KEY = 'neynar_authenticated_user';
|
||||||
const FARCASTER_FID = 9152;
|
const FARCASTER_FID = 9152;
|
||||||
|
|
||||||
@ -90,6 +99,7 @@ export function NeynarAuthButton() {
|
|||||||
const [storedAuth, setStoredAuth] = useState<StoredAuthState | null>(null);
|
const [storedAuth, setStoredAuth] = useState<StoredAuthState | null>(null);
|
||||||
const [signersLoading, setSignersLoading] = useState(false);
|
const [signersLoading, setSignersLoading] = useState(false);
|
||||||
const { context } = useMiniApp();
|
const { context } = useMiniApp();
|
||||||
|
const { data: session } = useSession();
|
||||||
// New state for unified dialog flow
|
// New state for unified dialog flow
|
||||||
const [showDialog, setShowDialog] = useState(false);
|
const [showDialog, setShowDialog] = useState(false);
|
||||||
const [dialogStep, setDialogStep] = useState<'signin' | 'access' | 'loading'>(
|
const [dialogStep, setDialogStep] = useState<'signin' | 'access' | 'loading'>(
|
||||||
@ -468,7 +478,7 @@ export function NeynarAuthButton() {
|
|||||||
nonce: nonce,
|
nonce: nonce,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextAuthResult = await backendSignIn('credentials', signInData);
|
const nextAuthResult = await backendSignIn('neynar', signInData);
|
||||||
if (nextAuthResult?.ok) {
|
if (nextAuthResult?.ok) {
|
||||||
setMessage(result.message);
|
setMessage(result.message);
|
||||||
setSignature(result.signature);
|
setSignature(result.signature);
|
||||||
@ -503,7 +513,10 @@ export function NeynarAuthButton() {
|
|||||||
setSignersLoading(true);
|
setSignersLoading(true);
|
||||||
|
|
||||||
if (useBackendFlow) {
|
if (useBackendFlow) {
|
||||||
await backendSignOut({ redirect: false });
|
// Only sign out from NextAuth if the current session is from Neynar provider
|
||||||
|
if (session?.user?.provider === 'neynar') {
|
||||||
|
await backendSignOut({ redirect: false });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
frontendSignOut();
|
frontendSignOut();
|
||||||
}
|
}
|
||||||
@ -528,7 +541,7 @@ export function NeynarAuthButton() {
|
|||||||
} finally {
|
} finally {
|
||||||
setSignersLoading(false);
|
setSignersLoading(false);
|
||||||
}
|
}
|
||||||
}, [useBackendFlow, frontendSignOut, pollingInterval]);
|
}, [useBackendFlow, frontendSignOut, pollingInterval, session]);
|
||||||
|
|
||||||
// The key fix: match the original library's authentication logic exactly
|
// The key fix: match the original library's authentication logic exactly
|
||||||
const authenticated =
|
const authenticated =
|
||||||
|
|||||||
@ -77,7 +77,7 @@ export function SignIn() {
|
|||||||
const nonce = await getNonce();
|
const nonce = await getNonce();
|
||||||
const result = await sdk.actions.signIn({ nonce });
|
const result = await sdk.actions.signIn({ nonce });
|
||||||
setSignInResult(result);
|
setSignInResult(result);
|
||||||
await signIn('credentials', {
|
await signIn('farcaster', {
|
||||||
message: result.message,
|
message: result.message,
|
||||||
signature: result.signature,
|
signature: result.signature,
|
||||||
redirect: false,
|
redirect: false,
|
||||||
@ -96,41 +96,46 @@ export function SignIn() {
|
|||||||
/**
|
/**
|
||||||
* Handles the sign-out process.
|
* Handles the sign-out process.
|
||||||
*
|
*
|
||||||
* This function clears the NextAuth session and resets the local
|
* This function clears the NextAuth session only if the current session
|
||||||
* sign-in result state to complete the sign-out flow.
|
* is using the Farcaster provider, and resets the local sign-in result state.
|
||||||
*
|
*
|
||||||
* @returns Promise<void>
|
* @returns Promise<void>
|
||||||
*/
|
*/
|
||||||
const handleSignOut = useCallback(async () => {
|
const handleSignOut = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setAuthState((prev) => ({ ...prev, signingOut: true }));
|
setAuthState((prev) => ({ ...prev, signingOut: true }));
|
||||||
await signOut({ redirect: false });
|
// Only sign out if the current session is from Farcaster provider
|
||||||
|
if (session?.user?.provider === 'farcaster') {
|
||||||
|
await signOut({ redirect: false });
|
||||||
|
}
|
||||||
setSignInResult(undefined);
|
setSignInResult(undefined);
|
||||||
} finally {
|
} finally {
|
||||||
setAuthState((prev) => ({ ...prev, signingOut: false }));
|
setAuthState((prev) => ({ ...prev, signingOut: false }));
|
||||||
}
|
}
|
||||||
}, []);
|
}, [session]);
|
||||||
|
|
||||||
// --- Render ---
|
// --- Render ---
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Authentication Buttons */}
|
{/* Authentication Buttons */}
|
||||||
{status !== 'authenticated' && (
|
{(status !== 'authenticated' ||
|
||||||
|
session?.user?.provider !== 'farcaster') && (
|
||||||
<Button onClick={handleSignIn} disabled={authState.signingIn}>
|
<Button onClick={handleSignIn} disabled={authState.signingIn}>
|
||||||
Sign In with Farcaster
|
Sign In with Farcaster
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{status === 'authenticated' && (
|
{status === 'authenticated' &&
|
||||||
<Button onClick={handleSignOut} disabled={authState.signingOut}>
|
session?.user?.provider === 'farcaster' && (
|
||||||
Sign out
|
<Button onClick={handleSignOut} disabled={authState.signingOut}>
|
||||||
</Button>
|
Sign out
|
||||||
)}
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Session Information */}
|
{/* Session Information */}
|
||||||
{session && (
|
{session && (
|
||||||
<div className='my-2 p-2 text-xs overflow-x-scroll rounded-lg font-mono border border-secondary-100'>
|
<div className="my-2 p-2 text-xs overflow-x-scroll rounded-lg font-mono border border-secondary-100">
|
||||||
<div className='font-semibold text-gray-500 mb-1'>Session</div>
|
<div className="font-semibold text-gray-500 mb-1">Session</div>
|
||||||
<div className='whitespace-pre'>
|
<div className="whitespace-pre">
|
||||||
{JSON.stringify(session, null, 2)}
|
{JSON.stringify(session, null, 2)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -138,17 +143,17 @@ export function SignIn() {
|
|||||||
|
|
||||||
{/* Error Display */}
|
{/* Error Display */}
|
||||||
{signInFailure && !authState.signingIn && (
|
{signInFailure && !authState.signingIn && (
|
||||||
<div className='my-2 p-2 text-xs overflow-x-scroll rounded-lg font-mono border border-secondary-100'>
|
<div className="my-2 p-2 text-xs overflow-x-scroll rounded-lg font-mono border border-secondary-100">
|
||||||
<div className='font-semibold text-gray-500 mb-1'>SIWF Result</div>
|
<div className="font-semibold text-gray-500 mb-1">SIWF Result</div>
|
||||||
<div className='whitespace-pre'>{signInFailure}</div>
|
<div className="whitespace-pre">{signInFailure}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Success Result Display */}
|
{/* Success Result Display */}
|
||||||
{signInResult && !authState.signingIn && (
|
{signInResult && !authState.signingIn && (
|
||||||
<div className='my-2 p-2 text-xs overflow-x-scroll rounded-lg font-mono border border-secondary-100'>
|
<div className="my-2 p-2 text-xs overflow-x-scroll rounded-lg font-mono border border-secondary-100">
|
||||||
<div className='font-semibold text-gray-500 mb-1'>SIWF Result</div>
|
<div className="font-semibold text-gray-500 mb-1">SIWF Result</div>
|
||||||
<div className='whitespace-pre'>
|
<div className="whitespace-pre">
|
||||||
{JSON.stringify(signInResult, null, 2)}
|
{JSON.stringify(signInResult, null, 2)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user