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.
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
To install UseSIWE and it's dependencies run the following command:
npm install @randombits/use-siwe wagmi ethers 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
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);
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);
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.
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>;
};
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);
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>;
};
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>;
};
UseSIWE accepts an object of options. Currently this consists of one optional setting:
const options: UseSiweOptions = {
baseUrl: "/v2/api/auth",
};
baseUrl
, optional: The base url for the auth API endpoints that is prepended to all requests. Defaults to:/api/auth
Context provider component that must wrap all components that use useSession
,
useSignIn
, useSignOut
, or useOptions
hooks.
import type { AppProps } from 'next/app';
import { SiweProvider } from '@randombits/use-siwe';
export default function MyApp({ Component, pageProps }: AppProps) {
return <SiweProvider>
<Component {...pageProps} />
</SiweProvider>;
}
options
, Optional: AUseSiweOptions
object.
A hook that returns the the current state of the users session.
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>;
};
Returns a UseQueryResult
(ref)
augmented with the following:
{
authenticated: boolean;
address?: string;
nonce?: string;
} & UseQueryResult
A hook that returns a signIn
function that will initiate a SIWE flow, as well
as the status of that signIn process.
import { useSignIn } from "@randombits/use-siwe";
const SignInButton = () => {
const { signIn, isLoading } = useSignIn();
return <button onClick={() => signIn()} disabled={isLoading}>Sign In with Ethereum</button>;
};
{
onSuccess: () => void,
onError: () => void,
}
Returns a UseMutationResult
(ref)
augmented with the following:
{
signIn: () => void,
SignInAsync: () => Promise<void>,
} & UseMutationResult
A hook that returns a signOut
function that when called will sign out the
current user and disconnect their wallet.
import { useSignOut } from "@randombits/use-siwe";
const SignOutButton = () => {
const { signOut, isLoading } = useSignOut();
return <button onClick={() => signOut()} disabled={isLoading}>Sign Out</button>;
};
{
onSuccess: () => void,
onError: () => void,
}
Returns a UseMutationResult
(ref)
augmented with the following:
{
signOut: () => void,
SignOutAsync: () => Promise<void>,
} & UseMutationResult
A hook that simply returns the options that have been set by in the
SiweProvider
component.
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>;
};
useSiweOptions
A function that returns a NextApiHandler
that will handle all auth API
routes.
import { withIronSessionApiRoute } from "iron-session/next";
import ironOptions from "lib/ironOptions";
import { siweApi } from "@randombits/use-siwe/next"
export default withIronSessionApiRoute(siweApi(), ironOptions);
NextApiHandler
A function that returns an express Router
that will handle all auth API
routes.
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);
Router
A function to retrieve the session data where using a hook doesn't make sense.
import { getSession } from "@randombits/use-siwe";
const addressOrNull = async () => {
const { address } = await getSession();
if (!address) return null;
return address;
};
options?: UseSiweOptions
{
authenticated: boolean;
address?: string;
nonce?: string;
}
Returns a SiweMessage
for the given address, chainId, and nonce.
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: MessageArgs
type MessageArgs = {
address: string,
chainId: number,
nonce: string,
};
SiweMessage
Returns a message ready to be signed according with the type defined in the SiweMessage object.
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: { message: SiweMessage }
string
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.
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: VerifyArgs
options?: UseSiweOptions
type VerifyArgs = {
message: SiweMessage,
signature: string,
};
boolean
A function to sign out the user where using a hook doesn't make sense.
import { signOut } from "@randombits/use-siwe";
// Logout a user after 1 hour
setTimeout(async () => {
await signOut();
window.location.href = "/session-expired";
}, 60 * 60 * 1000);
options?: UseSiweOptions
Promise<void>