jose (JavaScript)JWKS 与密钥发现
错误信息
multiple matching keys found in the JSON Web Key Set
jose (JavaScript) — JWKSMultipleMatchingKeys, code ERR_JWKS_MULTIPLE_MATCHING_KEYS
含义
JSON Web 密钥集中有不止一个密钥可以验证这个令牌,而 jose 拒绝猜测。当令牌头部没有 `kid`(或者多个密钥共用同一个 `kid`),且密钥集中有两个或更多类型和曲线都符合的密钥时,就会出现这种情况。jose 有意不在 `jwtVerify` 内部替你逐个尝试候选密钥;抛出的错误本身就是一个遍历候选密钥的异步迭代器,你可以主动选择逐个尝试。
原因
颁发者没有在令牌头部中写入 kid
常见许多小型或自行编写的颁发者在签名时只写 `{ "alg": "ES256" }`。只要 JWKS 中只有一个密钥,这样就能正常工作;一旦第一次轮换发布了第二个密钥,所有验证方都会出错。
发布的密钥没有 kid
常见即使令牌带有 `kid`,如果 JWKS 中的密钥没有 `kid`,也无法按标识符匹配。在轮换期间,这样的密钥集同时包含即将退役的密钥和新启用的密钥,两者看起来同样合理。
两个密钥共用同一个 kid
偶见有些密钥管理脚本根据固定名称或算法(`"es256"`)而不是密钥本身生成 `kid`,结果新旧密钥的标识符相同。使用 RFC 7638 指纹可以避免这个问题。
修复方法
- 1.
在每个令牌中标明签名密钥
稳妥的修复在颁发者一侧:为每个密钥分配唯一的 `kid`(RFC 7638 指纹就很合适),在 JWKS 中发布它,并把它写入每个令牌的受保护头部。
js import { SignJWT } from "jose"; // Issuer side: name the key in every token, and publish the same kid in the JWKS. const token = await new SignJWT(claims) .setProtectedHeader({ alg: "ES256", kid: "2026-10" }) .setExpirationTime("10m") .sign(privateKey); - 2.
主动选择逐个尝试候选密钥
如果你无法修改颁发者,就遍历这个错误:jose 会逐个给出候选密钥,只有其中某个密钥验证通过时,你才接受这个令牌。每多一个候选密钥,就要多做一次签名验证,所以要让密钥集保持精简。
js import { errors, jwtVerify } from "jose"; async function verifyWithCandidates(token, jwks, options) { try { return await jwtVerify(token, jwks, options); } catch (err) { if (err?.code !== "ERR_JWKS_MULTIPLE_MATCHING_KEYS") throw err; for await (const publicKey of err) { try { return await jwtVerify(token, publicKey, options); } catch (inner) { if (inner?.code === "ERR_JWS_SIGNATURE_VERIFICATION_FAILED") continue; throw inner; } } throw new errors.JWSSignatureVerificationFailed(); } }