Learn How to Build a Sign in Authentication flow with Solana wallets
AUTHENTICATION FLOW WITH SOLANA WALLETS

Introduction
In this tutorial, we will learn how to create a simple Authentication flow with Solana wallets. while building this program we will learn how to interact with Solana wallets, we also talk about NextAuth.js.
Who this article is for
This walkthrough assumes that you've written code in any programming language or have a basic understanding of typescript, and Reactjs with Solana.
Let's get started.
Table of Contents:
Overview
Prerequisite
Setup project
Clone NextAuthJS Example Repo
Install Solana and Wallet Adapter Dependencies
Create a Secret for Authentication
Create a Sign In Message Class
Add the Solana Wallet Adapter
Update Front End
Implement Backend API
Run Your dApp
Conclusion
Further resources
Overview
Web3 wallet authentication empowers users to control their data, eliminating traditional email logins for a more secure approach. As a developer, integrating web3 wallets opens up opportunities for decentralized applications and a user-centric experience. Follow our guide to use the Solana Wallet Adapter for authenticating users on your dApp (Web3 SSO)!
Prerequisite
To follow along with this guide, you will need the following:
Fork the NextAuth.js example repo
Basic experience with the Solana Wallet Adapter
Basic knowledge of the JavaScript, TypeScript, and React programming languages
Nodejs installed (version 16.15 or higher)
npm or yarn installed (We will be using npm to initialize our project and install the necessary packages. Feel free to use yarn instead if that is your preferred package manager)
TypeScript experience and ts-node installed
Basic knowledge of Authentication or NextAuth.js will be helpful but is not required.
A modern browser with a Solana Wallet extension installed (e.g., Phantom).
Setup project
create a new project directory in the terminal with this command.
mkdir sol-wallet-auth
cd sol-wallet-auth
Clone NextAuthJS Example Repo
NextAuth.js, the open-source authentication and authorization library for Next.js, streamlines user authentication and authorization in React-based web apps. With NextAuth.js, developers effortlessly integrate various authentication providers like email/password, Google, and Facebook, ensuring simplicity and security.
Clone the NextAuth.js example repository into your project folder. In your terminal, enter:
git clone https://github.com/nextauthjs/next-auth-example.git
cd next-auth-example
Install the dependencies that are in the repo by entering the following in your terminal:
yarn
#or
npm install
Install Solana and Wallet Adapter Dependencies
To kickstart the exercise, include the Solana Web3 library and essential dependencies. The Solana Wallet Adapter, a crucial library, seamlessly integrates Solana wallets into web applications. This unified API facilitates interaction with diverse Solana wallets, streamlining dApp development for any Solana wallet.
In your terminal, enter:
yarn add @solana/web3.js @solana-mobile/wallet-adapter-mobile @solana/wallet-adapter-base @solana/wallet-adapter-react @solana/wallet-adapter-react-ui @solana/wallet-adapter-wallets bs58 tweetnacl
#or
npm install @solana/web3.js @solana-mobile/wallet-adapter-mobile @solana/wallet-adapter-base @solana/wallet-adapter-react @solana/wallet-adapter-react-ui @solana/wallet-adapter-wallets bs58 tweetnacl
the Solana-Web3.js library and Solana Wallet Adapter dependencies, we added:
bs58 serves as a tool to convert binary data into a compact, human-friendly format, commonly used in cryptocurrency-related applications for encoding addresses and other identifiers.
tweetnacl is a user-friendly cryptographic library used for secure applications. It provides public-key cryptography, secure communication, storage, authentication, and random number generation, and supports complex cryptographic schemes. Being simple and compact, it's ideal for developers seeking ease of use. Proper implementation is vital for overall application security.
Create a Secret for Authentication
In NextAuth.js, safeguarding communication between the client and server relies on a crucial .env variable called NEXTAUTH_SECRET. Acting as a secret key, this string encrypts and decrypts the JSON Web Tokens (JWT) exchanged between the two. These tokens contain vital authentication info, like the user's ID and session data. Without a valid NEXTAUTH_SECRET, the client can't authenticate the user as it can't decrypt the JWT. Therefore, it's paramount to keep NEXTAUTH_SECRET private and avoid sharing it with others, using .env for added protection.
Open your project directory in an IDE(I prefer vs code) of choice and find a file named .env.local.example. Rename it to .env.local and replace its contents with this:
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=ENTER_A_SECRET_KEYYou can use any string for your secret
You can use any string for your secret.
Create a Sign In Message Class
From your main project directory, create a utils folder:
mkdir utils
And create a new file, SigninMessage.ts, (or whatever name suits you) in that directory:
cd utils
echo > SigninMessage.ts
Inside SigninMessage.ts paste this code:
import bs58 from "bs58";
import nacl from "tweetnacl";
type SignMessage = {
domain: string;
publicKey: string;
nonce: string;
statement: string;
};
export class SigninMessage {
domain: any;
publicKey: any;
nonce: any;
statement: any;
constructor({ domain, publicKey, nonce, statement }: SignMessage) {
this.domain = domain;
this.publicKey = publicKey;
this.nonce = nonce;
this.statement = statement;
}
prepare() {
return `${this.statement}${this.nonce}`;
}
async validate(signature: string) {
const msg = this.prepare();
const signatureUint8 = bs58.decode(signature);
const msgUint8 = new TextEncoder().encode(msg);
const pubKeyUint8 = bs58.decode(this.publicKey);
return nacl.sign.detached.verify(msgUint8, signatureUint8, pubKeyUint8);
}
}
The SigninMessage class, which handles the preparation and validation of signed messages. It utilizes the tweetnacl and bs58 libraries.
This accepts four Parameters:
domain: Represents the message's domain (string).publicKey: Contains the public key used for message validation (string).nonce: Signifies a unique value for the message (string).statement: Holds the actual content of the message (string).
How this class is validating:
The
SigninMessageclass has a constructor that takes an object withdomain,publicKey,nonce, andstatementproperties. It assigns these values to the respective class properties.The
preparemethod mergesstatementandnonceto create the message for signing.The
validatemethod receives asignaturestring as input. It does the following:Prepares the message using
preparemethod.Decodes the
signaturefrom Base58 to Uint8Array.Encodes the prepared message to Uint8Array using
TextEncoder.Decodes the
publicKeyfrom Base58 to Uint8Array.Verifies the authenticity of the message with
nacl.sign.detached.verify. If the signature matches the message and the provided public key, it returnstrue; otherwise, it returnsfalse
We will use this class in our authentication API.
Add the Solana Wallet Adapter
To use the Solana Wallet Adapter, we will need to add three wrappers to our app:
ConnectionProvider to share our Solana Connection across the app
WalletProvider to share our wallet context across the app
WalletModalProvider to enable us to use the modal UI for the wallet adapter across the app
To do this, open pages/_app.tsx and replace the imports with:
import { SessionProvider } from "next-auth/react";
import React, { useMemo } from "react";
import { ConnectionProvider, WalletProvider, } from "@solana/wallet-adapter-react";
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import { PhantomWalletAdapter } from "@solana/wallet-adapter-wallets";
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
import { clusterApiUrl } from "@solana/web3.js";
import type { AppProps } from "next/app";
import "@solana/wallet-adapter-react-ui/styles.css";
import "./styles.css";
export default function App({ Component, pageProps }: AppProps) {
const network = WalletAdapterNetwork.Devnet;
const endpoint = useMemo(() => clusterApiUrl(network), [network]);
const wallets = useMemo(
() => [
new PhantomWalletAdapter(),
],
[]
);
return (
<ConnectionProvider endpoint={endpoint}>
<WalletProvider wallets={wallets} autoConnect>
<WalletModalProvider>
<SessionProvider session={pageProps.session} refetchInterval={0}>
<Component {...pageProps} />
</SessionProvider>
</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
);
}
Note: We are just using Phantom for simplicity for this demo. Feel free to add any other Adapters you choose.
In this setup, we simply enclose our SessionProvider within Wallet and Connection wrappers. For this illustration, we'll utilize the default devnet and public RPC, as we won't perform actual network requests.
Now that our wallet adapter is set up, let's update our front end to connect our wallet and backend to authenticate it!
Update Front End
We will leave the body of the template alone for this example, but we need to update our Header to add functionality with our Wallet Adapter. Open components/header.tsx.
Go ahead and replace the contents of the entire file with the following:
import Link from "next/link";
import { getCsrfToken, signIn, signOut, useSession } from "next-auth/react";
import styles from "./header.module.css";
import { useWalletModal } from "@solana/wallet-adapter-react-ui";
import { useWallet } from "@solana/wallet-adapter-react";
import { SigninMessage } from "../utils/signmsg";
import bs58 from "bs58";
import { useEffect } from "react";
export default function Header() {
const { data: session, status } = useSession();
const loading = status === "loading";
const wallet = useWallet();
const walletModal = useWalletModal();
const handleSignIn = async () => {
try {
if (!wallet.connected) {
walletModal.setVisible(true);
}
const csrf = await getCsrfToken();
if (!wallet.publicKey || !csrf || !wallet.signMessage) return;
const message = new SigninMessage({
domain: window.location.host,
publicKey: wallet.publicKey?.toBase58(),
statement: `Sign this message to sign in to the app.`,
nonce: csrf,
});
const data = new TextEncoder().encode(message.prepare());
const signature = await wallet.signMessage(data);
const serializedSignature = bs58.encode(signature);
signIn("credentials", {
message: JSON.stringify(message),
redirect: false,
signature: serializedSignature,
});
} catch (error) {
console.log(error);
}
};
useEffect(() => {
if (wallet.connected && status === "unauthenticated") {
handleSignIn();
}
}, [wallet.connected]);
return (
<header>
<noscript>
<style>{`.nojs-show { opacity: 1; top: 0; }`}</style>
</noscript>
<div className={styles.signedInStatus}>
<p
className={`nojs-show ${
!session && loading ? styles.loading : styles.loaded
}`}
>
{!session && (
<>
<span className={styles.notSignedInText}>
You are not signed in
</span>
<span className={styles.buttonPrimary} onClick={handleSignIn}>
Sign in
</span>
</>
)}
{session?.user && (
<>
{session.user.image && (
<span
style={{ backgroundImage: `url('${session.user.image}')` }}
className={styles.avatar}
/>
)}
<span className={styles.signedInText}>
<small>Signed in as</small>
<br />
<strong>{session.user.email ?? session.user.name}</strong>
</span>
<a
href={`/api/auth/signout`}
className={styles.button}
onClick={(e) => {
e.preventDefault();
signOut();
}}
>
Sign out
</a>
</>
)}
</p>
</div>
<nav>
<ul className={styles.navItems}>
<li className={styles.navItem}>
<Link legacyBehavior href="/">
<a>Home</a>
</Link>
</li>
<li className={styles.navItem}>
<Link legacyBehavior href="/api/examples/protected">
<a>Protected API Route</a>
</Link>
</li>
<li className={styles.navItem}>
<Link legacyBehavior href="/me">
<a>Me</a>
</Link>
</li>
</ul>
</nav>
</header>
);
}
This code exports a React component, Header, which renders a header element with a navigation bar. It involves user authentication and integration with Solana wallets for signing in to the app securely. Let's explain its key functionalities in detail:
Importing Libraries and Functions: The component imports various libraries and functions required for authentication and Solana wallet integration.
Session and Wallet Handling: The component uses
useSessionfrom thenext-auth/reactlibrary to manage the user's session status. It also utilizesuseWalletanduseWalletModalfrom the@solana/wallet-adapter-reactand@solana/wallet-adapter-react-uilibraries, respectively, to interact with Solana wallets.Handle Sign In: The
handleSignInfunction is responsible for initiating the sign-in process. It checks if the wallet is connected, opens the wallet modal if not, and then proceeds to sign in using the Solana wallet. It creates a message using theSigninMessageclass, includes the domain, public key, statement, and nonce. The message is then signed by the wallet, and the signature is passed tosignInfromnext-auth/reactfor authentication.Auto Sign-In: The component uses
useEffectto trigger the sign-in process automatically when the wallet is connected, and the user is unauthenticated.Rendering: The component renders the header section, including the signed-in status, navigation, and links. It displays appropriate sign-in or sign-out buttons based on the user's session status.
User Interface: The UI handles different states, like showing loading indicators while authenticating or displaying the user's name and avatar once signed in.
TLDR: this code implements user authentication using NextAuth.js and allows users to sign in securely using their Solana wallets. It offers a seamless sign-in experience and navigation options for the web application.
Implement Backend API
So far, we have built a sign-in function and connected our app to the Solana Wallet Adapter. Now we need to implement our backend verification. We need to update two files in our API directory: pages/api:
pages/api/auth/[...nextauth].ts, which allows us to define custom routes for our authentication and authorization logic, andpages/api/examples/protected.ts, which we will use as an example route to alert the user if they have been authenticated or not
Open pages/api/auth/[...nextauth].ts and replace the contents of the file with:
import { NextApiRequest, NextApiResponse } from "next";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { getCsrfToken } from "next-auth/react";
import { SigninMessage } from "../../../utils/SigninMessage";
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
const providers = [
CredentialsProvider({
name: "Solana",
credentials: {
message: {
label: "Message",
type: "text",
},
signature: {
label: "Signature",
type: "text",
},
},
async authorize(credentials, req) {
try {
const signinMessage = new SigninMessage(
JSON.parse(credentials?.message || "{}")
);
const nextAuthUrl = new URL(process.env.NEXTAUTH_URL);
if (signinMessage.domain !== nextAuthUrl.host) {
return null;
}
const csrfToken = await getCsrfToken({ req: { ...req, body: null } });
if (signinMessage.nonce !== csrfToken) {
return null;
}
const validationResult = await signinMessage.validate(
credentials?.signature || ""
);
if (!validationResult)
throw new Error("Could not validate the signed message");
return {
id: signinMessage.publicKey,
};
} catch (e) {
return null;
}
},
}),
];
const isDefaultSigninPage =
req.method === "GET" && req.query.nextauth?.includes("signin");
// Hides Sign-In with Solana from the default sign page
if (isDefaultSigninPage) {
providers.pop();
}
return await NextAuth(req, res, {
providers,
session: {
strategy: "jwt",
},
secret: process.env.NEXTAUTH_SECRET,
callbacks: {
async session({ session, token }) {
// @ts-ignore
session.publicKey = token.sub;
if (session.user) {
session.user.name = token.sub;
session.user.image = `https://ui-avatars.com/api/?name=${token.sub}&background=random`;
}
return session;
},
},
});
}
This code implements a custom authentication endpoint (/api/auth) for NextAuth.js, enabling authentication using a custom provider called "Solana." The code handles the authentication process, validates signed messages from Solana wallets, and sets up the user session.
Let's break down the code in detail:
Imports: The code imports necessary libraries and functions, including
NextApiRequestandNextApiResponsefrom Next.js,NextAuthfromnext-auth,CredentialsProviderfromnext-auth/providers/credentials,getCsrfTokenfromnext-auth/react, and theSigninMessageclass from a custom utility file.Authentication Endpoint: The code defines an asynchronous function named
auththat serves as the authentication endpoint (/api/auth). It takes the Next.jsreq(request) andres(response) objects as parameters.Custom Solana Provider: The code sets up a custom authentication provider named "Solana" using
CredentialsProvider. It configures the provider to handle a message and signature as credentials during the sign-in process.Authorize Function: The
authorizefunction within the custom provider handles the authentication logic. It validates the incoming signed message, ensuring it matches the domain, nonce, and signature. If the validation is successful, it returns an object with the user's ID (id) extracted from the signed message.Default Sign-in Page Handling: The code checks if the current request is for the default sign-in page by examining the
req.query.nextauthparameter. If so, it removes the custom Solana provider from the list of providers to hide the "Sign-In with Solana" option on the default sign-in page.NextAuth Configuration: The code assembles the
providers,session,secret, andcallbacksoptions for the NextAuth configuration object.Session Callback: The
sessioncallback modifies the user session to include the public key (publicKey) extracted from the token'ssubfield. If a user is authenticated, it sets the user's name and generates an avatar URL based on the public key.NextAuth Execution: The code calls the
NextAuthfunction passing thereq,res, and the constructed configuration object. This handles the entire authentication process for NextAuth.js and returns the appropriate response.
TLDR: this code sets up a custom authentication endpoint using NextAuth.js. It allows users to sign in with Solana wallets using signed messages. The code validates the signed messages, sets up the user session, and handles the custom Solana authentication provider.
All we need now is to create a protected page that only our authorized users can see. Let's modify the default pages/api/examples/protected.ts. Open the file and replace its contents with this:
import type { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
const secret = process.env.NEXTAUTH_SECRET;
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const token = await getToken({ req, secret });
if (!token || !token.sub)
return res.send({
error: "User wallet not authenticated",
});
if (token) {
return res.send({
content:
"This is protected content. You can access this content because you are signed in with your Solana Wallet.",
});
}
res.send({
error: "You must be signed in with your Solana Wallet to view the protected content on this page.",
});
}
This is a Next.js API endpoint that is used to protect a specific page or resource, such that only authenticated users can access it.
Here's a detailed explanation of the code:
Imports: The code imports necessary libraries and types, including
NextApiRequestandNextApiResponsefrom Next.js andgetTokenfromnext-auth/jwt.Secret: The code retrieves the
NEXTAUTH_SECRETenvironment variable to use as the secret for decoding the authentication token.Handler Function: The code defines an asynchronous function named
handler, which serves as the API route's request handler. It takes the Next.jsreq(request) andres(response) objects as parameters.Get Token: The code uses
getTokenfromnext-auth/jwtto extract and decode the authentication token from the request's headers. The token contains user information, including the public key (sub) from the Solana wallet.Token Validation: The code checks if the token exists (
!token) or if it lacks asubfield, indicating that the user's Solana wallet is not authenticated. In such cases, it returns an error message indicating that the user's wallet is not authenticated.Protected Content Access: If the token exists and contains a
subfield, it means the user's Solana wallet is authenticated. The code returns a success response containing protected content that can be accessed because the user is signed in with their Solana wallet.Unauthenticated Content: If the token does not exist (i.e., the user is not authenticated), it returns an error message prompting the user to sign in with their Solana wallet to view the protected content on the page.
TLDR: this code acts as an API route that checks for user authentication with a Solana wallet. If the user is authenticated, it allows access to protected content. If not, it displays an error message instructing the user to sign in with their Solana wallet to access the protected content.
Yoo, That was a lot. We make it and it’s time to give it a shot.
Run Your dApp
Alright, let's do this! Open your terminal and enter:
npm run dev
You should see a page like this:

Click Protected API Route. You should see an error:
{"error":"User wallet not authenticated"}
Return back to the homepage, and click Sign in. You'll be prompted to connect your wallet and sign a message.
Now try again-->Click Protected API Route. You should see a protected message:
{"content":"This is protected content. You can access this content because you are signed in with your Solana Wallet."}
Congrats you make it happen! pet yourself on the back.
Conclusion
This tutorial article demonstrates how to implement an authentication flow with Solana wallets using TypeScript and NextAuth.js in a web application. It covers the following key points:
Overview: Web3 wallet authentication empowers users to control their data, offering a more secure approach. Integrating web3 wallets opens up opportunities for decentralized applications and a user-centric experience.
Prerequisites: Readers should have basic knowledge of TypeScript, ReactJS, and Solana, as well as experience with NextAuth.js. They also need Node.js, npm or yarn, and a modern browser with a Solana wallet extension (e.g., Phantom) installed.
Setup Project: The article guides readers to create a new project directory and clone the NextAuth.js example repository, as well as install necessary dependencies for Solana and the wallet adapter.
Create a Secret for Authentication: Readers learn how to create a secret key (NEXTAUTH_SECRET) using .env for encrypting and decrypting JSON Web Tokens (JWT) exchanged during authentication.
Create a Sign-In Message Class: The article introduces the SigninMessage class, which handles the preparation and validation of signed messages using the tweetnacl and bs58 libraries.
Add the Solana Wallet Adapter: Readers integrate the Solana Wallet Adapter into the app, including ConnectionProvider, WalletProvider, and WalletModalProvider.
Update Front End: The article guides readers through updating the header component to handle sign-in functionality with Solana wallets.
Implement Backend API: Custom authentication endpoints are implemented using NextAuth.js, enabling sign-in with Solana wallets through signed messages.
Protect a Page: A Next.js API endpoint is created to protect a specific page, ensuring only authenticated users with Solana wallets can access it.
Run Your dApp: Finally, readers are instructed to run the application to test the implemented authentication flow.
In summary, this tutorial article guides readers through the process of building an authentication flow with Solana wallets in a web application using TypeScript and NextAuth.js. It covers both front-end and back-end implementations to ensure secure sign-in functionality for users with Solana wallets.