OIDC login fails with ID-token-only responses after OAuth2 2.x upgrade

After updating our self-hosted Discourse environments on 11 September 2026, OIDC login started failing with:

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

Our older dev environment continued working.

Environment

  • Discourse running in Docker on an Azure Linux VM.
  • Azure Front Door → internal load balancer → VM.
  • Discourse hosted under /forum.
  • Azure AD B2C custom policies for OIDC.
  • VM outbound traffic routed through Azure Firewall.
  • Failing Discourse revision: 74f839fb7.
  • Working dev revision: 502aa3687.

The relevant dependency versions were:

Dependency Working dev Failing production
oauth2 1.4.11 2.0.25
omniauth-oauth2 1.7.3 1.9.0
jwt 2.10.1 3.2.0

The updated repository includes d881bf2d4aabcf2430863a96beafaeabaf915702, “DEPS: Upgrade oauth2 to 2.x” (#43523).

Minimal reproduction

This requires no network connection, Azure infrastructure or real credentials:

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

With OAuth2 1.4.11, the main token is empty and the ID token parameter remains "dummy-id".

With OAuth2 2.0.25, the main token becomes "dummy-id" and the ID token parameter becomes nil.

Connection to the Discourse failure

In the no-userinfo branch, the OIDC plugin constructs the token using:

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

Later, it attempts:

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

When the library consumes id_token as the main token, the subsequent parameter lookup returns nil.

Tested workaround

We changed the token construction to preserve the ID token from the original response:

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

After applying this patch and restarting the UAT container, login succeeded. No Front Door, firewall or B2C changes were needed for that recovery.

This preserves the existing claim and nonce validation and leaves the userinfo branch unchanged.

The raw live token response is not included; the library behavior was reproduced with fake data, and the workaround was validated through a real UAT login.

Is this already covered by an upstream fix? Otherwise, I can submit a focused PR with regression coverage.

1 like

This should’ve been fixed by:

Please let us know if you’re still seeing issues after deploying that commit.

6 likes

Thank you. This resolves the issue.