main
  1# frozen_string_literal: true
  2
  3RSpec.describe Saml::Kit::AuthenticationRequest do
  4  subject { described_class.new(raw_xml, configuration: configuration) }
  5
  6  let(:id) { Xml::Kit::Id.generate }
  7  let(:assertion_consumer_service_url) { "https://#{FFaker::Internet.domain_name}/acs" }
  8  let(:issuer) { FFaker::Movie.title }
  9  let(:destination) { FFaker::Internet.http_url }
 10  let(:name_id_format) { Saml::Kit::Namespaces::EMAIL_ADDRESS }
 11  let(:raw_xml) do
 12    described_class.build_xml(configuration: configuration) do |builder|
 13      builder.id = id
 14      builder.now = Time.now.utc
 15      builder.issuer = issuer
 16      builder.assertion_consumer_service_url = assertion_consumer_service_url
 17      builder.name_id_format = name_id_format
 18      builder.destination = destination
 19    end
 20  end
 21  let(:configuration) do
 22    Saml::Kit::Configuration.new do |config|
 23      config.generate_key_pair_for(use: :signing)
 24    end
 25  end
 26
 27  specify { expect(subject.name).to eql('AuthnRequest') }
 28  specify { expect(subject.issuer).to eql(issuer) }
 29  specify { expect(subject.id).to eql(id) }
 30  specify { expect(subject.assertion_consumer_service_url).to eql(assertion_consumer_service_url) }
 31  specify { expect(subject.name_id_format).to eql(name_id_format) }
 32  specify { expect(subject.destination).to eql(destination) }
 33  specify { expect(subject.force_authn).to be(false) }
 34
 35  describe '#valid?' do
 36    let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
 37    let(:metadata) { Saml::Kit::ServiceProviderMetadata.build(configuration: configuration) }
 38
 39    before do
 40      allow(configuration).to receive(:registry).and_return(registry)
 41      allow(registry).to receive(:metadata_for).and_return(metadata)
 42    end
 43
 44    it 'is valid when left untampered' do
 45      subject = described_class.new(raw_xml, configuration: configuration)
 46      expect(subject).to be_valid
 47    end
 48
 49    it 'is invalid if the document has been tampered with' do
 50      raw_xml.gsub!(issuer, 'corrupt')
 51      subject = described_class.new(raw_xml)
 52      expect(subject).to be_invalid
 53    end
 54
 55    it 'is invalid when blank' do
 56      subject = described_class.new('')
 57      expect(subject).to be_invalid
 58      expect(subject.errors[:content]).to be_present
 59    end
 60
 61    it 'is invalid when not an AuthnRequest' do
 62      xml = Saml::Kit::IdentityProviderMetadata.build.to_xml
 63      subject = described_class.new(xml)
 64      expect(subject).to be_invalid
 65      expect(subject.errors[:base]).to include(subject.error_message(:invalid))
 66    end
 67
 68    it 'is invalid when the fingerprint of the certificate does not match the registered fingerprint' do
 69      allow(metadata).to receive(:matches?).and_return(false)
 70      subject = described_class.build do |builder|
 71        builder.issuer = issuer
 72        builder.assertion_consumer_service_url = assertion_consumer_service_url
 73      end
 74
 75      expect(subject).to be_invalid
 76      expect(subject.errors[:fingerprint]).to be_present
 77    end
 78
 79    it 'is invalid when the service provider is not known' do
 80      allow(registry).to receive(:metadata_for).and_return(nil)
 81      subject = described_class.build
 82      expect(subject).to be_invalid
 83      expect(subject.errors[:provider]).to be_present
 84    end
 85
 86    it 'validates the schema of the request' do
 87      id = Xml::Kit::Id.generate
 88      key_pair = ::Xml::Kit::KeyPair.generate(use: :signing)
 89      signed_xml = ::Xml::Kit::Signatures.sign(key_pair: key_pair) do |xml, signature|
 90        xml.tag!('samlp:AuthnRequest', 'xmlns:samlp' => Saml::Kit::Namespaces::PROTOCOL, AssertionConsumerServiceURL: assertion_consumer_service_url, ID: id) do
 91          signature.template(id)
 92          xml.Fake do
 93            xml.NotAllowed 'Huh?'
 94          end
 95        end
 96      end
 97      expect(described_class.new(signed_xml)).to be_invalid
 98    end
 99
100    it 'validates a request without a signature' do
101      now = Time.now.utc
102      raw_xml = <<-XML.strip_heredoc
103        <samlp:AuthnRequest AssertionConsumerServiceURL='#{assertion_consumer_service_url}' ID='#{Xml::Kit::Id.generate}' IssueInstant='#{now.iso8601}' Version='2.0' xmlns:saml='#{Saml::Kit::Namespaces::ASSERTION}' xmlns:samlp='#{Saml::Kit::Namespaces::PROTOCOL}'>
104          <saml:Issuer>#{issuer}</saml:Issuer>
105          <samlp:NameIDPolicy AllowCreate='true' Format='#{Saml::Kit::Namespaces::EMAIL_ADDRESS}'/>
106        </samlp:AuthnRequest>
107      XML
108
109      subject = described_class.new(raw_xml, configuration: configuration)
110      subject.signature_verified!
111      expect(subject).to be_valid
112    end
113
114    it 'is valid when there is no signature, and the issuer is registered' do
115      now = Time.now.utc
116      raw_xml = <<-XML.strip_heredoc
117        <samlp:AuthnRequest AssertionConsumerServiceURL='#{assertion_consumer_service_url}' ID='#{Xml::Kit::Id.generate}' IssueInstant='#{now.iso8601}' Version='2.0' xmlns:saml='#{Saml::Kit::Namespaces::ASSERTION}' xmlns:samlp='#{Saml::Kit::Namespaces::PROTOCOL}'>
118          <saml:Issuer>#{issuer}</saml:Issuer>
119          <samlp:NameIDPolicy AllowCreate='true' Format='#{Saml::Kit::Namespaces::PERSISTENT}'/>
120        </samlp:AuthnRequest>
121      XML
122
123      allow(registry).to receive(:metadata_for).with(issuer).and_return(metadata)
124      subject = described_class.new(raw_xml, configuration: configuration)
125      expect(subject).to be_valid
126    end
127
128    it 'is invalid when there is no signature, and the issuer is not registered' do
129      now = Time.now.utc
130      raw_xml = <<-XML.strip_heredoc
131        <samlp:AuthnRequest AssertionConsumerServiceURL='#{assertion_consumer_service_url}' ID='#{Xml::Kit::Id.generate}' IssueInstant='#{now.iso8601}' Version='2.0' xmlns:saml='#{Saml::Kit::Namespaces::ASSERTION}' xmlns:samlp='#{Saml::Kit::Namespaces::PROTOCOL}'>
132          <saml:Issuer>#{issuer}</saml:Issuer>
133          <samlp:NameIDPolicy AllowCreate='true' Format='#{Saml::Kit::Namespaces::PERSISTENT}'/>
134        </samlp:AuthnRequest>
135      XML
136
137      allow(registry).to receive(:metadata_for).with(issuer).and_return(nil)
138      subject = described_class.new(raw_xml, configuration: configuration)
139      expect(subject).to be_invalid
140    end
141
142    context 'when the certificate is expired' do
143      let(:expired_certificate) do
144        certificate = OpenSSL::X509::Certificate.new
145        certificate.public_key = private_key.public_key
146        certificate.not_before = 1.day.ago
147        certificate.not_after = 1.second.ago
148        certificate
149      end
150      let(:private_key) { OpenSSL::PKey::RSA.new(2048) }
151      let(:digest_algorithm) { OpenSSL::Digest::SHA256.new }
152
153      before do
154        expired_certificate.sign(private_key, digest_algorithm)
155      end
156
157      it 'is invalid' do
158        document = described_class.build do |x|
159          x.embed_signature = true
160          certificate = ::Xml::Kit::Certificate.new(expired_certificate)
161          x.sign_with(certificate.to_key_pair(private_key))
162        end
163        subject = described_class.new(document.to_xml)
164        expect(subject).to be_invalid
165        expect(subject.errors[:certificate]).to be_present
166      end
167    end
168  end
169
170  describe '#assertion_consumer_service_url' do
171    let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
172    let(:metadata) { instance_double(Saml::Kit::ServiceProviderMetadata) }
173
174    it 'returns the ACS in the request' do
175      subject = described_class.build do |builder|
176        builder.assertion_consumer_service_url = assertion_consumer_service_url
177      end
178      expect(subject.assertion_consumer_service_url).to eql(assertion_consumer_service_url)
179    end
180
181    it 'returns nil' do
182      subject = described_class.build do |builder|
183        builder.assertion_consumer_service_url = nil
184      end
185
186      expect(subject.assertion_consumer_service_url).to be_nil
187    end
188  end
189
190  describe '#force_authn' do
191    context 'when set to true' do
192      subject { described_class.build { |x| x.force_authn = true } }
193
194      specify { expect(subject.force_authn).to be(true) }
195    end
196
197    context 'when set to false' do
198      subject { described_class.build { |x| x.force_authn = false } }
199
200      specify { expect(subject.force_authn).to be(false) }
201    end
202
203    context 'when not specified' do
204      subject { described_class.build { |x| x.force_authn = nil } }
205
206      specify { expect(subject.force_authn).to be(false) }
207    end
208  end
209
210  describe '.build' do
211    let(:url) { FFaker::Internet.uri('https') }
212    let(:entity_id) { FFaker::Internet.uri('https') }
213
214    it 'provides a nice API for building a request' do
215      result = described_class.build do |builder|
216        builder.issuer = entity_id
217        builder.assertion_consumer_service_url = url
218      end
219
220      expect(result).to be_instance_of(described_class)
221      expect(result.issuer).to eql(entity_id)
222      expect(result.assertion_consumer_service_url).to eql(url)
223    end
224
225    it 'can build a authnrequest without a nameid policy' do
226      result = described_class.build do |x|
227        x.issuer = entity_id
228        x.assertion_consumer_service_url = url
229        x.name_id_format = nil
230      end
231      expect(result).to be_instance_of(described_class)
232      result.registry = instance_double(Saml::Kit::DefaultRegistry, metadata_for: Saml::Kit::ServiceProviderMetadata.build)
233      expect(result).to be_valid
234      expect(result.to_xml).not_to include('NameIDPolicy')
235    end
236  end
237
238  describe '#response_for' do
239    let(:user) { User.new }
240    let(:provider) do
241      Saml::Kit::ServiceProviderMetadata.build do |x|
242        x.add_assertion_consumer_service(FFaker::Internet.uri('https'), binding: :http_post)
243      end
244    end
245
246    it 'serializes a response' do
247      allow(subject).to receive(:provider).and_return(provider)
248      _url, saml_params = subject.response_for(user, binding: :http_post, relay_state: FFaker::Movie.title)
249
250      response = provider.assertion_consumer_service_for(binding: :http_post).deserialize(saml_params)
251      expect(response).to be_instance_of(Saml::Kit::Response)
252    end
253
254    it 'serializes a response with the specified signing certificate' do
255      allow(subject).to receive(:provider).and_return(provider)
256      configuration = Saml::Kit::Configuration.new do |config|
257        config.generate_key_pair_for(use: :signing)
258      end
259      key_pair = configuration.key_pairs(use: :signing).first
260      _url, saml_params = subject.response_for(user, binding: :http_post, configuration: configuration) do |builder|
261        builder.sign_with(key_pair)
262      end
263
264      response = provider.assertion_consumer_service_for(binding: :http_post).deserialize(saml_params)
265      expect(response).to be_instance_of(Saml::Kit::Response)
266    end
267  end
268
269  describe '#signature.valid?' do
270    it 'returns true when the signature is valid' do
271      expect(subject.signature).to be_valid
272    end
273
274    it 'returns false when the signature does not match' do
275      raw_xml.gsub!(issuer, 'corrupt')
276      subject = described_class.new(raw_xml, configuration: configuration)
277      expect(subject.signature).not_to be_valid
278    end
279  end
280end