Skip to main content

Command Palette

Search for a command to run...

Learn How to Build a Sign in Authentication flow with Solana wallets

AUTHENTICATION FLOW WITH SOLANA WALLETS

Published
16 min read
Learn How to Build a Sign in 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:

  1. Overview

  2. Prerequisite

  3. Setup project

  4. Clone NextAuthJS Example Repo

  5. Install Solana and Wallet Adapter Dependencies

  6. Create a Secret for Authentication

  7. Create a Sign In Message Class

  8. Add the Solana Wallet Adapter

  9. Update Front End

  10. Implement Backend API

  11. Run Your dApp

  12. Conclusion

  13. 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:

  1. The SigninMessage class has a constructor that takes an object with domain, publicKey, nonce, and statement properties. It assigns these values to the respective class properties.

  2. The prepare method merges statement and nonce to create the message for signing.

  3. The validate method receives a signature string as input. It does the following:

    • Prepares the message using prepare method.

    • Decodes the signature from Base58 to Uint8Array.

    • Encodes the prepared message to Uint8Array using TextEncoder.

    • Decodes the publicKey from 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 returns true; otherwise, it returns false

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:

  1. Importing Libraries and Functions: The component imports various libraries and functions required for authentication and Solana wallet integration.

  2. Session and Wallet Handling: The component uses useSession from the next-auth/react library to manage the user's session status. It also utilizes useWallet and useWalletModal from the @solana/wallet-adapter-react and @solana/wallet-adapter-react-ui libraries, respectively, to interact with Solana wallets.

  3. Handle Sign In: The handleSignIn function 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 the SigninMessage class, includes the domain, public key, statement, and nonce. The message is then signed by the wallet, and the signature is passed to signIn from next-auth/react for authentication.

  4. Auto Sign-In: The component uses useEffect to trigger the sign-in process automatically when the wallet is connected, and the user is unauthenticated.

  5. 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.

  6. 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, and

  • pages/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:

  1. Imports: The code imports necessary libraries and functions, including NextApiRequest and NextApiResponse from Next.js, NextAuth from next-auth, CredentialsProvider from next-auth/providers/credentials, getCsrfToken from next-auth/react, and the SigninMessage class from a custom utility file.

  2. Authentication Endpoint: The code defines an asynchronous function named auth that serves as the authentication endpoint (/api/auth). It takes the Next.js req (request) and res (response) objects as parameters.

  3. 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.

  4. Authorize Function: The authorize function 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.

  5. Default Sign-in Page Handling: The code checks if the current request is for the default sign-in page by examining the req.query.nextauth parameter. 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.

  6. NextAuth Configuration: The code assembles the providers, session, secret, and callbacks options for the NextAuth configuration object.

  7. Session Callback: The session callback modifies the user session to include the public key (publicKey) extracted from the token's sub field. If a user is authenticated, it sets the user's name and generates an avatar URL based on the public key.

  8. NextAuth Execution: The code calls the NextAuth function passing the req, 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:

  1. Imports: The code imports necessary libraries and types, including NextApiRequest and NextApiResponse from Next.js and getToken from next-auth/jwt.

  2. Secret: The code retrieves the NEXTAUTH_SECRET environment variable to use as the secret for decoding the authentication token.

  3. Handler Function: The code defines an asynchronous function named handler, which serves as the API route's request handler. It takes the Next.js req (request) and res (response) objects as parameters.

  4. Get Token: The code uses getToken from next-auth/jwt to 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.

  5. Token Validation: The code checks if the token exists (!token) or if it lacks a sub field, 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.

  6. Protected Content Access: If the token exists and contains a sub field, 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.

  7. 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.

Further resources