class FakeIdp::SamlResponse
Constants
- ASSERTION_NAMESPACE
- BEARER_FORMAT
- CANONICAL_SCHEMA
- CANONICAL_VALUE
For the time being we're only supporting a single canonical schema since supporting multiple is inconsequential for our immediate need.
- DSIG
- EMAIL_ADDRESS_FORMAT
- ENTITY_FORMAT
- ENVELOPE_SCHEMA
- FEDERATION_SOURCE
- SAML_VERSION
- STATUS_CODE_VALUE
Public Class Methods
new( name_id:, issuer_uri:, saml_acs_url:, saml_request_id:, user_attributes:, algorithm_name:, certificate:, secret_key:, encryption_enabled: false )
click to toggle source
# File lib/fake_idp/saml_response.rb, line 25 def initialize( name_id:, issuer_uri:, saml_acs_url:, saml_request_id:, user_attributes:, algorithm_name:, certificate:, secret_key:, encryption_enabled: false ) @name_id = name_id @issuer_uri = issuer_uri @saml_acs_url = saml_acs_url @saml_request_id = saml_request_id @user_attributes = user_attributes @algorithm_name = algorithm_name @certificate = certificate @secret_key = secret_key @encryption_enabled = encryption_enabled @builder = Nokogiri::XML::Builder.new @timestamp = Time.now end
Public Instance Methods
build()
click to toggle source
# File lib/fake_idp/saml_response.rb, line 49 def build @builder[:samlp].Response(root_namespace_attributes) do |response| build_issuer_segment(response) build_status_segment(response) build_assertion_segment(response) end document_with_digest = replace_digest_value(@builder.to_xml) document = replace_signature_value(document_with_digest) encrypt_assertion!(document) end
Private Instance Methods
algorithm()
click to toggle source
# File lib/fake_idp/saml_response.rb, line 208 def algorithm raise "Algorithm name must be a Symbol" unless @algorithm_name.is_a?(Symbol) case @algorithm_name when :sha256 then OpenSSL::Digest::SHA256 when :sha384 then OpenSSL::Digest::SHA384 when :sha512 then OpenSSL::Digest::SHA512 else OpenSSL::Digest::SHA1 end end
assertion_namespace_attributes()
click to toggle source
# File lib/fake_idp/saml_response.rb, line 249 def assertion_namespace_attributes { "xmlns:saml" => ASSERTION_NAMESPACE, "ID" => assertion_reference_response_id, "IssueInstant" => @timestamp.strftime("%Y-%m-%dT%H:%M:%S"), "Version" => SAML_VERSION, } end
assertion_reference_response_id()
click to toggle source
# File lib/fake_idp/saml_response.rb, line 229 def assertion_reference_response_id @assertion_reference_response_id ||= "_#{SecureRandom.uuid}" end
authn_statement()
click to toggle source
# File lib/fake_idp/saml_response.rb, line 273 def authn_statement { "AuthnInstant" => @timestamp.strftime("%Y-%m-%dT%H:%M:%S"), "SessionIndex" => reference_response_id, } end
build_assertion_segment(parent_attribute)
click to toggle source
# File lib/fake_idp/saml_response.rb, line 132 def build_assertion_segment(parent_attribute) parent_attribute[:saml].Assertion(assertion_namespace_attributes) do |assertion| assertion[:saml].Issuer("Format" => ENTITY_FORMAT) do |issuer| issuer << @issuer_uri end build_assertion_signature(assertion) assertion[:saml].Subject do |subject| subject[:saml].NameID("Format" => EMAIL_ADDRESS_FORMAT) do |name_id| name_id << @name_id end subject[:saml].SubjectConfirmation("Method" => BEARER_FORMAT) do |subject_confirmation| subject_confirmation[:saml].SubjectConfirmationData(subject_confirmation_data) { "" } end end assertion[:saml].Conditions(saml_conditions) do |conditions| conditions[:saml].AudienceRestriction do |restriction| restriction[:saml].Audience { |audience| audience << @issuer_uri } end end assertion[:saml].AttributeStatement do |attribute_statement| @user_attributes.map do |name, value| attribute_statement[:saml].Attribute("Name" => name) do |attribute| attribute[:saml].AttributeValue { |attribute_value| attribute_value << value } end end end assertion[:saml].AuthnStatement(authn_statement) do |statement| statement[:saml].AuthnContext do |authn_context| authn_context[:saml].AuthnContextClassRef do |context_class_ref| context_class_ref << FEDERATION_SOURCE end end end end end
build_assertion_signature(parent_attribute)
click to toggle source
# File lib/fake_idp/saml_response.rb, line 174 def build_assertion_signature(parent_attribute) parent_attribute[:ds].Signature("xmlns:ds" => DSIG) do |signature| signature[:ds].SignedInfo("xmlns:ds" => DSIG) do |signed_info| signed_info[:ds].CanonicalizationMethod("Algorithm" => CANONICAL_SCHEMA) signed_info[:ds].SignatureMethod("Algorithm" => "#{DSIG}#{@algorithm_name}") signed_info[:ds].Reference("URI" => reference_uri) do |reference| reference[:ds].Transforms do |transform| transform[:ds].Transform("Algorithm" => ENVELOPE_SCHEMA) transform[:ds].Transform("Algorithm" => CANONICAL_SCHEMA) end reference[:ds].DigestMethod("Algorithm" => "#{DSIG}#{@algorithm_name}") # The digest_value is set and derived from creating a digest of the Assertion element # without the signature element after the document is generated reference[:ds].DigestValue("xmlns:ds" => DSIG) { |d| d << "" } end end # The signature_value is set and derived from signing the SignedInfo element after the # document is generated signature[:ds].SignatureValue { |signature_value| signature_value << "" } signature.KeyInfo("xmlns:ds" => DSIG) do |key_info| key_info[:ds].X509Data do |x509_data| x509_data[:ds].X509Certificate do |x509_certificate| x509_certificate << Base64.encode64(@certificate) end end end end end
build_issuer_segment(parent_attribute)
click to toggle source
# File lib/fake_idp/saml_response.rb, line 120 def build_issuer_segment(parent_attribute) parent_attribute[:saml].Issuer("xmlns:saml" => ASSERTION_NAMESPACE) do |issuer| issuer << @issuer_uri end end
build_status_segment(parent_attribute)
click to toggle source
# File lib/fake_idp/saml_response.rb, line 126 def build_status_segment(parent_attribute) parent_attribute[:samlp].Status do |status| status[:samlp].StatusCode("Value" => STATUS_CODE_VALUE) end end
encrypt_assertion!(document)
click to toggle source
# File lib/fake_idp/saml_response.rb, line 63 def encrypt_assertion!(document) return document unless @encryption_enabled document_copy = document.dup working_document = Nokogiri::XML(document) assertion = working_document.at_xpath("//saml:Assertion", "saml" => ASSERTION_NAMESPACE) encrypted_assertion_xml = FakeIdp::Encryptor.new( assertion.to_xml, @certificate, ).encrypt document_copy = Nokogiri::XML(document_copy) target_assertion_node = document_copy.at_xpath( "//saml:Assertion", "saml" => ASSERTION_NAMESPACE, ) # Replace Assertion node with encrypted assertion target_assertion_node.replace(encrypted_assertion_xml) document_copy.to_xml end
reference_response_id()
click to toggle source
# File lib/fake_idp/saml_response.rb, line 225 def reference_response_id @_reference_response_id ||= "_#{SecureRandom.uuid}" end
reference_uri()
click to toggle source
# File lib/fake_idp/saml_response.rb, line 233 def reference_uri "_#{assertion_reference_response_id}" end
replace_digest_value(document)
click to toggle source
# File lib/fake_idp/saml_response.rb, line 84 def replace_digest_value(document) document_copy = document.dup working_document = Nokogiri::XML(document) # The signature element needs to be removed from the assertion before creating a digest signature_element = working_document.at_xpath("//ds:Signature", "ds" => DSIG) signature_element.remove assertion_without_signature = working_document. at_xpath("//*[@ID=$id]", nil, "id" => assertion_reference_response_id) canon_hashed_element = assertion_without_signature.canonicalize(CANONICAL_VALUE) digest_value = Base64.encode64(algorithm.digest(canon_hashed_element)).strip # Replace digest node with the generated value document_copy = Nokogiri::XML(document_copy) target_digest_node = document_copy.at_xpath("//ds:DigestValue", "ds" => DSIG) target_digest_node.content = digest_value document_copy end
replace_signature_value(document)
click to toggle source
# File lib/fake_idp/saml_response.rb, line 105 def replace_signature_value(document) document_copy = document.dup signature_element = document.at_xpath("//ds:Signature", "ds" => DSIG) # The SignatureValue is a signed copy of the SignedInfo element signed_info_element = signature_element.at_xpath("./ds:SignedInfo", "ds" => DSIG) canon_string = signed_info_element.canonicalize(CANONICAL_VALUE) signature_value = sign(canon_string) target_signature_node = document_copy.at_xpath("//ds:SignatureValue", "ds" => DSIG) target_signature_node.content = signature_value document_copy.to_xml end
root_namespace_attributes()
click to toggle source
# File lib/fake_idp/saml_response.rb, line 237 def root_namespace_attributes { "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol", "Consent" => "urn:oasis:names:tc:SAML:2.0:consent:unspecified", "Destination" => @saml_acs_url, "ID" => reference_response_id, "InResponseTo" => @saml_request_id, "IssueInstant" => @timestamp.strftime("%Y-%m-%dT%H:%M:%S"), "Version" => SAML_VERSION, } end
saml_conditions()
click to toggle source
# File lib/fake_idp/saml_response.rb, line 266 def saml_conditions { "NotBefore" => (@timestamp - 5).strftime("%Y-%m-%dT%H:%M:%S"), "NotOnOrAfter" => (@timestamp + 60 * 60).strftime("%Y-%m-%dT%H:%M:%S"), } end
sign(data)
click to toggle source
# File lib/fake_idp/saml_response.rb, line 220 def sign(data) key = OpenSSL::PKey::RSA.new(@secret_key) Base64.encode64(key.sign(algorithm.new, data)).gsub(/\n/, "") end
subject_confirmation_data()
click to toggle source
# File lib/fake_idp/saml_response.rb, line 258 def subject_confirmation_data { "InResponseTo" => @saml_request_id, "NotOnOrAfter" => (@timestamp + 3 * 60).strftime("%Y-%m-%dT%H:%M:%S"), "Recipient" => @saml_acs_url, } end