Skip to main content
Version: 2.0

How to Authenticate Users with Solana Wallet Adapter

This tutorial covers how to create full-stack Web3 authentication for the Solana wallet adaptor, using the popular NextJS framework.

Introduction

This tutorial shows you how to create a NextJS application that allows users to log in using any wallet that uses the Solana wallet adapter.

After Web3 wallet authentication, the next-auth library creates a session cookie with an encrypted JWT (JWE) stored inside. It contains session info (such as an address, signed message, and expiration time) in the user's browser. It's a secure way to store users' info without a database, and it's impossible to read/modify the JWT without a secret key.

Once the user is logged in, they will be able to visit a page that displays all their user data.

You can find the repository with the final code here: GitHub.

info

You can find the final dapp with implemented style on our GitHub.

Prerequisites

  1. Create a Moralis account.
  2. Install and set up Visual Studio.
  3. Create your NextJS dapp (you can create it using create-next-app or follow the NextJS dapp tutorial).

Install the Required Dependencies

  1. Install @moralisweb3/next (if not installed), next-auth and @web3uikit/core dependencies:
npm install @moralisweb3/next next-auth @web3uikit/core
  1. To implement authentication using a Web3 wallet (e.g., Phantom), we need to use a Solana Web3 library. For the tutorial, we will use wagmi. So, let's install the wagmi dependency:
npm install bs58 tweetnacl \
@solana/wallet-adapter-base \
@solana/wallet-adapter-react \
@solana/wallet-adapter-react-ui \
@solana/wallet-adapter-wallets \
@solana/web3.js
  1. Add new environment variables in your .env.local file in the app root:
  • APP_DOMAIN: RFC 4501 DNS authority that is requesting the signing.
  • MORALIS_API_KEY: You can get it here.
  • NEXTAUTH_URL: Your app address. In the development stage, use http://localhost:3000.
  • NEXTAUTH_SECRET: Used for encrypting JWT tokens of users. You can put any value here or generate it on https://generate-secret.now.sh/32. Here's an .env.local example:
APP_DOMAIN=amazing.finance
MORALIS_API_KEY=xxxx
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=7197b3e8dbee5ea6274cab37245eec212
caution

Keep your NEXTAUTH_SECRET value in secret to prevent security problems.

caution

Every time you modify the .env.local file, you need to restart your dapp.

Wrapping App with Solana Wallet Provider and SessionProvider

  1. Create the pages/_app.jsx file. We need to wrap our pages with Solana Wallet Provider (docs) and SessionProvider (docs):
import "../styles/globals.css";
import { SessionProvider } from "next-auth/react";
import {
ConnectionProvider,
WalletProvider,
} from "@solana/wallet-adapter-react";
import {
PhantomWalletAdapter,
SolflareWalletAdapter,
} from "@solana/wallet-adapter-wallets";
import { clusterApiUrl } from "@solana/web3.js";
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import { useMemo } from "react";
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";

function MyApp({ Component, pageProps }) {
const network = WalletAdapterNetwork.Devnet;
const endpoint = useMemo(() => clusterApiUrl(network), [network]);
const wallets = useMemo(
() => [new PhantomWalletAdapter(), new SolflareWalletAdapter({ network })],
[network]
);
return (
<SessionProvider session={pageProps.session}>
<ConnectionProvider endpoint={endpoint}>
<WalletProvider wallets={wallets} autoConnect>
<WalletModalProvider>
<Component {...pageProps} />
</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
</SessionProvider>
);
}

export default MyApp;

info

NextJS uses the App component to initialize pages. You can override it and control the page initialization. Check out the NextJS docs.

Configure Next-Auth and MoralisNextAuth

  1. Create a new file, pages/api/auth/[...nextauth].ts, with the following content:
import NextAuth from 'next-auth';
import { MoralisNextAuthProvider } from '@moralisweb3/next';

export default NextAuth({
providers: [MoralisNextAuthProvider()],
// adding user info to the user session object
callbacks: {
async jwt({ token, user }) {
if (user) {
token.user = user;
}
return token;
},
async session({ session, token }) {
(session as { user: unknown }).user = token.user;
return session;
},
},
});
  1. Add an authenticating config to the pages/api/moralis/[...moralis].ts:
import { MoralisNextApi } from "@moralisweb3/next";

const DATE = new Date();
const FUTUREDATE = new Date(DATE);
FUTUREDATE.setDate(FUTUREDATE.getDate() + 1);

const { MORALIS_API_KEY, APP_DOMAIN, NEXTAUTH_URL } = process.env;

if (!MORALIS_API_KEY || !APP_DOMAIN || !NEXTAUTH_URL) {
throw new Error(
"Missing env variables. Please add the required env variables."
);
}

export default MoralisNextApi({
apiKey: MORALIS_API_KEY,
authentication: {
timeout: 120,
domain: APP_DOMAIN,
uri: NEXTAUTH_URL,
expirationTime: FUTUREDATE.toISOString(),
statement: "Sign message to authenticate.",
},
});

Create Wallet Component

  1. Create a new file under app/components/loginBtn/walletAdaptor.tsx:
import { useEffect } from "react";
import { useWallet } from "@solana/wallet-adapter-react";
import { WalletMultiButton } from "@solana/wallet-adapter-react-ui";
require("@solana/wallet-adapter-react-ui/styles.css");
import base58 from "bs58";
import { signIn, signOut } from "next-auth/react";
import { useAuthRequestChallengeSolana } from "@moralisweb3/next";
import React from "react";
export default function WalletAdaptor() {
const { publicKey, signMessage, disconnecting, disconnect, connected } =
useWallet();
const { requestChallengeAsync, error } = useAuthRequestChallengeSolana();
const signCustomMessage = async () => {
if (!publicKey) {
throw new Error("Wallet not avaiable to process request.");
}
const address = publicKey.toBase58();
const challenge = await requestChallengeAsync({
address,
network: "devnet",
});
const encodedMessage = new TextEncoder().encode(challenge?.message);
if (!encodedMessage) {
throw new Error("Failed to get encoded message.");
}

const signedMessage = await signMessage?.(encodedMessage);
const signature = base58.encode(signedMessage as Uint8Array);
try {
const authResponse = await signIn("moralis-auth", {
message: challenge?.message,
signature,
network: "Solana",
redirect: false,
});
if (authResponse?.error) {
throw new Error(authResponse.error);
}
} catch (e) {
disconnect();
console.log(e);
return;
}
};

useEffect(() => {
if (error) {
disconnect();
console.log(error);
}
}, [disconnect, error]);

useEffect(() => {
if (disconnecting) {
signOut({ redirect: false });
}
}, [disconnecting]);

useEffect(() => {
connected && signCustomMessage();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [connected]);

return <WalletMultiButton />;
}

Create Page to Sign-In

  1. Create a new page file, pages/index.jsx, with the following content:
  • You can get the app CSS from GitHub to style the app.
import React, { useEffect, useTransition } from "react";
import styles from "../styles/Home.module.css";
import { useRouter } from "next/router";
import { Typography } from "@web3uikit/core";
import { useSession } from "next-auth/react";
import WalletAdaptor from "../app/components/loginBtn/walletAdaptor";

export default function Home() {
const router = useRouter();
const { data: session, status } = useSession();
const [isPending, startTransition] = useTransition();

useEffect(() => {
startTransition(() => {
session && status === "authenticated" && router.push("./user");
});
}, [session, status]);

useEffect(() => {
startTransition(() => {
session && console.log(session);
});
}, [session]);

return (
<div className={styles.body}>
{!isPending && (
<div className={styles.card}>
<>
{!session ? (
<>
<Typography variant="body18">
Select Wallet for Authentication
</Typography>
<br />
<WalletAdaptor />
</>
) : (
<Typography variant="caption14">Loading...</Typography>
)}
</>
</div>
)}
</div>
);
}

Logout and User Profile Component

  1. Create components to perform the logout operation and to show the user data.
// File path
// app/components/logoutBtn/logoutBtn.js

import React from "react";
import { Button } from "@web3uikit/core";
import { signOut } from "next-auth/react";

export default function LogoutBtn() {
return (
<Button text="Logout" theme="outline" onClick={() => signOut()}></Button>
);
}

Showing the User Profile

  1. Let's create a user.jsx page to view user data when the user is logged in.
import React, { useEffect, useTransition } from "react";
import styles from "../styles/User.module.css";
import { getSession, signOut } from "next-auth/react";
import UserData from "../app/components/userData/userData";
import LogoutBtn from "../app/components/logoutBtn/logoutBtn";
import { WalletDisconnectButton } from "@solana/wallet-adapter-react-ui";
import { useWallet } from "@solana/wallet-adapter-react";
require("@solana/wallet-adapter-react-ui/styles.css");


export async function getServerSideProps(context) {
const session = await getSession(context);
if (!session) {
return { redirect: { destination: "/" } };
}
return {
props: { userSession: session },
};
}

export default function Home({ userSession }) {
const { publicKey, disconnecting, connected } = useWallet();
const [isPending, startTransition] = useTransition();

console.log(userSession);

useEffect(() => {
startTransition(() => {
publicKey && console.log(publicKey.toBase58());
});
}, [publicKey]);

useEffect(() => {
startTransition(() => {
disconnecting && signOut();
});
}, [disconnecting]);

useEffect(() => {
startTransition(() => {
console.log({ disconnecting });
});
}, [disconnecting]);

if (userSession) {
return (
<div className={styles.body}>
{!isPending && (
<div className={styles.card}>
<>
<UserData />
<div className={styles.buttonsRow}>
{connected || disconnecting ? (
<WalletDisconnectButton />
) : (
<LogoutBtn />
)}
</div>
</>
</div>
)}
</div>
);
}
}

Testing with any Solana Wallet

Visit http://localhost:3000 to test the authentication.

  1. Click on the Select Wallet button to select and connect to wallet:

  1. Connect to the Solana wallet extension

  1. Sign the message:

  1. After successful authentication, you will be redirected to the /user page:

And that completes the authentication process for Solana wallets using the Solana wallet adapter.