instructure/canvas-lms · error

Failed to decode OpenID Connect back-channel logout token: #

Error message

Failed to decode OpenID Connect back-channel logout token: #{jwt_string.inspect}

What it means

OpenID Connect back-channel logout sends a signed `logout_token` JWT. Canvas decodes it with verification skipped just to inspect claims; if the string is not a structurally decodable JWT, Canvas::Security::InvalidToken is raised, the controller logs this warning, renders 400 'Invalid logout token', and increments an :invalid_token failure stat.

Solutions

  1. Verify the IdP sends an unencrypted signed JWS logout_token (or configure decryption if your setup supports JWE)
  2. Inspect jwt_string in the log — confirm the param is populated and not truncated by proxies or request limits
  3. Confirm the client posts logout_token as a form param on the back-channel logout URL
  4. Check Canvas::Security key configuration (JWK/signing keys) matches the issuer

Example fix

// before
curl -X POST $HOST/login/openid_connect/logout -d 'token=...'
// after
curl -X POST $HOST/login/openid_connect/logout -d 'logout_token=eyJhbGciOi...'
Defensive patterns

Strategy: validation

Validate before calling

logout_token = params['logout_token']
valid = logout_token.present? && logout_token.split('.').length == 3

Type guard

def jwt_like?(str)
  str.is_a?(String) && str.split('.').length == 3 && str.match?(/\A[\w-]+\.[\w-]+\.[\w-]*\z/)
end

Try / catch

begin
  token = Canvas::Security.decode_jwt(jwt_string, [:skip_verification])
rescue Canvas::Security::InvalidToken
  render plain: 'Invalid logout token', status: :bad_request
end

Prevention

When it happens

Trigger: POST to the OpenID Connect back-channel logout endpoint where params['logout_token'] is missing, empty, truncated, or not a base64url JWS (decode_jwt with [:skip_verification] still requires valid JWT structure).

Common situations: IdP misconfigured to send an encrypted JWE the decoder can't parse; token truncated by a proxy; client posting the token under the wrong param name; non-OP callers hitting the endpoint.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


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

Appendix: source

Thrown at app/controllers/login/openid_connect_controller.rb:63

    unless Pseudonym.expire_oidc_session(logout_token, request)
      increment_statsd(:failure, reason: :no_session)
      return render plain: "No session found", status: :not_found
    end

    increment_statsd(:success)
    render plain: "OK", status: :ok
  end

  def validate_logout_token
    # NOT SUPPORTED without redis
    return render plain: "NOT SUPPORTED", status: :method_not_allowed unless Canvas.redis_enabled?

    jwt_string = request.request_parameters["logout_token"]

    logout_token = begin
      ::Canvas::Security.decode_jwt(jwt_string, [:skip_verification])
    rescue ::Canvas::Security::InvalidToken
      Rails.logger.warn("Failed to decode OpenID Connect back-channel logout token: #{jwt_string.inspect}")
      render plain: "Invalid logout token", status: :bad_request
      increment_statsd(:failure, reason: :invalid_token)
      nil
    end
    return unless logout_token

    unless (missing_claims = %w[aud iss iat exp events jti] - logout_token.keys).empty?
      render plain: "Missing claim#{"s" if missing_claims.length > 1} #{missing_claims.join(", ")}",
             status: :bad_request
      increment_statsd(:failure, reason: :missing_claims)
      return
    end
    unless logout_token["events"].is_a?(Hash) && logout_token["events"][OIDC_BACKCHANNEL_LOGOUT_EVENT_URN].is_a?(Hash)
      render plain: "Invalid events", status: :bad_request
      increment_statsd(:failure, reason: :invalid_events)
      return
    end
    unless logout_token["sid"] || logout_token["sub"]

View on GitHub (pinned to 1c9f0bb801)