Skip to content

Commit

Permalink
Merge remote-tracking branch 'onelogin/master'
Browse files Browse the repository at this point in the history
* onelogin/master:
  Explictly state Ruby 2.0.x support
  Related to PR SAML-Toolkits#269
  Fix SAML-Toolkits#299
  Fix SAML-Toolkits#306. Support WantAssertionsSigned
  Use settings.idp_cert_fingerprint_algorithm in idp_metadata_parser for fingerprint instead of SHA1
  Implement binding parsing in idp_metadata_parser
  • Loading branch information
alex-wood committed Apr 26, 2016
2 parents 4413590 + bf26b30 commit 9847ff6
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 23 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ We created a demo project for Rails4 that uses the latest version of this librar
### Supported versions of Ruby
* 1.8.7
* 1.9.x
* 2.0.x
* 2.1.x
* 2.2.x
* JRuby 1.7.19
Expand Down Expand Up @@ -386,7 +387,10 @@ The settings related to sign are stored in the `security` attribute of the setti
```ruby
settings.security[:authn_requests_signed] = true # Enable or not signature on AuthNRequest
settings.security[:logout_requests_signed] = true # Enable or not signature on Logout Request
settings.security[:logout_responses_signed] = true # Enable or not signature on Logout Response
settings.security[:logout_responses_signed] = true # Enable or not
signature on Logout Response
settings.security[:want_assertions_signed] = true # Enable or not
the requirement of signed assertion
settings.security[:metadata_signed] = true # Enable or not signature on Metadata
settings.security[:digest_method] = XMLSecurity::Document::SHA1
Expand Down
77 changes: 64 additions & 13 deletions lib/onelogin/ruby-saml/idp_metadata_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,27 +28,30 @@ class IdpMetadataParser
# IdP values
#
# @param (see IdpMetadataParser#get_idp_metadata)
# @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object
# @return (see IdpMetadataParser#get_idp_metadata)
# @raise (see IdpMetadataParser#get_idp_metadata)
def parse_remote(url, validate_cert = true)
def parse_remote(url, validate_cert = true, options = {})
idp_metadata = get_idp_metadata(url, validate_cert)
parse(idp_metadata)
parse(idp_metadata, options)
end

# Parse the Identity Provider metadata and update the settings with the IdP values
# @param idp_metadata [String]
# @param options [Hash] :settings to provide the OneLogin::RubySaml::Settings object
#
def parse(idp_metadata)
def parse(idp_metadata, options = {})
@document = REXML::Document.new(idp_metadata)

OneLogin::RubySaml::Settings.new.tap do |settings|
(options[:settings] || OneLogin::RubySaml::Settings.new).tap do |settings|
settings.idp_entity_id = idp_entity_id
settings.name_identifier_format = idp_name_id_format
settings.idp_sso_target_url = single_signon_service_url
settings.idp_slo_target_url = single_logout_service_url
settings.idp_sso_target_url = single_signon_service_url(options)
settings.idp_slo_target_url = single_logout_service_url(options)
settings.idp_cert = certificate_base64
settings.idp_cert_fingerprint = fingerprint
settings.idp_cert_fingerprint = fingerprint(settings.idp_cert_fingerprint_algorithm)
settings.idp_attribute_names = attribute_names
settings.idp_cert_fingerprint = fingerprint(settings.idp_cert_fingerprint_algorithm)
end
end

Expand Down Expand Up @@ -115,23 +118,61 @@ def idp_name_id_format
node.text if node
end

# @param binding_priority [Array]
# @return [String|nil] SingleSignOnService binding if exists
#
def single_signon_service_binding(binding_priority = nil)
nodes = REXML::XPath.match(
document,
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService/@Binding",
{ "md" => METADATA }
)
if binding_priority
values = nodes.map(&:value)
binding_priority.detect{ |binding| values.include? binding }
else
nodes.first.value if nodes.any?
end
end

# @param options [Hash]
# @return [String|nil] SingleSignOnService endpoint if exists
#
def single_signon_service_url
def single_signon_service_url(options = {})
binding = options[:sso_binding] || "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
node = REXML::XPath.first(
document,
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService/@Location",
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location",
{ "md" => METADATA }
)
node.value if node
end

# @param binding_priority [Array]
# @return [String|nil] SingleLogoutService binding if exists
#
def single_logout_service_binding(binding_priority = nil)
nodes = REXML::XPath.match(
document,
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService/@Binding",
{ "md" => METADATA }
)
if binding_priority
values = nodes.map(&:value)
binding_priority.detect{ |binding| values.include? binding }
else
nodes.first.value if nodes.any?
end
end

# @param options [Hash]
# @return [String|nil] SingleLogoutService endpoint if exists
#
def single_logout_service_url
def single_logout_service_url(options = {})
binding = options[:slo_binding] || "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
node = REXML::XPath.first(
document,
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService/@Location",
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService[@Binding=\"#{binding}\"]/@Location",
{ "md" => METADATA }
)
node.value if node
Expand All @@ -146,6 +187,14 @@ def certificate_base64
"/md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
{ "md" => METADATA, "ds" => DSIG }
)

unless node
node = REXML::XPath.first(
document,
"/md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
{ "md" => METADATA, "ds" => DSIG }
)
end
node.text if node
end
end
Expand All @@ -161,11 +210,13 @@ def certificate

# @return [String|nil] the SHA-1 fingerpint of the X509Certificate if it exists
#
def fingerprint
def fingerprint(fingerprint_algorithm)
@fingerprint ||= begin
if certificate
cert = OpenSSL::X509::Certificate.new(certificate)
Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(":")

fingerprint_alg = XMLSecurity::BaseDocument.new.algorithm(fingerprint_algorithm).new
fingerprint_alg.hexdigest(cert.to_der).upcase.scan(/../).join(":")
end
end
end
Expand Down
3 changes: 1 addition & 2 deletions lib/onelogin/ruby-saml/metadata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ def generate(settings, pretty_print=false)
sp_sso = root.add_element "md:SPSSODescriptor", {
"protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol",
"AuthnRequestsSigned" => settings.security[:authn_requests_signed],
# However we would like assertions signed if idp_cert_fingerprint or idp_cert is set
"WantAssertionsSigned" => !!(settings.idp_cert_fingerprint || settings.idp_cert)
"WantAssertionsSigned" => settings.security[:want_assertions_signed],
}

# Add KeyDescriptor if messages will be signed / encrypted
Expand Down
10 changes: 7 additions & 3 deletions lib/onelogin/ruby-saml/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ def initialize(response, options = {})
@options = options

@soft = true
if !options.empty? && !options[:settings].nil?
unless options[:settings].nil?
@settings = options[:settings]
if !options[:settings].soft.nil?
@soft = options[:settings].soft
unless @settings.soft.nil?
@soft = @settings.soft
end
end

Expand Down Expand Up @@ -448,6 +448,10 @@ def validate_signed_elements
return append_error("Found an unexpected number of Signature Element. SAML Response rejected")
end

if settings.security[:want_assertions_signed] && !(signed_elements.include? "Assertion")
return append_error("The Assertion of the Response is not signed and the SP requires it")
end

true
end

Expand Down
1 change: 1 addition & 0 deletions lib/onelogin/ruby-saml/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ def get_sp_key
:authn_requests_signed => false,
:logout_requests_signed => false,
:logout_responses_signed => false,
:want_assertions_signed => false,
:metadata_signed => false,
:embed_sign => false,
:digest_method => XMLSecurity::Document::SHA1,
Expand Down
8 changes: 4 additions & 4 deletions lib/onelogin/ruby-saml/slo_logoutrequest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ def initialize(request, options = {})
@options = options

@soft = true
if !options.empty? && !options[:settings].nil?
unless options[:settings].nil?
@settings = options[:settings]
if !options[:settings].soft.nil?
@soft = options[:settings].soft
unless @settings.soft.nil?
@soft = @settings.soft
end
end

Expand Down Expand Up @@ -213,7 +213,7 @@ def validate_request_state
# @raise [ValidationError] if soft == false and validation fails
#
def validate_issuer
return true if settings.idp_entity_id.nil? || issuer.nil?
return true if settings.nil? || settings.idp_entity_id.nil? || issuer.nil?

unless URI.parse(issuer) == URI.parse(settings.idp_entity_id)
return append_error("Doesn't match the issuer, expected: <#{settings.idp_entity_id}>, but was: <#{issuer}>")
Expand Down
26 changes: 26 additions & 0 deletions test/idp_metadata_parser_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,33 @@ def initialize; end
assert_equal "https://example.hello.com/access/saml/logout", settings.idp_slo_target_url
assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", settings.name_identifier_format
assert_equal ["AuthToken", "SSOStartPage"], settings.idp_attribute_names
assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
end

it "extract certificate from md:KeyDescriptor[@use='signing']" do
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
idp_metadata = read_response("idp_descriptor.xml")
settings = idp_metadata_parser.parse(idp_metadata)
assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
end

it "extract certificate from md:KeyDescriptor[@use='encryption']" do
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
idp_metadata = read_response("idp_descriptor.xml")
idp_metadata = idp_metadata.sub(/<md:KeyDescriptor use="signing">(.*?)<\/md:KeyDescriptor>/m, "")
settings = idp_metadata_parser.parse(idp_metadata)
assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
end

it "extract certificate from md:KeyDescriptor" do
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
idp_metadata = read_response("idp_descriptor.xml")
idp_metadata = idp_metadata.sub(/<md:KeyDescriptor use="signing">(.*?)<\/md:KeyDescriptor>/m, "")
idp_metadata = idp_metadata.sub('<md:KeyDescriptor use="encryption">', '<md:KeyDescriptor>')
settings = idp_metadata_parser.parse(idp_metadata)
assert_equal "F1:3C:6B:80:90:5A:03:0E:6C:91:3E:5D:15:FA:DD:B0:16:45:48:72", settings.idp_cert_fingerprint
end

end

describe "download and parse IdP descriptor file" do
Expand Down
14 changes: 14 additions & 0 deletions test/metadata_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ class MetadataTest < Minitest::Test
assert validate_xml!(xml_text, "saml-schema-metadata-2.0.xsd")
end

describe "WantAssertionsSigned" do
it "generates Service Provider Metadata with WantAssertionsSigned = false" do
settings.security[:want_assertions_signed] = false
assert_equal "false", spsso_descriptor.attribute("WantAssertionsSigned").value
assert validate_xml!(xml_text, "saml-schema-metadata-2.0.xsd")
end

it "generates Service Provider Metadata with WantAssertionsSigned = true" do
settings.security[:want_assertions_signed] = true
assert_equal "true", spsso_descriptor.attribute("WantAssertionsSigned").value
assert validate_xml!(xml_text, "saml-schema-metadata-2.0.xsd")
end
end

describe "when auth requests are signed" do
let(:key_descriptors) do
REXML::XPath.match(
Expand Down
20 changes: 20 additions & 0 deletions test/response_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,26 @@ class RubySamlTest < Minitest::Test
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
Expand Down

0 comments on commit 9847ff6

Please sign in to comment.