JWT

JWT 插件提供端点来检索 JWT 令牌,以及用于验证令牌的 JWKS 端点。

此插件并非会话的替代品。它旨在用于需要 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

请参阅 架构 部分,以手动添加字段。

使用

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

JWT

检索令牌

  1. 使用您的会话令牌

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

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

如果您的 auth 配置中添加了 bearer 插件,请确保在请求的 Authorization 标头中包含令牌。

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 与 jose 的示例

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。这应存储所有活动密钥,而不仅仅是当前密钥。
  • 如果使用本地化方法,请确保服务器在轮换时使用最新的私钥。根据部署情况,可能需要重启服务器。
  • 当使用远程方法时,请验证传输后负载未更改。如果可用,请使用完整性验证,如 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

表名:jwks

Field NameTypeKeyDescription
idstring每个 web 密钥的唯一标识符
publicKeystring-web 密钥的公钥部分
privateKeystring-web 密钥的私钥部分
createdAtDate-web 密钥创建的时间戳

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

选项

密钥对的算法

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

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

EdDSA

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

ES256

  • 无额外属性

RSA256

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

PS256

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

ECDH-ES

  • 可选属性crv
    • 可用选项:P-256P-384P-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 作为颁发者和受众。过期时间设置为 15 分钟。

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