1746 lines
96 KiB
Ruby
1746 lines
96 KiB
Ruby
require File.expand_path(File.join(File.dirname(__FILE__), "test_helper"))
|
|
|
|
require 'onelogin/ruby-saml/response'
|
|
|
|
class RubySamlTest < Minitest::Test
|
|
|
|
describe "Response" do
|
|
|
|
let(:settings) { OneLogin::RubySaml::Settings.new }
|
|
let(:response) { OneLogin::RubySaml::Response.new(response_document_without_recipient) }
|
|
let(:response_without_attributes) { OneLogin::RubySaml::Response.new(response_document_without_attributes) }
|
|
let(:response_with_multiple_attribute_statements) { OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_statements)) }
|
|
let(:response_without_reference_uri) { OneLogin::RubySaml::Response.new(response_document_without_reference_uri) }
|
|
let(:response_with_signed_assertion) { OneLogin::RubySaml::Response.new(response_document_with_signed_assertion) }
|
|
let(:response_with_ds_namespace_at_the_root) { OneLogin::RubySaml::Response.new(response_document_with_ds_namespace_at_the_root)}
|
|
let(:response_unsigned) { OneLogin::RubySaml::Response.new(response_document_unsigned) }
|
|
let(:response_wrapped) { OneLogin::RubySaml::Response.new(response_document_wrapped) }
|
|
let(:response_multiple_attr_values) { OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values)) }
|
|
let(:response_valid_signed) { OneLogin::RubySaml::Response.new(response_document_valid_signed) }
|
|
let(:response_valid_signed_without_recipient) { OneLogin::RubySaml::Response.new(response_document_valid_signed, {:skip_recipient_check => true })}
|
|
let(:response_valid_signed_without_x509certificate) { OneLogin::RubySaml::Response.new(response_document_valid_signed_without_x509certificate) }
|
|
let(:response_no_id) { OneLogin::RubySaml::Response.new(read_invalid_response("no_id.xml.base64")) }
|
|
let(:response_no_version) { OneLogin::RubySaml::Response.new(read_invalid_response("no_saml2.xml.base64")) }
|
|
let(:response_multi_assertion) { OneLogin::RubySaml::Response.new(read_invalid_response("multiple_assertions.xml.base64")) }
|
|
let(:response_no_conditions) { OneLogin::RubySaml::Response.new(read_invalid_response("no_conditions.xml.base64")) }
|
|
let(:response_no_conditions_with_skip) { OneLogin::RubySaml::Response.new(read_invalid_response("no_conditions.xml.base64"), { :skip_conditions => true }) }
|
|
let(:response_no_authnstatement) { OneLogin::RubySaml::Response.new(read_invalid_response("no_authnstatement.xml.base64")) }
|
|
let(:response_no_authnstatement_with_skip) { OneLogin::RubySaml::Response.new(read_invalid_response("no_authnstatement.xml.base64"), {:skip_authnstatement => true}) }
|
|
let(:response_empty_destination) { OneLogin::RubySaml::Response.new(read_invalid_response("empty_destination.xml.base64")) }
|
|
let(:response_empty_destination_with_skip) { OneLogin::RubySaml::Response.new(read_invalid_response("empty_destination.xml.base64"), {:skip_destination => true}) }
|
|
let(:response_no_status) { OneLogin::RubySaml::Response.new(read_invalid_response("no_status.xml.base64")) }
|
|
let(:response_no_statuscode) { OneLogin::RubySaml::Response.new(read_invalid_response("no_status_code.xml.base64")) }
|
|
let(:response_statuscode_responder) { OneLogin::RubySaml::Response.new(read_invalid_response("status_code_responder.xml.base64")) }
|
|
let(:response_statuscode_responder_and_msg) { OneLogin::RubySaml::Response.new(read_invalid_response("status_code_responer_and_msg.xml.base64")) }
|
|
let(:response_double_statuscode) { OneLogin::RubySaml::Response.new(response_document_double_status_code) }
|
|
let(:response_encrypted_attrs) { OneLogin::RubySaml::Response.new(response_document_encrypted_attrs) }
|
|
let(:response_no_signed_elements) { OneLogin::RubySaml::Response.new(read_invalid_response("no_signature.xml.base64")) }
|
|
let(:response_multiple_signed) { OneLogin::RubySaml::Response.new(read_invalid_response("multiple_signed.xml.base64")) }
|
|
let(:response_audience_self_closed) { OneLogin::RubySaml::Response.new(read_response("response_audience_self_closed_tag.xml.base64")) }
|
|
let(:response_invalid_audience) { OneLogin::RubySaml::Response.new(read_invalid_response("invalid_audience.xml.base64")) }
|
|
let(:response_invalid_audience_with_skip) { OneLogin::RubySaml::Response.new(read_invalid_response("invalid_audience.xml.base64"), {:skip_audience => true}) }
|
|
let(:response_invalid_signed_element) { OneLogin::RubySaml::Response.new(read_invalid_response("response_invalid_signed_element.xml.base64")) }
|
|
let(:response_invalid_issuer_assertion) { OneLogin::RubySaml::Response.new(read_invalid_response("invalid_issuer_assertion.xml.base64")) }
|
|
let(:response_invalid_issuer_message) { OneLogin::RubySaml::Response.new(read_invalid_response("invalid_issuer_message.xml.base64")) }
|
|
let(:response_no_issuer_response) { OneLogin::RubySaml::Response.new(read_invalid_response("no_issuer_response.xml.base64")) }
|
|
let(:response_no_issuer_assertion) { OneLogin::RubySaml::Response.new(read_invalid_response("no_issuer_assertion.xml.base64")) }
|
|
let(:response_no_nameid) { OneLogin::RubySaml::Response.new(read_invalid_response("no_nameid.xml.base64")) }
|
|
let(:response_empty_nameid) { OneLogin::RubySaml::Response.new(read_invalid_response("empty_nameid.xml.base64")) }
|
|
let(:response_wrong_spnamequalifier) { OneLogin::RubySaml::Response.new(read_invalid_response("wrong_spnamequalifier.xml.base64")) }
|
|
let(:response_duplicated_attributes) { OneLogin::RubySaml::Response.new(read_invalid_response("duplicated_attributes.xml.base64")) }
|
|
let(:response_no_subjectconfirmation_data) { OneLogin::RubySaml::Response.new(read_invalid_response("no_subjectconfirmation_data.xml.base64")) }
|
|
let(:response_no_subjectconfirmation_method) { OneLogin::RubySaml::Response.new(read_invalid_response("no_subjectconfirmation_method.xml.base64")) }
|
|
let(:response_invalid_subjectconfirmation_inresponse) { OneLogin::RubySaml::Response.new(read_invalid_response("invalid_subjectconfirmation_inresponse.xml.base64")) }
|
|
let(:response_invalid_subjectconfirmation_recipient) { OneLogin::RubySaml::Response.new(read_invalid_response("invalid_subjectconfirmation_recipient.xml.base64")) }
|
|
let(:response_invalid_subjectconfirmation_nb) { OneLogin::RubySaml::Response.new(read_invalid_response("invalid_subjectconfirmation_nb.xml.base64")) }
|
|
let(:response_invalid_subjectconfirmation_noa) { OneLogin::RubySaml::Response.new(read_invalid_response("invalid_subjectconfirmation_noa.xml.base64")) }
|
|
let(:response_invalid_signature_position) { OneLogin::RubySaml::Response.new(read_invalid_response("invalid_signature_position.xml.base64")) }
|
|
let(:response_encrypted_nameid) { OneLogin::RubySaml::Response.new(response_document_encrypted_nameid) }
|
|
|
|
def generate_audience_error(expected, actual)
|
|
s = actual.count > 1 ? 's' : '';
|
|
return "Invalid Audience#{s}. The audience#{s} #{actual.join(',')}, did not match the expected audience #{expected}"
|
|
end
|
|
|
|
it "raise an exception when response is initialized with nil" do
|
|
assert_raises(ArgumentError) { OneLogin::RubySaml::Response.new(nil) }
|
|
end
|
|
|
|
it "not filter available options only" do
|
|
options = { :skip_destination => true, :foo => :bar }
|
|
response = OneLogin::RubySaml::Response.new(response_document_valid_signed, options)
|
|
assert_includes response.options.keys, :skip_destination
|
|
assert_includes response.options.keys, :foo
|
|
end
|
|
|
|
it "be able to parse a document which contains ampersands" do
|
|
XMLSecurity::SignedDocument.any_instance.stubs(:digests_match?).returns(true)
|
|
OneLogin::RubySaml::Response.any_instance.stubs(:validate_conditions).returns(true)
|
|
|
|
ampersands_response = OneLogin::RubySaml::Response.new(ampersands_document)
|
|
ampersands_response.settings = settings
|
|
ampersands_response.settings.idp_cert_fingerprint = 'c51985d947f1be57082025050846eb27f6cab783'
|
|
|
|
assert !ampersands_response.is_valid?
|
|
assert_includes ampersands_response.errors, "SAML Response must contain 1 assertion"
|
|
end
|
|
|
|
describe "Prevent node text with comment attack (VU#475445)" do
|
|
before do
|
|
@response = OneLogin::RubySaml::Response.new(read_response('response_node_text_attack.xml.base64'))
|
|
end
|
|
|
|
it "receives the full NameID when there is an injected comment" do
|
|
assert_equal "support@onelogin.com", @response.name_id
|
|
end
|
|
|
|
it "receives the full AttributeValue when there is an injected comment" do
|
|
assert_equal "smith", @response.attributes["surname"]
|
|
end
|
|
end
|
|
|
|
describe "Another test to prevent with comment attack (VU#475445)" do
|
|
before do
|
|
@response = OneLogin::RubySaml::Response.new(read_response('response_node_text_attack2.xml.base64'), {:skip_recipient_check => true })
|
|
@response.settings = settings
|
|
@response.settings.idp_cert_fingerprint = ruby_saml_cert_fingerprint
|
|
end
|
|
|
|
it "receives the full NameID when there is an injected comment, validates the response" do
|
|
assert_equal "test@onelogin.com", @response.name_id
|
|
end
|
|
end
|
|
|
|
describe "Another test with CDATA injected" do
|
|
before do
|
|
@response = OneLogin::RubySaml::Response.new(read_response('response_node_text_attack3.xml.base64'), {:skip_recipient_check => true })
|
|
@response.settings = settings
|
|
@response.settings.idp_cert_fingerprint = ruby_saml_cert_fingerprint
|
|
end
|
|
|
|
it "it normalizes CDATA but reject SAMLResponse due signature invalidation" do
|
|
assert_equal "test@onelogin.com.evil.com", @response.name_id
|
|
assert !@response.is_valid?
|
|
assert_includes @response.errors, "Invalid Signature on SAML Response"
|
|
end
|
|
end
|
|
|
|
describe "Prevent XEE attack" do
|
|
before do
|
|
@response = OneLogin::RubySaml::Response.new(fixture(:attackxee))
|
|
end
|
|
|
|
it "false when evil attack vector is present, soft = true" do
|
|
@response.soft = true
|
|
assert !@response.send(:validate_structure)
|
|
assert_includes @response.errors, "Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd"
|
|
end
|
|
|
|
it "raise when evil attack vector is present, soft = false " do
|
|
@response.soft = false
|
|
|
|
assert_raises(OneLogin::RubySaml::ValidationError) do
|
|
@response.send(:validate_structure)
|
|
end
|
|
end
|
|
end
|
|
|
|
it "adapt namespace" do
|
|
refute_nil response.nameid
|
|
refute_nil response_without_attributes.nameid
|
|
refute_nil response_with_signed_assertion.nameid
|
|
end
|
|
|
|
it "default to raw input when a response is not Base64 encoded" do
|
|
decoded = Base64.decode64(response_document_without_attributes)
|
|
response_from_raw = OneLogin::RubySaml::Response.new(decoded)
|
|
assert response_from_raw.document
|
|
end
|
|
|
|
describe "Assertion" do
|
|
it "only retreive an assertion with an ID that matches the signature's reference URI" do
|
|
response_wrapped.stubs(:conditions).returns(nil)
|
|
settings.idp_cert_fingerprint = signature_fingerprint_1
|
|
response_wrapped.settings = settings
|
|
assert_nil response_wrapped.nameid
|
|
end
|
|
end
|
|
|
|
describe "#is_valid?" do
|
|
describe "soft = false" do
|
|
|
|
before do
|
|
response.soft = false
|
|
response_valid_signed.soft = false
|
|
end
|
|
|
|
it "raise when response is initialized with blank data" do
|
|
blank_response = OneLogin::RubySaml::Response.new('')
|
|
blank_response.soft = false
|
|
error_msg = "Blank response"
|
|
assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do
|
|
blank_response.is_valid?
|
|
end
|
|
assert_includes blank_response.errors, error_msg
|
|
end
|
|
|
|
it "raise when settings have not been set" do
|
|
error_msg = "No settings on response"
|
|
assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do
|
|
response.is_valid?
|
|
end
|
|
assert_includes response.errors, error_msg
|
|
end
|
|
|
|
it "raise when No fingerprint or certificate on settings" do
|
|
settings.idp_cert_fingerprint = nil
|
|
settings.idp_cert = nil
|
|
settings.idp_cert_multi = nil
|
|
response.settings = settings
|
|
error_msg = "No fingerprint or certificate on settings"
|
|
assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do
|
|
response.is_valid?
|
|
end
|
|
assert_includes response.errors, error_msg
|
|
end
|
|
|
|
it "raise when signature wrapping attack" do
|
|
response_wrapped.stubs(:conditions).returns(nil)
|
|
response_wrapped.stubs(:validate_subject_confirmation).returns(true)
|
|
settings.idp_cert_fingerprint = signature_fingerprint_1
|
|
response_wrapped.settings = settings
|
|
assert !response_wrapped.is_valid?
|
|
end
|
|
|
|
it "raise when no signature" do
|
|
settings.idp_cert_fingerprint = signature_fingerprint_1
|
|
response_no_signed_elements.settings = settings
|
|
response_no_signed_elements.soft = false
|
|
error_msg = "Found an unexpected number of Signature Element. SAML Response rejected"
|
|
assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do
|
|
response_no_signed_elements.is_valid?
|
|
end
|
|
end
|
|
|
|
it "raise when multiple signatures" do
|
|
settings.idp_cert_fingerprint = signature_fingerprint_1
|
|
response_multiple_signed.settings = settings
|
|
response_multiple_signed.soft = false
|
|
error_msg = "Duplicated ID. SAML Response rejected"
|
|
assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do
|
|
response_multiple_signed.is_valid?
|
|
end
|
|
end
|
|
|
|
it "validate SAML 2.0 XML structure" do
|
|
resp_xml = Base64.decode64(response_document_unsigned).gsub(/emailAddress/,'test')
|
|
response_unsigned_mod = OneLogin::RubySaml::Response.new(Base64.encode64(resp_xml))
|
|
response_unsigned_mod.stubs(:conditions).returns(nil)
|
|
settings.idp_cert_fingerprint = signature_fingerprint_1
|
|
response_unsigned_mod.settings = settings
|
|
response_unsigned_mod.soft = false
|
|
assert_raises(OneLogin::RubySaml::ValidationError, 'Digest mismatch') do
|
|
response_unsigned_mod.is_valid?
|
|
end
|
|
end
|
|
|
|
it "raise when encountering a condition that prevents the document from being valid" do
|
|
settings.idp_cert_fingerprint = ruby_saml_cert_fingerprint
|
|
response.settings = settings
|
|
response.soft = false
|
|
error_msg = "Current time is on or after NotOnOrAfter condition"
|
|
assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do
|
|
response.is_valid?
|
|
end
|
|
assert_includes response.errors[0], error_msg
|
|
end
|
|
|
|
it "raise when encountering a SAML Response with bad formatted" do
|
|
settings.idp_cert_fingerprint = signature_fingerprint_1
|
|
response_without_attributes.settings = settings
|
|
response_without_attributes.soft = false
|
|
assert_raises(OneLogin::RubySaml::ValidationError) do
|
|
response_without_attributes.is_valid?
|
|
end
|
|
end
|
|
|
|
it "raise when the inResponseTo value does not match the Request ID" do
|
|
settings.soft = false
|
|
settings.idp_cert_fingerprint = signature_fingerprint_1
|
|
opts = {}
|
|
opts[:settings] = settings
|
|
opts[:matches_request_id] = "invalid_request_id"
|
|
response_valid_signed = OneLogin::RubySaml::Response.new(response_document_valid_signed, opts)
|
|
error_msg = "The InResponseTo of the Response: _fc4a34b0-7efb-012e-caae-782bcb13bb38, does not match the ID of the AuthNRequest sent by the SP: invalid_request_id"
|
|
assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do
|
|
response_valid_signed.is_valid?
|
|
end
|
|
assert_includes response_valid_signed.errors, error_msg
|
|
end
|
|
|
|
it "raise when there is no valid audience" do
|
|
settings.idp_cert_fingerprint = signature_fingerprint_1
|
|
settings.sp_entity_id = 'invalid'
|
|
response_valid_signed.settings = settings
|
|
response_valid_signed.soft = false
|
|
error_msg = generate_audience_error(response_valid_signed.settings.sp_entity_id, ['https://someone.example.com/audience'])
|
|
assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do
|
|
response_valid_signed.is_valid?
|
|
end
|
|
assert_includes response_valid_signed.errors, error_msg
|
|
end
|
|
|
|
it "raise when no ID present in the SAML Response" do
|
|
settings.idp_cert_fingerprint = signature_fingerprint_1
|
|
response_no_id.settings = settings
|
|
response_no_id.soft = false
|
|
error_msg = "Missing ID attribute on SAML Response"
|
|
assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do
|
|
response_no_id.is_valid?
|
|
end
|
|
assert_includes response_no_id.errors, error_msg
|
|
end
|
|
|
|
it "raise when no 2.0 Version present in the SAML Response" do
|
|
settings.idp_cert_fingerprint = signature_fingerprint_1
|
|
response_no_version.settings = settings
|
|
response_no_version.soft = false
|
|
error_msg = "Unsupported SAML version"
|
|
assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do
|
|
response_no_version.is_valid?
|
|
end
|
|
assert_includes response_no_version.errors, error_msg
|
|
end
|
|
end
|
|
|
|
describe "soft = true" do
|
|
before do
|
|
response.soft = true
|
|
response_valid_signed.soft = true
|
|
end
|
|
|
|
it "return true when the response is initialized with valid data" do
|
|
response_valid_signed_without_recipient.stubs(:conditions).returns(nil)
|
|
response_valid_signed_without_recipient.settings = settings
|
|
response_valid_signed_without_recipient.settings.idp_cert_fingerprint = ruby_saml_cert_fingerprint
|
|
assert response_valid_signed_without_recipient.is_valid?
|
|
assert_empty response_valid_signed_without_recipient.errors
|
|
end
|
|
|
|
it "return true when the response is initialized with valid data and using certificate instead of fingerprint" do
|
|
response_valid_signed_without_recipient.stubs(:conditions).returns(nil)
|
|
response_valid_signed_without_recipient.settings = settings
|
|
response_valid_signed_without_recipient.settings.idp_cert = ruby_saml_cert_text
|
|
assert response_valid_signed_without_recipient.is_valid?
|
|
assert_empty response_valid_signed_without_recipient.errors
|
|
end
|
|
|
|
it "return false when response is initialized with blank data" do
|
|
blank_response = OneLogin::RubySaml::Response.new('')
|
|
blank_response.soft = true
|
|
assert !blank_response.is_valid?
|
|
assert_includes blank_response.errors, "Blank response"
|
|
end
|
|
|
|
it "return false if settings have not been set" do
|
|
assert !response.is_valid?
|
|
assert_includes response.errors, "No settings on response"
|
|
end
|
|
|
|
it "return false if fingerprint or certificate not been set on settings" do
|
|
response.settings = settings
|
|
assert !response.is_valid?
|
|
assert_includes response.errors, "No fingerprint or certificate on settings"
|
|
end
|
|
|
|
it "should be idempotent when the response is initialized with invalid data" do
|
|
response_unsigned.stubs(:conditions).returns(nil)
|
|
response_unsigned.settings = settings
|
|
assert !response_unsigned.is_valid?
|
|
assert !response_unsigned.is_valid?
|
|
end
|
|
|
|
it "should be idempotent when the response is initialized with valid data" do
|
|
response_valid_signed_without_recipient.stubs(:conditions).returns(nil)
|
|
response_valid_signed_without_recipient.settings = settings
|
|
response_valid_signed_without_recipient.settings.idp_cert_fingerprint = ruby_saml_cert_fingerprint
|
|
assert response_valid_signed_without_recipient.is_valid?
|
|
assert response_valid_signed_without_recipient.is_valid?
|
|
end
|
|
|
|
it "not allow signature wrapping attack" do
|
|
response_wrapped.stubs(:conditions).returns(nil)
|
|
response_wrapped.stubs(:validate_subject_confirmation).returns(true)
|
|
settings.idp_cert_fingerprint = signature_fingerprint_1
|
|
response_wrapped.settings = settings
|
|
assert !response_wrapped.is_valid?
|
|
end
|
|
|
|
it "support dynamic namespace resolution on signature elements" do
|
|
no_signature_response = OneLogin::RubySaml::Response.new(fixture("no_signature_ns.xml"))
|
|
no_signature_response.stubs(:conditions).returns(nil)
|
|
no_signature_response.stubs(:validate_subject_confirmation).returns(true)
|
|
no_signature_response.settings = settings
|
|
no_signature_response.settings.idp_cert_fingerprint = "28:74:9B:E8:1F:E8:10:9C:A8:7C:A9:C3:E3:C5:01:6C:92:1C:B4:BA"
|
|
XMLSecurity::SignedDocument.any_instance.expects(:validate_signature).returns(true)
|
|
assert no_signature_response.is_valid?
|
|
end
|
|
|
|
it "validate ADFS assertions" do
|
|
adfs_response = OneLogin::RubySaml::Response.new(fixture(:adfs_response_sha256))
|
|
adfs_response.stubs(:conditions).returns(nil)
|
|
adfs_response.stubs(:validate_subject_confirmation).returns(true)
|
|
settings.idp_cert_fingerprint = "28:74:9B:E8:1F:E8:10:9C:A8:7C:A9:C3:E3:C5:01:6C:92:1C:B4:BA"
|
|
adfs_response.settings = settings
|
|
adfs_response.soft = true
|
|
assert adfs_response.is_valid?
|
|
end
|
|
|
|
it "validate SAML 2.0 XML structure" do
|
|
resp_xml = Base64.decode64(response_document_unsigned).gsub(/emailAddress/,'test')
|
|
response_unsigned_mod = OneLogin::RubySaml::Response.new(Base64.encode64(resp_xml))
|
|
response_unsigned_mod.stubs(:conditions).returns(nil)
|
|
settings.idp_cert_fingerprint = signature_fingerprint_1
|
|
response_unsigned_mod.settings = settings
|
|
response_unsigned_mod.soft = true
|
|
assert !response_unsigned_mod.is_valid?
|
|
end
|
|
|
|
it "return false when encountering a condition that prevents the document from being valid" do
|
|
settings.idp_cert_fingerprint = ruby_saml_cert_fingerprint
|
|
response.settings = settings
|
|
error_msg = "Current time is on or after NotOnOrAfter condition"
|
|
assert !response.is_valid?
|
|
assert_includes response.errors[0], "Current time is on or after NotOnOrAfter condition"
|
|
end
|
|
|
|
it "return false when encountering a SAML Response with bad formatted" do
|
|
settings.idp_cert_fingerprint = signature_fingerprint_1
|
|
response_without_attributes.settings = settings
|
|
response_without_attributes.soft = true
|
|
error_msg = "Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd"
|
|
response_without_attributes.is_valid?
|
|
assert_includes response_without_attributes.errors, error_msg
|
|
end
|
|
|
|
it "return false when the inResponseTo value does not match the Request ID" do
|
|
settings.soft = true
|
|
settings.idp_cert_fingerprint = signature_fingerprint_1
|
|
opts = {}
|
|
opts[:settings] = settings
|
|
opts[:matches_request_id] = "invalid_request_id"
|
|
response_valid_signed = OneLogin::RubySaml::Response.new(response_document_valid_signed, opts)
|
|
response_valid_signed.is_valid?
|
|
assert_includes response_valid_signed.errors, "The InResponseTo of the Response: _fc4a34b0-7efb-012e-caae-782bcb13bb38, does not match the ID of the AuthNRequest sent by the SP: invalid_request_id"
|
|
end
|
|
|
|
it "return false when there is no valid audience" do
|
|
settings.idp_cert_fingerprint = signature_fingerprint_1
|
|
settings.sp_entity_id = 'invalid'
|
|
response_valid_signed.settings = settings
|
|
response_valid_signed.is_valid?
|
|
|
|
assert_includes response_valid_signed.errors, generate_audience_error(response_valid_signed.settings.sp_entity_id, ['https://someone.example.com/audience'])
|
|
end
|
|
|
|
it "return false when no ID present in the SAML Response" do
|
|
settings.idp_cert_fingerprint = signature_fingerprint_1
|
|
response_no_id.settings = settings
|
|
response_no_id.soft = true
|
|
response_no_id.is_valid?
|
|
assert_includes response_no_id.errors, "Missing ID attribute on SAML Response"
|
|
end
|
|
|
|
it "return false when no 2.0 Version present in the SAML Response" do
|
|
settings.idp_cert_fingerprint = signature_fingerprint_1
|
|
response_no_version.settings = settings
|
|
response_no_version.soft = true
|
|
error_msg = "Unsupported SAML version"
|
|
response_no_version.is_valid?
|
|
assert_includes response_no_version.errors, "Unsupported SAML version"
|
|
end
|
|
|
|
it "return true when a nil URI is given in the ds:Reference" do
|
|
settings.idp_cert = ruby_saml_cert_text
|
|
settings.assertion_consumer_service_url = "http://localhost:9001/v1/users/authorize/saml"
|
|
response_without_reference_uri.settings = settings
|
|
response_without_reference_uri.stubs(:conditions).returns(nil)
|
|
response_without_reference_uri.is_valid?
|
|
assert_empty response_without_reference_uri.errors
|
|
assert 'saml@user.com', response_without_reference_uri.attributes['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress']
|
|
end
|
|
|
|
it "collect errors when collect_errors=true" do
|
|
settings.idp_cert = ruby_saml_cert_text
|
|
settings.sp_entity_id = 'invalid'
|
|
response_invalid_subjectconfirmation_recipient.settings = settings
|
|
collect_errors = true
|
|
response_invalid_subjectconfirmation_recipient.is_valid?(collect_errors)
|
|
assert_includes response_invalid_subjectconfirmation_recipient.errors, generate_audience_error('invalid', ['http://stuff.com/endpoints/metadata.php'])
|
|
assert_includes response_invalid_subjectconfirmation_recipient.errors, "Invalid Signature on SAML Response"
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#validate_audience" do
|
|
it "return true when the audience is valid" do
|
|
response.settings = settings
|
|
response.settings.sp_entity_id = '{audience}'
|
|
assert response.send(:validate_audience)
|
|
assert_empty response.errors
|
|
end
|
|
|
|
it "return true when the audience is self closing and strict audience validation is not enabled" do
|
|
response_audience_self_closed.settings = settings
|
|
response_audience_self_closed.settings.sp_entity_id = '{audience}'
|
|
assert response_audience_self_closed.send(:validate_audience)
|
|
assert_empty response_audience_self_closed.errors
|
|
end
|
|
|
|
it "return false when the audience is self closing and strict audience validation is enabled" do
|
|
response_audience_self_closed.settings = settings
|
|
response_audience_self_closed.settings.security[:strict_audience_validation] = true
|
|
response_audience_self_closed.settings.sp_entity_id = '{audience}'
|
|
refute response_audience_self_closed.send(:validate_audience)
|
|
assert_includes response_audience_self_closed.errors, "Invalid Audiences. The <AudienceRestriction> element contained only empty <Audience> elements. Expected audience {audience}."
|
|
end
|
|
|
|
it "return false when the audience is invalid" do
|
|
response.settings = settings
|
|
response.settings.sp_entity_id = 'invalid_audience'
|
|
assert !response.send(:validate_audience)
|
|
assert_includes response.errors, generate_audience_error(response.settings.sp_entity_id, ['{audience}'])
|
|
end
|
|
end
|
|
|
|
describe "#validate_destination" do
|
|
it "return true when the destination of the SAML Response matches the assertion consumer service url" do
|
|
response.settings = settings
|
|
assert response.send(:validate_destination)
|
|
assert_empty response.errors
|
|
end
|
|
|
|
it "return false when the destination of the SAML Response does not match the assertion consumer service url" do
|
|
response.settings = settings
|
|
response.settings.assertion_consumer_service_url = 'invalid_acs'
|
|
assert !response.send(:validate_destination)
|
|
assert_includes response.errors, "The response was received at #{response.destination} instead of #{response.settings.assertion_consumer_service_url}"
|
|
end
|
|
|
|
it "return false when the destination of the SAML Response is empty" do
|
|
response_empty_destination.settings = settings
|
|
assert !response_empty_destination.send(:validate_destination)
|
|
assert_includes response_empty_destination.errors, "The response has an empty Destination value"
|
|
end
|
|
|
|
it "return true when the destination of the SAML Response is empty but skip_destination option is used" do
|
|
response_empty_destination_with_skip.settings = settings
|
|
assert response_empty_destination_with_skip.send(:validate_destination)
|
|
assert_empty response_empty_destination.errors
|
|
end
|
|
|
|
it "returns true on a case insensitive match on the domain" do
|
|
response_valid_signed_without_x509certificate.settings = settings
|
|
response_valid_signed_without_x509certificate.settings.assertion_consumer_service_url = 'http://APP.muDa.no/sso/consume'
|
|
assert response_valid_signed_without_x509certificate.send(:validate_destination)
|
|
assert_empty response_valid_signed_without_x509certificate.errors
|
|
end
|
|
|
|
it "returns true on a case insensitive match on the scheme" do
|
|
response_valid_signed_without_x509certificate.settings = settings
|
|
response_valid_signed_without_x509certificate.settings.assertion_consumer_service_url = 'HTTP://app.muda.no/sso/consume'
|
|
assert response_valid_signed_without_x509certificate.send(:validate_destination)
|
|
assert_empty response_valid_signed_without_x509certificate.errors
|
|
end
|
|
|
|
it "returns false on a case insenstive match on the path" do
|
|
response_valid_signed_without_x509certificate.settings = settings
|
|
response_valid_signed_without_x509certificate.settings.assertion_consumer_service_url = 'http://app.muda.no/SSO/consume'
|
|
assert !response_valid_signed_without_x509certificate.send(:validate_destination)
|
|
assert_includes response_valid_signed_without_x509certificate.errors, "The response was received at #{response_valid_signed_without_x509certificate.destination} instead of #{response_valid_signed_without_x509certificate.settings.assertion_consumer_service_url}"
|
|
end
|
|
|
|
it "returns true if it can't parse out a full URI." do
|
|
response_valid_signed_without_x509certificate.settings = settings
|
|
response_valid_signed_without_x509certificate.settings.assertion_consumer_service_url = 'presenter'
|
|
assert !response_valid_signed_without_x509certificate.send(:validate_destination)
|
|
assert_includes response_valid_signed_without_x509certificate.errors, "The response was received at #{response_valid_signed_without_x509certificate.destination} instead of #{response_valid_signed_without_x509certificate.settings.assertion_consumer_service_url}"
|
|
end
|
|
end
|
|
|
|
describe "#validate_issuer" do
|
|
it "return true when the issuer of the Message/Assertion matches the IdP entityId" do
|
|
response_valid_signed.settings = settings
|
|
assert response_valid_signed.send(:validate_issuer)
|
|
|
|
response_valid_signed.settings.idp_entity_id = 'https://app.onelogin.com/saml2'
|
|
assert response_valid_signed.send(:validate_issuer)
|
|
end
|
|
|
|
it "return false when the issuer of the Message does not match the IdP entityId" do
|
|
response_invalid_issuer_message.settings = settings
|
|
response_invalid_issuer_message.settings.idp_entity_id = 'http://idp.example.com/'
|
|
assert !response_invalid_issuer_message.send(:validate_issuer)
|
|
assert_includes response_invalid_issuer_message.errors, "Doesn't match the issuer, expected: <#{response_invalid_issuer_message.settings.idp_entity_id}>, but was: <http://invalid.issuer.example.com/>"
|
|
end
|
|
|
|
it "return false when the issuer of the Assertion does not match the IdP entityId" do
|
|
response_invalid_issuer_assertion.settings = settings
|
|
response_invalid_issuer_assertion.settings.idp_entity_id = 'http://idp.example.com/'
|
|
assert !response_invalid_issuer_assertion.send(:validate_issuer)
|
|
assert_includes response_invalid_issuer_assertion.errors, "Doesn't match the issuer, expected: <#{response_invalid_issuer_assertion.settings.idp_entity_id}>, but was: <http://invalid.issuer.example.com/>"
|
|
end
|
|
end
|
|
|
|
describe "#validate_num_assertion" do
|
|
it "return true when SAML Response contains 1 assertion" do
|
|
assert response.send(:validate_num_assertion)
|
|
assert_empty response.errors
|
|
end
|
|
|
|
it "return false when no 2.0 Version present in the SAML Response" do
|
|
assert !response_multi_assertion.send(:validate_num_assertion)
|
|
assert_includes response_multi_assertion.errors, "SAML Response must contain 1 assertion"
|
|
end
|
|
end
|
|
|
|
describe "validate_success_status" do
|
|
it "return true when the status is 'Success'" do
|
|
assert response.send(:validate_success_status)
|
|
assert_empty response.errors
|
|
end
|
|
|
|
it "return false when no Status provided" do
|
|
assert !response_no_status.send(:validate_success_status)
|
|
assert_includes response_no_status.errors, "The status code of the Response was not Success"
|
|
end
|
|
|
|
it "return false when no StatusCode provided" do
|
|
assert !response_no_statuscode.send(:validate_success_status)
|
|
assert_includes response_no_statuscode.errors, "The status code of the Response was not Success"
|
|
end
|
|
|
|
it "return false when the status is not 'Success'" do
|
|
assert !response_statuscode_responder.send(:validate_success_status)
|
|
assert_includes response_statuscode_responder.errors, "The status code of the Response was not Success, was Responder"
|
|
end
|
|
|
|
it "return false when the status is not 'Success', and shows the StatusMessage" do
|
|
assert !response_statuscode_responder_and_msg.send(:validate_success_status)
|
|
assert_includes response_statuscode_responder_and_msg.errors, "The status code of the Response was not Success, was Responder -> something_is_wrong"
|
|
end
|
|
|
|
it "return false when the status is not 'Success'" do
|
|
assert !response_double_statuscode.send(:validate_success_status)
|
|
assert_includes response_double_statuscode.errors, "The status code of the Response was not Success, was Requester => UnsupportedBinding"
|
|
end
|
|
end
|
|
|
|
describe "#validate_structure" do
|
|
it "return true when encountering a wellformed SAML Response" do
|
|
assert response.send(:validate_structure)
|
|
assert_empty response.errors
|
|
end
|
|
|
|
it "return false when encountering a mailformed element that prevents the document from being valid" do
|
|
response_without_attributes.soft = true
|
|
response_without_attributes.send(:validate_structure)
|
|
assert response_without_attributes.errors.include? "Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd"
|
|
end
|
|
|
|
it "raise when encountering a mailformed element that prevents the document from being valid" do
|
|
response_without_attributes.soft = false
|
|
assert_raises(OneLogin::RubySaml::ValidationError) {
|
|
response_without_attributes.send(:validate_structure)
|
|
}
|
|
end
|
|
end
|
|
|
|
describe "validate_formatted_x509_certificate" do
|
|
let(:response_with_formatted_x509certificate) {
|
|
OneLogin::RubySaml::Response.new(read_response("valid_response_with_formatted_x509certificate.xml.base64"), {
|
|
:skip_conditions => true,
|
|
:skip_subject_confirmation => true })
|
|
}
|
|
|
|
it "be able to parse the response wihout errors" do
|
|
response_with_formatted_x509certificate.settings = settings
|
|
response_with_formatted_x509certificate.settings.idp_cert = ruby_saml_cert_text
|
|
assert response_with_formatted_x509certificate.is_valid?
|
|
assert_empty response_with_formatted_x509certificate.errors
|
|
end
|
|
end
|
|
|
|
describe "#validate_in_response_to" do
|
|
it "return true when the inResponseTo value matches the Request ID" do
|
|
response = OneLogin::RubySaml::Response.new(response_document_valid_signed, :settings => settings, :matches_request_id => "_fc4a34b0-7efb-012e-caae-782bcb13bb38")
|
|
assert response.send(:validate_in_response_to)
|
|
assert_empty response.errors
|
|
end
|
|
|
|
it "return true when no Request ID is provided for checking" do
|
|
response = OneLogin::RubySaml::Response.new(response_document_valid_signed, :settings => settings)
|
|
assert response.send(:validate_in_response_to)
|
|
assert_empty response.errors
|
|
end
|
|
|
|
it "return false when the inResponseTo value does not match the Request ID" do
|
|
response = OneLogin::RubySaml::Response.new(response_document_valid_signed, :settings => settings, :matches_request_id => "invalid_request_id")
|
|
assert !response.send(:validate_in_response_to)
|
|
assert_includes response.errors, "The InResponseTo of the Response: _fc4a34b0-7efb-012e-caae-782bcb13bb38, does not match the ID of the AuthNRequest sent by the SP: invalid_request_id"
|
|
end
|
|
end
|
|
|
|
describe "#validate_audience" do
|
|
it "return true when the audience is valid" do
|
|
response_valid_signed.settings = settings
|
|
response_valid_signed.settings.sp_entity_id = "https://someone.example.com/audience"
|
|
assert response_valid_signed.send(:validate_audience)
|
|
assert_empty response_valid_signed.errors
|
|
end
|
|
|
|
it "return true when there is not sp_entity_id defined" do
|
|
response_valid_signed.settings = settings
|
|
response_valid_signed.settings.sp_entity_id = nil
|
|
assert response_valid_signed.send(:validate_audience)
|
|
assert_empty response_valid_signed.errors
|
|
end
|
|
|
|
it "return false when there is no valid audience" do
|
|
response_invalid_audience.settings = settings
|
|
response_invalid_audience.settings.sp_entity_id = "https://invalid.example.com/audience"
|
|
assert !response_invalid_audience.send(:validate_audience)
|
|
assert_includes response_invalid_audience.errors, generate_audience_error(response_invalid_audience.settings.sp_entity_id, ['http://invalid.audience.com'])
|
|
end
|
|
|
|
it "return true when there is no valid audience but skip_destination option is used" do
|
|
response_invalid_audience_with_skip.settings = settings
|
|
response_invalid_audience_with_skip.settings.sp_entity_id = "https://invalid.example.com/audience"
|
|
assert response_invalid_audience_with_skip.send(:validate_audience)
|
|
assert_empty response_invalid_audience_with_skip.errors
|
|
end
|
|
end
|
|
|
|
describe "#validate_issuer" do
|
|
it "return true when the issuer of the Message/Assertion matches the IdP entityId or it was empty" do
|
|
response_valid_signed.settings = settings
|
|
assert response_valid_signed.send(:validate_issuer)
|
|
assert_empty response_valid_signed.errors
|
|
|
|
response_valid_signed.settings.idp_entity_id = 'https://app.onelogin.com/saml2'
|
|
assert response_valid_signed.send(:validate_issuer)
|
|
assert_empty response_valid_signed.errors
|
|
end
|
|
|
|
it "return false when the issuer of the Message does not match the IdP entityId" do
|
|
response_invalid_issuer_message.settings = settings
|
|
response_invalid_issuer_message.settings.idp_entity_id = 'http://idp.example.com/'
|
|
assert !response_invalid_issuer_message.send(:validate_issuer)
|
|
assert_includes response_invalid_issuer_message.errors, "Doesn't match the issuer, expected: <#{response_invalid_issuer_message.settings.idp_entity_id}>, but was: <http://invalid.issuer.example.com/>"
|
|
end
|
|
|
|
it "return false when the issuer of the Assertion does not match the IdP entityId" do
|
|
response_invalid_issuer_assertion.settings = settings
|
|
response_invalid_issuer_assertion.settings.idp_entity_id = 'http://idp.example.com/'
|
|
assert !response_invalid_issuer_assertion.send(:validate_issuer)
|
|
assert_includes response_invalid_issuer_assertion.errors, "Doesn't match the issuer, expected: <#{response_invalid_issuer_assertion.settings.idp_entity_id}>, but was: <http://invalid.issuer.example.com/>"
|
|
end
|
|
|
|
it "return false when the no issuer at the Response" do
|
|
response_no_issuer_response.settings = settings
|
|
response_no_issuer_response.settings.idp_entity_id = 'http://idp.example.com/'
|
|
assert !response_no_issuer_response.send(:validate_issuer)
|
|
assert_includes response_no_issuer_response.errors, "Issuer of the Response not found or multiple."
|
|
end
|
|
|
|
it "return false when the no issuer at the Assertion" do
|
|
response_no_issuer_assertion.settings = settings
|
|
response_no_issuer_assertion.settings.idp_entity_id = 'http://idp.example.com/'
|
|
assert !response_no_issuer_assertion.send(:validate_issuer)
|
|
assert_includes response_no_issuer_assertion.errors, "Issuer of the Assertion not found or multiple."
|
|
end
|
|
end
|
|
|
|
describe "#validate_subject_confirmation" do
|
|
it "return true when valid subject confirmation" do
|
|
response_valid_signed.settings = settings
|
|
response_valid_signed.settings.assertion_consumer_service_url = 'recipient'
|
|
assert response_valid_signed.send(:validate_subject_confirmation)
|
|
assert_empty response_valid_signed.errors
|
|
end
|
|
|
|
it "return false when no subject confirmation data" do
|
|
response_no_subjectconfirmation_data.settings = settings
|
|
assert !response_no_subjectconfirmation_data.send(:validate_subject_confirmation)
|
|
assert_includes response_no_subjectconfirmation_data.errors, "A valid SubjectConfirmation was not found on this Response"
|
|
end
|
|
|
|
it "return false when no valid subject confirmation method" do
|
|
response_no_subjectconfirmation_method.settings = settings
|
|
assert !response_no_subjectconfirmation_method.send(:validate_subject_confirmation)
|
|
assert_includes response_no_subjectconfirmation_method.errors, "A valid SubjectConfirmation was not found on this Response"
|
|
end
|
|
|
|
it "return false when invalid inresponse" do
|
|
response_invalid_subjectconfirmation_inresponse.settings = settings
|
|
assert !response_invalid_subjectconfirmation_inresponse.send(:validate_subject_confirmation)
|
|
assert_includes response_invalid_subjectconfirmation_inresponse.errors, "A valid SubjectConfirmation was not found on this Response"
|
|
end
|
|
|
|
it "return false when invalid NotBefore" do
|
|
response_invalid_subjectconfirmation_nb.settings = settings
|
|
assert !response_invalid_subjectconfirmation_nb.send(:validate_subject_confirmation)
|
|
assert_includes response_invalid_subjectconfirmation_nb.errors, "A valid SubjectConfirmation was not found on this Response"
|
|
end
|
|
|
|
it "return false when invalid NotOnOrAfter" do
|
|
response_invalid_subjectconfirmation_noa.settings = settings
|
|
assert !response_invalid_subjectconfirmation_noa.send(:validate_subject_confirmation)
|
|
assert_includes response_invalid_subjectconfirmation_noa.errors, "A valid SubjectConfirmation was not found on this Response"
|
|
end
|
|
|
|
it "return true when valid subject confirmation recipient" do
|
|
response_valid_signed.settings = settings
|
|
response_valid_signed.settings.assertion_consumer_service_url = 'recipient'
|
|
assert response_valid_signed.send(:validate_subject_confirmation)
|
|
assert_empty response_valid_signed.errors
|
|
assert_empty response_valid_signed.errors
|
|
end
|
|
|
|
it "return false when invalid subject confirmation recipient" do
|
|
response_valid_signed.settings = settings
|
|
response_valid_signed.settings.assertion_consumer_service_url = 'not-the-recipient'
|
|
assert !response_valid_signed.send(:validate_subject_confirmation)
|
|
assert_includes response_valid_signed.errors, "A valid SubjectConfirmation was not found on this Response"
|
|
end
|
|
|
|
it "return false when invalid subject confirmation recipient, but skipping the check(default)" do
|
|
response_valid_signed_without_recipient.settings = settings
|
|
response_valid_signed_without_recipient.settings.assertion_consumer_service_url = 'not-the-recipient'
|
|
assert response_valid_signed_without_recipient.send(:validate_subject_confirmation)
|
|
assert_empty response_valid_signed_without_recipient.errors
|
|
end
|
|
|
|
it "return true when the skip_subject_confirmation option is passed and the subject confirmation is valid" do
|
|
opts = {}
|
|
opts[:skip_subject_confirmation] = true
|
|
response_with_skip = OneLogin::RubySaml::Response.new(response_document_valid_signed, opts)
|
|
response_with_skip.settings = settings
|
|
response_with_skip.settings.assertion_consumer_service_url = 'recipient'
|
|
Time.expects(:now).times(0) # ensures the test isn't run and thus Time.now.utc is never called within the test
|
|
assert response_with_skip.send(:validate_subject_confirmation)
|
|
assert_empty response_with_skip.errors
|
|
end
|
|
|
|
it "return true when the skip_subject_confirmation option is passed and the response has an invalid subject confirmation" do
|
|
opts = {}
|
|
opts[:skip_subject_confirmation] = true
|
|
response_with_skip = OneLogin::RubySaml::Response.new(read_invalid_response("invalid_subjectconfirmation_noa.xml.base64"), opts)
|
|
response_with_skip.settings = settings
|
|
Time.expects(:now).times(0) # ensures the test isn't run and thus Time.now.utc is never called within the test
|
|
assert response_with_skip.send(:validate_subject_confirmation)
|
|
assert_empty response_with_skip.errors
|
|
end
|
|
end
|
|
|
|
describe "#validate_session_expiration" do
|
|
it "return true when the session has not expired" do
|
|
response_valid_signed.settings = settings
|
|
assert response_valid_signed.send(:validate_session_expiration)
|
|
assert_empty response_valid_signed.errors
|
|
end
|
|
|
|
it "return false when the session has expired" do
|
|
response.settings = settings
|
|
assert !response.send(:validate_session_expiration)
|
|
assert_includes response.errors, "The attributes have expired, based on the SessionNotOnOrAfter of the AuthnStatement of this Response"
|
|
end
|
|
|
|
it "returns true when the session has expired, but is still within the allowed_clock_drift" do
|
|
drift = (Time.now - Time.parse("2010-11-19T21:57:37Z")) * 60 # seconds ago that this assertion expired
|
|
drift += 10 # add a buffer of 10 seconds to make sure the test passes
|
|
opts = {}
|
|
opts[:allowed_clock_drift] = drift
|
|
|
|
response_with_drift = OneLogin::RubySaml::Response.new(response_document_without_recipient, opts)
|
|
response_with_drift.settings = settings
|
|
assert response_with_drift.send(:validate_session_expiration)
|
|
assert_empty response_with_drift.errors
|
|
end
|
|
end
|
|
|
|
describe "#validate_signature" do
|
|
it "return true when the signature is valid" do
|
|
settings.idp_cert_fingerprint = ruby_saml_cert_fingerprint
|
|
response_valid_signed.settings = settings
|
|
assert response_valid_signed.send(:validate_signature)
|
|
assert_empty response_valid_signed.errors
|
|
end
|
|
|
|
it "return true when the signature is valid and ds namespace is at the root" do
|
|
settings.idp_cert_fingerprint = '5614657ab692b960480389723a36446a5fe1f7ec'
|
|
response_with_ds_namespace_at_the_root.settings = settings
|
|
assert response_with_ds_namespace_at_the_root.send(:validate_signature)
|
|
assert_empty response_with_ds_namespace_at_the_root.errors
|
|
end
|
|
|
|
it "return true when the signature is valid and fingerprint provided" do
|
|
settings.idp_cert_fingerprint = '49:EC:3F:A4:71:8A:1E:C9:DB:70:A7:CC:33:36:96:F0:48:8C:4E:DA'
|
|
xml = 'PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIERlc3RpbmF0aW9uPSJodHRwczovL2NvZGVycGFkLmlvL3NhbWwvYWNzIiBJRD0iXzEwOGE1ZTg0MDllYzRjZjlhY2QxYzQ2OWU5ZDcxNGFkIiBJblJlc3BvbnNlVG89Il80ZmZmYWE2MC02OTZiLTAxMzMtMzg4Ni0wMjQxZjY1YzA2OTMiIElzc3VlSW5zdGFudD0iMjAxNS0xMS0wOVQyMzo1NTo0M1oiIFZlcnNpb249IjIuMCI+PHNhbWw6SXNzdWVyIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPmh0dHBzOi8vbG9naW4uaHVsdS5jb208L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIj48L2RzOkNhbm9uaWNhbGl6YXRpb25NZXRob2Q+PGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSI+PC9kczpTaWduYXR1cmVNZXRob2Q+PGRzOlJlZmVyZW5jZSBVUkk9IiNfMTA4YTVlODQwOWVjNGNmOWFjZDFjNDY5ZTlkNzE0YWQiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSI+PC9kczpUcmFuc2Zvcm0+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyI+PC9kczpUcmFuc2Zvcm0+PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSI+PC9kczpEaWdlc3RNZXRob2Q+PGRzOkRpZ2VzdFZhbHVlPm9sQllXbTQyRi9oZm0xdHJYTHk2a3V6MXlMUT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+dXNRTmY5WGpKTDRlOXVucnVCdWViSnQ3R0tXM2hJUk9teWVqTm1NMHM4WFhlWHN3WHc4U3ZCZi8zeDNNWEpkWnpNV0pOM3ExN2tGWHN2bTVna1JzbkE9PTwvZHM6U2lnbmF0dXJlVmFsdWU+PGRzOktleUluZm8+PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU+TUlJQ1FEQ0NBZXFnQXdJQkFnSUpBSVZOdzVLRzR1aTFNQTBHQ1NxR1NJYjNEUUVCQlFVQU1Fd3hDekFKQmdOVkJBWVRBa2RDTVJJd0VBWURWUVFJRXdsQ1pYSnJjMmhwY21VeEVEQU9CZ05WQkFjVEIwNWxkMkoxY25reEZ6QVZCZ05WQkFvVERrMTVJRU52YlhCaGJua2dUSFJrTUI0WERURXlNVEF5TlRBMk1qY3pORm9YRFRJeU1UQXlNekEyTWpjek5Gb3dUREVMTUFrR0ExVUVCaE1DUjBJeEVqQVFCZ05WQkFnVENVSmxjbXR6YUdseVpURVFNQTRHQTFVRUJ4TUhUbVYzWW5WeWVURVhNQlVHQTFVRUNoTU9UWGtnUTI5dGNHRnVlU0JNZEdRd1hEQU5CZ2txaGtpRzl3MEJBUUVGQUFOTEFEQklBa0VBd1NOL2dpMzNSbXBBUW9MUWo3UDZ6QW5OVDBSbjdiakMzMjNuM3ExT25mdm52UjBmUWp2TnQ3ckRrQTVBdjVRbk02VjRZVU5Vbk1mYk9RcTBXTGJMU3dJREFRQUJvNEd1TUlHck1CMEdBMVVkRGdRV0JCUWZJSDFvZkJWcHNSQWNJTUsyaGJsN25nTVRZREI4QmdOVkhTTUVkVEJ6Z0JRZklIMW9mQlZwc1JBY0lNSzJoYmw3bmdNVFlLRlFwRTR3VERFTE1Ba0dBMVVFQmhNQ1IwSXhFakFRQmdOVkJBZ1RDVUpsY210emFHbHlaVEVRTUE0R0ExVUVCeE1IVG1WM1luVnllVEVYTUJVR0ExVUVDaE1PVFhrZ1EyOXRjR0Z1ZVNCTWRHU0NDUUNGVGNPU2h1TG90VEFNQmdOVkhSTUVCVEFEQVFIL01BMEdDU3FHU0liM0RRRUJCUVVBQTBFQXFvZ1YzdVBjbEtYRG1EWk1UN3ZsUFl4TEFxQ0dIWnRsQ3h6NGhNNEtTdGxEMi9HTmMxWGlMYjFoL0swQ0pMRG9zckVJYm0zd2lPMk12VEVSclZZU01RPT08L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiPjwvc2FtbHA6U3RhdHVzQ29kZT48L3NhbWxwOlN0YXR1cz48c2FtbDpBc3NlcnRpb24geG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9Il8wMTg4MmRhOTM2OTQ0ZDFlYTZlZmY0NDA2NTc2MzFiNSIgSXNzdWVJbnN0YW50PSIyMDE1LTExLTA5VDIzOjU1OjQzWiIgVmVyc2lvbj0iMi4wIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9sb2dpbi5odWx1LmNvbTwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+PGRzOlNpZ25lZEluZm8+PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiPjwvZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZD48ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIj48L2RzOlNpZ25hdHVyZU1ldGhvZD48ZHM6UmVmZXJlbmNlIFVSST0iI18wMTg4MmRhOTM2OTQ0ZDFlYTZlZmY0NDA2NTc2MzFiNSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIj48L2RzOlRyYW5zZm9ybT48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIj48L2RzOlRyYW5zZm9ybT48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIj48L2RzOkRpZ2VzdE1ldGhvZD48ZHM6RGlnZXN0VmFsdWU+cmo2YzhucC9BUmV0ZkJ1dWVOSzNPS0xDYnowPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5hR05FemZHM1dLcExKc2ZLRGJSNmpva2d6OEFnZ0FIRVVESEZyd0dsTHVQeWpyNEl3M09NcFNkV2gyL01YK1F3M1dPTk5mNHJNalh5TGVZSFJIVGpMQT09PC9kczpTaWduYXR1cmVWYWx1ZT48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDUURDQ0FlcWdBd0lCQWdJSkFJVk53NUtHNHVpMU1BMEdDU3FHU0liM0RRRUJCUVVBTUV3eEN6QUpCZ05WQkFZVEFrZENNUkl3RUFZRFZRUUlFd2xDWlhKcmMyaHBjbVV4RURBT0JnTlZCQWNUQjA1bGQySjFjbmt4RnpBVkJnTlZCQW9URGsxNUlFTnZiWEJoYm5rZ1RIUmtNQjRYRFRFeU1UQXlOVEEyTWpjek5Gb1hEVEl5TVRBeU16QTJNamN6TkZvd1RERUxNQWtHQTFVRUJoTUNSMEl4RWpBUUJnTlZCQWdUQ1VKbGNtdHphR2x5WlRFUU1BNEdBMVVFQnhNSFRtVjNZblZ5ZVRFWE1CVUdBMVVFQ2hNT1RYa2dRMjl0Y0dGdWVTQk1kR1F3WERBTkJna3Foa2lHOXcwQkFRRUZBQU5MQURCSUFrRUF3U04vZ2kzM1JtcEFRb0xRajdQNnpBbk5UMFJuN2JqQzMyM24zcTFPbmZ2bnZSMGZRanZOdDdyRGtBNUF2NVFuTTZWNFlVTlVuTWZiT1FxMFdMYkxTd0lEQVFBQm80R3VNSUdyTUIwR0ExVWREZ1FXQkJRZklIMW9mQlZwc1JBY0lNSzJoYmw3bmdNVFlEQjhCZ05WSFNNRWRUQnpnQlFmSUgxb2ZCVnBzUkFjSU1LMmhibDduZ01UWUtGUXBFNHdUREVMTUFrR0ExVUVCaE1DUjBJeEVqQVFCZ05WQkFnVENVSmxjbXR6YUdseVpURVFNQTRHQTFVRUJ4TUhUbVYzWW5WeWVURVhNQlVHQTFVRUNoTU9UWGtnUTI5dGNHRnVlU0JNZEdTQ0NRQ0ZUY09TaHVMb3RUQU1CZ05WSFJNRUJUQURBUUgvTUEwR0NTcUdTSWIzRFFFQkJRVUFBMEVBcW9nVjN1UGNsS1hEbURaTVQ3dmxQWXhMQXFDR0hadGxDeHo0aE00S1N0bEQyL0dOYzFYaUxiMWgvSzBDSkxEb3NyRUlibTN3aU8yTXZURVJyVllTTVE9PTwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIiBTUE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vY29kZXJwYWQuaW8iPm1hdHQuanVyaWtAaHVsdS5jb208L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBJblJlc3BvbnNlVG89Il80ZmZmYWE2MC02OTZiLTAxMzMtMzg4Ni0wMjQxZjY1YzA2OTMiIE5vdE9uT3JBZnRlcj0iMjAxNS0xMS0xMFQwMDoxMDo0M1oiIFJlY2lwaWVudD0iaHR0cHM6Ly9jb2RlcnBhZC5pby9zYW1sL2FjcyI+PC9zYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE1LTExLTA5VDIyOjU1OjQzWiIgTm90T25PckFmdGVyPSIyMDE1LTExLTEwVDAwOjEwOjQzWiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL2NvZGVycGFkLmlvPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNS0xMS0wOVQyMzo1NTo0M1oiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9IkdpdmVuLW5hbWUiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlPk1hdHQ8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iU3VybmFtZSI+PHNhbWw6QXR0cmlidXRlVmFsdWU+SnVyaWs8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iRW1haWwiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlPm1hdHQuanVyaWtAaHVsdS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4='
|
|
response_x = OneLogin::RubySaml::Response.new(xml)
|
|
response_x.settings = settings
|
|
assert response_x.send(:validate_signature)
|
|
assert_empty response_x.errors
|
|
end
|
|
|
|
it "return false when no fingerprint" do
|
|
settings.idp_cert_fingerprint = nil
|
|
settings.idp_cert = nil
|
|
response.settings = settings
|
|
assert !response.send(:validate_signature)
|
|
assert_includes response.errors, "Invalid Signature on SAML Response"
|
|
end
|
|
|
|
it "return false when the signature is invalid" do
|
|
settings.idp_cert_fingerprint = signature_fingerprint_1
|
|
response.settings = settings
|
|
assert !response.send(:validate_signature)
|
|
assert_includes response.errors, "Fingerprint mismatch"
|
|
assert_includes response.errors, "Invalid Signature on SAML Response"
|
|
end
|
|
|
|
it "return false when no X509Certificate and not cert provided at settings" do
|
|
settings.idp_cert_fingerprint = ruby_saml_cert_fingerprint
|
|
settings.idp_cert = nil
|
|
response_valid_signed_without_x509certificate.settings = settings
|
|
assert !response_valid_signed_without_x509certificate.send(:validate_signature)
|
|
assert_includes response_valid_signed_without_x509certificate.errors, "Invalid Signature on SAML Response"
|
|
end
|
|
|
|
it "return false when cert expired and check_idp_cert_expiration enabled" do
|
|
settings.idp_cert_fingerprint = nil
|
|
settings.idp_cert = ruby_saml_cert_text
|
|
settings.security[:check_idp_cert_expiration] = true
|
|
response_valid_signed.settings = settings
|
|
assert !response_valid_signed.send(:validate_signature)
|
|
assert_includes response_valid_signed.errors, "IdP x509 certificate expired"
|
|
end
|
|
|
|
it "return false when X509Certificate and the cert provided at settings mismatches" do
|
|
settings.idp_cert_fingerprint = nil
|
|
settings.idp_cert = signature_1
|
|
response_valid_signed_without_x509certificate.settings = settings
|
|
assert !response_valid_signed_without_x509certificate.send(:validate_signature)
|
|
assert_includes response_valid_signed_without_x509certificate.errors, "Key validation error"
|
|
assert_includes response_valid_signed_without_x509certificate.errors, "Invalid Signature on SAML Response"
|
|
end
|
|
|
|
it "return false when X509Certificate has invalid content" do
|
|
settings.idp_cert_fingerprint = nil
|
|
settings.idp_cert = ruby_saml_cert_text
|
|
content = read_response('response_with_signed_message_and_assertion.xml')
|
|
content = content.sub(/<ds:X509Certificate>.*<\/ds:X509Certificate>/,
|
|
"<ds:X509Certificate>an-invalid-certificate</ds:X509Certificate>")
|
|
response_invalid_x509certificate = OneLogin::RubySaml::Response.new(content)
|
|
response_invalid_x509certificate.settings = settings
|
|
assert !response_invalid_x509certificate.send(:validate_signature)
|
|
assert_includes response_invalid_x509certificate.errors, "Document Certificate Error"
|
|
assert_includes response_invalid_x509certificate.errors, "Invalid Signature on SAML Response"
|
|
end
|
|
|
|
it "return true when X509Certificate and the cert provided at settings matches" do
|
|
settings.idp_cert_fingerprint = nil
|
|
settings.idp_cert = ruby_saml_cert_text
|
|
response_valid_signed_without_x509certificate.settings = settings
|
|
assert response_valid_signed_without_x509certificate.send(:validate_signature)
|
|
assert_empty response_valid_signed_without_x509certificate.errors
|
|
end
|
|
|
|
it "return false when signature wrapping attack" do
|
|
signature_wrapping_attack = read_invalid_response("signature_wrapping_attack.xml.base64")
|
|
response_wrapped = OneLogin::RubySaml::Response.new(signature_wrapping_attack)
|
|
response_wrapped.stubs(:conditions).returns(nil)
|
|
response_wrapped.stubs(:validate_subject_confirmation).returns(true)
|
|
settings.idp_cert_fingerprint = "afe71c28ef740bc87425be13a2263d37971da1f9"
|
|
response_wrapped.settings = settings
|
|
assert !response_wrapped.send(:validate_signature)
|
|
assert_includes response_wrapped.errors, "Invalid Signature on SAML Response"
|
|
assert_includes response_wrapped.errors, "Signed element id #pfxc3d2b542-0f7e-8767-8e87-5b0dc6913375 is not found"
|
|
end
|
|
end
|
|
|
|
describe "#validate_signature with multiple idp certs" do
|
|
it "return true when at least a cert on idp_cert_multi is valid" do
|
|
settings.idp_cert_multi = {
|
|
:signing => [ruby_saml_cert_text2, ruby_saml_cert_text],
|
|
:encryption => []
|
|
}
|
|
response_valid_signed.settings = settings
|
|
res = response_valid_signed.send(:validate_signature)
|
|
assert_empty response_valid_signed.errors
|
|
end
|
|
|
|
it "return false when none cert on idp_cert_multi is valid" do
|
|
settings.idp_cert_fingerprint = ruby_saml_cert_fingerprint
|
|
settings.idp_cert_multi = {
|
|
:signing => [ruby_saml_cert_text2, ruby_saml_cert_text2],
|
|
:encryption => []
|
|
}
|
|
response_valid_signed.settings = settings
|
|
assert !response_valid_signed.send(:validate_signature)
|
|
assert_includes response_valid_signed.errors, "Certificate of the Signature element does not match provided certificate"
|
|
assert_includes response_valid_signed.errors, "Invalid Signature on SAML Response"
|
|
end
|
|
end
|
|
|
|
describe "#validate nameid" do
|
|
it "return false when no nameid element and required by settings" do
|
|
settings.security[:want_name_id] = true
|
|
response_no_nameid.settings = settings
|
|
assert !response_no_nameid.send(:validate_name_id)
|
|
assert_includes response_no_nameid.errors, "No NameID element found in the assertion of the Response"
|
|
end
|
|
|
|
it "return false when no nameid element and required by settings" do
|
|
response_empty_nameid.settings = settings
|
|
assert !response_empty_nameid.send(:validate_name_id)
|
|
assert_includes response_empty_nameid.errors, "An empty NameID value found"
|
|
end
|
|
|
|
it "return false when no nameid value" do
|
|
response_empty_nameid.settings = settings
|
|
assert !response_empty_nameid.send(:validate_name_id)
|
|
assert_includes response_empty_nameid.errors, "An empty NameID value found"
|
|
end
|
|
|
|
it "return false when wrong_spnamequalifier" do
|
|
settings.sp_entity_id = 'sp_entity_id'
|
|
response_wrong_spnamequalifier.settings = settings
|
|
assert !response_wrong_spnamequalifier.send(:validate_name_id)
|
|
assert_includes response_wrong_spnamequalifier.errors, "The SPNameQualifier value mistmatch the SP entityID value."
|
|
end
|
|
|
|
it "return true when no nameid element but not required by settings" do
|
|
settings.security[:want_name_id] = false
|
|
response_no_nameid.settings = settings
|
|
assert response_no_nameid.send(:validate_name_id)
|
|
end
|
|
|
|
it "return true when nameid is valid and response_wrong_spnamequalifier matches the SP issuer" do
|
|
settings.sp_entity_id = 'wrong-sp-entityid'
|
|
response_wrong_spnamequalifier.settings = settings
|
|
assert response_wrong_spnamequalifier.send(:validate_name_id)
|
|
end
|
|
end
|
|
|
|
describe "#nameid" do
|
|
it "extract the value of the name id element" do
|
|
assert_equal "support@onelogin.com", response.nameid
|
|
assert_equal "someone@example.com", response_with_signed_assertion.nameid
|
|
end
|
|
|
|
it "be extractable from an OpenSAML response" do
|
|
response_open_saml = OneLogin::RubySaml::Response.new(fixture(:open_saml))
|
|
assert_equal "someone@example.org", response_open_saml.nameid
|
|
end
|
|
|
|
it "be extractable from a Simple SAML PHP response" do
|
|
response_ssp = OneLogin::RubySaml::Response.new(fixture(:simple_saml_php))
|
|
assert_equal "someone@example.com", response_ssp.nameid
|
|
end
|
|
end
|
|
|
|
describe "#name_id_format" do
|
|
it "extract the value of the name id element" do
|
|
assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", response.name_id_format
|
|
assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", response_with_signed_assertion.name_id_format
|
|
end
|
|
end
|
|
|
|
describe "#sessionindex" do
|
|
it "extract the value of the sessionindex element" do
|
|
response = OneLogin::RubySaml::Response.new(fixture(:simple_saml_php))
|
|
assert_equal "_51be37965feb5579d803141076936dc2e9d1d98ebf", response.sessionindex
|
|
end
|
|
end
|
|
|
|
describe "#check_one_conditions" do
|
|
it "return false when none or more than one conditions element" do
|
|
response_no_conditions.soft = true
|
|
assert !response_no_conditions.send(:validate_one_conditions)
|
|
assert_includes response_no_conditions.errors, "The Assertion must include one Conditions element"
|
|
end
|
|
|
|
it "return true when one conditions element" do
|
|
response.soft = true
|
|
assert response.send(:validate_one_conditions)
|
|
end
|
|
|
|
it "return true when no conditions are present and skip_conditions is true" do
|
|
response_no_conditions_with_skip.soft = true
|
|
assert response_no_conditions_with_skip.send(:validate_one_conditions)
|
|
end
|
|
end
|
|
|
|
describe "#check_one_authnstatement" do
|
|
it "return false when none or more than one authnstatement element" do
|
|
response_no_authnstatement.soft = true
|
|
assert !response_no_authnstatement.send(:validate_one_authnstatement)
|
|
assert_includes response_no_authnstatement.errors, "The Assertion must include one AuthnStatement element"
|
|
end
|
|
|
|
it "return true when one authnstatement element" do
|
|
response.soft = true
|
|
assert response.send(:validate_one_authnstatement)
|
|
end
|
|
|
|
it "return true when SAML Response is empty but skip_authstatement option is used" do
|
|
response_no_authnstatement_with_skip.soft = true
|
|
assert response_no_authnstatement_with_skip.send(:validate_one_authnstatement)
|
|
assert_empty response_empty_destination_with_skip.errors
|
|
end
|
|
end
|
|
|
|
describe "#check_conditions" do
|
|
it "check time conditions" do
|
|
response.soft = true
|
|
assert !response.send(:validate_conditions)
|
|
response_time_updated = OneLogin::RubySaml::Response.new(response_document_without_recipient_with_time_updated)
|
|
response_time_updated.soft = true
|
|
assert response_time_updated.send(:validate_conditions)
|
|
Timecop.freeze(Time.parse("2011-06-14T18:25:01.516Z")) do
|
|
response_with_saml2_namespace = OneLogin::RubySaml::Response.new(response_document_with_saml2_namespace)
|
|
response_with_saml2_namespace.soft = true
|
|
assert response_with_saml2_namespace.send(:validate_conditions)
|
|
end
|
|
end
|
|
|
|
it "optionally allows for clock drift on NotBefore" do
|
|
settings.soft = true
|
|
|
|
# The NotBefore condition in the document is 2011-06-14T18:21:01.516Z
|
|
Timecop.freeze(Time.parse("2011-06-14T18:21:01Z")) do
|
|
special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new(
|
|
response_document_with_saml2_namespace,
|
|
:allowed_clock_drift => 0.515,
|
|
:settings => settings
|
|
)
|
|
assert !special_response_with_saml2_namespace.send(:validate_conditions)
|
|
|
|
special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new(
|
|
response_document_with_saml2_namespace,
|
|
:allowed_clock_drift => 0.516
|
|
)
|
|
assert special_response_with_saml2_namespace.send(:validate_conditions)
|
|
|
|
special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new(
|
|
response_document_with_saml2_namespace,
|
|
:allowed_clock_drift => '0.515',
|
|
:settings => settings
|
|
)
|
|
assert !special_response_with_saml2_namespace.send(:validate_conditions)
|
|
|
|
special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new(
|
|
response_document_with_saml2_namespace,
|
|
:allowed_clock_drift => '0.516'
|
|
)
|
|
assert special_response_with_saml2_namespace.send(:validate_conditions)
|
|
end
|
|
end
|
|
|
|
it "optionally allows for clock drift on NotOnOrAfter" do
|
|
# Java Floats behave differently than MRI
|
|
java = defined?(RUBY_ENGINE) && %w[jruby truffleruby].include?(RUBY_ENGINE)
|
|
|
|
settings.soft = true
|
|
|
|
# The NotBefore condition in the document is 2011-06-1418:31:01.516Z
|
|
Timecop.freeze(Time.parse("2011-06-14T18:31:02Z")) do
|
|
special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new(
|
|
response_document_with_saml2_namespace,
|
|
:allowed_clock_drift => 0.483,
|
|
:settings => settings
|
|
)
|
|
assert !special_response_with_saml2_namespace.send(:validate_conditions)
|
|
|
|
special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new(
|
|
response_document_with_saml2_namespace,
|
|
:allowed_clock_drift => java ? 0.485 : 0.484
|
|
)
|
|
assert special_response_with_saml2_namespace.send(:validate_conditions)
|
|
|
|
special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new(
|
|
response_document_with_saml2_namespace,
|
|
:allowed_clock_drift => '0.483',
|
|
:settings => settings
|
|
)
|
|
assert !special_response_with_saml2_namespace.send(:validate_conditions)
|
|
|
|
special_response_with_saml2_namespace = OneLogin::RubySaml::Response.new(
|
|
response_document_with_saml2_namespace,
|
|
:allowed_clock_drift => java ? '0.485' : '0.484'
|
|
)
|
|
assert special_response_with_saml2_namespace.send(:validate_conditions)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#attributes" do
|
|
it "extract the first attribute in a hash accessed via its symbol" do
|
|
assert_equal "demo", response.attributes[:uid]
|
|
end
|
|
|
|
it "extract the first attribute in a hash accessed via its name" do
|
|
assert_equal "demo", response.attributes["uid"]
|
|
end
|
|
|
|
it "extract all attributes" do
|
|
assert_equal "demo", response.attributes[:uid]
|
|
assert_equal "value", response.attributes[:another_value]
|
|
end
|
|
|
|
it "work for implicit namespaces" do
|
|
assert_equal "someone@example.com", response_with_signed_assertion.attributes["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"]
|
|
end
|
|
|
|
it "extract attributes from all AttributeStatement tags" do
|
|
assert_equal "smith", response_with_multiple_attribute_statements.attributes[:surname]
|
|
assert_equal "bob", response_with_multiple_attribute_statements.attributes[:firstname]
|
|
end
|
|
|
|
it "not raise on responses without attributes" do
|
|
assert_equal OneLogin::RubySaml::Attributes.new, response_unsigned.attributes
|
|
end
|
|
|
|
describe "#encrypted attributes" do
|
|
it "raise error when the assertion contains encrypted attributes but no private key to decrypt" do
|
|
settings.private_key = nil
|
|
response_encrypted_attrs.settings = settings
|
|
assert_raises(OneLogin::RubySaml::ValidationError, "An EncryptedAttribute found and no SP private key found on the settings to decrypt it") do
|
|
attrs = response_encrypted_attrs.attributes
|
|
end
|
|
end
|
|
|
|
it "extract attributes when the assertion contains encrypted attributes and the private key is provided" do
|
|
settings.certificate = ruby_saml_cert_text
|
|
settings.private_key = ruby_saml_key_text
|
|
response_encrypted_attrs.settings = settings
|
|
attributes = response_encrypted_attrs.attributes
|
|
assert_equal "test", attributes[:uid]
|
|
assert_equal "test@example.com", attributes[:mail]
|
|
end
|
|
end
|
|
|
|
it "return false when validating a response with duplicate attributes" do
|
|
response_duplicated_attributes.settings = settings
|
|
response_duplicated_attributes.options[:check_duplicated_attributes] = true
|
|
assert !response_duplicated_attributes.send(:validate_no_duplicated_attributes)
|
|
assert_includes response_duplicated_attributes.errors, "Found an Attribute element with duplicated Name"
|
|
end
|
|
|
|
it "return true when validating a response with duplicate attributes but skip check" do
|
|
response_duplicated_attributes.settings = settings
|
|
assert response_duplicated_attributes.send(:validate_no_duplicated_attributes)
|
|
end
|
|
|
|
describe "#multiple values" do
|
|
it "extract single value as string" do
|
|
assert_equal "demo", response_multiple_attr_values.attributes[:uid]
|
|
end
|
|
|
|
it "extract single value as string in compatibility mode off" do
|
|
OneLogin::RubySaml::Attributes.single_value_compatibility = false
|
|
assert_equal ["demo"], response_multiple_attr_values.attributes[:uid]
|
|
# classes are not reloaded between tests so restore default
|
|
OneLogin::RubySaml::Attributes.single_value_compatibility = true
|
|
end
|
|
|
|
it "extract first of multiple values as string for b/w compatibility" do
|
|
assert_equal 'value1', response_multiple_attr_values.attributes[:another_value]
|
|
end
|
|
|
|
it "extract first of multiple values as string for b/w compatibility in compatibility mode off" do
|
|
OneLogin::RubySaml::Attributes.single_value_compatibility = false
|
|
assert_equal ['value1', 'value2'], response_multiple_attr_values.attributes[:another_value]
|
|
OneLogin::RubySaml::Attributes.single_value_compatibility = true
|
|
end
|
|
|
|
it "return array with all attributes when asked in XML order" do
|
|
assert_equal ['value1', 'value2'], response_multiple_attr_values.attributes.multi(:another_value)
|
|
end
|
|
|
|
it "return array with all attributes when asked in XML order in compatibility mode off" do
|
|
OneLogin::RubySaml::Attributes.single_value_compatibility = false
|
|
assert_equal ['value1', 'value2'], response_multiple_attr_values.attributes.multi(:another_value)
|
|
OneLogin::RubySaml::Attributes.single_value_compatibility = true
|
|
end
|
|
|
|
it "return first of multiple values when multiple Attribute tags in XML" do
|
|
assert_equal 'role1', response_multiple_attr_values.attributes[:role]
|
|
end
|
|
|
|
it "return first of multiple values when multiple Attribute tags in XML in compatibility mode off" do
|
|
OneLogin::RubySaml::Attributes.single_value_compatibility = false
|
|
assert_equal ['role1', 'role2', 'role3'], response_multiple_attr_values.attributes[:role]
|
|
OneLogin::RubySaml::Attributes.single_value_compatibility = true
|
|
end
|
|
|
|
it "return all of multiple values in reverse order when multiple Attribute tags in XML" do
|
|
assert_equal ['role1', 'role2', 'role3'], response_multiple_attr_values.attributes.multi(:role)
|
|
end
|
|
|
|
it "return all of multiple values in reverse order when multiple Attribute tags in XML in compatibility mode off" do
|
|
OneLogin::RubySaml::Attributes.single_value_compatibility = false
|
|
assert_equal ['role1', 'role2', 'role3'], response_multiple_attr_values.attributes.multi(:role)
|
|
OneLogin::RubySaml::Attributes.single_value_compatibility = true
|
|
end
|
|
|
|
it "return all of multiple values when multiple Attribute tags in multiple AttributeStatement tags" do
|
|
OneLogin::RubySaml::Attributes.single_value_compatibility = false
|
|
assert_equal ['role1', 'role2', 'role3'], response_with_multiple_attribute_statements.attributes.multi(:role)
|
|
OneLogin::RubySaml::Attributes.single_value_compatibility = true
|
|
end
|
|
|
|
it "return nil value correctly" do
|
|
assert_nil response_multiple_attr_values.attributes[:attribute_with_nil_value]
|
|
end
|
|
|
|
it "return nil value correctly when not in compatibility mode off" do
|
|
OneLogin::RubySaml::Attributes.single_value_compatibility = false
|
|
assert_equal [nil], response_multiple_attr_values.attributes[:attribute_with_nil_value]
|
|
OneLogin::RubySaml::Attributes.single_value_compatibility = true
|
|
end
|
|
|
|
it "return multiple values including nil and empty string" do
|
|
response = OneLogin::RubySaml::Response.new(fixture(:response_with_multiple_attribute_values))
|
|
assert_equal ["", "valuePresent", nil, nil], response.attributes.multi(:attribute_with_nils_and_empty_strings)
|
|
end
|
|
|
|
it "return multiple values from [] when not in compatibility mode off" do
|
|
OneLogin::RubySaml::Attributes.single_value_compatibility = false
|
|
assert_equal ["", "valuePresent", nil, nil], response_multiple_attr_values.attributes[:attribute_with_nils_and_empty_strings]
|
|
OneLogin::RubySaml::Attributes.single_value_compatibility = true
|
|
end
|
|
|
|
it "check what happens when trying retrieve attribute that does not exists" do
|
|
assert_nil response_multiple_attr_values.attributes[:attribute_not_exists]
|
|
assert_nil response_multiple_attr_values.attributes.single(:attribute_not_exists)
|
|
assert_nil response_multiple_attr_values.attributes.multi(:attribute_not_exists)
|
|
|
|
OneLogin::RubySaml::Attributes.single_value_compatibility = false
|
|
assert_nil response_multiple_attr_values.attributes[:attribute_not_exists]
|
|
assert_nil response_multiple_attr_values.attributes.single(:attribute_not_exists)
|
|
assert_nil response_multiple_attr_values.attributes.multi(:attribute_not_exists)
|
|
OneLogin::RubySaml::Attributes.single_value_compatibility = true
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
describe "#session_expires_at" do
|
|
it "extract the value of the SessionNotOnOrAfter attribute" do
|
|
assert response.session_expires_at.is_a?(Time)
|
|
end
|
|
|
|
it "return nil when the value of the SessionNotOnOrAfter is not set" do
|
|
assert_nil response_without_attributes.session_expires_at
|
|
end
|
|
end
|
|
|
|
describe "#success" do
|
|
it "find a status code that says success" do
|
|
response.success?
|
|
end
|
|
end
|
|
|
|
describe '#xpath_first_from_signed_assertion' do
|
|
it 'not allow arbitrary code execution' do
|
|
malicious_response_document = fixture('response_eval', false)
|
|
malicious_response = OneLogin::RubySaml::Response.new(malicious_response_document)
|
|
malicious_response.send(:xpath_first_from_signed_assertion)
|
|
assert_nil $evalled
|
|
end
|
|
end
|
|
|
|
describe '#sign_document' do
|
|
it 'Sign an unsigned SAML Response XML and initiate the SAML object with it' do
|
|
xml = Base64.decode64(fixture("test_sign.xml"))
|
|
|
|
document = XMLSecurity::Document.new(xml)
|
|
|
|
formatted_cert = OneLogin::RubySaml::Utils.format_cert(ruby_saml_cert_text)
|
|
cert = OpenSSL::X509::Certificate.new(formatted_cert)
|
|
|
|
formatted_private_key = OneLogin::RubySaml::Utils.format_private_key(ruby_saml_key_text)
|
|
private_key = OpenSSL::PKey::RSA.new(formatted_private_key)
|
|
document.sign_document(private_key, cert)
|
|
|
|
signed_response = OneLogin::RubySaml::Response.new(document.to_s)
|
|
settings.assertion_consumer_service_url = "http://recipient"
|
|
settings.idp_cert = ruby_saml_cert_text
|
|
signed_response.settings = settings
|
|
Timecop.freeze(Time.parse("2015-03-18T04:50:24Z")) do
|
|
assert signed_response.is_valid?
|
|
end
|
|
assert_empty signed_response.errors
|
|
end
|
|
end
|
|
|
|
describe '#want_assertion_signed' do
|
|
before do
|
|
settings.security[:want_assertions_signed] = true
|
|
@signed_assertion = OneLogin::RubySaml::Response.new(response_document_with_signed_assertion, :settings => settings)
|
|
@no_signed_assertion = OneLogin::RubySaml::Response.new(response_document_valid_signed, :settings => settings)
|
|
end
|
|
|
|
|
|
it 'returns false if :want_assertion_signed enabled and Assertion not signed' do
|
|
assert !@no_signed_assertion.send(:validate_signed_elements)
|
|
assert_includes @no_signed_assertion.errors, "The Assertion of the Response is not signed and the SP requires it"
|
|
|
|
end
|
|
|
|
it 'returns true if :want_assertion_signed enabled and Assertion is signed' do
|
|
assert @signed_assertion.send(:validate_signed_elements)
|
|
assert_empty @signed_assertion.errors
|
|
end
|
|
end
|
|
|
|
describe "retrieve nameID" do
|
|
it 'is possible when nameID inside the assertion' do
|
|
response_valid_signed.settings = settings
|
|
assert_equal "test@onelogin.com", response_valid_signed.nameid
|
|
end
|
|
|
|
it 'is not possible when encryptID inside the assertion but no private key' do
|
|
response_encrypted_nameid.settings = settings
|
|
assert_raises(OneLogin::RubySaml::ValidationError, "An EncryptedID found and no SP private key found on the settings to decrypt it") do
|
|
assert_equal "test@onelogin.com", response_encrypted_nameid.nameid
|
|
end
|
|
|
|
assert_raises(OneLogin::RubySaml::ValidationError, "An EncryptedID found and no SP private key found on the settings to decrypt it") do
|
|
assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", response_encrypted_nameid.name_id_format
|
|
end
|
|
end
|
|
|
|
it 'is possible when encryptID inside the assertion and settings has the private key' do
|
|
settings.private_key = ruby_saml_key_text
|
|
response_encrypted_nameid.settings = settings
|
|
assert_equal "test@onelogin.com", response_encrypted_nameid.nameid
|
|
assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", response_encrypted_nameid.name_id_format
|
|
end
|
|
|
|
end
|
|
|
|
describe 'try to initialize an encrypted response' do
|
|
it 'raise if an encrypted assertion is found and no sp private key to decrypt it' do
|
|
error_msg = "An EncryptedAssertion found and no SP private key found on the settings to decrypt it. Be sure you provided the :settings parameter at the initialize method"
|
|
|
|
assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do
|
|
response = OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion)
|
|
end
|
|
|
|
assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do
|
|
response2 = OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings)
|
|
end
|
|
|
|
settings.certificate = ruby_saml_cert_text
|
|
settings.private_key = ruby_saml_key_text
|
|
assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do
|
|
response3 = OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion)
|
|
response3.settings
|
|
end
|
|
end
|
|
|
|
it 'raise if an encrypted assertion is found and the sp private key is wrong' do
|
|
settings.certificate = ruby_saml_cert_text
|
|
wrong_private_key = ruby_saml_key_text.sub!('A', 'B')
|
|
settings.private_key = wrong_private_key
|
|
|
|
error_msg = "Neither PUB key nor PRIV key: nested asn1 error"
|
|
assert_raises(OpenSSL::PKey::RSAError, error_msg) do
|
|
response = OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings)
|
|
end
|
|
end
|
|
|
|
it 'return true if an encrypted assertion is found and settings initialized with private_key' do
|
|
settings.certificate = ruby_saml_cert_text
|
|
settings.private_key = ruby_saml_key_text
|
|
response = OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings)
|
|
assert response.decrypted_document
|
|
|
|
response2 = OneLogin::RubySaml::Response.new(signed_message_encrypted_signed_assertion, :settings => settings)
|
|
assert response2.decrypted_document
|
|
|
|
response3 = OneLogin::RubySaml::Response.new(unsigned_message_encrypted_signed_assertion, :settings => settings)
|
|
assert response3.decrypted_document
|
|
|
|
response4 = OneLogin::RubySaml::Response.new(unsigned_message_encrypted_unsigned_assertion, :settings => settings)
|
|
assert response4.decrypted_document
|
|
|
|
assert OneLogin::RubySaml::Response.new(
|
|
Base64.encode64(File.read('test/responses/unsigned_encrypted_adfs.xml')),
|
|
:settings => settings
|
|
).decrypted_document
|
|
end
|
|
end
|
|
|
|
describe "retrieve nameID and attributes from encrypted assertion" do
|
|
|
|
before do
|
|
settings.idp_cert_fingerprint = 'EE:17:4E:FB:A8:81:71:12:0D:2A:78:43:BC:E7:0C:07:58:79:F4:F4'
|
|
settings.sp_entity_id = 'http://rubysaml.com:3000/saml/metadata'
|
|
settings.assertion_consumer_service_url = 'http://rubysaml.com:3000/saml/acs'
|
|
settings.certificate = ruby_saml_cert_text
|
|
settings.private_key = ruby_saml_key_text
|
|
end
|
|
|
|
it 'is possible when signed_message_encrypted_unsigned_assertion' do
|
|
response = OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings)
|
|
Timecop.freeze(Time.parse("2015-03-19T14:30:31Z")) do
|
|
assert response.is_valid?
|
|
assert_empty response.errors
|
|
assert_equal "test", response.attributes[:uid]
|
|
assert_equal "98e2bb61075e951b37d6b3be6954a54b340d86c7", response.nameid
|
|
end
|
|
end
|
|
|
|
it 'is possible when signed_message_encrypted_signed_assertion' do
|
|
response = OneLogin::RubySaml::Response.new(signed_message_encrypted_signed_assertion, :settings => settings)
|
|
Timecop.freeze(Time.parse("2015-03-19T14:30:31Z")) do
|
|
assert response.is_valid?
|
|
assert_empty response.errors
|
|
assert_equal "test", response.attributes[:uid]
|
|
assert_equal "98e2bb61075e951b37d6b3be6954a54b340d86c7", response.nameid
|
|
end
|
|
end
|
|
|
|
it 'is possible when unsigned_message_encrypted_signed_assertion' do
|
|
response = OneLogin::RubySaml::Response.new(unsigned_message_encrypted_signed_assertion, :settings => settings)
|
|
Timecop.freeze(Time.parse("2015-03-19T14:30:31Z")) do
|
|
assert response.is_valid?
|
|
assert_empty response.errors
|
|
assert_equal "test", response.attributes[:uid]
|
|
assert_equal "98e2bb61075e951b37d6b3be6954a54b340d86c7", response.nameid
|
|
end
|
|
end
|
|
|
|
it 'is not possible when unsigned_message_encrypted_unsigned_assertion' do
|
|
response = OneLogin::RubySaml::Response.new(unsigned_message_encrypted_unsigned_assertion, :settings => settings)
|
|
Timecop.freeze(Time.parse("2015-03-19T14:30:31Z")) do
|
|
assert !response.is_valid?
|
|
assert_includes response.errors, "Found an unexpected number of Signature Element. SAML Response rejected"
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#decrypt_assertion" do
|
|
before do
|
|
settings.private_key = ruby_saml_key_text
|
|
end
|
|
|
|
describe "check right settings" do
|
|
|
|
it "is not possible to decrypt the assertion if no private key" do
|
|
response = OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings)
|
|
|
|
encrypted_assertion_node = REXML::XPath.first(
|
|
response.document,
|
|
"(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
|
|
{ "p" => "urn:oasis:names:tc:SAML:2.0:protocol", "a" => "urn:oasis:names:tc:SAML:2.0:assertion" }
|
|
)
|
|
response.settings.private_key = nil
|
|
|
|
error_msg = "An EncryptedAssertion found and no SP private key found on the settings to decrypt it"
|
|
assert_raises(OneLogin::RubySaml::ValidationError, error_msg) do
|
|
decrypted = response.send(:decrypt_assertion, encrypted_assertion_node)
|
|
end
|
|
end
|
|
|
|
it "is possible to decrypt the assertion if private key" do
|
|
response = OneLogin::RubySaml::Response.new(signed_message_encrypted_unsigned_assertion, :settings => settings)
|
|
|
|
encrypted_assertion_node = REXML::XPath.first(
|
|
response.document,
|
|
"(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
|
|
{ "p" => "urn:oasis:names:tc:SAML:2.0:protocol", "a" => "urn:oasis:names:tc:SAML:2.0:assertion" }
|
|
)
|
|
decrypted = response.send(:decrypt_assertion, encrypted_assertion_node)
|
|
|
|
encrypted_assertion_node2 = REXML::XPath.first(
|
|
decrypted,
|
|
"(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
|
|
{ "p" => "urn:oasis:names:tc:SAML:2.0:protocol", "a" => "urn:oasis:names:tc:SAML:2.0:assertion" }
|
|
)
|
|
assert_nil encrypted_assertion_node2
|
|
assert decrypted.name, "Assertion"
|
|
end
|
|
|
|
it "is possible to decrypt the assertion if private key provided and EncryptedKey RetrievalMethod presents in response" do
|
|
settings.private_key = ruby_saml_key_text
|
|
resp = read_response('response_with_retrieval_method.xml')
|
|
response = OneLogin::RubySaml::Response.new(resp, :settings => settings)
|
|
|
|
encrypted_assertion_node = REXML::XPath.first(
|
|
response.document,
|
|
"(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
|
|
{ "p" => "urn:oasis:names:tc:SAML:2.0:protocol", "a" => "urn:oasis:names:tc:SAML:2.0:assertion" }
|
|
)
|
|
decrypted = response.send(:decrypt_assertion, encrypted_assertion_node)
|
|
|
|
encrypted_assertion_node2 = REXML::XPath.first(
|
|
decrypted,
|
|
"(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
|
|
{ "p" => "urn:oasis:names:tc:SAML:2.0:protocol", "a" => "urn:oasis:names:tc:SAML:2.0:assertion" }
|
|
)
|
|
|
|
assert_nil encrypted_assertion_node2
|
|
assert decrypted.name, "Assertion"
|
|
end
|
|
|
|
it "is possible to decrypt the assertion if private key but no saml namespace on the Assertion Element that is inside the EncryptedAssertion" do
|
|
unsigned_message_encrypted_assertion_without_saml_namespace = read_response('unsigned_message_encrypted_assertion_without_saml_namespace.xml.base64')
|
|
response = OneLogin::RubySaml::Response.new(unsigned_message_encrypted_assertion_without_saml_namespace, :settings => settings)
|
|
encrypted_assertion_node = REXML::XPath.first(
|
|
response.document,
|
|
"(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
|
|
{ "p" => "urn:oasis:names:tc:SAML:2.0:protocol", "a" => "urn:oasis:names:tc:SAML:2.0:assertion" }
|
|
)
|
|
decrypted = response.send(:decrypt_assertion, encrypted_assertion_node)
|
|
|
|
encrypted_assertion_node2 = REXML::XPath.first(
|
|
decrypted,
|
|
"(/p:Response/EncryptedAssertion/)|(/p:Response/a:EncryptedAssertion/)",
|
|
{ "p" => "urn:oasis:names:tc:SAML:2.0:protocol", "a" => "urn:oasis:names:tc:SAML:2.0:assertion" }
|
|
)
|
|
assert_nil encrypted_assertion_node2
|
|
assert decrypted.name, "Assertion"
|
|
end
|
|
end
|
|
|
|
describe "check different encrypt methods supported" do
|
|
it "EncryptionMethod DES-192 && Key Encryption Algorithm RSA-1_5" do
|
|
unsigned_message_des192_encrypted_signed_assertion = read_response('unsigned_message_des192_encrypted_signed_assertion.xml.base64')
|
|
response = OneLogin::RubySaml::Response.new(unsigned_message_des192_encrypted_signed_assertion, :settings => settings)
|
|
assert_equal "test", response.attributes[:uid]
|
|
assert_equal "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7", response.nameid
|
|
end
|
|
|
|
it "EncryptionMethod AES-128 && Key Encryption Algorithm RSA-OAEP-MGF1P" do
|
|
unsigned_message_aes128_encrypted_signed_assertion = read_response('unsigned_message_aes128_encrypted_signed_assertion.xml.base64')
|
|
response = OneLogin::RubySaml::Response.new(unsigned_message_aes128_encrypted_signed_assertion, :settings => settings)
|
|
assert_equal "test", response.attributes[:uid]
|
|
assert_equal "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7", response.nameid
|
|
end
|
|
|
|
it "EncryptionMethod AES-192 && Key Encryption Algorithm RSA-OAEP-MGF1P" do
|
|
unsigned_message_aes192_encrypted_signed_assertion = read_response('unsigned_message_aes192_encrypted_signed_assertion.xml.base64')
|
|
response = OneLogin::RubySaml::Response.new(unsigned_message_aes192_encrypted_signed_assertion, :settings => settings)
|
|
assert_equal "test", response.attributes[:uid]
|
|
assert_equal "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7", response.nameid
|
|
end
|
|
|
|
it "EncryptionMethod AES-256 && Key Encryption Algorithm RSA-OAEP-MGF1P" do
|
|
unsigned_message_aes256_encrypted_signed_assertion = read_response('unsigned_message_aes256_encrypted_signed_assertion.xml.base64')
|
|
response = OneLogin::RubySaml::Response.new(unsigned_message_aes256_encrypted_signed_assertion, :settings => settings)
|
|
assert_equal "test", response.attributes[:uid]
|
|
assert_equal "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7", response.nameid
|
|
end
|
|
|
|
it "EncryptionMethod AES-128-GCM && Key Encryption Algorithm RSA-OAEP-MGF1P" do
|
|
return unless OpenSSL::Cipher.ciphers.include? 'AES-128-GCM'
|
|
unsigned_message_aes128gcm_encrypted_signed_assertion = read_response('unsigned_message_aes128gcm_encrypted_signed_assertion.xml.base64')
|
|
response = OneLogin::RubySaml::Response.new(unsigned_message_aes128gcm_encrypted_signed_assertion, :settings => settings)
|
|
assert_equal "test", response.attributes[:uid]
|
|
assert_equal "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7", response.nameid
|
|
end
|
|
|
|
it "EncryptionMethod AES-192-GCM && Key Encryption Algorithm RSA-OAEP-MGF1P" do
|
|
return unless OpenSSL::Cipher.ciphers.include? 'AES-192-GCM'
|
|
unsigned_message_aes192gcm_encrypted_signed_assertion = read_response('unsigned_message_aes192gcm_encrypted_signed_assertion.xml.base64')
|
|
response = OneLogin::RubySaml::Response.new(unsigned_message_aes192gcm_encrypted_signed_assertion, :settings => settings)
|
|
assert_equal "test", response.attributes[:uid]
|
|
assert_equal "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7", response.nameid
|
|
end
|
|
|
|
it "EncryptionMethod AES-256-GCM && Key Encryption Algorithm RSA-OAEP-MGF1P" do
|
|
return unless OpenSSL::Cipher.ciphers.include? 'AES-256-GCM'
|
|
unsigned_message_aes256gcm_encrypted_signed_assertion = read_response('unsigned_message_aes256gcm_encrypted_signed_assertion.xml.base64')
|
|
response = OneLogin::RubySaml::Response.new(unsigned_message_aes256gcm_encrypted_signed_assertion, :settings => settings)
|
|
assert_equal "test", response.attributes[:uid]
|
|
assert_equal "_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7", response.nameid
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
describe "#status_code" do
|
|
it 'urn:oasis:names:tc:SAML:2.0:status:Responder' do
|
|
assert_equal response_statuscode_responder.status_code, 'urn:oasis:names:tc:SAML:2.0:status:Responder'
|
|
end
|
|
|
|
it 'urn:oasis:names:tc:SAML:2.0:status:Requester and urn:oasis:names:tc:SAML:2.0:status:UnsupportedBinding' do
|
|
assert_equal response_double_statuscode.status_code, 'urn:oasis:names:tc:SAML:2.0:status:Requester | urn:oasis:names:tc:SAML:2.0:status:UnsupportedBinding'
|
|
end
|
|
end
|
|
describe "test qualified name id in attributes" do
|
|
|
|
it "parsed the nameid" do
|
|
response = OneLogin::RubySaml::Response.new(read_response("signed_nameid_in_atts.xml"), :settings => settings)
|
|
response.settings.idp_cert_fingerprint = 'c51985d947f1be57082025050846eb27f6cab783'
|
|
assert_empty response.errors
|
|
assert_equal "test", response.attributes[:uid]
|
|
assert_equal "http://idp.example.com/metadata.php/ZdrjpwEdw22vKoxWAbZB78/gQ7s=", response.attributes.single('urn:oid:1.3.6.1.4.1.5923.1.1.1.10')
|
|
end
|
|
end
|
|
|
|
describe "test unqualified name id in attributes" do
|
|
|
|
it "parsed the nameid" do
|
|
response = OneLogin::RubySaml::Response.new(read_response("signed_unqual_nameid_in_atts.xml"), :settings => settings)
|
|
response.settings.idp_cert_fingerprint = 'c51985d947f1be57082025050846eb27f6cab783'
|
|
assert_empty response.errors
|
|
assert_equal "test", response.attributes[:uid]
|
|
assert_equal "ZdrjpwEdw22vKoxWAbZB78/gQ7s=", response.attributes.single('urn:oid:1.3.6.1.4.1.5923.1.1.1.10')
|
|
end
|
|
end
|
|
|
|
describe "signature wrapping attack with encrypted assertion" do
|
|
it "should not be valid" do
|
|
settings.private_key = ruby_saml_key_text
|
|
signature_wrapping_attack = read_invalid_response("encrypted_new_attack.xml.base64")
|
|
response_wrapped = OneLogin::RubySaml::Response.new(signature_wrapping_attack, :settings => settings)
|
|
response_wrapped.stubs(:conditions).returns(nil)
|
|
response_wrapped.stubs(:validate_subject_confirmation).returns(true)
|
|
settings.idp_cert_fingerprint = "385b1eec71143f00db6af936e2ea12a28771d72c"
|
|
assert !response_wrapped.is_valid?
|
|
assert_includes response_wrapped.errors, "Found an invalid Signed Element. SAML Response rejected"
|
|
end
|
|
end
|
|
|
|
describe "signature wrapping attack - concealed SAML response body" do
|
|
it "should not be valid" do
|
|
signature_wrapping_attack = read_invalid_response("response_with_concealed_signed_assertion.xml")
|
|
response_wrapped = OneLogin::RubySaml::Response.new(signature_wrapping_attack, :settings => settings)
|
|
settings.idp_cert_fingerprint = '4b68c453c7d994aad9025c99d5efcf566287fe8d'
|
|
response_wrapped.stubs(:conditions).returns(nil)
|
|
response_wrapped.stubs(:validate_subject_confirmation).returns(true)
|
|
assert !response_wrapped.is_valid?
|
|
assert_includes response_wrapped.errors, "SAML Response must contain 1 assertion"
|
|
end
|
|
end
|
|
|
|
describe "signature wrapping attack - doubled signed assertion SAML response" do
|
|
it "should not be valid" do
|
|
signature_wrapping_attack = read_invalid_response("response_with_doubled_signed_assertion.xml")
|
|
response_wrapped = OneLogin::RubySaml::Response.new(signature_wrapping_attack, :settings => settings)
|
|
settings.idp_cert_fingerprint = '4b68c453c7d994aad9025c99d5efcf566287fe8d'
|
|
response_wrapped.stubs(:conditions).returns(nil)
|
|
response_wrapped.stubs(:validate_subject_confirmation).returns(true)
|
|
assert !response_wrapped.is_valid?
|
|
assert_includes response_wrapped.errors, "SAML Response must contain 1 assertion"
|
|
end
|
|
end
|
|
end
|
|
end
|