JWT

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

此插件并非用于替代会话(session),而是为需要 JWT 令牌的服务而设计。如果您希望使用 JWT 令牌进行身份验证,请查看 Bearer 插件

安装

将插件添加到您的 auth 配置中

auth.ts
import { betterAuth } from "better-auth"
import { jwt } from "better-auth/plugins"

export const auth = betterAuth({
    plugins: [ 
        jwt(), 
    ] 
})

迁移数据库

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

npx @better-auth/cli migrate
npx @better-auth/cli generate

请参阅 Schema(模式) 部分以手动添加字段。

使用

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

JWT

获取令牌

  1. 使用会话令牌

要获取令牌,请调用 /token 端点。这将返回以下内容:

{
  "token": "ey..."
}

请确保在请求的 Authorization 头部中包含令牌,并在身份验证配置中添加了 bearer 插件。

await fetch("/api/auth/token", {
  headers: {
    "Authorization": `Bearer ${token}`
  },
})
  1. set-auth-jwt 头部获取

当调用 getSession 方法时,会在 set-auth-jwt 头部返回一个 JWT,您可以直接将其发送到您的服务。

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

验证令牌

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

由于此密钥不会频繁更改,因此可以无限期缓存。 用于签名 JWT 的密钥 ID (kid) 包含在令牌的头部中。 如果收到具有不同 kid 的 JWT,建议重新获取 JWKS。

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

OAuth 提供者模式

如果您正在使系统符合 oAuth 标准(例如使用 OIDC 或 MCP 插件时),您必须禁用 /token 端点(oAuth 等效于 /oauth2/token)并禁用设置 jwt 头部(oAuth 等效于 /oauth2/userinfo)。

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

使用远程 JWKS 的示例

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('Token validation failed:', error)
    throw error
  }
}

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

使用本地 JWKS 的示例

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('Token validation failed:', error)
    throw error
  }
}

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

远程 JWKS URL

禁用 /jwks 端点,并在任何发现(如 OIDC)中使用此端点。

当您的 JWKS 不托管在 /jwks 路径下,或者您的 JWKS 使用证书签名并放置在 CDN 上时,此功能非常有用。

注意:您必须指定用于签名的不对称算法。

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

自定义签名

这是一个高级功能。必须提供此插件之外的配置。

实现说明:

  • 如果使用 sign 函数,则必须定义 remoteUrl。该 URL 应存储所有活跃密钥,而不仅仅是当前密钥。
  • 如果使用本地化方法,请确保服务器在密钥轮换时使用最新的私钥。根据部署情况,可能需要重启服务器。
  • 使用远程方法时,请验证传输后负载是否未更改。如果可用,请使用 CRC32 或 SHA256 校验等完整性验证。

本地化签名

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 KMSAmazon KMSAzure Key Vault),此功能将非常有用。

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}`
      // verifyJwt(jwt)
      return jwt
    },
  },
})

数据库结构

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

JWKS(JSON Web 密钥集)

表名:jwks

字段名称类型Key描述
idstring每个 Web 密钥的唯一标识符
publicKeystring-Web 密钥的公共部分
privateKeystring-Web 密钥的私有部分
createdAtDate-Web 密钥创建的时间戳

您可以自定义 jwks 表的表名和字段。有关如何自定义插件模式的更多信息,请参阅数据库概念文档

配置选项

密钥对的算法

用于生成密钥对的算法。默认为使用 Ed25519 曲线的 EdDSA 算法。以下是可用的选项:

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

EdDSA

  • 默认曲线: Ed25519
  • 可选属性: crv
    • 可用选项: Ed25519, Ed448
    • 默认值: Ed25519

ES256

  • 无额外属性

RSA256

  • 可选属性: modulusLength
    • 期望值为数字
    • 默认值: 2048

PS256

  • 可选属性: modulusLength
    • 期望值为数字
    • 默认值: 2048

ECDH-ES

  • 可选属性: crv
    • 可用选项: P-256, P-384, P-521
    • 默认值: P-256

ES512

  • 无额外属性

禁用私钥加密

默认情况下,私钥使用 AES256 GCM 进行加密。您可以通过将 disablePrivateKeyEncryption 选项设置为 true 来禁用此功能。

出于安全考虑,建议保持私钥加密状态。

auth.ts
jwt({
  jwks: {
    disablePrivateKeyEncryption: true
  }
})

修改 JWT 载荷

默认情况下,整个用户对象会被添加到 JWT 载荷中。你可以通过为 definePayload 选项提供一个函数来修改载荷内容。

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

修改签发者、受众、主题或过期时间

如果没有指定,系统将使用 BASE_URL 作为签发者,并将受众设置为 BASE_URL。默认过期时间为 15 分钟。

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