← Back to Blog
nextjs web3 next-auth auth

dApp powered by Auth.js, Web3 and NextJS 14

In this tutorial, we will integrate NextAuth (now rebranded as Auth.js) with a Web3 credentials provider in a Next.js 14 application. We'll use WalletConnect & Wagmi to create a robust authentication flow.

Prerequisites

Getting Started

Create a new Next.js 14 project and install the necessary dependencies:

npx create-next-app@latest my-web3-app
cd my-web3-app
npm install next-auth@latest

Web3 Integration with WAGMI

WAGMI is a collection of React hooks and utilities designed to help developers build Web3 applications with ease.

npm install wagmi [email protected] @tanstack/react-query

Create a WAGMI provider in src/web3/wagmi/provider.tsx:

"use client";

import type { PropsWithChildren } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { type Config, type State, WagmiProvider } from "wagmi";

const client = new QueryClient();

type Props = PropsWithChildren<{
  config: Config,
  initialState?: State,
}>;

const Provider = ({ children, config, initialState }: Props) => {
  return (
    <WagmiProvider config={config} initialState={initialState}>
      <QueryClientProvider client={client}>{children}</QueryClientProvider>
    </WagmiProvider>
  );
};

export default Provider;

Web3Modal + WAGMI

Install the web3modal integration:

npm install @web3modal/wagmi

Create the config in src/web3/web3modal/config.ts:

"use client";

import { defaultWagmiConfig } from "@web3modal/wagmi/react/config";
import { cookieStorage, createStorage } from "wagmi";
import { mainnet, sepolia } from "wagmi/chains";

export const projectId = process.env.NEXT_PUBLIC_WALLET_CONNECT_ID;

const chains = [mainnet, sepolia] as const;

const metadata = {
  name: "My dApp",
  description: "Used to connect your wallet to use dApp",
  url: "http://localhost:3000",
  icons: [],
};

export const config = defaultWagmiConfig({
  chains,
  projectId,
  metadata,
  ssr: true,
  storage: createStorage({
    storage: cookieStorage,
  }),
});

Once configured, you can test the wallet connection:

Web3Modal wallet connection demo

AuthJS Setup

Create app/server/auth.ts:

import Credentials from "next-auth/providers/credentials";
import { getServerSession } from "next-auth";
import { isAddress } from "ethers/address";
import type { DefaultSession, NextAuthOptions } from "next-auth"

declare module "next-auth" {
  interface Session extends DefaultSession {
    address?: string;
  }
}

export const authOptions: NextAuthOptions = {
  callbacks: {
    session: ({ session, token }) => {
      session.address = token.sub;
      return session;
    },
  },
  providers: [
    Credentials({
      name: "Credentials",
      credentials: {
        address: {
          label: "Address",
          type: "text",
          placeholder: "0x0",
        },
      },
      async authorize(credentials) {
        if (!credentials?.address) return null;
        if (!isAddress(credentials?.address)) return null;
        return { id: credentials?.address };
      },
    }),
  ],
  session: { strategy: "jwt" },
  pages: {
    signIn: "/",
    signOut: "/",
    error: "/",
    newUser: "/",
  },
};

export const getServerAuthSession = () => getServerSession(authOptions);

Here's a diagram showing the authentication flow:

Auth.js and Web3 authentication flow diagram

The useWeb3Auth Hook

Create app/web3/hooks/use-web3-auth.ts:

"use client";

import { useAccount } from "wagmi";
import { useWeb3Modal, useWeb3ModalEvents } from "@web3modal/wagmi/react";
import { signIn, useSession, signOut } from "next-auth/react";
import { useEffect, useCallback, useState } from "react";

type Return = [
  isConnected: boolean,
  connect: () => Promise<void>,
  disconnect: () => Promise<void>,
];

export const useWeb3Auth = (): Return => {
  const event = useWeb3ModalEvents();
  const { status } = useSession();
  const { open } = useWeb3Modal();
  const { isConnected, address } = useAccount();
  const callbackUrl = "/swap";
  const [isOk, setOk] = useState(false);

  useEffect(() => {
    if (status === "loading") return;
    setOk(status === "authenticated");
  }, [status]);

  useEffect(() => {
    const handleConnectSuccess = async () => {
      if (event?.data?.event === "CONNECT_SUCCESS") {
        await signIn("credentials", { address, callbackUrl });
      }
    };
    if (event && address) {
      handleConnectSuccess().catch(console.error);
    }
  }, [event, address]);

  const authenticate = useCallback(async () => {
    if (!isConnected) await open();
    if (!isOk && isConnected) {
      await signIn("credentials", { address, callbackUrl });
    }
  }, [isOk, isConnected, address, open]);

  const disconnect = async () => {
    await signOut({ callbackUrl: "/" });
  };

  return [isOk, authenticate, disconnect];
};

Web3Button Component

Create app/components/web3-button.tsx:

"use client";

import { useWeb3Auth } from "~/web3/hooks/use-web3-auth";

export const Web3Button = () => {
  const [isAuthenticated, connect, disconnect] = useWeb3Auth();

  if (!isAuthenticated) {
    return (
      <button onClick={connect}>
        Connect Wallet
      </button>
    );
  } else {
    return (
      <button onClick={disconnect}>
        Disconnect Wallet
      </button>
    );
  }
};

Results

Here's the complete app in action:

Complete dApp connecting to MetaMask wallet

Conclusion

In this blog we saw how to setup NextJS, Auth.js and Web3 to connect to a wallet as an authentication provider. Compared to traditional providers such as OAuth or Email, Web3 provides a simple and easy to use way to connect to dApps and provide both authorization and authentication.

As always I was your host Break Zero and if you liked this article make sure to share as it would mean the world to me!

Thank you for reading!