Skip to main content

NextAuth

About NextAuth​

NextAuth is a complete open-source authentication solution for Next.js applications.

It is designed from the ground up to support Next.js and Serverless.

Requirements​

Install the Web3Modal SIWE package.​

npm i @web3modal/siwe siwe ethers next-auth

Set up your API route​

Add NEXTAUTH_SECRET as an environment variable, it will be used to encrypt and decrypt user sessions.

Create your API route api/auth/[...nextauth].ts and paste the following code into it.

import type { SIWESession } from '@web3modal/core'
import type { NextApiRequest, NextApiResponse } from 'next'
import nextAuth from 'next-auth'
import credentialsProvider from 'next-auth/providers/credentials'
import { getCsrfToken } from 'next-auth/react'
import { SiweMessage } from 'siwe'
import { ethers } from 'ethers'

declare module 'next-auth' {
interface Session extends SIWESession {
address: string
chainId: number
}
}

/*
* For more information on each option (and a full list of options) go to
* https://next-auth.js.org/configuration/options
*/
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
const nextAuthSecret = process.env['NEXTAUTH_SECRET']
if (!nextAuthSecret) {
throw new Error('NEXTAUTH_SECRET is not set')
}
// Get your projectId on https://cloud.walletconnect.com
const projectId = process.env['NEXT_PUBLIC_PROJECT_ID']
if (!projectId) {
throw new Error('NEXT_PUBLIC_PROJECT_ID is not set')
}

const providers = [
credentialsProvider({
name: 'Ethereum',
credentials: {
message: {
label: 'Message',
type: 'text',
placeholder: '0x0'
},
signature: {
label: 'Signature',
type: 'text',
placeholder: '0x0'
}
},
async authorize(credentials) {
try {
if (!credentials?.message) {
throw new Error('SiweMessage is undefined')
}
const siwe = new SiweMessage(credentials.message)
const provider = new ethers.JsonRpcProvider(
`https://rpc.walletconnect.com/v1?chainId=eip155:${siwe.chainId}&projectId=${projectId}`
)
const nonce = await getCsrfToken({ req: { headers: req.headers } })
const result = await siwe.verify(
{
signature: credentials?.signature || '',
nonce
},
{ provider }
)

if (result.success) {
return {
id: `eip155:${siwe.chainId}:${siwe.address}`
}
}

return null
} catch (e) {
return null
}
}
})
]

const isDefaultSigninPage = req.method === 'GET' && req.query?.['nextauth']?.includes('signin')

// Hide Sign-In with Ethereum from default sign page
if (isDefaultSigninPage) {
providers.pop()
}

return await nextAuth(req, res, {
// https://next-auth.js.org/configuration/providers/oauth
secret: nextAuthSecret,
providers,
session: {
strategy: 'jwt'
},
callbacks: {
session({ session, token }) {
if (!token.sub) {
return session
}

const [, chainId, address] = token.sub.split(':')
if (chainId && address) {
session.address = address
session.chainId = parseInt(chainId, 10)
}

return session
}
}
})
}

Configure your SIWE Client​

import { SiweMessage } from 'siwe'
import { createSIWEConfig } from '@web3modal/siwe'
import { getCsrfToken, signIn, signOut, getSession } from 'next-auth/react'
import type { SIWECreateMessageArgs, SIWESession, SIWEVerifyMessageArgs } from '@web3modal/core'

const siweConfig = createSIWEConfig({
createMessage: ({ nonce, address, chainId }: SIWECreateMessageArgs) =>
new SiweMessage({
version: '1',
domain: window.location.host,
uri: window.location.origin,
address,
chainId,
nonce,
// Human-readable ASCII assertion that the user will sign, and it must not contain `\n`.
statement: 'Sign in With Ethereum.'
}).prepareMessage(),
getNonce: async () => {
const nonce = await getCsrfToken()
if (!nonce) {
throw new Error('Failed to get nonce!')
}

return nonce
},
getSession: async () => {
const session = await getSession()
if (!session) {
throw new Error('Failed to get session!')
}

const { address, chainId } = session as unknown as SIWESession

return { address, chainId }
},
verifyMessage: async ({ message, signature }: SIWEVerifyMessageArgs) => {
try {
const success = await signIn('credentials', {
message,
redirect: false,
signature,
callbackUrl: '/protected'
})

return Boolean(success?.ok)
} catch (error) {
return false
}
},
signOut: async () => {
try {
await signOut({
redirect: false
})

return true
} catch (error) {
return false
}
}
})

Initialize Web3Modal with your siweConfig.​

createWeb3Modal({
siweConfig,
// Refer to https://docs.walletconnect.com/web3modal/react/about for the other options.
wagmiConfig, // or ethersConfig
projectId,
chains
})

Add the SessionProvider in your _app.tsx​

import { SessionProvider } from 'next-auth/react'
// Use of the <SessionProvider> is mandatory to allow components that call
// `useSession()` anywhere in your application to access the `session` object.
export default function App({
Component,
pageProps
}: AppProps<{
session: Session
}>) {
return (
<SessionProvider session={pageProps.session} refetchInterval={0}>
<Component {...pageProps} />
</SessionProvider>
)
}

SIWE Config reference​

interface SIWEConfig {
// Required
getNonce: () => Promise<string>
createMessage: (args: SIWECreateMessageArgs) => string
verifyMessage: (args: SIWEVerifyMessageArgs) => Promise<boolean>
getSession: () => Promise<SIWESession | null>
signOut: () => Promise<boolean>

// Optional
onSignIn?: (session?: SIWESession) => void
onSignOut?: () => void
// Defaults to true
enabled?: boolean
// In milliseconds, defaults to 5 minutes
nonceRefetchIntervalMs?: number
// In milliseconds, defaults to 5 minutes
sessionRefetchIntervalMs?: number
// Defaults to true
signOutOnDisconnect?: boolean
// Defaults to true
signOutOnAccountChange?: boolean
// Defaults to true
signOutOnNetworkChange?: boolean
}

Required​

getNonce​

The getNonce method functions as a safeguard against spoofing, akin to a CSRF token. The siwe package provides a generateNonce() helper, or you can utilize an existing CSRF token from your backend if available.

createMessage​

The official siwe package offers a straightforward method for generating an EIP-4361-compatible message, which can subsequently be authenticated using the same package. The nonce parameter is derived from your getNonce endpoint, while the address and chainId variables are sourced from the presently connected wallet.

verifyMessage​

The verifyMessage method should lean on the siwe package's new

SiweMessage(message).validate(signature)

to ensure the message is valid, has not been tampered with, and has been appropriately signed by the wallet address.

getSession​

The backend session should store the associated address and chainId and return it via the getSession method.

signOut​

The users session can be destroyed calling signOut.

Optional​

onSignIn (session?: SIWESession) => void​

Callback when user signs in.

onSignOut () => void​

Callback when user signs out.

enabled boolean - defaults to true​

Whether or not to enable SIWE. Defaults to true.

nonceRefetchIntervalMs number - defaults to 300000ms (5 minutes)​

How often to refetch the nonce, in milliseconds.

sessionRefetchIntervalMs number - defaults to 300000ms (5 minutes)​

How often to refetch the session, in milliseconds.

signOutOnDisconnect boolean - defaults to true​

Whether or not to sign out when the user disconnects their wallet.

signOutOnAccountChange boolean - defaults to true​

Users will be signed out and redirected to the SIWE view to sign a new message in order to keep the SIWE session in sync with the connected account.

signOutOnNetworkChange boolean - defaults to true​

Users will be signed out and redirected to the SIWE view to sign a new message in order to keep the SIWE session in sync with the connected account/network.