Сообщение об ошибке
unexpected "iss" claim value
jose (JavaScript) — JWTClaimValidationFailed, code ERR_JWT_CLAIM_VALIDATION_FAILED
Что это означает
Криптографическая проверка токена прошла, но его утверждение `iss` не равно параметру `issuer`, который вы передали в `jwtVerify`. jose сравнивает строки точно (можно передать и массив разрешённых издателей), поэтому любое расхождение — косая черта в конце, другой путь тенанта, другое окружение — приводит к отказу. Именно закрепление издателя не даёт принять токен от другого поставщика удостоверений или от другого тенанта того же провайдера.
Почему это происходит
Отличается косая черта в конце или схема
частоПровайдеры здесь непоследовательны: у Auth0 издатель `https://tenant.auth0.com/` с косой чертой, у Keycloak и Entra ID v2.0 (`…/v2.0`) URL издателя без неё, а в токенах Entra ID v1.0 стоит `https://sts.windows.net/<tenant>/` с косой чертой. Для точного сравнения `https://login.example.com` и `https://login.example.com/` — разные издатели.
Не тот тенант, realm или сервер авторизации
частоМультитенантные провайдеры включают тенант в строку издателя: Entra ID — `https://login.microsoftonline.com/<tenant-id>/v2.0`, Keycloak — `https://host/realms/<realm>`, Okta — `https://<org>.okta.com/oauth2/<server-id>`. Эта ошибка возникает, если проверять по серверу по умолчанию, когда токены приходят от собственного сервера (или путать эндпоинты Entra v1.0 и v2.0).
Токен staging-окружения проверяют в рабочей среде
иногдаВ токенах из staging-тенанта или тенанта разработчика указан издатель этого тенанта. Когда конфигурацию копируют между окружениями, настройки проверяющей стороны и источник токенов перестают совпадать.
Законных издателей несколько, а настроен один
иногдаСервисы, которые принимают токены от нескольких поставщиков удостоверений, иногда настраивают только первого из них. Решение — продуманный список разрешённых издателей с отдельным набором ключей для каждого, а не отказ от параметра `issuer`.
Как исправить
- 1.
Берите строку издателя из документа обнаружения
По спецификации значение `issuer` в `/.well-known/openid-configuration` — ровно то, что провайдер записывает в `iss`. Скопируйте его в свою конфигурацию байт в байт.
bash # The issuer string is defined by the provider's discovery document. curl -s https://login.example.com/.well-known/openid-configuration | jq -r .issuer - 2.
Закрепите именно это значение в jwtVerify
Не убирайте параметр `issuer` (без него jose вообще не проверяет `iss`) и храните значение в одной константе, общей для URL JWKS и проверяющей стороны.
js import { createRemoteJWKSet, jwtVerify } from "jose"; const ISSUER = "https://login.example.com/"; // exactly as in "issuer" from discovery const jwks = createRemoteJWKSet(new URL("https://login.example.com/.well-known/jwks.json")); const { payload } = await jwtVerify(token, jwks, { algorithms: ["ES256"], issuer: ISSUER }); - 3.
Принимайте нескольких издателей, у каждого — свой набор ключей
Если ваш сервис действительно доверяет нескольким издателям, выбирайте набор ключей по (ещё не проверенному) `iss` токена из явного списка разрешённых и затем проверяйте токен, закрепив этого издателя. Никогда не позволяйте проверять токены одного издателя ключами другого.
js import { createRemoteJWKSet, decodeJwt, jwtVerify } from "jose"; // Each trusted issuer gets its own key set; an unknown iss is rejected before any key is used. const trusted = new Map([ ["https://login.example.com/", createRemoteJWKSet(new URL("https://login.example.com/.well-known/jwks.json"))], ["https://partner.example.org", createRemoteJWKSet(new URL("https://partner.example.org/jwks"))], ]); const { iss } = decodeJwt(token); const keys = trusted.get(iss); if (!keys) throw new Error("untrusted issuer"); await jwtVerify(token, keys, { algorithms: ["ES256", "RS256"], issuer: iss });