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(),
]
})
迁移数据库
运行迁移或生成模式以将必要的字段和表添加到数据库中。
npx @better-auth/cli migrate
npx @better-auth/cli generate
请参阅 Schema(模式) 部分以手动添加字段。
使用
安装插件后,您可以通过相应的端点开始使用 JWT 和 JWKS 插件来获取令牌和 JWKS。
JWT
获取令牌
- 使用会话令牌
要获取令牌,请调用 /token
端点。这将返回以下内容:
{
"token": "ey..."
}
请确保在请求的 Authorization
头部中包含令牌,并在身份验证配置中添加了 bearer
插件。
await fetch("/api/auth/token", {
headers: {
"Authorization": `Bearer ${token}`
},
})
- 从
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
)。
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 上时,此功能非常有用。
注意:您必须指定用于签名的不对称算法。
jwt({
jwks: {
remoteUrl: "https://example.com/.well-known/jwks.json",
keyPairConfig: {
alg: 'ES256',
},
}
})
自定义签名
这是一个高级功能。必须提供此插件之外的配置。
实现说明:
- 如果使用
sign
函数,则必须定义remoteUrl
。该 URL 应存储所有活跃密钥,而不仅仅是当前密钥。 - 如果使用本地化方法,请确保服务器在密钥轮换时使用最新的私钥。根据部署情况,可能需要重启服务器。
- 使用远程方法时,请验证传输后负载是否未更改。如果可用,请使用 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}`
// verifyJwt(jwt)
return jwt
},
},
})
数据库结构
JWT 插件会向数据库添加以下表:
JWKS(JSON Web 密钥集)
表名:jwks
字段名称 | 类型 | Key | 描述 |
---|---|---|---|
id | string | 每个 Web 密钥的唯一标识符 | |
publicKey | string | - | Web 密钥的公共部分 |
privateKey | string | - | Web 密钥的私有部分 |
createdAt | Date | - | Web 密钥创建的时间戳 |
您可以自定义 jwks
表的表名和字段。有关如何自定义插件模式的更多信息,请参阅数据库概念文档。
配置选项
密钥对的算法
用于生成密钥对的算法。默认为使用 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
}
})
修改 JWT 载荷
默认情况下,整个用户对象会被添加到 JWT 载荷中。你可以通过为 definePayload
选项提供一个函数来修改载荷内容。
jwt({
jwt: {
definePayload: ({user}) => {
return {
id: user.id,
email: user.email,
role: user.role
}
}
}
})
修改签发者、受众、主题或过期时间
如果没有指定,系统将使用 BASE_URL
作为签发者,并将受众设置为 BASE_URL
。默认过期时间为 15 分钟。
jwt({
jwt: {
issuer: "https://example.com",
audience: "https://example.com",
expirationTime: "1h",
getSubject: (session) => {
// 默认主题是用户ID
return session.user.email
}
}
})