Commit 66aa31b

mo <mo.khan@gmail.com>
2017-11-04 18:59:17
allow url registration of metadata.
1 parent e8385f5
lib/saml/kit/authentication_request.rb
@@ -52,7 +52,7 @@ module Saml
       end
 
       def service_provider
-        registry.service_provider_metadata_for(issuer)
+        registry.metadata_for(issuer)
       end
 
       def registry
lib/saml/kit/default_registry.rb
@@ -9,9 +9,41 @@ module Saml
         @items[metadata.entity_id] = metadata
       end
 
-      def service_provider_metadata_for(entity_id)
+      def register_url(url, verify_ssl: true)
+        content = HttpApi.new(url, verify_ssl: verify_ssl).get
+        register(Saml::Kit::Metadata.from(content))
+      end
+
+      def metadata_for(entity_id)
         @items[entity_id]
       end
+
+      class HttpApi
+        attr_reader :uri, :verify_ssl
+
+        def initialize(url, verify_ssl: true)
+          @uri = URI.parse(url)
+          @verify_ssl = verify_ssl
+        end
+
+        def get
+          execute(Net::HTTP::Get.new(uri.request_uri)).body
+        end
+
+        def execute(request)
+          http.request(request)
+        end
+
+        private
+
+        def http
+          http = Net::HTTP.new(uri.host, uri.port)
+          http.read_timeout = 30
+          http.use_ssl = uri.is_a?(URI::HTTPS)
+          http.verify_mode = OpenSSL::SSL::VERIFY_NONE unless verify_ssl
+          http
+        end
+      end
     end
   end
 end
lib/saml/kit/identity_provider_metadata.rb
@@ -6,14 +6,14 @@ module Saml
       end
 
       def single_sign_on_services
-        xpath = "/md:EntityDescriptor/md:IDPSSODescriptor/md:SingleSignOnService"
+        xpath = "/md:EntityDescriptor/md:#{name}/md:SingleSignOnService"
         find_all(xpath).map do |item|
           { binding: item.attribute("Binding").value, location: item.attribute("Location").value }
         end
       end
 
       def attributes
-        find_all("/md:EntityDescriptor/md:IDPSSODescriptor/saml:Attribute").map do |item|
+        find_all("/md:EntityDescriptor/md:#{name}/saml:Attribute").map do |item|
           {
             format: item.attribute("NameFormat").value,
             friendly_name: item.attribute("FriendlyName").value,
@@ -86,7 +86,7 @@ module Saml
         def entity_descriptor_options
           {
             'xmlns': Namespaces::METADATA,
-            'xmlns:ds': Namespaces::SIGNATURE,
+            'xmlns:ds': Namespaces::XMLDSIG,
             'xmlns:saml': Namespaces::ASSERTION,
             ID: "_#{id}",
             entityID: entity_id,
lib/saml/kit/metadata.rb
@@ -6,7 +6,7 @@ module Saml
       METADATA_XSD = File.expand_path("./xsd/saml-schema-metadata-2.0.xsd", File.dirname(__FILE__)).freeze
       NAMESPACES = {
         "NameFormat": Namespaces::ATTR_SPLAT,
-        "ds": Namespaces::SIGNATURE,
+        "ds": Namespaces::XMLDSIG,
         "md": Namespaces::METADATA,
         "saml": Namespaces::ASSERTION,
       }.freeze
@@ -16,10 +16,10 @@ module Saml
       validate :must_match_xsd
       validate :must_have_valid_signature
 
-      attr_reader :xml, :descriptor_name
+      attr_reader :xml, :name
 
-      def initialize(descriptor_name, xml)
-        @descriptor_name = descriptor_name
+      def initialize(name, xml)
+        @name = name
         @xml = xml
       end
 
@@ -28,11 +28,11 @@ module Saml
       end
 
       def name_id_formats
-        find_all("/md:EntityDescriptor/md:#{descriptor_name}/md:NameIDFormat").map(&:text)
+        find_all("/md:EntityDescriptor/md:#{name}/md:NameIDFormat").map(&:text)
       end
 
       def certificates
-        xpath = "/md:EntityDescriptor/md:#{descriptor_name}/md:KeyDescriptor"
+        xpath = "/md:EntityDescriptor/md:#{name}/md:KeyDescriptor"
         find_all(xpath).map do |item|
           cert = item.at_xpath("./ds:KeyInfo/ds:X509Data/ds:X509Certificate", NAMESPACES).text
           {
@@ -52,7 +52,7 @@ module Saml
       end
 
       def single_logout_services
-        xpath = "/md:EntityDescriptor/md:#{descriptor_name}/md:SingleLogoutService"
+        xpath = "/md:EntityDescriptor/md:#{name}/md:SingleLogoutService"
         find_all(xpath).map do |item|
           {
             binding: item.attribute("Binding").value,
@@ -65,6 +65,16 @@ module Saml
         @xml
       end
 
+      def self.from(content)
+        hash = Hash.from_xml(content)
+        entity_descriptor = hash["EntityDescriptor"]
+        if entity_descriptor.keys.include?("SPSSODescriptor")
+          Saml::Kit::ServiceProviderMetadata.new(content)
+        elsif entity_descriptor.keys.include?("IDPSSODescriptor")
+          Saml::Kit::IdentityProviderMetadata.new(content)
+        end
+      end
+
       private
 
       def document
@@ -80,7 +90,7 @@ module Saml
       end
 
       def metadata
-        find_by("/md:EntityDescriptor/md:#{descriptor_name}").present?
+        find_by("/md:EntityDescriptor/md:#{name}").present?
       end
 
       def must_contain_descriptor
@@ -114,7 +124,7 @@ module Saml
       end
 
       def error_message(key)
-        I18n.translate(key, scope: "saml/kit.errors.#{descriptor_name}")
+        I18n.translate(key, scope: "saml/kit.errors.#{name}")
       end
     end
   end
lib/saml/kit/namespaces.rb
@@ -13,7 +13,6 @@ module Saml
       PERSISTENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
       POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
       PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
-      SIGNATURE = "http://www.w3.org/2000/09/xmldsig#"
       SUCCESS = "urn:oasis:names:tc:SAML:2.0:status:Success"
       TRANSIENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
       UNSPECIFIED = "urn:oasis:names:tc:SAML:2.0:consent:unspecified"
lib/saml/kit/service_provider_metadata.rb
@@ -6,7 +6,7 @@ module Saml
       end
 
       def assertion_consumer_services
-        find_all("/md:EntityDescriptor/md:SPSSODescriptor/md:AssertionConsumerService").map do |item|
+        find_all("/md:EntityDescriptor/md:#{name}/md:AssertionConsumerService").map do |item|
           {
             binding: item.attribute("Binding").value,
             location: item.attribute("Location").value,
lib/saml/kit.rb
@@ -1,11 +1,12 @@
 require "saml/kit/version"
 
 require "active_model"
+require "active_support/core_ext/date/calculations"
 require "active_support/core_ext/hash/conversions"
 require "active_support/core_ext/numeric/time"
 require "active_support/duration"
-require "active_support/core_ext/date/calculations"
 require "builder"
+require "net/http"
 require "nokogiri"
 require "securerandom"
 require "xmldsig"
spec/saml/authentication_request_spec.rb
@@ -48,7 +48,7 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
 
     before :each do
       allow(Saml::Kit.configuration).to receive(:registry).and_return(registry)
-      allow(registry).to receive(:service_provider_metadata_for).and_return(service_provider_metadata)
+      allow(registry).to receive(:metadata_for).and_return(service_provider_metadata)
       allow(service_provider_metadata).to receive(:matches?).and_return(true)
     end
 
@@ -93,7 +93,7 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
     end
 
     it 'is valid when an the ACS is available via the registry' do
-      allow(registry).to receive(:service_provider_metadata_for).with(issuer)
+      allow(registry).to receive(:metadata_for).with(issuer)
         .and_return(service_provider_metadata)
       allow(service_provider_metadata).to receive(:matches?).and_return(true)
       allow(service_provider_metadata).to receive(:assertion_consumer_services).and_return([
@@ -127,8 +127,8 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
       subject = builder.build
 
       allow(Saml::Kit.configuration).to receive(:registry).and_return(registry)
-      allow(registry).to receive(:service_provider_metadata_for).and_return(service_provider_metadata)
-      allow(registry).to receive(:service_provider_metadata_for).with(issuer).and_return(service_provider_metadata)
+      allow(registry).to receive(:metadata_for).and_return(service_provider_metadata)
+      allow(registry).to receive(:metadata_for).with(issuer).and_return(service_provider_metadata)
       allow(service_provider_metadata).to receive(:assertion_consumer_services).and_return([
         { location: acs_url, binding: Saml::Kit::Namespaces::POST }
       ])
spec/saml/default_registry_spec.rb
@@ -2,18 +2,46 @@ require 'spec_helper'
 
 RSpec.describe Saml::Kit::DefaultRegistry do
   subject { described_class.new }
+  let(:entity_id) { FFaker::Internet.http_url }
+  let(:service_provider_metadata) do
+    builder = Saml::Kit::ServiceProviderMetadata::Builder.new
+    builder.entity_id = entity_id
+    builder.build
+  end
+  let(:identity_provider_metadata) do
+    builder = Saml::Kit::IdentityProviderMetadata::Builder.new
+    builder.entity_id = entity_id
+    builder.build
+  end
 
-  describe "#service_provider_metadata_for" do
-    let(:entity_id) { FFaker::Internet.http_url }
-    let(:service_provider_metadata) do
-      builder = Saml::Kit::ServiceProviderMetadata::Builder.new
-      builder.entity_id = entity_id
-      builder.build
-    end
-
+  describe "#metadata_for" do
     it 'returns the metadata for the entity_id' do
       subject.register(service_provider_metadata)
-      expect(subject.service_provider_metadata_for(entity_id)).to eql(service_provider_metadata)
+      expect(subject.metadata_for(entity_id)).to eql(service_provider_metadata)
+    end
+  end
+
+  describe "#register_url" do
+    let(:url) { FFaker::Internet.http_url }
+
+    it 'fetches the SP metadata from a remote url and registers it' do
+      stub_request(:get, url).
+        to_return(status: 200, body: service_provider_metadata.to_xml)
+      subject.register_url(url)
+
+      result = subject.metadata_for(entity_id)
+      expect(result).to be_present
+      expect(result).to be_instance_of(Saml::Kit::ServiceProviderMetadata)
+    end
+
+    it 'fetches the IDP metadata from a remote url' do
+      stub_request(:get, url).
+        to_return(status: 200, body: identity_provider_metadata.to_xml)
+      subject.register_url(url)
+
+      result = subject.metadata_for(entity_id)
+      expect(result).to be_present
+      expect(result).to be_instance_of(Saml::Kit::IdentityProviderMetadata)
     end
   end
 end
spec/spec_helper.rb
@@ -1,7 +1,8 @@
 require "bundler/setup"
 require "saml/kit"
-require "ffaker"
 require "active_support/testing/time_helpers"
+require "ffaker"
+require "webmock/rspec"
 
 RSpec.configure do |config|
   config.include ActiveSupport::Testing::TimeHelpers
saml-kit.gemspec
@@ -39,4 +39,5 @@ Gem::Specification.new do |spec|
   spec.add_development_dependency "rake", "~> 10.0"
   spec.add_development_dependency "rspec", "~> 3.0"
   spec.add_development_dependency "ffaker", "~> 2.7"
+  spec.add_development_dependency "webmock", "~> 3.1"
 end