После обновления наших 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 с покрытием регрессионных тестов.