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.