Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow metadata to be retrieved from source containing data for multiple entities #373

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,21 @@ The following attributes are set:
* idp_slo_target_url
* idp_cert_fingerprint

### Retrieve one Entity Descriptor when many exist in Metadata

If the Meta data contains the data for many SAML entities, the relevant Entity
Descriptor can be specified when retrieving the settings from the
IdpMetadataParser:

```ruby
validate_cert = true
settings = idp_metadata_parser.parse_remote(
"https://example.com/auth/saml2/idp/metadata",
validate_cert,
entity_id: "http//example.com/target/entity"
)
```

## Retrieving Attributes

If you are using `saml:AttributeStatement` to transfer data like the username, you can access all the attributes through `response.attributes`. It contains all the `saml:AttributeStatement`s with its 'Name' as an indifferent key and one or more `saml:AttributeValue`s as values. The value returned depends on the value of the
Expand Down Expand Up @@ -411,9 +426,9 @@ 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
settings.security[:logout_responses_signed] = true # Enable or not
signature on Logout Response
settings.security[:want_assertions_signed] = true # Enable or not
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

Expand All @@ -426,7 +441,7 @@ The settings related to sign are stored in the `security` attribute of the setti
```

Notice that the RelayState parameter is used when creating the Signature on the HTTP-Redirect Binding.
Remember to provide it to the Signature builder if you are sending a `GET RelayState` parameter or the
Remember to provide it to the Signature builder if you are sending a `GET RelayState` parameter or the
signature validation process will fail at the Identity Provider.

The Service Provider will sign the request/responses with its private key.
Expand Down
140 changes: 77 additions & 63 deletions lib/onelogin/ruby-saml/idp_metadata_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class IdpMetadataParser

attr_reader :document
attr_reader :response
attr_reader :parse_options

# Parse the Identity Provider metadata and update the settings with the
# IdP values
Expand All @@ -36,22 +37,24 @@ def parse_remote(url, validate_cert = true, options = {})
end

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

settings = options[:settings]
settings = parse_options[:settings]
if settings.nil? || settings.is_a?(Hash)
settings = OneLogin::RubySaml::Settings.new(settings || {})
end

settings.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(options)
settings.idp_slo_target_url = single_logout_service_url(options)
settings.idp_sso_target_url = single_signon_service_url
settings.idp_slo_target_url = single_logout_service_url
settings.idp_cert = certificate_base64
settings.idp_cert_fingerprint = fingerprint(settings.idp_cert_fingerprint_algorithm)
settings.idp_attribute_names = attribute_names
Expand All @@ -67,56 +70,58 @@ def parse(idp_metadata, options = {})
# @raise [HttpError] Failure to fetch remote IdP metadata
def get_idp_metadata(url, validate_cert)
uri = URI.parse(url)
if uri.scheme == "http"
response = Net::HTTP.get_response(uri)
meta_text = response.body
elsif uri.scheme == "https"
http = Net::HTTP.new(uri.host, uri.port)
raise ArgumentError.new("url must begin with http or https") unless /^https?/ =~ uri.scheme
http = Net::HTTP.new(uri.host, uri.port)

if uri.scheme == "https"
http.use_ssl = true
# Most IdPs will probably use self signed certs
if validate_cert
http.verify_mode = OpenSSL::SSL::VERIFY_PEER

# Net::HTTP in Ruby 1.8 did not set the default certificate store
# automatically when VERIFY_PEER was specified.
if RUBY_VERSION < '1.9' && !http.ca_file && !http.ca_path && !http.cert_store
http.cert_store = OpenSSL::SSL::SSLContext::DEFAULT_CERT_STORE
end
else
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
http.verify_mode = validate_cert ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE

# Net::HTTP in Ruby 1.8 did not set the default certificate store
# automatically when VERIFY_PEER was specified.
if RUBY_VERSION < '1.9' && !http.ca_file && !http.ca_path && !http.cert_store
http.cert_store = OpenSSL::SSL::SSLContext::DEFAULT_CERT_STORE
end
get = Net::HTTP::Get.new(uri.request_uri)
response = http.request(get)
meta_text = response.body
else
raise ArgumentError.new("url must begin with http or https")
end

unless response.is_a? Net::HTTPSuccess
raise OneLogin::RubySaml::HttpError.new("Failed to fetch idp metadata")
end
get = Net::HTTP::Get.new(uri.request_uri)
response = http.request(get)
return response.body if response.is_a? Net::HTTPSuccess

raise OneLogin::RubySaml::HttpError.new(
"Failed to fetch idp metadata: #{response.code}: #{response.message}"
)
end

def entity_descriptor
@entity_descriptor ||= REXML::XPath.first(
document,
entity_descriptor_path,
namespace
)
end

meta_text
def entity_descriptor_path
path = "//md:EntityDescriptor"
entity_id = parse_options[:entity_id]
return path unless entity_id
path << "[@entityID=\"#{entity_id}\"]"
end

# @return [String|nil] IdP Entity ID value if exists
#
def idp_entity_id
node = REXML::XPath.first(
document,
"/md:EntityDescriptor/@entityID",
{ "md" => METADATA }
)
node.value if node
entity_descriptor.attributes["entityID"]
end

# @return [String|nil] IdP Name ID Format value if exists
#
def idp_name_id_format
node = REXML::XPath.first(
document,
"/md:EntityDescriptor/md:IDPSSODescriptor/md:NameIDFormat",
{ "md" => METADATA }
entity_descriptor,
"md:IDPSSODescriptor/md:NameIDFormat",
namespace
)
node.text if node
end
Expand All @@ -126,9 +131,9 @@ def idp_name_id_format
#
def single_signon_service_binding(binding_priority = nil)
nodes = REXML::XPath.match(
document,
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService/@Binding",
{ "md" => METADATA }
entity_descriptor,
"md:IDPSSODescriptor/md:SingleSignOnService/@Binding",
namespace
)
if binding_priority
values = nodes.map(&:value)
Expand All @@ -141,12 +146,12 @@ def single_signon_service_binding(binding_priority = nil)
# @param options [Hash]
# @return [String|nil] SingleSignOnService endpoint if exists
#
def single_signon_service_url(options = {})
binding = options[:sso_binding] || "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
def single_signon_service_url
binding = parse_options[:sso_binding] || "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
node = REXML::XPath.first(
document,
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location",
{ "md" => METADATA }
entity_descriptor,
"md:IDPSSODescriptor/md:SingleSignOnService[@Binding=\"#{binding}\"]/@Location",
namespace
)
node.value if node
end
Expand All @@ -156,9 +161,9 @@ def single_signon_service_url(options = {})
#
def single_logout_service_binding(binding_priority = nil)
nodes = REXML::XPath.match(
document,
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService/@Binding",
{ "md" => METADATA }
entity_descriptor,
"md:IDPSSODescriptor/md:SingleLogoutService/@Binding",
namespace
)
if binding_priority
values = nodes.map(&:value)
Expand All @@ -171,12 +176,12 @@ def single_logout_service_binding(binding_priority = nil)
# @param options [Hash]
# @return [String|nil] SingleLogoutService endpoint if exists
#
def single_logout_service_url(options = {})
binding = options[:slo_binding] || "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
def single_logout_service_url
binding = parse_options[:slo_binding] || "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
node = REXML::XPath.first(
document,
"/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleLogoutService[@Binding=\"#{binding}\"]/@Location",
{ "md" => METADATA }
entity_descriptor,
"md:IDPSSODescriptor/md:SingleLogoutService[@Binding=\"#{binding}\"]/@Location",
namespace
)
node.value if node
end
Expand All @@ -186,16 +191,16 @@ def single_logout_service_url(options = {})
def certificate_base64
@certificate_base64 ||= begin
node = REXML::XPath.first(
document,
"/md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
{ "md" => METADATA, "ds" => DSIG }
entity_descriptor,
"md:IDPSSODescriptor/md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
namespace
)

unless node
node = REXML::XPath.first(
document,
"/md:EntityDescriptor/md:IDPSSODescriptor/md:KeyDescriptor/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
{ "md" => METADATA, "ds" => DSIG }
entity_descriptor,
"md:IDPSSODescriptor/md:KeyDescriptor/ds:KeyInfo/ds:X509Data/ds:X509Certificate",
namespace
)
end
node.text if node
Expand Down Expand Up @@ -228,12 +233,21 @@ def fingerprint(fingerprint_algorithm = XMLSecurity::Document::SHA1)
#
def attribute_names
nodes = REXML::XPath.match(
document,
"/md:EntityDescriptor/md:IDPSSODescriptor/saml:Attribute/@Name",
{ "md" => METADATA, "NameFormat" => NAME_FORMAT, "saml" => SAML_ASSERTION }
entity_descriptor,
"md:IDPSSODescriptor/saml:Attribute/@Name",
namespace
)
nodes.map(&:value)
end

def namespace
{
"md" => METADATA,
"NameFormat" => NAME_FORMAT,
"saml" => SAML_ASSERTION,
"ds" => DSIG
}
end
end
end
end
44 changes: 36 additions & 8 deletions test/idp_metadata_parser_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ def initialize; end

settings = idp_metadata_parser.parse(idp_metadata)

assert_equal "https://example.hello.com/access/saml/idp.xml", settings.idp_entity_id
assert_equal "https://example.hello.com/access/saml/login", settings.idp_sso_target_url
assert_equal "https://hello.example.com/access/saml/idp.xml", settings.idp_entity_id
assert_equal "https://hello.example.com/access/saml/login", settings.idp_sso_target_url
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
assert_equal "https://example.hello.com/access/saml/logout", settings.idp_slo_target_url
assert_equal "https://hello.example.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
Expand Down Expand Up @@ -90,10 +90,10 @@ def initialize; end
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
settings = idp_metadata_parser.parse_remote(@url)

assert_equal "https://example.hello.com/access/saml/idp.xml", settings.idp_entity_id
assert_equal "https://example.hello.com/access/saml/login", settings.idp_sso_target_url
assert_equal "https://hello.example.com/access/saml/idp.xml", settings.idp_entity_id
assert_equal "https://hello.example.com/access/saml/login", settings.idp_sso_target_url
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
assert_equal "https://example.hello.com/access/saml/logout", settings.idp_slo_target_url
assert_equal "https://hello.example.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 OpenSSL::SSL::VERIFY_PEER, @http.verify_mode
Expand Down Expand Up @@ -130,10 +130,38 @@ def initialize; end
idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new

exception = assert_raises(OneLogin::RubySaml::HttpError) do
idp_metadata_parser.parse_remote("https://example.hello.com/access/saml/idp.xml")
idp_metadata_parser.parse_remote("https://hello.example.com/access/saml/idp.xml")
end

assert_equal("Failed to fetch idp metadata", exception.message)
assert_match("Failed to fetch idp metadata", exception.message)
end
end

describe "parsing metadata with many entity descriptors" do
before do
@idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new
@idp_metadata = read_response("idp_multiple_descriptors.xml")
@settings = @idp_metadata_parser.parse(@idp_metadata)
end

it "should find first descriptor" do
assert_equal "https://foo.example.com/access/saml/idp.xml", @settings.idp_entity_id
end

it "should find named descriptor" do
entity_id = "https://bar.example.com/access/saml/idp.xml"
settings = @idp_metadata_parser.parse(
@idp_metadata, :entity_id => entity_id
)
assert_equal entity_id, settings.idp_entity_id
end

it "should retreive data" do
assert_equal "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", @settings.name_identifier_format
assert_equal "https://hello.example.com/access/saml/login", @settings.idp_sso_target_url
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
assert_equal "https://hello.example.com/access/saml/logout", @settings.idp_slo_target_url
assert_equal ["AuthToken", "SSOStartPage"], @settings.idp_attribute_names
end
end
end
Loading