JWT

在无法使用 session 的服务中使用 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 auth migrate
npx auth generate

如需手动添加字段,请参见 Schema 部分。

将客户端插件添加到你的 auth 客户端

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

export const authClient = createAuthClient({
  plugins: [
    jwtClient() 
  ]
})

用法

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

JWT

获取令牌

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

  1. 使用客户端插件(推荐)
import { authClient } from "@/lib/auth-client"

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

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

  1. 使用你的 session 令牌

调用 /token 端点可获取令牌,返回如下:

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

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

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

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

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。

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

使用 jose 和远程 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('令牌验证失败:', 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('令牌验证失败:', error)
    throw error
  }
}

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

OAuth 提供者模式

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

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

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

远程 JWKS 地址

禁用 /jwks 端点,并使用此地址用于任何发现(例如 OIDC)。

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

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

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

自定义 JWKS 路径

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

有以下用途:

  • 遵循 OAuth 2.0/OIDC 规范(例如 /.well-known/jwks.json
  • 匹配应用中的现有 API 规则
  • 避免与其他端点路径冲突

服务器配置:

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

客户端配置:

服务器使用自定义 jwksPath 时,客户端必须使用相同路径配置:

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() 方法:

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

客户端配置的 jwksPath 必须与服务器配置匹配,否则客户端将无法获取 JWKS。

自定义签名

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

实现要点:

  • 使用 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}`
      // 验证 JWT (verifyJwt(jwt))
      return jwt
    },
  },
})

Schema

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

JWKS

表名:jwks

Table
字段
类型
描述
id
string
pk
每个 Web 密钥的唯一标识符
publicKey
string
Web 密钥的公钥部分
privateKey
string
Web 密钥的私钥部分
createdAt
Date
Web 密钥的创建时间戳
expiresAt
Date
?
Web 密钥的过期时间戳

你可以自定义 jwks 表的表名和字段。详见 数据库概念文档 获取如何自定义插件 Schema 的更多信息。

选项

密钥对算法

用于生成密钥对的算法,默认是基于 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
  }
})

密钥轮换

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

默认值为 undefined(禁用)。

auth.ts
jwt({
  jwks: {
    rotationInterval: 60 * 60 * 24 * 30, // 30 天
    gracePeriod: 60 * 60 * 24 * 30 // 30 天
  }
})
  • rotationInterval:密钥轮换的秒数间隔。
  • gracePeriod:轮换后旧密钥副本仍有效的秒数。用于允许客户端验证由旧密钥签发的令牌。默认是 30 天。

修改 JWT 载荷

默认将整个用户对象加入 JWT 载荷。你可以通过 definePayload 选项传入函数修改载荷。

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

修改 Issuer、Audience、Subject 或过期时间

若不设置,则默认使用 BASE_URL 作为发行者,Audience 也设为 BASE_URL,过期时间为 15 分钟。

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、外部服务或内存等其他位置。

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