# JWT (/docs/plugins/jwt)

在无法使用 session 的服务中使用 JWT 令牌进行用户认证



JWT 插件提供了用于获取 JWT 令牌的端点以及用于验证令牌的 JWKS 端点。

<Callout type="info">
  该插件并非用来替代 session。它适用于需要使用 JWT 令牌的服务。如果你想使用 JWT 令牌进行身份验证，请查看 [Bearer 插件](/docs/plugins/bearer)。
</Callout>

<Callout type="info">
  JWT 插件的 `/token` 端点和 `set-auth-jwt` 头会生成可由 JWKS 验证的 JWT，供外部服务使用。它们与 Better Auth 的主 `session_token` cookie 相互独立。

  如果启用 `session.cookieCache.strategy = "jwt"`，`session_data` cookie 默认仍使用 Better Auth 密钥和 HS256。若要让该 cookie-cache JWT 也能通过 JWT 插件的 JWKS 进行验证，请在 `jwt()` 上设置 `sessionCookieCache: true`。
</Callout>

## 安装 [#安装]

<Steps>
  <Step>
    ### 将插件添加到你的 **auth** 配置 [#将插件添加到你的-auth-配置]

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

    export const auth = betterAuth({
        plugins: [
            jwt(), // [!code highlight]
        ]
    })
    ```
  </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 { jwtClient } from "better-auth/client/plugins" // [!code highlight]

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

## 用法 [#用法]

安装插件后，你可以通过相应的端点开始使用 JWT 和 JWKS 插件来获取令牌和 JWKS。

## JWT [#jwt]

### 获取令牌 [#获取令牌]

有多种方式可以获取 JWT 令牌：

1. **使用客户端插件（推荐）**

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

const { data, error } = await authClient.token()
if (error) {
  // 处理错误
}
if (data) {
  const jwtToken = data.token
  // 使用此令牌对外部服务进行身份验证请求
}
```

这是客户端应用程序获取 JWT 令牌以进行外部 API 认证的推荐方法。

2. **使用你的 session 令牌**

调用 `/token` 端点可获取令牌，返回如下：

要获取令牌，请调用 `/token` 端点。返回结果如下：

```json
  { 
    "token": "ey..."
  }
```

如果你的 auth 配置中添加了 `bearer` 插件，请确保在请求的 `Authorization` 头中包含该令牌。

```ts
await fetch("/api/auth/token", {
  headers: {
    "Authorization": `Bearer ${token}`
  },
})
```

3. **从 `set-auth-jwt` 头获取**

当你调用 `getSession` 方法时，JWT 会包含在 `set-auth-jwt` 响应头中，你可以直接将其用于服务请求。

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

await authClient.getSession({
  fetchOptions: {
    onSuccess: (ctx)=>{
      const jwt = ctx.response.headers.get("set-auth-jwt")
    }
  }
})
```

### 验证令牌 [#验证令牌]

令牌可以在你自己的服务中验证，无需额外的验证调用或数据库检查。
为此会使用 JWKS。公钥可以从 `/api/auth/jwks` 端点获取。

由于该密钥不经常变更，可以长期缓存。
用于签名 JWT 的密钥 ID (`kid`) 包含在令牌头中。
如果接收到的 JWT 带有不同的 `kid`，建议重新获取 JWKS。

```json
  {
    "keys": [
        {
            "crv": "Ed25519",
            "x": "bDHiLTt7u-VIU7rfmcltcFhaHKLVvWFy-_csKZARUEU",
            "kty": "OKP",
            "kid": "c5c7995d-0037-4553-8aee-b5b620b89b23"
        }
    ]
  }
```

#### 使用 jose 和远程 JWKS 的示例 [#使用-jose-和远程-jwks-的示例]

```ts
import { jwtVerify, createRemoteJWKSet } from 'jose'

async function validateToken(token: string) {
  try {
    const JWKS = createRemoteJWKSet(
      new URL('http://localhost:3000/api/auth/jwks')
    )
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: 'http://localhost:3000', // 应匹配你的 JWT 签发者，即 BASE_URL
      audience: 'http://localhost:3000', // 应匹配 JWT 受众，通常也是 BASE_URL
    })
    return payload
  } catch (error) {
    console.error('令牌验证失败:', error)
    throw error
  }
}

// 使用示例
const token = 'your.jwt.token' // 这是你从 /api/auth/token 端点获取的令牌
const payload = await validateToken(token)
```

#### 使用本地 JWKS 的示例 [#使用本地-jwks-的示例]

```ts
import { jwtVerify, createLocalJWKSet } from 'jose'


async function validateToken(token: string) {
  try {
    /**
     * 这是你从 /api/auth/jwks 端点获取的 JWKS
     */
    const storedJWKS = {
      keys: [{
        //...
      }]
    };
    const JWKS = createLocalJWKSet({
      keys: storedJWKS.data?.keys!,
    })
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: 'http://localhost:3000', // 应匹配你的 JWT 签发者，即 BASE_URL
      audience: 'http://localhost:3000', // 应匹配 JWT 受众，通常也是 BASE_URL
    })
    return payload
  } catch (error) {
    console.error('令牌验证失败:', error)
    throw error
  }
}

// 使用示例
const token = 'your.jwt.token' // 这是你从 /api/auth/token 端点获取的令牌
const payload = await validateToken(token)
```

### OAuth 提供者模式 [#oauth-提供者模式]

如果你要使系统符合 OAuth 标准（例如结合使用 OIDC 或 MCP 插件），你**必须**禁用 `/token` 端点（OAuth 等效的 `/oauth2/token`）和禁用设置 JWT 头（OAuth 等效 `/oauth2/userinfo`）。

```ts title="auth.ts"
import { betterAuth } from "better-auth";

betterAuth({
  disabledPaths: [
    "/token",
  ],
  plugins: [jwt({
    disableSettingJwtHeader: true,
  })]
})
```

### 远程 JWKS 地址 [#远程-jwks-地址]

禁用 `/jwks` 端点，并使用此地址用于任何发现（例如 OIDC）。

当 JWKS 不托管在 `/jwks`，或你的 JWKS 由证书签名并放在 CDN 上时，此功能非常有用。

注意：你**必须**指定用于签名的非对称算法。

```ts title="auth.ts"
jwt({
  jwks: {
    remoteUrl: "https://example.com/.well-known/jwks.json",
    keyPairConfig: {
      alg: 'ES256',
    },
  }
})
```

### 自定义 JWKS 路径 [#自定义-jwks-路径]

默认情况下，JWKS 端点位于 `/jwks`。你可以使用 `jwksPath` 选项自定义路径。

这在以下情况下非常有用：

* 遵循 OAuth 2.0/OIDC 约定（例如 `/.well-known/jwks.json`）
* 匹配应用程序中现有的 API 约定
* 避免与其他端点路径冲突

**服务器配置：**

```ts title="auth.ts"
jwt({
  jwks: {
    jwksPath: "/.well-known/jwks.json"
  }
})
```

**客户端配置：**

服务器使用自定义 `jwksPath` 时，客户端**必须**使用相同路径配置：

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

export const authClient = createAuthClient({
  plugins: [
    jwtClient({
      jwks: {
        jwksPath: "/.well-known/jwks.json" // 必须与服务器配置匹配
      }
    })
  ]
})
```

然后你可以正常使用 `jwks()` 方法：

```ts
const { data, error } = await authClient.jwks()
if (data) {
  // 使用 data.keys 验证 JWT 令牌
}
```

<Callout type="warning">
  客户端配置的 `jwksPath` **必须**与服务器配置匹配，否则客户端将无法获取 JWKS。
</Callout>

### 自定义签名 [#自定义签名]

这是高级功能。插件外部**必须**提供配置。

实现者注意事项：

* 如果使用 `sign` 函数，`remoteUrl` 必须已定义。这将存储所有活动密钥，而不仅仅是当前密钥。
* 如果使用本地化方法，请确保服务器在轮换时使用的是最新的私钥。根据部署情况，可能需要重启服务器。
* 使用远程方法时，请验证载荷在传输过程中未更改。如果可用，请使用完整性验证（如 CRC32 或 SHA256 检查）。

#### 本地签名示例 [#本地签名示例]

```ts title="auth.ts"
jwt({
  jwks: {
    remoteUrl: "https://example.com/.well-known/jwks.json",
    keyPairConfig: {
      alg: 'EdDSA',
    },
  },
  jwt: {
    sign: async (jwtPayload: JWTPayload) => {
      // 伪代码示例
      return await new SignJWT(jwtPayload)
        .setProtectedHeader({
          alg: "EdDSA",
          kid: process.env.currentKid,
          typ: "JWT",
        })
        .sign(process.env.clientPrivateKey);
    },
  },
})
```

#### 远程签名示例 [#远程签名示例]

如果使用远程密钥管理服务（例如 [Google KMS](https://cloud.google.com/kms/docs/encrypt-decrypt-rsa#kms-encrypt-asymmetric-nodejs)、[Amazon KMS](https://docs.aws.amazon.com/kms/latest/APIReference/API_Sign.html) 或 [Azure Key Vault](https://learn.microsoft.com/en-us/rest/api/keyvault/keys/sign/sign?view=rest-keyvault-keys-7.4\&tabs=HTTP)），此功能非常有用。

```ts title="auth.ts"
jwt({
  jwks: {
    remoteUrl: "https://example.com/.well-known/jwks.json",
    keyPairConfig: {
      alg: 'ES256',
    },
  },
  jwt: {
    sign: async (jwtPayload: JWTPayload) => {
      // 伪代码示例
      const headers = JSON.stringify({ kid: '123', alg: 'ES256', typ: 'JWT' })
      const payload = JSON.stringify(jwtPayload)
      const encodedHeaders = Buffer.from(headers).toString('base64url')
      const encodedPayload = Buffer.from(payload).toString('base64url')
      const hash = createHash('sha256')
      const data = `${encodedHeaders}.${encodedPayload}`
      hash.update(Buffer.from(data))
      const digest = hash.digest()
      const sig = await remoteSign(digest)
      // 校验完整性 (integrityCheck(sig))
      const jwt = `${data}.${sig}`
      // 验证 JWT (verifyJwt(jwt))
      return jwt
    },
  },
})
```

## Schema [#schema]

JWT 插件会向数据库添加以下表：

### JWKS [#jwks]

表名：`jwks`。



<DatabaseTable name="jwks" fields="jwksTableFields" />

<Callout>
  你可以自定义 `jwks` 表的表名和字段。详见 [数据库概念文档](/docs/concepts/database#自定义表名)，了解如何自定义插件 Schema 的更多信息。
</Callout>

## 选项 [#选项]

### 会话 Cookie 缓存 [#会话-cookie-缓存]

将 `sessionCookieCache` 设置为 `true`，使用插件本地管理的密钥对会话 Cookie 缓存值进行签名。这要求将 `session.cookieCache.strategy` 设置为 `"jwt"`。

```ts title="auth.ts"
jwt({
  sessionCookieCache: true
})
```

### 密钥对算法 [#密钥对算法]

用于生成密钥对的算法，默认为基于 **Ed25519** 曲线的 **EdDSA**。可用选项如下：

```ts title="auth.ts"
jwt({
  jwks: {
    keyPairConfig: {
      alg: "EdDSA",
      crv: "Ed25519"
    }
  }
})
```

#### EdDSA [#eddsa]

* **默认曲线**：`Ed25519`
* **可选属性**：`crv`
  * 可用选项：`Ed25519`、`Ed448`
  * 默认值：`Ed25519`

#### ES256 [#es256]

* 无额外属性

#### RSA256 [#rsa256]

* **可选属性**：`modulusLength`
  * 需要一个数字
  * 默认值：`2048`

#### PS256 [#ps256]

* **可选属性**：`modulusLength`
  * 需要一个数字
  * 默认值：`2048`

#### ECDH-ES [#ecdh-es]

* **可选属性**：`crv`
  * 可用选项：`P-256`、`P-384`、`P-521`
  * 默认值：`P-256`

#### ES512 [#es512]

* 无额外属性

### 禁用私钥加密 [#禁用私钥加密]

默认情况下，私钥使用 AES256 GCM 加密。你可以将 `disablePrivateKeyEncryption` 选项设置为 `true` 来禁用加密。

出于安全考虑，建议保持私钥加密。

```ts title="auth.ts"
jwt({
  jwks: {
    disablePrivateKeyEncryption: true
  }
})
```

### 密钥轮换 [#密钥轮换]

你可以通过设置 `rotationInterval` 选项启用密钥轮换。该选项会按照指定的间隔自动轮换密钥对。

默认值为 `undefined`（已禁用）。

```ts title="auth.ts"
jwt({
  jwks: {
    rotationInterval: 60 * 60 * 24 * 30, // 30 天
    gracePeriod: 60 * 60 * 24 * 30 // 30 天
  }
})
```

* `rotationInterval`：密钥对轮换间隔，单位为秒。
* `gracePeriod`：轮换后旧密钥对继续有效的时间，单位为秒。这允许客户端验证使用旧密钥对签名的令牌。默认值为 30 天。

### 修改 JWT 载荷 [#修改-jwt-载荷]

默认情况下，整个用户对象都会加入 JWT 载荷。你可以通过 `definePayload` 选项传入函数来修改载荷。

```ts title="auth.ts"
jwt({
  jwt: {
    definePayload: ({user}) => {
      return {
        id: user.id,
        email: user.email,
        role: user.role
      }
    }
  }
})
```

### 修改颁发者、受众、主题或过期时间 [#修改颁发者受众主题或过期时间]

如果未提供，将使用 `BASE_URL` 作为颁发者，并将受众设置为 `BASE_URL`。过期时间设置为 15 分钟。

```ts title="auth.ts"
jwt({
  jwt: {
    issuer: "https://example.com",
    audience: "https://example.com",
    expirationTime: "1h",
    getSubject: (session) => {
      // 默认 subject 是用户 ID
      return session.user.email
    }
  }
})
```

### 自定义适配器 [#自定义适配器]

默认情况下，JWT 插件会从你的数据库中存储和获取 JWKS。你可以提供自定义适配器来覆盖此行为，使 JWKS 可以存储在 Redis、外部服务或内存等其他位置。

```ts title="auth.ts"
jwt({
  adapter: {
    getJwks: async (ctx) => {
      // 自定义实现，获取所有 JWKS
      // 覆盖默认的数据库查询
      return await yourCustomStorage.getAllKeys()
    },
    createJwk: async (ctx, webKey) => {
      // 自定义实现，创建新密钥
      // 覆盖默认数据库插入
      return await yourCustomStorage.createKey(webKey)
    }
  }
})
```

