Skip to main content
Version: 2.2

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.