Вход через OIDC не работает с ответами, содержащими только ID-token, после обновления до OAuth2 2.x

После обновления наших self-hosted окружений Discourse 11 сентября 2026 года вход через OIDC начал завершаться ошибкой:

(oidc) Authentication failure! jwt_decode_failed:
JWT::DecodeError, Nil JSON web token

Наше старое dev-окружение продолжало работать.

Окружение

  • Discourse работает в Docker на виртуальной машине Azure Linux.
  • Azure Front Door → внутренний балансировщик нагрузки → ВМ.
  • Discourse размещен по пути /forum.
  • Пользовательские политики Azure AD B2C для OIDC.
  • Исходящий трафик ВМ маршрутизируется через Azure Firewall.
  • Версия Discourse с ошибкой: 74f839fb7.
  • Рабочая dev-версия: 502aa3687.

Соответствующие версии зависимостей:

Зависимость Рабочая dev Ошибочная production
oauth2 1.4.11 2.0.25
omniauth-oauth2 1.7.3 1.9.0
jwt 2.10.1 3.2.0

Обновленный репозиторий включает коммит d881bf2d4aabcf2430863a96beafaeabaf915702, «DEPS: Upgrade oauth2 to 2.x» (#43523).

Минимальное воспроизведение

Для этого не требуется подключение к сети, инфраструктура Azure или реальные учетные данные:

require "oauth2"

puts Gem.loaded_specs.fetch("oauth2").version

client = OAuth2::Client.new(
  "dummy",
  "dummy",
  site: "https://example.invalid"
)

token = OAuth2::AccessToken.from_hash(
  client,
  {"id_token" => "dummy-id"}
)

puts "Main token: #{token.token.inspect}"
puts "ID token parameter: #{token["id_token"].inspect}"

С OAuth2 1.4.11 основной токен пуст, а параметр ID-токена остается "dummy-id".

С OAuth2 2.0.25 основной токен становится "dummy-id", а параметр ID-токена становится nil.

Связь с ошибкой в Discourse

В ветке no-userinfo плагин OIDC конструирует токен с помощью:

::OAuth2::AccessToken.from_hash(client, response.parsed)

Затем он пытается выполнить:

::JWT.decode(access_token["id_token"], nil, false).first

Когда библиотека использует id_token в качестве основного токена, последующий поиск параметра возвращает nil.

Проверенное обходное решение

Мы изменили конструирование токена, чтобы сохранить ID-токен из исходного ответа:

payload = response.parsed
token = ::OAuth2::AccessToken.from_hash(client, payload)
token.params["id_token"] = payload["id_token"] if payload["id_token"]
token

После применения этого патча и перезапуска UAT-контейнера вход стал успешным. Для этого восстановления не потребовалось вносить изменения в Front Door, брандмауэр или B2C.

Это сохраняет существующую валидацию claim и nonce и не изменяет ветку userinfo.

Сырой ответ с живым токеном не включен; поведение библиотеки было воспроизведено с фиктивными данными, а обходное решение было проверено через реальный вход в UAT.

Это уже покрыто исправлением upstream? В противном случае я могу отправить сфокусированный PR с покрытием регрессионных тестов.

1 лайк

Это должно было быть исправлено в:

Пожалуйста, сообщите нам, если после развёртывания этого коммита вы по-прежнему сталкиваетесь с проблемами.

7 лайков

Спасибо. Это решает проблему.

1 лайк