Docs

Next Auth

This page will show you how to integrate Auth with NextAuth to let your users login with their wallets.

More specifically, we'll show you how to use our ThirdwebAuthProvider which is fully compatible with standard NextAuth providers to add support for thirdweb Auth to your apps.

If you want to interact with a working version of the Auth + NextAuth integration that we'll be building in this guide, you can checkout the following GitHub repository, or clone it with the command below:

npx thirdweb create app --template thirdweb-auth-next-auth
thirdweb-auth-next-auth

A working example of Auth + Next Auth

Getting Started

To add Auth + NextAuth to your application, you'll need to install the @thirdweb-dev/auth and @thirdweb-dev/react packages and the ethers peer dependency, along with the actual next-auth package itself:

npm install next-auth @thirdweb-dev/auth @thirdweb-dev/react ethers@5

Now, we can configure the ThirdwebAuthProvider on our NextAuth API routes to add support for wallet based login, in addition to all other existing authentication methods. The ThirdwebAuthProvider uses NextAuth's Credentials Provider underneath to enable custom wallet based authentication (for those curious to dig deeper, you can take a look at the ThirdwebAuthProvider implementation).

import {
  ThirdwebAuthProvider,
  authSession,
} from "@thirdweb-dev/auth/next-auth";
import NextAuth from "next-auth";

export const authOptions: NextAuthOptions = {
  providers: [
    // Add the thirdweb auth provider to the providers configuration
    ThirdwebAuthProvider({
      domain: process.env.NEXT_PUBLIC_THIRDWEB_AUTH_DOMAIN || "",
    }),
    // other providers...
  ],
  callbacks: {
    // Add the authSession callback to the callbacks configuration
    session: authSession,
  },
};

export default NextAuth(authOptions);

Here, we configure the ThirdwebAuthProvider with a domain coming from NEXT_PUBLIC_THIRDWEB_AUTH_DOMAIN environment variable, which we can set in the .env.local file as follows:

NEXT_PUBLIC_THIRDWEB_AUTH_DOMAIN=example.com

The domain is used to prevent phishing attacks when your users login with their wallets - you can learn more about this in the how auth works documentation.

It's important to note that we created the authOptions as a separate object which we exported from the file - we'll use these options later to authenticate the user on the server-side.

Additionally, we pass the authSession callback function to the session callback of NextAuth. This is used to ensure that we always extract the wallet address of the logged in user to expose it on the session object for the client and server to access. We'll discuss custom usage of this function more in later sections.

Then, on the frontend, we first need to configure the necessary SessionProvider and ThirdwebProvider in our pages/_app.tsx file to be able to use Auth:

import { ThirdwebProvider } from "@thirdweb-dev/react";
import { SessionProvider } from "next-auth/react";

export default function MyApp({
  Component,
  pageProps: { session, ...pageProps },
}) {
  return (
    <SessionProvider session={session}>
      <ThirdwebProvider
        clientId="your-client-id"
        authConfig={{
          // Here we specify the domain, which should match the domain
          // configured on the backend
          domain: process.env.NEXT_PUBLIC_THIRDWEB_AUTH_DOMAIN || "",
        }}
      >
        <Component {...pageProps} />
      </ThirdwebProvider>
    </SessionProvider>
  );
}

With this setup, we can now use the useAuth hook and NextAuth's signIn function to login the user with their wallet:

import { signIn, signOut, useSession } from "next-auth/react";
import { useAuth, useAddress, useMetaMask } from "@thirdweb-dev/react";

export default function Home() {
  const address = useAddress();
  const connect = useMetamask();
  const auth = useAuth();
  const { data: session } = useSession();

  async function loginWithWallet() {
    // Prompt the user to sign a login with wallet message
    const payload = await auth?.login();

    // Then send the payload to next auth as login credentials
    // using the "credentials" provider method
    await signIn("credentials", {
      payload: JSON.stringify(payload),
      redirect: false,
    });
  }

  return (
    <div>
      {isLoggedIn ? (
        <button onClick={() => logout()}>Logout</button>
      ) : address ? (
        <button onClick={() => loginWithWallet()}>Login</button>
      ) : (
        <button onClick={() => connect()}>Connect</button>
      )}

      <pre>Connected Wallet: {address}</pre>
      <pre>User: {session.user || "N/A"}</pre>
    </div>
  );
}

Here, we use the useMetaMask and useAddress to let the user connect their wallet and get the address of the connected wallet on the client. Then, we use the loginWithWallet function to log th user into the backend, and the user will then be accessible via the useSession hook.

Usage

Authenticating the user on the server

NextAuth's getServerSession function can be used to authenticate the user on the server. It will return the active session data if the user is logged in, or null if the user is not authenticated.

In order to use this function, we need to pass in the incoming request and response objects, as well as the authOptions that we previously configured in our next-auth file.

It can be used in any server-side context, including in Next.js API routes, as well as server-side functions like getServerSideProps and getStaticProps.

If the user is logged in with their wallet (via the ThirdwebAuthProvider), then the session.user.address value will be exposed to read the wallet address of the logged in user:

import { getServerSession } from "next-auth";
import { authOptions } from "./auth/[...nextauth]";

export default async (req, res) => {
  // Get the session on the server-side by passing in our previously configured authOptions
  const session = await getServerSession(req, res, authOptions);

  if (!session) {
    res.status(401).json({ message: "Not authorized." });
    return;
  }

  // Get the wallet address if the user is logged in with their wallet
  // Otherwise get their email
  return res.status(200).json({
    message: `This is a secret for ${
      session.user?.address || session.user?.email
    }`,
  });
};

Validating the login request

By default, the Auth API will validate the login request by checking that the user requesting to login successfully signed a valid sign-in with wallet message. However, this doesn't perform specific checks on the exact contents of the payload, aside from the domain used for anti-phishing.

If you want to add specific checks to enforce the exact data on the login payload signed by users, you can use the authOptions configuration on the ThirdwebAuthProvider:

export default NextAuth({
  providers: [
    ThirdwebAuthProvider({
      domain: process.env.NEXT_PUBLIC_THIRDWEB_AUTH_DOMAIN || "",
      // Enforce that the user's login message has these exact values
      authOptions: {
        statement: "I agree to the terms of service",
        uri: "https://frontend.example.com",
        resources: ["https://terms-of-service.example.com"],
        version: "1",
        chainId: "1",
      },
    }),
  ],
  ...
});

Note that when you enforce these checks on the server-side, you'll also want to pass in the proper parameters to the login function on your client-side application to ensure that the login payload gets the correct format. You can see an example of how to do this in the React section.

Prevent replay attacks

Since the sign-in with wallet payload is used to login to your server, it's important to prevent third parties from being able to reuse old login payloads to falsely authenticate as other users. This reuse of old login payloads is called a replay attack.

Luckily, all sign-in with wallet payloads include a nonce field which is a random string generated when the request was created. If you are using a database, or have somewhere to store nonces, you can ensure that each nonce is only used once:

export default NextAuth({
  providers: [
    ThirdwebAuthProvider({
      domain: process.env.NEXT_PUBLIC_THIRDWEB_AUTH_DOMAIN || "",
      // Enforce that the user's login message has these exact values
      authOptions: {
        validateNonce: async (nonce: string) => {
          // Check in database or storage if nonce exists
          const nonceExists = await dbExample.nonceExists(nonce);
          if (nonceExists) {
            throw new Error("Nonce has already been used!");
          }

          // Otherwise save nonce in database or storage for later validation
          await dbExample.saveNonce(nonce);
        }
      },
    }),
  ],
  ...
});

Add custom logic to the session callback

By default, you can pass the authSession function straight to the session callback, but in some cases you may want to add your own custom logic into the session callback. In this case, you can wrap the authSession function as follows:

export default NextAuth({
  providers: [
    ...
  ],
  callbacks: {
    async session({ session, user, token }) {
      const sessionWithAddress = authSession({ session, token });

      // Run your own custom logic here
      sessionWithAddress.user.customData = "custom data";

      // Make sure to return the session with the address
      return sessionWithAddress;
    }
  }
});

Getting proper user types for TypeScript

By default, TypeScript won't know about the session.user.address field that's populated onto the user object and available through the useSession and getSession functions, so it will give you type errors if you try to access that value, even if it's defined. To solve this, you can override the type for the Session object by creating a next-auth.d.ts file in your project root:

import NextAuth from "next-auth";

declare module "next-auth" {
  interface Session {
    user: {
      id: string;
      username: string;
      email: string;
      // Here we add that the user object may have an address field
      address?: string;
      [key: string]: string;
    };
  }
}