Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Example of SIWE custom integration with NextAuth #187

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ NEXTAUTH_URL=http://localhost:8080
NEXTAUTH_SECRET=
DISCORD_AUTH_CLIENT_ID=
DISCORD_AUTH_CLIENT_SECRET=
NEXT_PUBLIC_PROJECT_ID=
NEXT_PUBLIC_RPC_ENDPOINT="https://auto-evm-0.taurus.subspace.network/ws"
6 changes: 6 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@
"@heroicons/react": "^2.1.5",
"@ipld/dag-pb": "^4.1.2",
"@peculiar/webcrypto": "^1.5.0",
"@rainbow-me/rainbowkit": "^2.2.3",
"@tanstack/react-query": "^5.66.0",
"@uidotdev/usehooks": "^2.4.1",
"byte-size": "^9.0.0",
"bytes": "^3.1.2",
"clsx": "^2.1.1",
"ethers": "^6.13.5",
"fflate": "^0.8.2",
"graphql": "^16.9.0",
"jsonwebtoken": "^9.0.2",
Expand All @@ -39,10 +42,13 @@
"react-hot-toast": "^2.4.1",
"react-json-view": "^1.21.3",
"rxjs": "^7.8.1",
"siwe": "^3.0.0",
"streamsaver": "^2.0.6",
"tailwind-merge": "^3.0.1",
"usehooks-ts": "^3.1.0",
"util-browser": "^0.0.2",
"viem": "2.x",
"wagmi": "^2.14.10",
"zod": "^3.23.8",
"zustand": "^5.0.0"
},
Expand Down
57 changes: 56 additions & 1 deletion frontend/src/app/api/auth/[...nextauth]/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { JsonRpcProvider } from 'ethers';
import { AuthOptions } from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import CredentialsProvider from 'next-auth/providers/credentials';
import DiscordProvider from 'next-auth/providers/discord';
import GoogleProvider from 'next-auth/providers/google';
import { getCsrfToken } from 'next-auth/react';
import { SiweMessage, VerifyOpts, VerifyParams } from 'siwe';
import {
generateAccessToken,
invalidateRefreshToken,
Expand All @@ -19,6 +23,57 @@ export const authOptions: AuthOptions = {
clientId: process.env.DISCORD_AUTH_CLIENT_ID as string,
clientSecret: process.env.DISCORD_AUTH_CLIENT_SECRET as string,
}),
CredentialsProvider({
id: 'auto-evm',
name: 'Auto-EVM',
credentials: {
address: { label: 'EVM Address', type: 'text', placeholder: '0x...' },
message: { label: 'Message', type: 'text', placeholder: '0x...' },
signature: { label: 'Signature', type: 'text', placeholder: '0x...' },
},
authorize: async (credentials) => {
try {
if (!process.env.NEXT_PUBLIC_RPC_ENDPOINT)
throw new Error('Missing Auto-EVM RPC URL');

if (
!credentials ||
!credentials.address ||
!credentials.message ||
!credentials.signature
)
throw new Error('Missing credentials');

const signature = credentials.signature;
const message = new SiweMessage(credentials.message);

if (message.address !== credentials.address)
throw new Error('Invalid address');

const verifyParams: VerifyParams = {
signature,
nonce: message.nonce,
domain: message.domain,
};

const verifyOpts: VerifyOpts = {
provider: new JsonRpcProvider(process.env.NEXT_PUBLIC_RPC_ENDPOINT),
suppressExceptions: false,
};

if (!message.verify(verifyParams, verifyOpts))
throw new Error('Invalid signature');

return {
id: 'evm:' + credentials.address,
name: credentials.address,
};
} catch (error) {
console.error('Nova authorize error', error);
return null;
}
},
}),
],
callbacks: {
async jwt({ account, token }) {
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Web3Provider } from '@/contexts/web3';
import { Metadata } from 'next';
import localFont from 'next/font/local';
import './globals.css';
import { ToasterSetup } from '../components/ToasterSetup';
import './globals.css';

const geistSans = localFont({
src: './fonts/GeistVF.woff',
Expand Down Expand Up @@ -33,7 +34,7 @@ export const metadata: Metadata = {
'web3 storage',
'peer-to-peer storage',
'encrypted storage',
'data persistence'
'data persistence',
],
icons: {
icon: '/favicon.ico',
Expand Down Expand Up @@ -72,7 +73,7 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<Web3Provider>{children}</Web3Provider>
<ToasterSetup />
</body>
</html>
Expand Down
208 changes: 208 additions & 0 deletions frontend/src/components/common/WalletIcon.tsx

Large diffs are not rendered by default.

65 changes: 65 additions & 0 deletions frontend/src/contexts/web3.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use client';

import {
darkTheme,
getDefaultConfig,
RainbowKitProvider,
} from '@rainbow-me/rainbowkit';
import '@rainbow-me/rainbowkit/styles.css';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { FC, ReactNode, useState } from 'react';
import { WagmiProvider } from 'wagmi';
import { Chain } from 'wagmi/chains';

export const nova: Chain = {
id: 490000,
name: 'Auto EVM - Autonomys Testnet',
nativeCurrency: {
decimals: 18,
name: 'tAI3',
symbol: 'tAI3',
},
rpcUrls: {
default: {
http: [process.env.NEXT_PUBLIC_RPC_ENDPOINT || ''],
},
public: {
http: [process.env.NEXT_PUBLIC_RPC_ENDPOINT || ''],
},
},
blockExplorers: {
default: {
name: 'Auto EVM Explorer',
url: 'https://blockscout.taurus.autonomys.xyz/',
},
},
};

const config = getDefaultConfig({
appName: 'Auto Drive',
projectId: process.env.NEXT_PUBLIC_PROJECT_ID || '',
chains: [nova],
ssr: true,
});

export const Web3Provider: FC<{ children: ReactNode }> = ({ children }) => {
const [queryClient] = useState(() => new QueryClient());

return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider
theme={darkTheme({
accentColor: '#0A8DD0',
accentColorForeground: 'white',
borderRadius: 'small',
fontStack: 'system',
overlayBlur: 'small',
})}
>
{children}
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
};
56 changes: 52 additions & 4 deletions frontend/src/views/Home/SignInButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { signIn } from 'next-auth/react';
import { useConnectModal } from '@rainbow-me/rainbowkit';
import { LoaderCircle } from 'lucide-react';
import { BuiltInProviderType } from 'next-auth/providers';
import { getCsrfToken, signIn } from 'next-auth/react';
import { useCallback, useState } from 'react';
import { SiweMessage } from 'siwe';
import { useAccount, useSignMessage } from 'wagmi';
import { DiscordIcon } from '../../components/common/DiscordIcon';
import { GoogleIcon } from '../../components/common/GoogleIcon';
import { BuiltInProviderType } from 'next-auth/providers';
import { LoaderCircle } from 'lucide-react';
import { WalletIcon } from '../../components/common/WalletIcon';

export const SigningInButtons = () => {
const [isClicked, setIsClicked] = useState<BuiltInProviderType>();
const { openConnectModal } = useConnectModal();
const { address } = useAccount();
const { signMessageAsync } = useSignMessage();
const [isClicked, setIsClicked] = useState<
BuiltInProviderType | 'auto-evm'
>();

const handleGoogleAuth = useCallback(() => {
setIsClicked('google');
Expand All @@ -16,6 +25,36 @@ export const SigningInButtons = () => {
setIsClicked('discord');
signIn('discord');
}, []);
const handleAutoEVM = useCallback(async () => {
setIsClicked('auto-evm');
if (openConnectModal) openConnectModal();

if (address) {
const siweMessage = new SiweMessage({
address,
chainId: 490000,
domain: window.location.host,
statement: 'Sign in to Auto Drive.',
uri: window.location.origin,
version: '1',
nonce: await getCsrfToken(),
issuedAt: new Date().toISOString(),
expirationTime: new Date(
Date.now() + 1000 * 7 * 24 * 60 * 60,
).toISOString(),
});
const message = siweMessage.prepareMessage();
const signature = await signMessageAsync({
message,
});
signIn('auto-evm', {
address,
message,
signature,
redirect: false,
});
}
}, [openConnectModal, address, signMessageAsync]);

return (
<div className='flex flex-col gap-2'>
Expand All @@ -37,6 +76,15 @@ export const SigningInButtons = () => {
Sign in with Discord
{isClicked === 'discord' && <LoaderCircle className='animate-spin' />}
</button>
<button
onClick={handleAutoEVM}
className='flex w-full max-w-xs transform items-center justify-center rounded-full border-2 border-backgroundDarker bg-white px-6 py-3 font-bold text-backgroundDarker transition duration-300 ease-in-out hover:-translate-y-1 hover:scale-105 hover:opacity-80 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-2'
aria-label='Sign in with Auto-EVM'
>
<WalletIcon />
Sign in with Auto-EVM
{isClicked === 'auto-evm' && <LoaderCircle className='animate-spin' />}
</button>
</div>
);
};
Loading
Loading