JWT
在无法使用 session 的服务中使用 JWT 令牌进行用户认证
JWT 插件提供了用于获取 JWT 令牌的端点以及用于验证令牌的 JWKS 端点。
该插件并非用来替代 session。它适用于需要使用 JWT 令牌的服务。如果你想使用 JWT 令牌进行身份验证,请查看 Bearer 插件。
安装
将插件添加到你的 auth 配置中
import { betterAuth } from "better-auth"
import { jwt } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [
jwt(),
]
})将客户端插件添加到你的 auth 客户端
import { createAuthClient } from "better-auth/client"
import { jwtClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [
jwtClient()
]
})用法
安装插件后,你可以通过相应端点开始使用 JWT 和 JWKS 插件以获取令牌及 JWKS。
JWT
获取令牌
有多种方式可以获取 JWT 令牌:
- 使用客户端插件(推荐)
import { authClient } from "@/lib/auth-client"
const { data, error } = await authClient.token()
if (error) {
// 处理错误
}
if (data) {
const jwtToken = data.token
// 使用此令牌对外部服务进行身份验证请求
}这是客户端应用程序获取 JWT 令牌以进行外部 API 认证的推荐方法。
- 使用你的 session 令牌
调用 /token 端点可获取令牌,返回如下:
{
"token": "ey..."
}如果你的 auth 配置中添加了 bearer 插件,请确保在请求的 Authorization 头中包含该令牌。
await fetch("/api/auth/token", {
headers: {
"Authorization": `Bearer ${token}`
},
})- 从
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)。
import { betterAuth } from "better-auth";
betterAuth({
disabledPaths: [
"/token",
],
plugins: [jwt({
disableSettingJwtHeader: true,
})]
})远程 JWKS 地址
禁用 /jwks 端点,并使用此地址用于任何发现(例如 OIDC)。
当 JWKS 不托管在 /jwks,或你的 JWKS 由证书签名并放在 CDN 上时,此功能非常有用。
注意:你必须指定用于签名的非对称算法。
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 规则
- 避免与其他端点路径冲突
服务器配置:
jwt({
jwks: {
jwksPath: "/.well-known/jwks.json"
}
})客户端配置:
服务器使用自定义 jwksPath 时,客户端必须使用相同路径配置:
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 等完整性校验。
本地签名示例
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、Amazon KMS 或 Azure Key Vault。
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
你可以自定义 jwks 表的表名和字段。详见 数据库概念文档 获取如何自定义插件 Schema 的更多信息。
选项
密钥对算法
用于生成密钥对的算法,默认是基于 Ed25519 曲线的 EdDSA。可用选项如下:
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 禁用加密。
出于安全考虑,建议保持私钥加密。
jwt({
jwks: {
disablePrivateKeyEncryption: true
}
})密钥轮换
你可以通过设置 rotationInterval 选项启用密钥轮换。该选项会在指定间隔内自动轮换密钥对。
默认值为 undefined(禁用)。
jwt({
jwks: {
rotationInterval: 60 * 60 * 24 * 30, // 30 天
gracePeriod: 60 * 60 * 24 * 30 // 30 天
}
})rotationInterval:密钥轮换的秒数间隔。gracePeriod:轮换后旧密钥副本仍有效的秒数。用于允许客户端验证由旧密钥签发的令牌。默认是 30 天。
修改 JWT 载荷
默认将整个用户对象加入 JWT 载荷。你可以通过 definePayload 选项传入函数修改载荷。
jwt({
jwt: {
definePayload: ({user}) => {
return {
id: user.id,
email: user.email,
role: user.role
}
}
}
})修改 Issuer、Audience、Subject 或过期时间
若不设置,则默认使用 BASE_URL 作为发行者,Audience 也设为 BASE_URL,过期时间为 15 分钟。
jwt({
jwt: {
issuer: "https://example.com",
audience: "https://example.com",
expirationTime: "1h",
getSubject: (session) => {
// 默认 subject 是用户 ID
return session.user.email
}
}
})自定义适配器
默认情况下,JWT 插件会从你的数据库中存储和获取 JWKS。你可以提供自定义适配器覆盖此行为,使 JWKS 可存储在 Redis、外部服务或内存等其他位置。
jwt({
adapter: {
getJwks: async (ctx) => {
// 自定义实现,获取所有 JWKS
// 覆盖默认的数据库查询
return await yourCustomStorage.getAllKeys()
},
createJwk: async (ctx, webKey) => {
// 自定义实现,创建新密钥
// 覆盖默认数据库插入
return await yourCustomStorage.createKey(webKey)
}
}
})