module Samlr::Tools::ResponseBuilder

Use this for building test data, not ready to use for production data

Public Class Methods

build(options = {}) click to toggle source
# File lib/samlr/tools/response_builder.rb, line 11
def self.build(options = {})
  issue_instant     = options[:issue_instant]  || Samlr::Tools::Timestamp.stamp
  response_id       = options[:response_id]    || Samlr::Tools.uuid
  assertion_id      = options[:assertion_id]   || Samlr::Tools.uuid
  status_code       = options[:status_code]    || "urn:oasis:names:tc:SAML:2.0:status:Success"
  name_id_format    = options[:name_id_format] || EMAIL_FORMAT
  subject_conf_m    = options[:subject_conf_m] || "urn:oasis:names:tc:SAML:2.0:cm:bearer"
  version           = options[:version]        || "2.0"
  auth_context      = options[:auth_context]   || "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
  issuer            = options[:issuer]         || "ResponseBuilder IdP"
  attributes        = options[:attributes]     || {}
  name_id           = options[:name_id]
  name_qualifier    = options[:name_qualifier]
  sp_name_qualifier = options[:sp_name_qualifier]

  # Mandatory for responses
  destination     = options.fetch(:destination)
  in_response_to  = options.fetch(:in_response_to)
  not_on_or_after = options.fetch(:not_on_or_after)
  not_before      = options.fetch(:not_before)
  audience        = options.fetch(:audience)

  # Signature settings
  sign_assertion  = [ true, false ].member?(options[:sign_assertion]) ? options[:sign_assertion] : true
  sign_response   = [ true, false ].member?(options[:sign_response]) ? options[:sign_response] : true

  # Fixture controls
  skip_assertion  = options[:skip_assertion]
  skip_conditions = options[:skip_conditions]

  builder = Nokogiri::XML::Builder.new(:encoding => "UTF-8") do |xml|
    xml.Response("xmlns:samlp" => NS_MAP["samlp"], "ID" => response_id, "InResponseTo" => in_response_to, "Version" => version, "IssueInstant" => issue_instant, "Destination" => destination) do
      xml.doc.root.add_namespace_definition("saml", NS_MAP["saml"])
      xml.doc.root.namespace = xml.doc.root.namespace_definitions.find { |ns| ns.prefix == "samlp" }

      xml["saml"].Issuer(issuer)
      xml["samlp"].Status { |xml| xml["samlp"].StatusCode("Value" => status_code) }

      unless skip_assertion
        xml["saml"].Assertion("xmlns:saml" => NS_MAP["saml"], "ID" => assertion_id, "IssueInstant" => issue_instant, "Version" => "2.0") do
          xml["saml"].Issuer(issuer)

          xml["saml"].Subject do
            name_id_options = { "Format" => name_id_format}
            name_id_options.merge!("NameQualifier" => name_qualifier) unless name_qualifier.nil?
            name_id_options.merge!("SPNameQualifier" => sp_name_qualifier) unless sp_name_qualifier.nil?

            xml["saml"].NameID(name_id, name_id_options) unless name_id.nil?

            xml["saml"].SubjectConfirmation("Method" => subject_conf_m) do
              xml["saml"].SubjectConfirmationData("InResponseTo" => in_response_to, "NotOnOrAfter" => not_on_or_after, "Recipient" => destination)
            end
          end

          unless skip_conditions
            xml["saml"].Conditions("NotBefore" => not_before, "NotOnOrAfter" => not_on_or_after) do
              Array(audience).each do |audience_nodes|
                xml["saml"].AudienceRestriction do
                  Array(audience_nodes).each do |audience_value|
                    xml["saml"].Audience(audience_value)
                  end
                end
              end
            end
          end

          xml["saml"].AuthnStatement("AuthnInstant" => issue_instant, "SessionIndex" => assertion_id) do
            xml["saml"].AuthnContext do
              xml["saml"].AuthnContextClassRef(auth_context)
            end
          end

          unless attributes.empty?
            xml["saml"].AttributeStatement do
              attributes.keys.sort.each do |name|
                xml["saml"].Attribute("Name" => name) do
                  values = Array(attributes[name])
                  values.each do |value|
                    xml["saml"].AttributeValue(value, "xmlns:xsi" => NS_MAP["xsi"], "xmlns:xs" => NS_MAP["xs"], "xsi:type" => "xs:string")
                  end
                end
              end
            end
          end
        end
      end
    end
  end

  # The core response is ready, not on to signing
  response = builder.doc
  assertion_options = options.merge(:skip_keyinfo => options[:skip_assertion_keyinfo])
  response = sign(response, assertion_id, assertion_options) if sign_assertion

  response_options = options.merge(:skip_keyinfo => options[:skip_response_keyinfo])
  response = sign(response, response_id, response_options)  if sign_response

  response.to_xml(COMPACT)
end
digest(document, element, options) click to toggle source
# File lib/samlr/tools/response_builder.rb, line 133
def self.digest(document, element, options)
  c14n_method   = options[:c14n_method]   || "http://www.w3.org/2001/10/xml-exc-c14n#"
  sign_method   = options[:sign_method]   || "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
  digest_method = options[:digest_method] || "http://www.w3.org/2000/09/xmldsig#sha1"
  env_signature = options[:env_signature] || "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
  namespaces    = options[:namespaces]    || [ "#default", "samlp", "saml", "ds", "xs", "xsi" ]

  canoned       = element.canonicalize(C14N, namespaces)
  digest_value  = Base64.encode64(OpenSSL::Digest::SHA1.new.digest(canoned)).delete("\n")

  builder = Nokogiri::XML::Builder.new(:encoding => "UTF-8") do |xml|
    xml.Signature("xmlns" => NS_MAP["ds"]) do

      xml.SignedInfo do
        xml.CanonicalizationMethod("Algorithm" => c14n_method)
        xml.SignatureMethod("Algorithm" => sign_method)

        xml.Reference("URI" => "##{element['ID']}") do
          xml.Transforms do
            xml.Transform("Algorithm" => env_signature)
            xml.Transform("Algorithm" => c14n_method) do
              xml.InclusiveNamespaces("xmlns" => c14n_method, "PrefixList" => namespaces.join(" "))
            end
          end
          xml.DigestMethod("Algorithm" => digest_method)
          xml.DigestValue(digest_value)
        end
      end
    end
  end

  builder.doc.root
end
sign(document, element_id, options) click to toggle source
# File lib/samlr/tools/response_builder.rb, line 111
def self.sign(document, element_id, options)
  certificate  = options[:certificate] || Samlr::Tools::CertificateBuilder.new
  element      = document.at("//*[@ID='#{element_id}']")
  digest       = digest(document, element, options)
  canoned      = digest.at("./ds:SignedInfo", NS_MAP).canonicalize(C14N)
  signature    = certificate.sign(canoned)
  skip_keyinfo = options[:skip_keyinfo]

  Nokogiri::XML::Builder.with(digest) do |xml|
    xml.SignatureValue(signature)
    xml.KeyInfo do
      xml.X509Data do
        xml.X509Certificate(certificate.x509_as_pem)
      end
    end unless skip_keyinfo
  end
  # digest.root.last_element_child.after "<SignatureValue>#{signature}</SignatureValue>"
  element.at("./saml:Issuer", NS_MAP).add_next_sibling(digest)

  document
end