Skip to content

The easiest way to add Sign-in with Ethereum to your app.

License

Notifications You must be signed in to change notification settings

random-bits-studio/use-siwe

Repository files navigation

UseSIWE

UseSIWE is a library that provides react hooks and API endpoints that make it dead simple to add Sign-In with Ethereum functionality to your react application.

🌈 Works with RainbowKit

The easiest way to use this library is with RainbowKit! Check out the RainbowKit authentication adapter for UseSiwe here: https://github.com/random-bits-studio/rainbowkit-use-siwe-auth

Table of Contents

Installation

To install UseSIWE and it's dependencies run the following command:

npm install @randombits/use-siwe wagmi ethers iron-session

Getting Started

Configure settings for iron-session

Copy and paste the following code into a new file in your project:

// lib/ironOptions.ts

import { IronSessionOptions } from 'iron-session';

if (!process.env.IRON_SESSION_PASSWORD)
  throw new Error('IRON_SESSION_PASSWORD must be set');

const ironOptions: IronSessionOptions = {
  password: process.env.IRON_SESSION_PASSWORD,
  cookieName: 'session',
  cookieOptions: {
    secure: process.env.NODE_ENV === "production",
  },
};

declare module "iron-session" {
  interface IronSessionData {
    address?: string | undefined;
    nonce?: string | undefined;
  }
}

export default ironOptions;

Remember to set IRON_SESSION_PASSWORD in your .env.local file for development, and in your production environment through your hosting provider settings. The password must be at least 32 characters long. You can use https://1password.com/password-generator/ to generate strong passwords.

For full reference of possible options see: https://github.com/vvo/iron-session#ironoptions

Typing session data The type definition of IronSessionData in the example above provides a type definition to the data passed to api functions in req.session. address and nonce are used and set by UseSIWE; if you plan on storing other data in the session, feel free to add additional types here.

For more information see: https://github.com/vvo/iron-session#typing-session-data-with-typescript

Setting up the API routes

Next.js

Copy and past the following code into pages/api/auth/[[...route]].ts:

import { withIronSessionApiRoute } from "iron-session/next";
import ironOptions from "lib/ironOptions";
import { siweApi } from "@randombits/use-siwe/next"

export default withIronSessionApiRoute(siweApi(), ironOptions);

Express.js

To add auth routes to your existing express API, add the following:

import express from "express";
import { ironSession } from "iron-session/express";
import ironOptions from "./ironOptions.js";
import { authRouter } from "@randombits/use-siwe/express";

const app = express();

// Add iron session middleware before all routes that will use session data
app.use(ironSession(ironOptions));

// Your existing api routes here...

// Add UseSIWE auth routes
app.use('/auth', authRouter());

app.listen(3001);

Wrapping your application with SiweProvider

Any component that uses the any of the UseSIWE hooks must be wrapped with the SiweProvider component. For a Next.js application we recommend doing so in pages/_app.tsx like in the example below:

// pages/_app.tsx

import type { AppProps } from 'next/app';
import { configureChains, mainnet } from 'wagmi';
import { publicProvider } from 'wagmi/providers/public';
import { SiweProvider } from '@randombits/use-siwe';

const { chains, provider, webSocketProvider } = configureChains(
  [mainnet],
  [publicProvider()],
);

const client = createClient({
  autoConnect: true,
  provider,
  webSocketProvider,
});

export default function MyApp({ Component, pageProps }: AppProps) {
  return (
    <WagmiConfig client={client}>
      <SiweProvider>
        <Component {...pageProps} />
      </SiweProvider>
    </WagmiConfig>
  );
}

Important: The SiweProvider must be inside a WagmiConfig component.

Using the hooks

Checking if a user is authenticated

Client-side

Check to see is a user is authenticated with the useSession hook like in the example below:

import { useSession } from "@randombits/use-siwe";

export const AuthCheck = () => {
  const { isLoading, authenticated, address } = useSession();

  if (isLoading) return <p>Loading...</p>;
  if (!authenticated) return <p>Not authenticated</p>;
  return <p>{address} is Authenticated</p>;
};

Server-side

For API routes, wrap your API handler with withIronSessionApiRoute and check to see if req.session.address is set. If a user is authenticated, req.session.address will be set to their address, otherwise it will be undefined.

import ironOptions from '@/lib/ironOptions'
import { withIronSessionApiRoute } from 'iron-session/next/dist'
import type { NextApiHandler } from 'next'

const handler: NextApiHandler = (req, res) => {
  if (!req.session.address) return res.status(401).send("Unauthorized");
  res.status(200).send(`Hello, ${req.session.address}!`);
}

export default withIronSessionApiRoute(handler, ironOptions);

Signing In

Login the user by calling the signIn function returned by the useSignIn hook:

import { useSignIn } from "@randombits/use-siwe";

const SignInButton = () => {
  const { signIn, isLoading } = useSignIn();
  return <button onClick={() => signIn()} disabled={isLoading}>Sign In with Ethereum</button>;
};

Signing Out

Logout the user by calling the signOut function returned by the useSignOut hook:

import { useSignOut } from "@randombits/use-siwe";

const SignOutButton = () => {
  const { signOut, isLoading } = useSignOut();
  return <button onClick={() => signOut()} disabled={isLoading}>Sign Out</button>;
};

API

Types

UseSiweOptions

UseSIWE accepts an object of options. Currently this consists of one optional setting:

Usage

const options: UseSiweOptions = {
  baseUrl: "/v2/api/auth",
};

Options

  • baseUrl, optional: The base url for the auth API endpoints that is prepended to all requests. Defaults to: /api/auth

Components

SiweProvider

Context provider component that must wrap all components that use useSession, useSignIn, useSignOut, or useOptions hooks.

Usage

import type { AppProps } from 'next/app';
import { SiweProvider } from '@randombits/use-siwe';

export default function MyApp({ Component, pageProps }: AppProps) {
  return <SiweProvider>
    <Component {...pageProps} />
  </SiweProvider>;
}

Props

  • options, Optional: A UseSiweOptions object.

Hooks

useSession

A hook that returns the the current state of the users session.

Usage

import { useSession } from "@randombits/use-siwe";

export const Component = () => {
  const { isLoading, authenticated, address } = useSession();

  if (isLoading) return <div>Loading...</div>;
  if (!authenticated) return <div>Not Signed In</div>;
  return <div>Hello, {address}!</div>;
};

Return Value

Returns a UseQueryResult (ref) augmented with the following:

{
  authenticated: boolean;
  address?: string;
  nonce?: string;
} & UseQueryResult

useSignIn

A hook that returns a signIn function that will initiate a SIWE flow, as well as the status of that signIn process.

Usage

import { useSignIn } from "@randombits/use-siwe";

const SignInButton = () => {
  const { signIn, isLoading } = useSignIn();
  return <button onClick={() => signIn()} disabled={isLoading}>Sign In with Ethereum</button>;
};

Options

{
  onSuccess: () => void,
  onError: () => void,
}

Return Value

Returns a UseMutationResult (ref) augmented with the following:

{
  signIn: () => void,
  SignInAsync: () => Promise<void>,
} & UseMutationResult

useSignOut

A hook that returns a signOut function that when called will sign out the current user and disconnect their wallet.

Usage

import { useSignOut } from "@randombits/use-siwe";

const SignOutButton = () => {
  const { signOut, isLoading } = useSignOut();
  return <button onClick={() => signOut()} disabled={isLoading}>Sign Out</button>;
};

Options

{
  onSuccess: () => void,
  onError: () => void,
}

Return Value

Returns a UseMutationResult (ref) augmented with the following:

{
  signOut: () => void,
  SignOutAsync: () => Promise<void>,
} & UseMutationResult

useOptions

A hook that simply returns the options that have been set by in the SiweProvider component.

Usage

import { useOptions, verify } from "@randombits/use-siwe";

const verifyButton = (props) => {
  const options = useOptions();
  const handleClick = () => verify({
    message: props.message,
    signature: props.signature,
  }, options);

  return <button onClick={() => handleClick()}>Verify Signature</button>;
};

Return Value

useSiweOptions

Routes

Next.js: SiweApi

A function that returns a NextApiHandler that will handle all auth API routes.

Usage

import { withIronSessionApiRoute } from "iron-session/next";
import ironOptions from "lib/ironOptions";
import { siweApi } from "@randombits/use-siwe/next"

export default withIronSessionApiRoute(siweApi(), ironOptions);

Return Value

NextApiHandler

Express.js: SiweApi

A function that returns an express Router that will handle all auth API routes.

Usage

import express from "express";
import { ironSession } from "iron-session/express";
import ironOptions from "./ironOptions.js";
import { authRouter } from "@randombits/use-siwe/express";

const app = express();

app.use(ironSession(ironOptions));
app.use('/auth', authRouter());

app.listen(3001);

Return Value

Router

Functions

getSession

A function to retrieve the session data where using a hook doesn't make sense.

Usage

import { getSession } from "@randombits/use-siwe";

const addressOrNull = async () => {
  const { address } = await getSession();
  if (!address) return null;
  return address;
};

Args

  • options?: UseSiweOptions

Return Value

{
  authenticated: boolean;
  address?: string;
  nonce?: string;
}

createMessage

Returns a SiweMessage for the given address, chainId, and nonce.

Usage

import { createMessage, getMessageBody } from "@randombits/use-siwe";

const debugMessage = (address, chainId, nonce) => {
  const message = createMessage({ address, chainId, nonce });
  const messageBody = getMessageBody({ message });
  console.log({ message, messageBody });
};

Args

  • args: MessageArgs
type MessageArgs = {
  address: string,
  chainId: number,
  nonce: string,
};

Return Value

SiweMessage

getMessageBody

Returns a message ready to be signed according with the type defined in the SiweMessage object.

Usage

import { createMessage, getMessageBody } from "@randombits/use-siwe";

const debugMessage = (address, chainId, nonce) => {
  const message = createMessage({ address, chainId, nonce });
  const messageBody = getMessageBody({ message });
  console.log({ message, messageBody });
};

Args

  • args: { message: SiweMessage }

Return Value

string

verify

Takes a message and a signature as arguments and attempts to verify them using the auth API. A successful verification will create a session for the user.

Usage

import { verify } from "@randombits/use-siwe";

const verifyButton = (props) => {
  const handleClick = () => {
    const success = verify({
      message: props.message,
      signature: props.signature,
    });

    if (!success) return console.error("VERIFICATION FAILED");
    console.log("SIGNATURE VERIFIED");
  };

  return <button onClick={() => handleClick()}>Verify Signature</button>;
};

Args

  • args: VerifyArgs
  • options?: UseSiweOptions
type VerifyArgs = {
  message: SiweMessage,
  signature: string,
};

Return Value

boolean

signOut

A function to sign out the user where using a hook doesn't make sense.

Usage

import { signOut } from "@randombits/use-siwe";

// Logout a user after 1 hour
setTimeout(async () => {
  await signOut();
  window.location.href = "/session-expired";
}, 60 * 60 * 1000);

Args

  • options?: UseSiweOptions

Return Value

Promise<void>