main
  1# frozen_string_literal: true
  2
  3RSpec.describe Saml::Kit::ServiceProviderMetadata do
  4  let(:entity_id) { FFaker::Internet.uri('https') }
  5  let(:acs_post_url) { FFaker::Internet.uri('https') }
  6  let(:acs_redirect_url) { FFaker::Internet.uri('https') }
  7  let(:logout_post_url) { FFaker::Internet.uri('https') }
  8  let(:logout_redirect_url) { FFaker::Internet.uri('https') }
  9
 10  describe described_class do
 11    subject do
 12      described_class.build do |builder|
 13        builder.entity_id = entity_id
 14        builder.add_assertion_consumer_service(acs_post_url, binding: :http_post)
 15        builder.add_assertion_consumer_service(acs_redirect_url, binding: :http_redirect)
 16        builder.add_single_logout_service(logout_post_url, binding: :http_post)
 17        builder.add_single_logout_service(logout_redirect_url, binding: :http_redirect)
 18      end
 19    end
 20
 21    it 'returns each of the certificates' do
 22      expected_certificates = Saml::Kit.configuration.certificates.map do |x|
 23        ::Xml::Kit::Certificate.new(x.stripped, use: x.use)
 24      end
 25      expect(subject.certificates).to match_array(expected_certificates)
 26    end
 27
 28    it 'returns each acs url and binding' do
 29      expect(subject.assertion_consumer_services.map(&:to_h)).to match_array([
 30        { location: acs_post_url, binding: Saml::Kit::Bindings::HTTP_POST },
 31        { location: acs_redirect_url, binding: Saml::Kit::Bindings::HTTP_REDIRECT },
 32      ])
 33    end
 34
 35    it 'returns each logout url and binding' do
 36      expect(subject.single_logout_services.map(&:to_h)).to match_array([
 37        { location: logout_post_url, binding: Saml::Kit::Bindings::HTTP_POST },
 38        { location: logout_redirect_url, binding: Saml::Kit::Bindings::HTTP_REDIRECT },
 39      ])
 40    end
 41
 42    it 'returns each of the nameid formats' do
 43      expect(subject.name_id_formats).to match_array([
 44        Saml::Kit::Namespaces::PERSISTENT
 45      ])
 46    end
 47
 48    it 'returns the entity id' do
 49      expect(subject.entity_id).to eql(entity_id)
 50    end
 51  end
 52
 53  describe '#validate' do
 54    let(:service_provider_metadata) do
 55      described_class.build(configuration: configuration) do |builder|
 56        builder.entity_id = entity_id
 57        builder.add_assertion_consumer_service(acs_post_url, binding: :http_post)
 58        builder.add_assertion_consumer_service(acs_redirect_url, binding: :http_redirect)
 59        builder.add_single_logout_service(logout_post_url, binding: :http_post)
 60        builder.add_single_logout_service(logout_redirect_url, binding: :http_redirect)
 61      end.to_xml
 62    end
 63    let(:configuration) do
 64      Saml::Kit::Configuration.new do |config|
 65        config.generate_key_pair_for(use: :signing)
 66      end
 67    end
 68
 69    it 'valid when given valid service provider metadata' do
 70      expect(described_class.new(service_provider_metadata)).to be_valid
 71    end
 72
 73    it 'is invalid, when given identity provider metadata' do
 74      subject = described_class.new(IO.read('spec/fixtures/metadata/okta.xml'))
 75      expect(subject).to be_invalid
 76      expect(subject.errors[:base]).to include(I18n.translate('saml/kit.errors.SPSSODescriptor.invalid'))
 77    end
 78
 79    it 'is invalid, when the metadata is nil' do
 80      subject = described_class.new(nil)
 81      expect(subject).to be_invalid
 82      expect(subject.errors[:metadata]).to include("can't be blank")
 83    end
 84
 85    it 'is invalid, when the metadata does not validate against the xsd schema' do
 86      xml = ::Builder::XmlMarkup.new
 87      xml.instruct!
 88      xml.EntityDescriptor 'xmlns': Saml::Kit::Namespaces::METADATA do
 89        xml.SPSSODescriptor do
 90          xml.Fake foo: :bar
 91        end
 92      end
 93      subject = described_class.new(xml.target!)
 94      expect(subject).not_to be_valid
 95      expect(subject.errors[:base][0]).to include("1:0: ERROR: Element '{urn:oasis:names:tc:SAML:2.0:metadata}EntityDescriptor'")
 96    end
 97
 98    it 'is invalid, when the signature is invalid' do
 99      new_url = 'https://myserver.com/hacked'
100      metadata_xml = service_provider_metadata.gsub(acs_post_url, new_url)
101      subject = described_class.new(metadata_xml)
102      expect(subject).to be_invalid
103      expect(subject.errors[:digest_value]).to include('is invalid.')
104    end
105
106    it 'is invalid when 0 ACS endpoints are specified' do
107      xml = <<-XML.strip_heredoc
108        <?xml version="1.0" encoding="UTF-8"?>
109        <EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="#{::Xml::Kit::Id.generate}" entityID="#{entity_id}">
110          <SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
111            <SingleLogoutService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri('https')}"/>
112            <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
113          </SPSSODescriptor>
114        </EntityDescriptor>
115      XML
116      expect(described_class.new(xml)).to be_invalid
117    end
118  end
119
120  describe '#matches?' do
121    subject { described_class.build(configuration: configuration) }
122
123    let(:configuration) do
124      config = Saml::Kit::Configuration.new
125      config.generate_key_pair_for(use: :signing)
126      config
127    end
128
129    it 'returns true when the fingerprint matches one of the signing certificates' do
130      certificate = Hash.from_xml(subject.to_xml)['EntityDescriptor']['Signature']['KeyInfo']['X509Data']['X509Certificate']
131      fingerprint = ::Xml::Kit::Fingerprint.new(certificate)
132      expect(subject).to be_matches(fingerprint)
133    end
134
135    it 'returns false when the fingerprint does not match one of the signing certificates' do
136      certificate, = ::Xml::Kit::SelfSignedCertificate.new.create(passphrase: 'password')
137      fingerprint = ::Xml::Kit::Fingerprint.new(certificate)
138      expect(subject).not_to be_matches(fingerprint)
139    end
140  end
141
142  describe '.build' do
143    let(:assertion_consumer_service_url) { FFaker::Internet.uri('https') }
144
145    it 'provides a nice API for building metadata' do
146      result = described_class.build do |builder|
147        builder.entity_id = entity_id
148        builder.add_assertion_consumer_service(assertion_consumer_service_url, binding: :http_post)
149      end
150
151      expect(result).to be_instance_of(described_class)
152      expect(result.entity_id).to eql(entity_id)
153      expect(result.assertion_consumer_service_for(binding: :http_post).location).to eql(assertion_consumer_service_url)
154    end
155  end
156end