instructure/canvas-lms · error

Must provide exactly one IDPSSODescriptor; found #

Error message

Must provide exactly one IDPSSODescriptor; found #{idps.length}

What it means

AuthenticationProvider::SAML#populate_from_metadata requires SAML metadata XML to contain exactly one IDPSSODescriptor. If the parsed entity has zero or multiple IdP descriptors, configuration is ambiguous and the provider refuses to populate settings.

Solutions

  1. Provide metadata containing exactly one IDPSSODescriptor, or ensure idp_entity_id is set so a Group can be narrowed to the matching single entity
  2. Extract/download the specific IdP entity's metadata from the federation aggregate instead of the whole aggregate
  3. Validate the metadata XML locally (count IDPSSODescriptor elements) before saving

Example fix

# before
provider.populate_from_metadata_xml(aggregated_federation_xml)
# after
entity = SAML2::Entity.parse(aggregated_federation_xml)
provider.idp_entity_id = 'https://idp.example.com/saml'
provider.populate_from_metadata_xml(aggregated_federation_xml)
Defensive patterns

Strategy: validation

Validate before calling

entity = SAML2::Entity.parse(xml)
idps = entity.identity_providers
raise 'need exactly one IdP' unless idps.length == 1

Type guard

null

Try / catch

begin
  provider.populate_from_metadata_xml(xml)
rescue RuntimeError => e
  Rails.logger.warn("SAML metadata rejected: #{e.message}")
  flash[:error] = 'Metadata must contain exactly one IdP'
end

Prevention

When it happens

Trigger: Calling populate_from_metadata (via download_metadata or populate_from_metadata_xml) with metadata containing 0 or 2+ IDPSSODescriptor elements (e.g. an EntitiesDescriptor/Group with several IdPs and no idp_entity_id set, or SP-only metadata).

Common situations: Admin pastes aggregated federation metadata (many IdPs) instead of a single IdP's metadata; uploads metadata XML missing the IdP SSDescriptor; entity_id mismatch causes a Group not to be narrowed to one entity.

Related errors


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

Appendix: source

Thrown at app/models/authentication_provider/saml.rb:297

    # support using 'false' to disable
    value = nil if ::Canvas::Plugin.value_to_boolean(value, ignore_unrecognized: true) == false

    unless [nil,
            SAML2::Bindings::HTTPRedirect::SigAlgs::RSA_SHA1,
            SAML2::Bindings::HTTPRedirect::SigAlgs::RSA_SHA256].include?(value)
      errors.add("Unsupported signing algorithm #{value}")
      return
    end
    settings["sig_alg"] = value
  end

  def self.name_id_formats
    SAML2::NameID::Format.constants.map { |const| SAML2::NameID::Format.const_get(const, false) }.sort_by(&:downcase)
  end

  def populate_from_metadata(entity)
    idps = entity.identity_providers
    raise "Must provide exactly one IDPSSODescriptor; found #{idps.length}" unless idps.length == 1

    idp = idps.first
    self.idp_entity_id = entity.entity_id
    self.log_in_url = idp.single_sign_on_services.find { |ep| ep.binding == SAML2::Bindings::HTTPRedirect::URN }.try(:location)
    self.log_out_url = idp.single_logout_services.find { |ep| ep.binding == SAML2::Bindings::HTTPRedirect::URN }.try(:location)
    self.certificate_fingerprint = idp.signing_keys.filter_map(&:fingerprint).join(" ").presence || idp.keys.first&.fingerprint

    recognized_formats = (idp.name_id_formats & self.class.name_id_formats)
    if recognized_formats.length == 1
      self.identifier_format = recognized_formats.first
    elsif identifier_format != SAML2::NameID::Format::UNSPECIFIED &&
          !recognized_formats.include?(identifier_format)
      self.identifier_format = SAML2::NameID::Format::UNSPECIFIED
    end

    settings[:signing_certificates] = idp.signing_keys.filter_map(&:x509)
    settings[:signing_keys] = idp.signing_keys.filter_map(&:key).map(&:to_s)
    case idp.want_authn_requests_signed?

View on GitHub (pinned to 1c9f0bb801)