错误信息
unexpected "aud" claim value
jose (JavaScript) — JWTClaimValidationFailed, code ERR_JWT_CLAIM_VALIDATION_FAILED
含义
签名验证通过了,但令牌的 `aud` 声明中不包含你作为 `audience` 传给 `jwtVerify` 的值。jose 按字符串精确比较:只要令牌 `aud`(字符串或数组)中至少有一个值等于你允许的某个受众,检查就会通过。令牌本身是真实的——只是它签发给了另一个接收方;接受它就意味着为一个 API 签发的令牌可以被重放到另一个 API。
原因
令牌是为另一个 API 签发的
常见一个授权服务器常常为多个资源服务器服务。客户端拿到订单 API 的令牌后把它发给计费 API,就会得到这个错误——这说明检查在正常发挥作用。错误的 `err.payload` 属性显示了令牌实际携带的声明。
同一个 API,标识符写法不同
常见受众匹配按字节精确比较:`https://api.example.com` 与 `https://api.example.com/`(末尾斜杠)、http 与 https,或者 `api://orders` 这样的逻辑名称与 API 的 URL,都是不同的值。身份提供商会从自己的配置中原样复制标识符,因此验证方必须使用完全相同的写法。
在需要访问令牌的地方发送了 ID 令牌
偶见在 OpenID Connect 中,ID 令牌的 `aud` 是让用户登录的那个应用的客户端 ID,而不是你的 API。因此,转发 ID 令牌而不是访问令牌的前端,无法通过其后方任何一个 API 的受众检查。
客户端没有请求特定的受众
偶见一些提供商(Auth0、Okta 自定义授权服务器、配置了受众映射器的 Keycloak)只有在令牌请求中指明你的 API 时,才会把它写入 `aud`。如果没有 `audience` 参数或 RFC 8707 的 `resource` 参数,令牌会签发给默认受众,例如 userinfo 端点。
修复方法
- 1.
把令牌携带的值与你期望的值并排打印出来
解码声明(但不要信任它们),逐个字符比较 `aud` 与你的 `audience` 选项。哪一边错了就修正哪一边——验证方的选项或颁发者的配置——但要保留这项检查。
js import { decodeJwt, jwtVerify } from "jose"; console.log("token aud:", decodeJwt(token).aud); // string or array of strings await jwtVerify(token, jwks, { algorithms: ["ES256"], issuer: "https://issuer.example.com", audience: "https://api.example.com/orders", // exact string match }); - 2.
为你要调用的 API 请求令牌
如果令牌是签发给另一个受众的,就应该在客户端修复:在令牌请求中指明目标 API,让授权服务器把它写入 `aud`。
bash # OAuth 2.0 client credentials: ask the authorization server for a token # minted for the API you are about to call (Auth0 uses "audience", # RFC 8707 resource indicators use "resource"). curl -s https://issuer.example.com/oauth/token \ -d grant_type=client_credentials \ -d client_id="$CLIENT_ID" -d client_secret="$CLIENT_SECRET" \ -d audience=https://api.example.com/orders - 3.
显式允许多个标识符,前提是它们都属于你
`audience` 接受数组。列出每个合法指代这个 API 的标识符(例如从 URL 迁移到逻辑名称期间),而不是删除该选项——没有 `audience`,jose 根本不会检查 `aud`。
js await jwtVerify(token, jwks, { algorithms: ["ES256"], audience: ["https://api.example.com/orders", "orders-api"], // any one must match });