Échec de la connexion OIDC avec des réponses ID-token-only après la mise à niveau OAuth2 2.x

Après avoir mis à jour nos environnements Discourse auto-hébergés le 11 septembre 2026, la connexion OIDC a commencé à échouer avec l’erreur suivante :

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

Notre ancien environnement de développement continuait de fonctionner.

Environnement

  • Discourse exécuté dans Docker sur une machine virtuelle Linux Azure.
  • Azure Front Door → équilibreur de charge interne → VM.
  • Discourse hébergé sous /forum.
  • Politiques personnalisées Azure AD B2C pour OIDC.
  • Le trafic sortant de la VM est routé via Azure Firewall.
  • Révision Discourse en échec : 74f839fb7.
  • Révision de développement fonctionnelle : 502aa3687.

Les versions des dépendances pertinentes étaient les suivantes :

Dépendance Dev fonctionnel Production en échec
oauth2 1.4.11 2.0.25
omniauth-oauth2 1.7.3 1.9.0
jwt 2.10.1 3.2.0

Le dépôt mis à jour inclut d881bf2d4aabcf2430863a96beafaeabaf915702, « DEPS : Mise à niveau de oauth2 vers 2.x » (#43523).

Reproduction minimale

Ceci ne nécessite aucune connexion réseau, aucune infrastructure Azure ni de vraies identifiants :

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}"

Avec OAuth2 1.4.11, le jeton principal est vide et le paramètre du jeton ID reste "dummy-id".

Avec OAuth2 2.0.25, le jeton principal devient "dummy-id" et le paramètre du jeton ID devient nil.

Lien avec l’échec de Discourse

Dans la branche sans userinfo, le plugin OIDC construit le jeton en utilisant :

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

Ensuite, il tente :

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

Lorsque la bibliothèque consomme id_token comme jeton principal, la recherche du paramètre ultérieure renvoie nil.

Contournement testé

Nous avons modifié la construction du jeton pour préserver le jeton ID de la réponse d’origine :

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

Après avoir appliqué ce correctif et redémarré le conteneur UAT, la connexion a réussi. Aucune modification de Front Door, du pare-feu ou de B2C n’était nécessaire pour cette récupération.

Ceci préserve la validation existante des revendications (claims) et de la nonce, et laisse la branche userinfo inchangée.

La réponse brute du jeton en direct n’est pas incluse ; le comportement de la bibliothèque a été reproduit avec des données factices, et le contournement a été validé par une vraie connexion UAT.

Est-ce que c’est déjà couvert par un correctif en amont ? Sinon, je peux soumettre une PR ciblée avec une couverture de régression.

1 « J'aime »

Cela aurait dû être corrigé par :

Merci de nous faire savoir si vous rencontrez encore des problèmes après avoir déployé cet engagement.

7 « J'aime »

Merci. Cela résout le problème.

1 « J'aime »