instructure/canvas-lms · error · OAuthValidationError

Invalid nonce claim in ID Token

Error message

Invalid nonce claim in ID Token

What it means

Raised in claims() when the ID token's nonce claim does not equal the nonce Canvas stored in the OAuth transaction (token.options[:nonce]). Nonce binds the ID token to this specific authorization request, preventing token replay/injection; a mismatch means the token may not correspond to this login attempt.

Solutions

  1. Verify the IdP echoes back the exact nonce sent in the authorization request (enable nonce support).
  2. Clear stale OAuth state and start a fresh login (avoid replayed/back-button callbacks).
  3. Ensure Canvas sessions are shared across all app nodes (Redis/shared session store) so token.options[:nonce] matches.
  4. Test the raw ID token to see what nonce the IdP actually returned and correct the request flow.

Example fix

# before: IdP configured without nonce support
authorization_url without nonce param -> token nonce nil
// after: enable OIDC nonce in IdP client settings so the returned ID token includes the request nonce
Defensive patterns

Strategy: validation

Validate before calling

payload = JSON.parse(Base64.urlsafe_decode64(id_token.split(".")[1]))
raise "nonce mismatch" unless payload["nonce"] == stored_nonce

Try / catch

begin
  provider.claims(token)
rescue OAuthValidationError => e
  Rails.logger.warn("OIDC nonce mismatch: #{e.message}")
  redirect_to login_path, alert: "Login session expired; please try again."
end

Prevention

When it happens

Trigger: claims(token) during unique_id/persist_to_session/provider_attributes finds id_token['nonce'] != token.options[:nonce] — e.g. the IdP echoes a different nonce, the request's nonce was regenerated, or the IdP does not support nonce and omits/alters it.

Common situations: Non-compliant IdP that ignores the nonce request parameter; back-button/replayed authorization reusing an old code with a stale token; load-balanced Canvas where session data (nonce) is not shared across nodes; clock/issue where the auth request was started before a provider settings change.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/8b4811f12cbae8e1. Report an issue: GitHub.

Appendix: source

Thrown at app/models/authentication_provider/open_id_connect.rb:295

          raise OAuthValidationError, t({ one: "Missing claim %{claims}", other: "Missing claims %{claims}" },
                                        count: missing_claims.length,
                                        claims: missing_claims.join(", "))
        end

        unless Array(id_token["aud"]).include?(client_id)
          raise OAuthValidationError, t("Invalid JWT audience: %{audience}", audience: id_token["aud"].inspect)
        end

        if self.class.validate_issuer?
          if issuer.blank?
            raise OAuthValidationError, t("No issuer configured for OpenID Connect provider")
          end
          unless issuer === id_token["iss"] # rubocop:disable Style/CaseEquality -- may be a string or a RegEx
            raise OAuthValidationError, t("Invalid JWT issuer: %{issuer}", issuer: id_token["iss"])
          end
        end
        unless id_token["nonce"] == token.options[:nonce]
          raise OAuthValidationError, t("Invalid nonce claim in ID Token")
        end

        if (signature_error = validate_signature(id_token))
          raise OAuthValidationError, t("Invalid signature: %{signature_error}", signature_error:)
        end

        # we have a userinfo endpoint, and we don't have everything we want,
        # then request more
        if userinfo_endpoint.present? && !(requested_claims - id_token.keys).empty?
          userinfo = token.get(userinfo_endpoint).parsed
          debug_set(:userinfo, userinfo.to_json) if instance_debugging
          # but only use it if it's for the user we logged in as
          # see http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
          if userinfo["sub"] == id_token["sub"]
            id_token.merge!(userinfo)
          end
        end
        id_token

View on GitHub (pinned to 1c9f0bb801)