main
  1# frozen_string_literal: true
  2
  3RSpec.describe Saml::Kit::IdentityProviderMetadata do
  4  subject { described_class.new(raw_metadata) }
  5
  6  describe 'okta metadata' do
  7    let(:raw_metadata) { IO.read('spec/fixtures/metadata/okta.xml') }
  8    let(:certificate) do
  9      Hash.from_xml(raw_metadata)['EntityDescriptor']['IDPSSODescriptor']['KeyDescriptor']['KeyInfo']['X509Data']['X509Certificate']
 10    end
 11
 12    it { expect(subject.entity_id).to eql('http://www.okta.com/1') }
 13    it { expect(subject.name_id_formats).to match_array([Saml::Kit::Namespaces::EMAIL_ADDRESS, Saml::Kit::Namespaces::UNSPECIFIED_NAMEID]) }
 14
 15    it do
 16      location = 'https://dev.oktapreview.com/app/example/1/sso/saml'
 17      expect(subject.single_sign_on_services.map(&:to_h)).to match_array([
 18        { binding: Saml::Kit::Bindings::HTTP_POST, location: location },
 19        { binding: Saml::Kit::Bindings::HTTP_REDIRECT, location: location },
 20      ])
 21    end
 22
 23    it { expect(subject.single_logout_services).to be_empty }
 24
 25    it do
 26      fingerprint = '9F:74:13:3B:BC:5A:7B:8B:2D:4F:8B:EF:1E:88:EB:D1:AE:BC:19:BF:CA:19:C6:2F:0F:4B:31:1D:68:98:B0:1B'
 27      expect(subject.certificates).to match_array([::Xml::Kit::Certificate.new(certificate, use: :signing)])
 28      expect(subject.certificates.first.fingerprint.to_s).to eql(fingerprint)
 29    end
 30
 31    it { expect(subject.attributes).to be_empty }
 32  end
 33
 34  describe 'active directory' do
 35    let(:raw_metadata) { IO.read('spec/fixtures/metadata/ad_2012.xml') }
 36    let(:xml_hash) { Hash.from_xml(raw_metadata) }
 37    let(:signing_certificate) do
 38      xml_hash['EntityDescriptor']['IDPSSODescriptor']['KeyDescriptor'].find { |x| x['use'] == 'signing' }['KeyInfo']['X509Data']['X509Certificate']
 39    end
 40    let(:encryption_certificate) do
 41      xml_hash['EntityDescriptor']['IDPSSODescriptor']['KeyDescriptor'].find { |x| x['use'] == 'encryption' }['KeyInfo']['X509Data']['X509Certificate']
 42    end
 43
 44    it { expect(subject.entity_id).to eql('http://www.example.com/adfs/services/trust') }
 45
 46    it do
 47      expect(subject.name_id_formats).to match_array([
 48        Saml::Kit::Namespaces::EMAIL_ADDRESS,
 49        Saml::Kit::Namespaces::PERSISTENT,
 50        Saml::Kit::Namespaces::TRANSIENT,
 51      ])
 52    end
 53
 54    it do
 55      location = 'https://www.example.com/adfs/ls/'
 56      expect(subject.single_sign_on_services.map(&:to_h)).to match_array([
 57        { location: location, binding: Saml::Kit::Bindings::HTTP_REDIRECT },
 58        { location: location, binding: Saml::Kit::Bindings::HTTP_POST },
 59      ])
 60    end
 61
 62    it do
 63      location = 'https://www.example.com/adfs/ls/'
 64      expect(subject.single_logout_services.map(&:to_h)).to match_array([
 65        { location: location, binding: Saml::Kit::Bindings::HTTP_REDIRECT },
 66        { location: location, binding: Saml::Kit::Bindings::HTTP_POST },
 67      ])
 68    end
 69
 70    it do
 71      expect(subject.certificates).to match_array([
 72        ::Xml::Kit::Certificate.new(signing_certificate, use: :signing),
 73        ::Xml::Kit::Certificate.new(encryption_certificate, use: :encryption),
 74      ])
 75    end
 76
 77    it { expect(subject.attributes).to be_present }
 78  end
 79
 80  describe '#validate' do
 81    it 'valid when given valid identity provider metadata' do
 82      subject = described_class.build do |builder|
 83        builder.attributes = [:email]
 84        builder.add_single_sign_on_service(FFaker::Internet.http_url, binding: :http_post)
 85        builder.add_single_sign_on_service(FFaker::Internet.http_url, binding: :http_redirect)
 86        builder.add_single_logout_service(FFaker::Internet.http_url, binding: :http_post)
 87        builder.add_single_logout_service(FFaker::Internet.http_url, binding: :http_redirect)
 88      end
 89      expect(subject).to be_valid
 90    end
 91
 92    it 'is invalid, when given service provider metadata' do
 93      service_provider_metadata = Saml::Kit::ServiceProviderMetadata.build.to_xml
 94      subject = described_class.new(service_provider_metadata)
 95      expect(subject).not_to be_valid
 96      expect(subject.errors[:base]).to include(I18n.translate('saml/kit.errors.IDPSSODescriptor.invalid'))
 97    end
 98
 99    it 'is invalid, when the metadata is nil' do
100      subject = described_class.new(nil)
101      expect(subject).not_to be_valid
102      expect(subject.errors[:metadata]).to include("can't be blank")
103    end
104
105    it 'is invalid, when the metadata does not validate against the xsd schema' do
106      xml = ::Builder::XmlMarkup.new
107      xml.instruct!
108      xml.EntityDescriptor 'xmlns': Saml::Kit::Namespaces::METADATA do
109        xml.IDPSSODescriptor do
110          xml.Fake foo: :bar
111        end
112      end
113      subject = described_class.new(xml.target!)
114      expect(subject).not_to be_valid
115      expect(subject.errors[:base][0]).to include("1:0: ERROR: Element '{urn:oasis:names:tc:SAML:2.0:metadata}EntityDescriptor'")
116    end
117
118    it 'is invalid, when the signature is invalid' do
119      old_url = 'https://www.example.com/adfs/ls/'
120      new_url = 'https://myserver.com/hacked'
121      metadata_xml = IO.read('spec/fixtures/metadata/ad_2012.xml').gsub(old_url, new_url)
122
123      subject = described_class.new(metadata_xml)
124      expect(subject).to be_invalid
125      expect(subject.errors[:base]).to be_empty
126      expect(subject.errors[:digest_value]).to match_array(['is invalid.'])
127      expect(subject.errors[:signature]).to match_array(['is invalid.'])
128    end
129  end
130
131  describe '#single_sign_on_service_for' do
132    subject do
133      described_class.build do |builder|
134        builder.add_single_sign_on_service(redirect_url, binding: :http_redirect)
135        builder.add_single_sign_on_service(post_url, binding: :http_post)
136      end
137    end
138
139    let(:post_url) { FFaker::Internet.http_url }
140    let(:redirect_url) { FFaker::Internet.http_url }
141
142    it 'returns the POST binding' do
143      result = subject.single_sign_on_service_for(binding: :http_post)
144      expect(result.location).to eql(post_url)
145      expect(result.binding).to eql(Saml::Kit::Bindings::HTTP_POST)
146    end
147
148    it 'returns the HTTP_REDIRECT binding' do
149      result = subject.single_sign_on_service_for(binding: :http_redirect)
150      expect(result.location).to eql(redirect_url)
151      expect(result.binding).to eql(Saml::Kit::Bindings::HTTP_REDIRECT)
152    end
153
154    it 'returns nil if the binding cannot be found' do
155      expect(subject.single_sign_on_service_for(binding: :soap)).to be_nil
156    end
157  end
158
159  describe '#want_authn_requests_signed' do
160    it 'returns true when enabled' do
161      subject = described_class.build do |builder|
162        builder.want_authn_requests_signed = true
163      end
164      expect(subject.want_authn_requests_signed).to be(true)
165    end
166
167    it 'returns false when disabled' do
168      subject = described_class.build do |builder|
169        builder.want_authn_requests_signed = false
170      end
171      expect(subject.want_authn_requests_signed).to be(false)
172    end
173
174    it 'returns true when the attribute is missing' do
175      xml = described_class.build do |builder|
176        builder.want_authn_requests_signed = false
177      end.to_xml.gsub('WantAuthnRequestsSigned="false"', '')
178      subject = described_class.new(xml)
179      expect(subject.want_authn_requests_signed).to be(true)
180    end
181  end
182
183  describe '#single_logout_service_for' do
184    let(:redirect_url) { FFaker::Internet.uri('https') }
185    let(:post_url) { FFaker::Internet.uri('https') }
186    let(:subject) do
187      described_class.build do |builder|
188        builder.add_single_logout_service(redirect_url, binding: :http_redirect)
189        builder.add_single_logout_service(post_url, binding: :http_post)
190      end
191    end
192
193    it 'returns the location for the matching binding' do
194      expect(subject.single_logout_service_for(binding: :http_post).location).to eql(post_url)
195      expect(subject.single_logout_service_for(binding: :http_redirect).location).to eql(redirect_url)
196    end
197
198    it 'returns nil if the binding is not available' do
199      expect(subject.single_logout_service_for(binding: :soap)).to be_nil
200    end
201  end
202
203  describe '.build' do
204    let(:url) { FFaker::Internet.uri('https') }
205    let(:entity_id) { FFaker::Internet.uri('https') }
206
207    it 'provides a nice API for building metadata' do
208      result = described_class.build do |builder|
209        builder.entity_id = entity_id
210        builder.add_single_sign_on_service(url, binding: :http_post)
211      end
212
213      expect(result).to be_instance_of(described_class)
214      expect(result.entity_id).to eql(entity_id)
215      expect(result.single_sign_on_service_for(binding: :http_post).location).to eql(url)
216    end
217  end
218
219  describe '#login_request_for' do
220    it 'returns a serialized login request' do
221      subject = described_class.build do |x|
222        x.add_single_sign_on_service(FFaker::Internet.uri('https'), binding: :http_post)
223      end
224      _url, saml_params = subject.login_request_for(binding: :http_post, relay_state: FFaker::Movie.title)
225      result = subject.single_sign_on_service_for(binding: :http_post).deserialize(saml_params)
226      expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
227    end
228  end
229end