# Sign In With Ethereum (SIWE) (/docs/plugins/siwe)

Better Auth 的以太坊登录插件



以太坊登录（Sign in with Ethereum，SIWE）插件允许用户使用其以太坊钱包按照 [ERC-4361 标准](https://eips.ethereum.org/EIPS/eip-4361)进行身份验证。此插件提供灵活性，支持你自行实现消息验证与 nonce 生成逻辑。

## 安装 [#安装]

<Steps>
  <Step>
    ### 添加服务器插件 [#添加服务器插件]

    将 SIWE 插件添加到你的认证配置中：

    ```ts title="auth.ts"
    import { betterAuth } from "better-auth";
    import { siwe } from "better-auth/plugins"; // [!code highlight]

    export const auth = betterAuth({
        plugins: [
            siwe({
                domain: "example.com",
                emailDomainName: "example.com", // 可选
                anonymous: false, // 可选，默认为 true
                getNonce: async () => {
                    // Return an ERC-4361 nonce: 8-250 alphanumeric characters
                    return "A1b2C3d4E5f6G7h8J";
                },
                verifyMessage: async (args) => {
                    // 在这里实现你的 SIWE 消息验证逻辑
                    // 这应该验证签名是否与消息匹配
                    return true; // 如果签名有效则返回 true
                },
                ensLookup: async (args) => {
                    // 可选：实现 ENS 查询以获取用户名和头像
                    return {
                        name: "user.eth",
                        avatar: "https://example.com/avatar.png"
                    };
                },
            }),
        ],
    });
    ```
  </Step>

  <Step>
    ### 迁移数据库 [#迁移数据库]

    运行迁移或生成模式以向数据库添加必要的字段和表。

    <Tabs items="[&#x22;migrate&#x22;, &#x22;generate&#x22;]">
      <Tab value="migrate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install">
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx auth migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx auth migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx auth migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x auth migrate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>

      <Tab value="generate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install">
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx auth generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx auth generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx auth generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x auth generate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>
    </Tabs>

    请参阅 [Schema](#schema) 部分以手动添加字段。
  </Step>

  <Step>
    ### 添加客户端插件 [#添加客户端插件]

    ```ts title="auth-client.ts"
    import { createAuthClient } from "better-auth/client";
    import { siweClient } from "better-auth/client/plugins"; // [!code highlight]

    export const authClient = createAuthClient({
    plugins: [
        siweClient() // [!code highlight]
    ]
    });
    ```
  </Step>
</Steps>

## 使用方法 [#使用方法]

### 生成 Nonce [#生成-nonce]

在要求钱包签名之前，为此次登录尝试签发一个 nonce。Nonce 不会与钱包地址或 Chain ID 绑定，因为单步钱包流程可能要等到钱包签署 SIWE 消息后才知道这两个值。

```ts
import { authClient } from "@/lib/auth-client";

const { data, error } = await authClient.siwe.nonce();

if (data) {
  console.log("Nonce:", data.nonce);
}
```

### 以太坊登录 [#以太坊登录]

生成 nonce 并创建 SIWE 消息后，验证签名以完成身份认证：

```ts
import { authClient } from "@/lib/auth-client";

const { data, error } = await authClient.siwe.verify({
  message: "Your ERC-4361 SIWE message string",
  signature: "0x...", // The signature from the user's wallet
  email: "user@example.com", // optional, required if anonymous is false
});

if (data) {
  console.log("认证成功:", data.user);
}
```

<Callout type="warning">
  `message` 必须是有效的 [ERC-4361](https://eips.ethereum.org/EIPS/eip-4361) 消息（每个标准 SIWE 客户端都会生成此类消息）。在接受签名之前，插件会解析消息，使用匹配的服务器签发的 **nonce**，从已签名的消息中推导出 **address** 和 **Chain ID**，要求已签名的 **domain** 与你配置的 `domain` 匹配，并遵守消息的 `Expiration Time`／`Not Before` 限制。仅恢复签名**并不足够**——这种绑定可确保签名只有在与其生成所针对的消息以及当前服务器签发的 nonce 一起提供时才会被接受。如果任何已签名字段无效，验证将失败并返回 401（`UNAUTHORIZED_SIWE_MESSAGE_MISMATCH`）。
</Callout>

<Callout type="warning">
  SIWE 签名只能证明对钱包的控制权，而不能证明你传入的 `email` 的所有权。插件会将该电子邮件按未验证状态存储，并且仅在它尚未被使用时将其绑定到新账户。当 `anonymous` 为 `false` 且所提供的电子邮件已属于另一个账户时，新钱包账户会改为使用基于钱包派生的地址创建，因此一次登录无法附加另一个账户已拥有的电子邮件。
</Callout>

### 特定链的消息 [#特定链的消息]

链的选择应包含在 ERC-4361 消息中，而不是验证请求正文中。生成 nonce，使用钱包地址和目标 Chain ID 构建 SIWE 消息，要求钱包对该确切消息进行签名，然后验证已签名的消息：

```ts
import { authClient } from "@/lib/auth-client";

const nonce = await authClient.siwe.nonce();

const { data, error } = await authClient.siwe.verify({
  // The signed ERC-4361 message contains the wallet address,
  // Chain ID: 137, and Nonce: nonce.data?.nonce.
  message,
  signature,
});
```

<Callout type="warning">
  已签名的 SIWE 消息必须包含一个正数 Chain ID。验证会从该已签名的 Chain ID 中推导钱包身份；如果消息缺少有效的 Chain ID，验证将失败并返回 401 错误。
</Callout>

## 配置选项 [#配置选项]

### 服务器配置 [#服务器配置]

SIWE 插件接受以下配置选项：

* **domain**：你的应用程序的域名（生成 SIWE 消息时必需）
* **emailDomainName**：钱包派生地址使用的自定义电子邮件域名。如果省略，插件会使用 `{walletAddress}@siwe.placeholder.invalid`
* **anonymous**：是否允许无需电子邮件的匿名登录。默认为 `true`
* **getNonce**：为每次登录尝试生成全局唯一 nonce 的函数。你必须实现此函数，使其返回一个经过加密安全处理的 ERC-4361 nonce：由 8-250 个字母数字字符组成。必须返回 `Promise<string>`
* **verifyMessage**：验证 SIWE 消息签名的函数。此函数只需针对所提供的地址执行签名恢复（例如 viem 的 `verifyMessage`），并返回 `Promise<boolean>`——插件会在创建会话之前独立验证消息的 nonce、domain、address、Chain ID 和时间限制
* **ensLookup**：用于查询以太坊地址的 ENS 名称和头像的可选函数

### 客户端配置 [#客户端配置]

SIWE 客户端插件不需要任何配置选项：

```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client";
import { siweClient } from "better-auth/client/plugins";

export const authClient = createAuthClient({
  plugins: [
    siweClient(),
  ],
});
```

## 数据表结构 [#数据表结构]

SIWE 插件新增了一个 `walletAddress` 表，用于存储用户钱包关联信息：

| 字段        | 类型      | 描述               |
| --------- | ------- | ---------------- |
| id        | string  | 主键               |
| userId    | string  | 关联用户的 user.id    |
| address   | string  | 以太坊钱包地址          |
| chainId   | number  | 链 ID（例如以太坊主网为 1） |
| isPrimary | boolean | 是否为用户的主钱包        |
| createdAt | date    | 创建时间戳            |

## 示例实现 [#示例实现]

下面是一个完整示例，展示如何实现 SIWE 认证：

```ts title="auth.ts"
import { betterAuth } from "better-auth";
import { siwe } from "better-auth/plugins";
import { generateRandomString } from "better-auth/crypto";
import { verifyMessage, createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";

export const auth = betterAuth({
  database: {
    // 你的数据库配置
  },
  plugins: [
    siwe({
      domain: "myapp.com",
      emailDomainName: "myapp.com",
      anonymous: false,
      getNonce: async () => {
        // 生成加密安全的随机 nonce
        return generateRandomString(32, "a-z", "A-Z", "0-9");
      },
      verifyMessage: async ({ message, signature, address }) => {
        try {
          // 使用 viem 库验证签名（推荐）
          const isValid = await verifyMessage({
            address: address as `0x${string}`,
            message,
            signature: signature as `0x${string}`,
          });
          return isValid;
        } catch (error) {
          console.error("SIWE 验证失败:", error);
          return false;
        }
      },
      ensLookup: async ({ walletAddress }) => {
        try {
          // 可选：使用 viem 查询 ENS 名称和头像
          const client = createPublicClient({
            chain: mainnet,
            transport: http(),
          });

          const ensName = await client.getEnsName({
            address: walletAddress as `0x${string}`,
          });

          const ensAvatar = ensName
            ? await client.getEnsAvatar({
                name: ensName,
              })
            : null;

          return {
            name: ensName || walletAddress,
            avatar: ensAvatar || "",
          };
        } catch {
          return {
            name: walletAddress,
            avatar: "",
          };
        }
      },
    }),
  ],
});
```

