JWT
JWT 插件提供端点来检索 JWT 令牌,以及用于验证令牌的 JWKS 端点。
此插件并非会话的替代品。它旨在用于需要 JWT 令牌的服务。如果您希望使用 JWT 令牌进行身份验证,请查看 Bearer 插件。
安装
将插件添加到您的 auth 配置
import { betterAuth } from "better-auth"
import { jwt } from "better-auth/plugins"
export const auth = betterAuth({
plugins: [
jwt(),
]
})使用
安装插件后,您就可以开始使用 JWT 和 JWKS 插件,通过各自的端点获取令牌和 JWKS。
JWT
检索令牌
- 使用您的会话令牌
要获取令牌,请调用 /token 端点。这将返回以下内容:
{
"token": "ey..."
}如果您的 auth 配置中添加了 bearer 插件,请确保在请求的 Authorization 标头中包含令牌。
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 与 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 上,则此选项很有用。
注意:您 必须 指定用于签名的非对称算法。
jwt({
jwks: {
remoteUrl: "https://example.com/.well-known/jwks.json",
keyPairConfig: {
alg: 'ES256',
},
}
})自定义签名
这是一个高级功能。此插件之外的配置 必须 提供。
实现者:
- 如果使用
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}`
// verifyJwt(jwt)
return jwt
},
},
})架构
JWT 插件向数据库添加以下表:
JWKS
表名:jwks
| Field Name | Type | Key | Description |
|---|---|---|---|
| id | string | 每个 web 密钥的唯一标识符 | |
| publicKey | string | - | web 密钥的公钥部分 |
| privateKey | string | - | web 密钥的私钥部分 |
| createdAt | Date | - | web 密钥创建的时间戳 |
您可以自定义 jwks 表的表名和字段。请参阅 数据库概念文档,以获取有关如何自定义插件架构的更多信息。
选项
密钥对的算法
用于生成密钥对的算法。默认使用 EdDSA 和 Ed25519 曲线。以下是可用选项:
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 作为颁发者和受众。过期时间设置为 15 分钟。
jwt({
jwt: {
issuer: "https://example.com",
audience: "https://example.com",
expirationTime: "1h",
getSubject: (session) => {
// 默认情况下,主体是用户 ID
return session.user.email
}
}
})