Strengthen Your DApp with SIWE
The introduction of this article follows the EIP-4361 Sign-In with Ethereum standards.
SIWE (Sign-In with Ethereum) is a method of user identity verification on Ethereum, similar to how a wallet initiates a transaction, indicating that the user has control over that wallet.
Currently, the authentication method is very straightforward; you only need to sign the information in the wallet plugin, and common wallet plugins already support this.
This article considers the signature scenario on Ethereum; other chains like Solana and SUI are not within the scope of this discussion.
Does Your Project Need SIWE?
SIWE is designed to address the issue of wallet address verification. So if you have the following requirements, you might consider using SWIE:
- Your DApp has its own user system;
- You need to query information related to user privacy.
However, if your DApp primarily functions as a query platform, like applications such as Etherscan, it is fine to operate without SIWE.
You might wonder: after connecting through a wallet on the DApp, doesn't that signify ownership of the wallet?
Yes, but not entirely. For the frontend, after you connect via the wallet, it indicates your identity. However, for some backend-support-required API calls, you cannot prove your identity just by passing your address. Anyone can "borrow" your identity if the address is the only thing being passed, as addresses are public information.
The Principles and Processes of SIWE
The SIWE process can be summarized in three steps: Connect Wallet – Sign – Obtain Identity Token. We will elaborate on these three steps.
Connect Wallet
Connecting a wallet is a common WEB3 operation, and you can connect your wallet in the DApp through a wallet plugin.
Sign
In SIWE, the signing process involves obtaining a nonce value, wallet signing, and backend signature verification.
Obtaining the nonce value references the nonce design used in ETH transactions, requiring a call to the backend for retrieval. The backend generates a random nonce upon receiving the request and associates it with the current address to prepare for the upcoming signature.
Once the frontend receives the nonce value, it needs to construct the signature contents. The SIWE signature can include the nonce value obtained, domain name, chain ID, signature content, etc. Generally, we will use the signing method provided by the wallet to sign the content.
After constructing the signature, it is sent to the backend.
Obtain Identity Token
After the backend verifies the signature successfully, it will return the corresponding user identity token, which could be a JWT. The frontend can then include the respective address and identity token in subsequent requests to indicate ownership of the wallet.
Practice
Many components and libraries already support developers in quickly integrating wallet connections and SIWE. We can demonstrate a practical operation with the goal of enabling your DApp to return JWTs for user identity verification.
Note: This DEMO is meant to introduce the basic SIWE process, and using it in a production environment may pose security issues.
Preparation
This article develops an application using Next.js, so developers should have their Node.js environment ready. One advantage of using Next.js is that we can directly develop a full-stack project without needing to split it into frontend and backend projects.
Install Dependencies
First, we install Next.js. In your project directory, enter the following command:
npx create-next-app@14
Follow the prompts to install Next.js, and you should see the following content:
After entering the project directory, you'll find that the Next.js scaffold has done a lot of work for us. We can run the project in the directory:
npm run dev
After that, according to the terminal prompt, navigate to localhost: 3000
to see that a basic Next.js project is up and running.
Install SIWE Related Dependencies
Based on previous introductions, SIWE needs to rely on a login system, so we need to connect our project to a wallet. Here, we use Ant Design Web3 because:
- It is completely free and currently under active maintenance.
- As a WEB3 component library, its user experience is similar to ordinary component libraries, with no additional mental burden.
- It supports SIWE
We need to enter in the terminal:
npm install antd @ant-design/web3 @ant-design/web3-wagmi wagmi viem @tanstack/react-query --save
Introduce Wagmi
Ant Design Web3's SIWE relies on the Wagmi library for implementation, so we need to introduce the relevant components in the project. In layout.tsx
, we import the corresponding Provider, allowing the whole project to use the Hooks provided by Wagmi.
We first define the configuration for the WagmiProvider, as follows:
"use client";
import { getNonce, verifyMessage } from "@/app/api";
import {
Mainnet,
MetaMask,
OkxWallet,
TokenPocket,
WagmiWeb3ConfigProvider,
WalletConnect,
} from "@ant-design/web3-wagmi";
import { QueryClient } from "@tanstack/react-query";
import React from "react";
import { createSiweMessage } from "viem/siwe";
import { http } from "wagmi";
import { JwtProvider } from "./JwtProvider";
const YOUR_WALLET_CONNECT_PROJECT_ID = "xxxxxxxxxx";
const queryClient = new QueryClient();
const WagmiProvider: React.FC<React.PropsWithChildren> = ({ children }) => {
const [jwt, setJwt] = React.useState<string | null>(null);
return (
<WagmiWeb3ConfigProvider
eip6963={{
autoAddInjectedWallets: true,
}}
ens
siwe={{
getNonce: async (address) => (await getNonce(address)).data,
createMessage: (props) => {
return createSiweMessage({ ...props, statement: "Ant Design Web3" });
},
verifyMessage: async (message, signature) => {
const jwt = (await verifyMessage(message, signature)).data;
setJwt(jwt);
return !!jwt;
},
}}
chains={[Mainnet]}
transports={{
[Mainnet.id]: http(),
}}
walletConnect={{
projectId: YOUR_WALLET_CONNECT_PROJECT_ID,
}}
wallets={[
MetaMask(),
WalletConnect(),
TokenPocket({
group: "Popular",
}),
OkxWallet(),
]}
queryClient={queryClient}
>
<JwtProvider.Provider value={jwt}>{children}</JwtProvider.Provider>
</WagmiWeb3ConfigProvider>
);
};
export default WagmiProvider;
We use the Provider provided by Ant Design Web3 and define some interfaces for SIWE; we will detail the specific implementations later.
After this, we will introduce a wallet connection button, which adds a connection entry in the frontend.
At this point, we have already integrated SIWE, and the steps are quite simple.
Next, we need to define a connection button to achieve wallet connection and signing, as shown below:
"use client";
import type { Account } from "@ant-design/web3";
import { ConnectButton, Connector } from "@ant-design/web3";
import { Flex, Space } from "antd";
import React from "react";
import { JwtProvider } from "./JwtProvider";
export default function App() {
const jwt = React.useContext(JwtProvider);
const renderSignBtnText = (
defaultDom: React.ReactNode,
account?: Account
) => {
const { address } = account ?? {};
const ellipsisAddress = address
? `${address.slice(0, 6)}...${address.slice(-6)}`
: "";
return `Sign in as ${ellipsisAddress}`;
};
return (
<>
<Flex vertical>
<Connector
modalProps={{
mode: "simple",
}}
>
<ConnectButton signBtnTextRender={renderSignBtnText} />
</Connector>
<div className="text-center">{jwt}</div>
</Flex>
</>
);
}
This implementation creates the simplest SIWE login framework.
API Implementation
According to the introduction above, SIWE needs several APIs to help the backend verify user identity. Let's implement a simple version.
Nonce
The nonce is used to ensure that the content signed by the wallet changes each time, improving signature reliability. This nonce generation needs to be associated with the user-provided address to enhance verification accuracy.
The implementation of nonce is straightforward. First, we generate a random string (composed of letters and digits), and then associate this nonce with the address:
import { randomBytes } from "crypto";
import { addressMap } from "../cache";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const address = searchParams.get("address");
if (!address) {
throw new Error("Invalid address");
}
const nonce = randomBytes(16).toString("hex");
addressMap.set(address, nonce);
return Response.json({
data: nonce,
});
}
signMessage
The purpose of signMessage is to create the signature content. This functionality is usually accomplished through the wallet plugin, and we generally do not need to configure it; we simply specify the method. In this DEMO, we use the signing method from Wagmi.
verifyMessage
After the user signs the content, the pre-signed content and signature need to be sent to the backend for validation. The backend parses the signature to compare it with the expected content. If they match, the verification is successful.
Additionally, we need to perform some security checks on the signed content, such as whether the nonce value matches the one we provided to the user. Upon successful verification, we return the corresponding user JWT for subsequent permission validation, as shown in the sample code:
import { createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";
import jwt from "jsonwebtoken";
import { parseSiweMessage } from "viem/siwe";
import { addressMap } from "../cache";
const JWT_SECRET = "your-secret-key"; // Please use a more secure key and add corresponding expiration checks, etc
const publicClient = createPublicClient({
chain: mainnet,
transport: http(),
});
export async function POST(request: Request) {
const { signature, message } = await request.json();
const { nonce, address = "0x" } = parseSiweMessage(message);
console.log("nonce", nonce, address, addressMap);
// Verify that the nonce value is consistent
if (!nonce || nonce !== addressMap.get(address)) {
throw new Error("Invalid nonce");
}
// Verify the signature content
const valid = await publicClient.verifySiweMessage({
message,
address,
signature,
});
if (!valid) {
throw new Error("Invalid signature");
}
// Generate a JWT and return it
const token = jwt.sign({ address }, JWT_SECRET, { expiresIn: "1h" });
return Response.json({
data: token,
});
}
At this point, a basic SIWE login DApp implementation is complete.
Some Optimization Suggestions
Currently, when performing SIWE login, if we use the default RPC node, the validation process can take nearly 30 seconds. Therefore, it is strongly recommended to use dedicated node services to improve the interface response time. This article uses ZAN Node service; you can visit the ZAN Node service console to obtain the respective RPC connection.
Once we obtain the HTTPS RPC connection to the Ethereum mainnet, we can replace the default RPC in the code for publicClient
:
const publicClient = createPublicClient({
chain: mainnet,
transport: http('https://api.zan.top/node/v1/eth/mainnet/xxxx'), // use your ZAN node service RPC URL
});
After the replacement, the verification time can be significantly reduced, and the speed of the interface will dramatically improve.
About ZAN
As a technology brand of Ant Digital Technologies for Web3 products and services, ZAN provides rich and reliable services for business innovations and a development platform for Web3 endeavors.
The ZAN product family includes ZAN Node Service, ZAN PowerZebra (zk acceleration), ZAN Identity (Know your customers and clients), ZAN Smart Contract Review, with more products in the pipeline.