Build an Web3 Authentication Method with Solana Wallets
Tags: nextjs, web3, webdev
Published: 2023-07-24 08:54:15

##Overview

Web3 offers a unique authentication method using Web3 wallets. This method eliminates the need for traditional email login and provides a secure and private way for developers to implement authentication for users on their platforms.

In this example, we will explore how to use Solana wallets to create and sign messages, and verify their signatures in the back-end or we can use [decentralized identifier (DID)](https://www.w3.org/TR/did-core/).

## What You Will Do

In this guide, we will create a simple React app, that allow you authenticate user’s.

- Setting your react app.
- Implement front-end user interface.
- Integrate the Solana Wallet extension (e.g., [Phantom](https://phantom.app/)).
- Create user identifier function with user’s wallet.
- Implement back-end APIs

## What You Will Need

To follow along with this guide, you will need the following:

- Basic knowledge of the JavaScript, TypeScript, and React programming languages
- [Nodejs](https://nodejs.org/en/) installed (version 16.15 or higher)
- npm or yarn installed (We will be using yarn to initialize our project and
install the necessary packages. Feel free to use npm instead if that is
your preferred package manager)
- A modern browser with a Solana Wallet extension installed (e.g., [Phantom](https://phantom.app/))

#Set Up Your React App

Creating a new project in your terminal following:

```bash
npx create-next-app@latest
```

On installation, you'll see the following prompts

Select “Yes” if you need to use it, otherwise select “No”.

```bash
What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like to use `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the default import alias? No / Yes
```

Read more about [Nextjs](https://nextjs.org/docs/getting-started/installation#automatic-installation) installation. If you not familiar with Nextjs, you can create react app with [Vite](https://vitejs.dev/guide/) following:

```bash
yarn create vite
#or
npm create vite@latest
```
Setup project structure

These should include any database schema changes, or any changes to structured fields, e.g. an existing JSON column.

From your main project directory, create a “util” and “components” folder:

```bash
mkdir util components
```

The result look like image below:

![Screenshot 2023-07-22 at 17.48.54.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/thsds9qr9foajdmc9hms.png)

This is [app router](https://nextjs.org/docs/app/building-your-application/routing#the-app-router) in Next 13.

**Install dependencies**

From your main project directory in your terminal, enter:

```bash
yarn add bs58 tweetnacl
```

We will use [tweetnacl](https://www.npmjs.com/package/tweetnacl) and [bs58](https://www.npmjs.com/package/bs58) to Implement user identifier function.

```bash
yarn add next-auth
```

I use NextAuth.js for authentication in this example. If you create an app with Vite, feel free to use other libraries or your own solution.

## Implement front-end user interface

In components folder, create AuthenButton folder and `index.tsx`, `type.ts` file

```bash
mkdir AuthenButton
cd AuthenButton
touch index.tsx type.ts
```

**Similar to AuthenButton, create an Avatar component.**

Go ahead and replace the contents of the entrie file file with the following: 

`AuthenButton/index.tsx`

```tsx
import React from "react";
import { AuthButtonProps } from "./types";
import { Avatar } from "../Avatar";

export function AuthenButton({
  buttonlabel = "Connect",
  buttonBackground,
  avatarSrc = "",
  customButton,
  customAvatar,
  onClick,
  onAvatarClick,
  address,
}: AuthButtonProps) {
  const renderButton = () => {
    return customButton ? (
      customButton(buttonlabel, onClick)
    ) : (
      <button
        style={{ backgroundColor: buttonBackground }}
        className="buttonContainer"
        onClick={onClick}
      >
        {buttonlabel}
      </button>
    );
  };
  const renderAvatar = () => {
    return customAvatar ? (
      customAvatar(address, avatarSrc, onAvatarClick)
    ) : (
      <div className="avatarContainer">
        <p className="address" title={address}>
          {address.slice(0, 5)}...
          {address.slice(address.length - 5, address.length)}
        </p>
        <Avatar avatarSrc={avatarSrc} />
      </div>
    );
  };
  return (
    <div>{!address || address === "" ? renderButton() : renderAvatar()}</div>
  );
}
```

 `AuthenButton/types.ts`

```tsx
import { ReactNode } from "react";

export type AuthButtonProps = {
  buttonlabel?: string;
  buttonBackground?: string;
  address: string;
  avatarSrc?: string | any;
  onClick?: () => void;
  onAvatarClick?: () => void;
  customButton?: (buttonlabel: string, onClick?: () => void) => ReactNode;
  customAvatar?: (
    address: string,
    avatarSrc: string | any,
    onClick?: () => void
  ) => ReactNode;
};
```

`Avatar/index.tsx`

```tsx
/* eslint-disable @next/next/no-img-element */
// import Image from "next/image";
import React from "react";

type avatarProps = {
  avatarSrc: string;
  onClick?: () => void;
};

export function Avatar({ avatarSrc }: avatarProps) {
  return (
    <div className="container">
      <img width={32} height={32} src={avatarSrc} alt="avatar" />
      {/* <Image width={32} height={32} src={avatarSrc} alt="avatar" /> */}
    </div>
  );
}
```

## Integrate the Solana Wallet extension

Make sure you install extension [phantom](https://phantom.app/download) to your browser.

> The Phantom browser extension and mobile in-app browser are both designed to interact with web applications.
> 

In this guide I will use Direct Integration Phantom to web application.

**Direct Integration**

> The most direct way to interact with Phantom is via the provider that Phantom injects into your web application. This provider is globally available at `window.phantom` and its methods will always include Phantom's most up-to-date functionality. This documentation is dedicated to covering all aspects of the provider.
> 

Another quick and easy way to get up and running with Phantom is via the [Solana Wallet Adapter](https://github.com/solana-labs/wallet-adapter/).

Now, Let's implement code to integrate with Phantom Wallet and get the wallet address.

You can see more detail in [phantom docs](https://docs.phantom.app/solana/detecting-the-provider).

create `utli/index.ts` file:

```tsx
export const getProvider = () => {
  if ("phantom" in window) {
    const { phantom }: any = window;
    const provider = phantom.solana;

    if (provider?.isPhantom) {
      return provider;
    }
  }
  return null;
};
```

update `app/page.tsx` file:

```tsx
"use client";
import React from "react";
import { getProvider } from "@/util";
import { AuthenButton } from "@/components";
export default function Home() {
  const [walletAddress, setWalletAddress] = React.useState("");

	const onConnect = async () => {
    try {
      const provider = getProvider();

      if (!provider) {
        window.open("https://phantom.app/", "_blank");
      }

      const resp = await provider.connect();
      console.log("Connect", resp.publicKey.toString());
      
      setWalletAddress(resp.publicKey.toString());
    } catch (error) {
      console.error(error);
    }
  };
  return (
    <main
      style={{
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        height: "100vh",
      }}
    >
      <AuthenButton
        onClick={onConnect}
        buttonlabel="SignIn by Wallet"
        address={walletAddress}
      />
    </main>
  );
}
```

`app/page.module.css`

```css
.main {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    height: 70vh;
    padding: 8px;
}

.main>div {
    padding: 6px 0;
}

.header {
    display: flex;
    padding: 12px 8px;
    border-bottom: 1px solid white;
}

.logoContainer {
    flex: 1;
}
```

After running the app and clicking the button, your wallet address should appear in the console.

## Create user identifier function with user’s wallet.

From your `utils/signature.ts` folder:

```tsx
import bs58 from "bs58";
import nacl from "tweetnacl";

interface signature {
  signature: string;
  publicKey: any;
}
export class Signature {
  public static create(nonce: string) {
    return new TextEncoder().encode(nonce);
  }
  public static async validate(
    { signature, publicKey }: signature,
    nonceMsgUint8: Uint8Array
  ) {
    const signatureUint8 = bs58.decode(signature);
    const pubKeyUint8 = bs58.decode(publicKey);

    return nacl.sign.detached.verify(
      nonceMsgUint8,
      signatureUint8,
      pubKeyUint8
    );
  }
}
```

This code defines a class called "Signature," which is used to validate a signature.

> *A nonce (short for "number used once") is a unique value that 
is generated and used once to prevent replay attacks. A replay attack is
 a network attack in which a hacker intercepts and resends a valid data 
transmission, tricking the system into accepting the same data multiple 
times.*

- create() method to encode a nonce and use it for verification.
- validate() method decodes the signature and the publicKey using the *bs58* library, and then uses the nacl library to verify if the signature is valid using the nacl.sign.detached.verify method.

## Implement back-end APIs

Next, create or replace the contents of the entrie file file with the following: 

`.env`

```tsx
NEXTAUTH_URL=http://localhost:3000/
NEXTAUTH_SECRET=YOUR_SECRET
```

You can generate a secure secret by typing `openssl rand -hex 32` in terminal:

![Screenshot 2023-07-23 at 21.02.30.png](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kudos7lg7u9cutpnqh21.png)

Make sure to update `NEXTAUTH_SECRET` with that value.

`app/api/auth/[...nextauth]/route.ts`

```tsx
import { Signature } from "@/util";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { getCsrfToken } from "next-auth/react";

const providers = [
  CredentialsProvider({
    name: "web3-auth",
    credentials: {
      signature: {
        label: "Signature",
        type: "text",
      },
      message: {
        label: "Message",
        type: "text",
      },
    },
    async authorize(credentials, req) {
      const { publicKey, host } = JSON.parse(credentials?.message || "{}");

      const nextAuthUrl = new URL(process.env.NEXTAUTH_URL || "");

      if (host !== nextAuthUrl.host) {
        return null;
      }
      const crsf = await getCsrfToken({ req: { ...req, body: null } });

      if (!crsf) {
        return null;
      }
      const nonceUnit8 = Signature.create(crsf);

      const isValidate = await Signature.validate(
        {
          signature: credentials?.signature || "",
          publicKey,
        },
        nonceUnit8
      );

      if (!isValidate) {
        throw new Error("Could not validate the signed message");
      }

      return { id: publicKey };
    },
  }),
];

const handler = NextAuth({
  session: {
    strategy: "jwt",
  },
  providers,
  callbacks: {
    session({ session, token }) {
      if (session.user) {
        session.user.name = token.sub;
        session.user.image = `https://ui-avatars.com/api/?name=${token.sub}`;
      }
      return session;
    },
  },
});
export { handler as GET, handler as POST };
```

we need add config for `NextAuth()` 

- **Providers**: In this case just one provider that uses the *CredentialsProvider* from `next-auth`. The CredentialsProvider is configured with `message` and `signature` fields used as the authentication credentials. The `message` and `signature` will  send by client.
    - authorize function of the *CredentialsProvider* provided *credentials* and *req* for our validate.
    - Verifying the `client host` of the message matches the `server host` of the NextAuth URL to prevent call API in third party tools.
    - Validating the signature of the client using the validate method of the *Signature class*
- **Callbacks:** The session callback that sets the publicKey and image URL of the user in the session.
- **Session:** Configure your session settings, such as determining whether to use JWT or a database, setting the idle session expiration duration, or implementing write operation throttling for database usage.

`app/providers.tsx`

```tsx
"use client";

import { SessionProvider } from "next-auth/react";

type Props = {
  children?: React.ReactNode;
};

export const NextAuthProvider = ({ children }: Props) => {
  return <SessionProvider>{children}</SessionProvider>;
};
```

We will get session in the client so we need create a `providers` component wrap the `SessionProvider`. The SessionProvider component only runs on the client side, we need to create a providers component for it. This is because the `layout.tsx` file runs on the server side, and the SessionProvider component cannot be used directly there.

`app/layout.tsx`

```tsx
import "./globals.css";
import { NextAuthProvider } from "./providers";
import "./styles.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "llmas web3 auth",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <NextAuthProvider>{children}</NextAuthProvider>
      </body>
    </html>
  );
}
```

`app/page.tsx`

```tsx
"use client";
import { getCsrfToken, signIn, signOut } from "next-auth/react";
import styles from "./page.module.css";
import React from "react";
import { Signature, getProvider } from "@/util";
import { AuthenButton } from "@/components";
import bs58 from "bs58";
import { useSession } from "next-auth/react";
import Image from "next/image";
import Logo from "../public/logo.png";

export default function Home() {
  const { data: session } = useSession();
  const onConnect = async () => {
    try {
      const provider = getProvider();

      if (!provider) {
        window.open("https://phantom.app/", "_blank");
      }

      const resp = await provider.connect();
      console.log("Connect", resp.publicKey.toString());
      const csrf = await getCsrfToken();
      if (resp && csrf) {
        const noneUnit8 = Signature.create(csrf);
        const { signature } = await provider.signMessage(noneUnit8);
        const serializedSignature = bs58.encode(signature);
        const message = {
          host: window.location.host,
          publicKey: resp.publicKey.toString(),
          nonce: csrf,
        };
        const response = await signIn("credentials", {
          message: JSON.stringify(message),
          signature: serializedSignature,
          redirect: false,
        });
        if (response?.error) {
          console.log("Error occured:", response.error);
          return;
        }
      } else {
        console.log("Could not connect to wallet");
      }
    } catch (error) {
      console.error(error);
    }
  };
  return (
    <>
      <header className={styles.header}>
        <div className={styles.logoContainer}>
          <h1>logo</h1>
        </div>
        <AuthenButton
          avatarSrc={session?.user?.image}
          onClick={onConnect}
          buttonlabel="SignIn by Wallet"
          address={session?.user?.name || ""}
        />
      </header>
      <main className={styles.main}>
        <Image width={64} height={64} src={Logo} alt="user avatar" />
        <h4>llmas-laptrinh</h4>
        <div>
          <h3>Address: {session?.user?.name}</h3>
          <p>Expires: {new Date(session?.expires || "").toTimeString()}</p>
        </div>

        {session !== null && (
          <button className="buttonContainer" onClick={() => signOut()}>
            SignOut
          </button>
        )}
      </main>
    </>
  );
}
```

Let’s break this code

- The useSession() hook to get the current session data and status.
- We integrate with Phantom Wallet using getProvider(), such as *connecting* and *creating* `signMessage`. If don’t have provider, redirect user to dowload page.
- We will send data to CredentialsProvider using the `signIn()` method. The `message` and `signature` are two fields that we defined in the `credentials` property of CredentialsProvider.

##Run Your Code

In root folder, open your terminal and enter:

```bash
yarn dev 
```

You should see a page like this:

![demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cksqel2o88h57bnnzuzb.png)

##Full Source

- https://github.com/llamas-laptrinh/web3-auth/tree/main/example

##Reference

- https://blog.logrocket.com/build-web3-authentication-flow-react-ether-js-ceramic/#setting-up-react-app-ceramic
- https://www.quicknode.com/guides/solana-development/dapps/how-to-authenticate-users-with-a-solana-wallet#overview
- https://reactjsexample.com/an-example-on-how-to-use-solana-wallet-adapter-as-a-web-authentication-method/
- https://codevoweb.com/setup-and-use-nextauth-in-nextjs-13-app-directory/
- https://dev.to/abhikbanerjee99/nextjs-13-using-next-auth-the-web3-way-17kg