This article follows the EIP-4361 Sign-In with Ethereum rules
SIWE (Sign-In with Ethereum) is a way to verify user identity on Ethereum, similar to a wallet initiating a transaction, indicating that the user has control over the wallet.
The current identity authentication method is already very simple. You only need to sign the information in the wallet plug-in. Common wallet plug-ins already support it.
The signature scenario considered in this article is on Ethereum. Other scenarios such as Solana and SUI are not within the scope of this article.
Does your project need SIWE?
SIWE is designed to solve the identity authentication problem of wallet addresses, so if you have the following needs, you can consider using SWIE:
- Your Dapp has its own user system;
- The information to be queried is related to user privacy;
But if your Dapp is a query-based function, such as applications like etherscan, it is also possible without SIWE.
You may have a question: after I connect to the Dapp through my wallet, doesn’t it mean that I have ownership of the wallet?
Yes, but not entirely. For the front-end, it is true that you have revealed your identity after you have connected through the wallet, but for some interface calls that require back-end support, you have no way to reveal your identity. If you only pass your address in the interface, then anyone can "borrow" your identity, after all, the address is public information.
Principles and processes of SIWE
The SIWE process can be summarized into three steps: connect wallet - sign - obtain identity. We will introduce these three steps in detail.
Connect your wallet
Connecting a wallet is a common WEB3 operation. You can connect your wallet in Dapp through a wallet plug-in.
sign
In SIWE, the signing steps include obtaining the Nonce value, wallet signature, and backend signature verification.
The design of obtaining the Nonce value should refer to the Nonce value in ETH transactions, and it is also necessary to call the backend interface to obtain it. After receiving the request, the backend will generate a random Nonce value and associate it with the current address to prepare for the subsequent signature.
After the front end obtains the Nonce value, it needs to construct the signature content. The signature content that SIWE can design includes the obtained Nonce value, domain name, chain ID, signature content, etc. We generally use the signature method provided by the wallet to sign the content.
After the signature is built, it is finally sent to the backend.
Get the identity token
After the backend verifies the signature and passes it, it will return the corresponding user identity, which can be JWT. The frontend will then send the corresponding address and identity when sending a backend request to indicate its ownership of the wallet.
Practice
There are already many components and libraries that support developers to quickly access wallet connections and SIWE. We can actually put it into practice. The goal of practice is to enable your Dapp to return JWT for user identity verification.
Note that this DEMO is only used to introduce the basic process of SIWE, and may cause security issues when used in a production environment.
Preparation
This article uses nextjs to develop applications, so developers need to prepare a nodejs environment. One advantage of using nextjs is that we can directly develop a full-stack project without splitting it into two projects: front-end and back-end.
Install Dependencies
First, we install nextjs. In your project directory, enter the following command in the command line:
npx create-next-app@14
Install nextjs as prompted, and you can see the following:
After entering the project directory, you can see that the nextjs scaffolding has done a lot of work for us. We can run the project in the project directory:
npm run dev
Then, follow the prompts on the terminal and go to localhost: 3000
to see that a basic NextJS project is already running.
Install SIWE related dependencies
According to the previous introduction, SIWE needs to rely on the login system, so we need to connect our project to the wallet. Here we use Ant Design Web3 ( https://web3.ant.design/ ) because:
- It is completely free and is currently actively maintained.
- As a WEB3 component library, its user experience is similar to that of ordinary component libraries, without any additional mental burden
- And 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
Introducing Wagmi
Ant Design Web3's SIWE relies on the Wagmi library, so related components need to be introduced in the project. We introduce the corresponding Provider in layout.tsx
, so that the entire project can use the Hooks provided by Wagmi.
We first define the configuration of WagmiProvider, the code is 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 = "c07c0051c2055890eade3556618e38a6"; const queryClient = new QueryClient(); const WagmiProvider: React.FC = ({ children }) => { const [jwt, setJwt] = React.useState(null); return ( (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} > {children} ); }; export default WagmiProvider;
We used the Provider provided by Ant Design Web3 and defined some interfaces of SIWE. We will introduce the implementation of the specific interfaces later.
Then we introduce the button to connect to the wallet, so that a connection entry can be added to the front end.
At this point, even if SIWE is already connected, the steps are very simple.
After that, we need to define a connection button to connect the wallet and signature. The code is as follows:
"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 ( <>
{jwt}
); }
In this way, we have implemented the simplest SIWE login framework.
Interface Implementation
According to the above introduction, SIWE needs some interfaces to help the backend verify the user's identity. Now let's implement it simply.
Nonce
The purpose of Nonce is to change the signature content generated by the wallet each time when signing, so as to improve the reliability of the signature. The generation of this Nonce needs to be associated with the address passed in by the user to improve the accuracy of verification.
The implementation of Nonce is very straightforward. First, we generate a random string (composed of letters and numbers), and then associate the nonce with the address. The code is as follows:
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 function of signMessage is to sign the content. This function is usually completed through a wallet plug-in. We usually do not need to configure it, but only need to specify the method. In this Demo, Wagmi's signature method is used.
verifyMessage
After the user signs the content, the content before the signature and the signature need to be sent to the backend for verification. The backend parses the corresponding content from the signature for comparison. If they are consistent, the verification is successful.
In addition, some security checks need to be performed on the signed content, such as whether the Nonce value in the signature content is consistent with the one we distributed to the user. After the verification is passed, the corresponding user JWT needs to be returned for subsequent permission verification. The sample code is as follows:
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"; // 请使用更安全的密钥,并添加对应的过期校验等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); // 校验nonce 值是否一致if (!nonce || nonce !== addressMap.get(address)) { throw new Error("Invalid nonce"); } // 校验签名内容const valid = await publicClient.verifySiweMessage({ message, address, signature, }); if (!valid) { throw new Error("Invalid signature"); } // 生成jwt 并返回const token = jwt.sign({ address }, JWT_SECRET, { expiresIn: "1h" }); return Response.json({ data: token, }); }
At this point, a Dapp that basically implements SIWE login has been developed.
Some optimization items
Now when we log in to SIWE, if we use the default RPC node, the verification process will take nearly 30 seconds, so it is strongly recommended to use a dedicated node service to improve the response time of the interface. This article uses ZAN's node service ( https://zan.top/home/node-service?chInfo=ch_WZ ), you can go to the ZAN node service console to obtain the corresponding RPC connection.
After we obtain the HTTPS RPC connection to the Ethereum mainnet, we replace the default RPC of publicClient
in the code:
const publicClient = createPublicClient({ chain: mainnet, transport: http('https://api.zan.top/node/v1/eth/mainnet/xxxx'), //获取到的ZAN 节点服务RPC });
After the replacement, the verification time can be significantly reduced and the interface speed can be significantly increased.