main
  1# frozen_string_literal: true
  2
  3RSpec.describe Saml::Kit::Response do
  4  subject { described_class.build(user, request) }
  5
  6  let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: ::Xml::Kit::Id.generate, issuer: FFaker::Internet.uri('https'), assertion_consumer_service_url: FFaker::Internet.uri('https'), name_id_format: Saml::Kit::Namespaces::PERSISTENT, provider: nil, signed?: true, trusted?: true) }
  7  let(:user) { User.new(attributes: { id: SecureRandom.uuid }) }
  8
  9  specify { expect(subject.status_code).to eql(Saml::Kit::Namespaces::SUCCESS) }
 10  specify { expect(subject.in_response_to).to eql(request.id) }
 11
 12  describe '#valid?' do
 13    subject { described_class.build(user, request, configuration: configuration) }
 14
 15    let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
 16    let(:metadata) { instance_double(Saml::Kit::IdentityProviderMetadata) }
 17
 18    let(:configuration) do
 19      Saml::Kit::Configuration.new do |config|
 20        config.entity_id = request.issuer
 21        config.registry = registry
 22        config.generate_key_pair_for(use: :signing)
 23      end
 24    end
 25
 26    it 'is valid' do
 27      allow(registry).to receive(:metadata_for).and_return(metadata)
 28      allow(metadata).to receive(:matches?).and_return(true)
 29      expect(subject).to be_valid
 30    end
 31
 32    it 'is invalid when blank' do
 33      allow(registry).to receive(:metadata_for).and_return(nil)
 34      subject = described_class.new('')
 35      expect(subject).to be_invalid
 36      expect(subject.errors[:content]).to be_present
 37      expect(subject.errors[:assertion]).to match_array(['is missing.'])
 38    end
 39
 40    it 'is invalid if the document has been tampered with' do
 41      allow(registry).to receive(:metadata_for).and_return(metadata)
 42      allow(metadata).to receive(:matches?).and_return(true)
 43      status_code = FFaker::Movie.title
 44      xml = described_class.build(user, request) do |builder|
 45        builder.status_code = status_code
 46      end.to_xml.gsub(status_code, 'TAMPERED')
 47      subject = described_class.new(xml)
 48      expect(subject).to be_invalid
 49    end
 50
 51    it 'is invalid when not a Response' do
 52      allow(registry).to receive(:metadata_for).and_return(nil)
 53      subject = described_class.new(Saml::Kit::IdentityProviderMetadata.build.to_xml)
 54      expect(subject).to be_invalid
 55      expect(subject.errors[:base]).to include(subject.error_message(:invalid))
 56    end
 57
 58    it 'is invalid when the fingerprint of the certificate does not match the registered fingerprint' do
 59      allow(registry).to receive(:metadata_for).and_return(metadata)
 60      allow(metadata).to receive(:matches?).and_return(false)
 61      expect(subject).to be_invalid
 62      expect(subject.errors[:fingerprint]).to be_present
 63    end
 64
 65    it 'validates the schema of the response' do
 66      allow(registry).to receive(:metadata_for).and_return(metadata)
 67      allow(metadata).to receive(:matches?).and_return(true)
 68      id = Xml::Kit::Id.generate
 69      key_pair = ::Xml::Kit::KeyPair.generate(use: :signing)
 70      signed_xml = ::Xml::Kit::Signatures.sign(key_pair: key_pair) do |xml, signature|
 71        xml.tag! 'samlp:Response', 'xmlns:samlp' => Saml::Kit::Namespaces::PROTOCOL, ID: id do
 72          signature.template(id)
 73          xml.Fake do
 74            xml.NotAllowed 'Huh?'
 75          end
 76        end
 77      end
 78      subject = described_class.new(signed_xml)
 79      expect(subject).to be_invalid
 80      expect(subject.errors[:base]).to be_present
 81    end
 82
 83    it 'validates the version' do
 84      allow(registry).to receive(:metadata_for).and_return(metadata)
 85      allow(metadata).to receive(:matches?).and_return(true)
 86      subject = described_class.build(user, request) do |builder|
 87        builder.version = '1.1'
 88      end
 89      expect(subject).to be_invalid
 90      expect(subject.errors[:version]).to be_present
 91    end
 92
 93    it 'validates the id' do
 94      allow(registry).to receive(:metadata_for).and_return(metadata)
 95      allow(metadata).to receive(:matches?).and_return(true)
 96      subject = described_class.build(user, request) do |builder|
 97        builder.id = nil
 98      end
 99      expect(subject).to be_invalid
100      expect(subject.errors[:id]).to be_present
101    end
102
103    it 'validates the status code' do
104      allow(registry).to receive(:metadata_for).and_return(metadata)
105      allow(metadata).to receive(:matches?).and_return(true)
106      subject = described_class.build(user, request) do |builder|
107        builder.status_code = Saml::Kit::Namespaces::REQUESTER_ERROR
108      end
109      expect(subject).to be_invalid
110      expect(subject.errors[:status_code]).to be_present
111    end
112
113    it 'validates the InResponseTo' do
114      allow(registry).to receive(:metadata_for).and_return(metadata)
115      allow(metadata).to receive(:matches?).and_return(true)
116      xml = described_class.build(user, request).to_xml
117      subject = described_class.new(xml, request_id: SecureRandom.uuid)
118
119      expect(subject).to be_invalid
120      expect(subject.errors[:in_response_to]).to be_present
121    end
122
123    it 'is invalid after a valid session window' do
124      allow(registry).to receive(:metadata_for).and_return(metadata)
125      allow(metadata).to receive(:matches?).and_return(true)
126
127      subject = described_class.build(user, request)
128      travel_to Saml::Kit.configuration.session_timeout.from_now + 5.seconds
129      expect(subject).not_to be_valid
130      expect(subject.errors[:assertion]).to match_array(['must not be expired.'])
131    end
132
133    it 'is invalid before the valid session window' do
134      allow(registry).to receive(:metadata_for).and_return(metadata)
135      allow(metadata).to receive(:matches?).and_return(true)
136
137      subject = described_class.build(user, request)
138      travel_to((Saml::Kit.configuration.clock_drift + 1.second).before(Time.now))
139      expect(subject).to be_invalid
140      expect(subject.errors[:assertion]).to match_array(['must not be expired.'])
141    end
142
143    it 'is invalid when the audience does not match the expected issuer' do
144      allow(registry).to receive(:metadata_for).and_return(metadata)
145      allow(metadata).to receive(:matches?).and_return(true)
146
147      allow(configuration).to receive(:issuer).and_return(FFaker::Internet.uri('https'))
148      allow(request).to receive(:issuer).and_return(FFaker::Internet.uri('https'))
149
150      expect(subject).to be_invalid
151      expect(subject.errors[:audience]).to be_present
152    end
153
154    it 'is invalid' do
155      now = Time.now.utc
156      destination = FFaker::Internet.uri('https')
157      raw_xml = <<-XML.strip_heredoc
158        <?xml version="1.0"?>
159        <samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{Xml::Kit::Id.generate}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{destination}" Consent="#{Saml::Kit::Namespaces::UNSPECIFIED}" InResponseTo="#{request.id}">
160          <Issuer xmlns="#{Saml::Kit::Namespaces::ASSERTION}">#{request.issuer}</Issuer>
161          <samlp:Status>
162            <samlp:StatusCode Value="#{Saml::Kit::Namespaces::RESPONDER_ERROR}"/>
163          </samlp:Status>
164        </samlp:Response>
165      XML
166
167      allow(registry).to receive(:metadata_for).with(request.issuer).and_return(metadata)
168      subject = described_class.new(raw_xml)
169      expect(subject).to be_invalid
170    end
171
172    it 'is invalid when there are 2 assertions' do
173      id = Xml::Kit::Id.generate
174      issuer = FFaker::Internet.uri('https')
175      key_pair = ::Xml::Kit::KeyPair.generate(use: :signing)
176      response_options = {
177        ID: id,
178        Version: '2.0',
179        IssueInstant: Time.now.iso8601,
180        Consent: Saml::Kit::Namespaces::UNSPECIFIED,
181        InResponseTo: request.id,
182        xmlns: Saml::Kit::Namespaces::PROTOCOL,
183      }
184      assertion_options = {
185        ID: Xml::Kit::Id.generate,
186        IssueInstant: Time.now.iso8601,
187        Version: '2.0',
188        xmlns: Saml::Kit::Namespaces::ASSERTION,
189      }
190      raw_xml = ::Xml::Kit::Signatures.sign(key_pair: key_pair) do |xml, signature|
191        xml.instruct!
192        xml.Response response_options do
193          xml.Issuer(issuer, xmlns: Saml::Kit::Namespaces::ASSERTION)
194          xml.Status do
195            xml.StatusCode Value: Saml::Kit::Namespaces::SUCCESS
196          end
197          xml.Assertion(assertion_options) do
198            xml.Issuer issuer
199            signature.template(assertion_options[:ID])
200            xml.Subject do
201              xml.NameID FFaker::Internet.email, Format: Saml::Kit::Namespaces::EMAIL_ADDRESS
202              xml.SubjectConfirmation Method: Saml::Kit::Namespaces::BEARER do
203                xml.SubjectConfirmationData '', InResponseTo: request.id, NotOnOrAfter: 3.hours.from_now.utc.iso8601, Recipient: FFaker::Internet.uri('https')
204              end
205            end
206            xml.Conditions NotBefore: Time.now.utc.iso8601, NotOnOrAfter: 3.hours.from_now.utc.iso8601 do
207              xml.AudienceRestriction do
208                xml.Audience request.issuer
209              end
210            end
211            xml.AuthnStatement AuthnInstant: Time.now.iso8601, SessionIndex: assertion_options[:ID], SessionNotOnOrAfter: 3.hours.from_now.utc.iso8601 do
212              xml.AuthnContext do
213                xml.AuthnContextClassRef Saml::Kit::Namespaces::PASSWORD
214              end
215            end
216          end
217          new_options = assertion_options.merge(ID: Xml::Kit::Id.generate)
218          xml.Assertion(new_options) do
219            xml.Issuer issuer
220            xml.Subject do
221              xml.NameID FFaker::Internet.email, Format: Saml::Kit::Namespaces::EMAIL_ADDRESS
222              xml.SubjectConfirmation Method: Saml::Kit::Namespaces::BEARER do
223                xml.SubjectConfirmationData '', InResponseTo: request.id, NotOnOrAfter: 3.hours.from_now.utc.iso8601, Recipient: FFaker::Internet.uri('https')
224              end
225            end
226            xml.Conditions NotBefore: Time.now.utc.iso8601, NotOnOrAfter: 3.hours.from_now.utc.iso8601 do
227              xml.AudienceRestriction do
228                xml.Audience request.issuer
229              end
230            end
231            xml.AuthnStatement AuthnInstant: Time.now.iso8601, SessionIndex: new_options[:ID], SessionNotOnOrAfter: 3.hours.from_now.utc.iso8601 do
232              xml.AuthnContext do
233                xml.AuthnContextClassRef Saml::Kit::Namespaces::PASSWORD
234              end
235            end
236          end
237        end
238      end
239      subject = described_class.new(raw_xml)
240      expect(subject).not_to be_valid
241      expect(subject.errors.full_messages).to include('must contain a single Assertion.')
242    end
243
244    it 'is invalid if there are two assertions (one signed and the other unsigned)' do
245      raw_xml = IO.read('spec/fixtures/unsigned_response_two_assertions.xml')
246      subject = described_class.new(raw_xml)
247      expect(subject).not_to be_valid
248      expect(subject.errors.full_messages).to include('must contain a single Assertion.')
249    end
250
251    it 'is invalid when the assertion has a signature and has been tampered with' do
252      user = User.new(attributes: { token: SecureRandom.uuid })
253      request = Saml::Kit::AuthenticationRequest.build
254      document = described_class.build(user, request, configuration: configuration) do |x|
255        x.embed_signature = false
256        x.assertion.embed_signature = true
257      end
258
259      altered_xml = document.to_xml.gsub(/token/, 'heck')
260      subject = described_class.new(altered_xml)
261      expect(subject).not_to be_valid
262      expect(subject.errors[:digest_value]).to be_present
263    end
264
265    it 'is invalid when we do not have a private key to decrypt the assertion' do
266      xml = described_class.build_xml(user, request) do |x|
267        x.encrypt_with(::Xml::Kit::KeyPair.generate(use: :encryption))
268      end
269
270      subject = described_class.new(xml)
271      expect(subject).to be_invalid
272      expect(subject.errors[:assertion]).to match_array(['cannot be decrypted.'])
273    end
274  end
275
276  describe '#signed?' do
277    let(:now) { Time.now.utc }
278    let(:id) { Xml::Kit::Id.generate }
279    let(:url) { FFaker::Internet.uri('https') }
280
281    it 'returns true when the Assertion is signed' do
282      xml = <<-XML.strip_heredoc
283        <?xml version="1.0"?>
284        <samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
285          <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0">
286            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
287              <ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
288                <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
289                <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
290                <ds:Reference URI="##{id}">
291                  <ds:Transforms>
292                    <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
293                    <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
294                  </ds:Transforms>
295                  <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
296                  <ds:DigestValue></ds:DigestValue>
297                </ds:Reference>
298              </ds:SignedInfo>
299              <ds:SignatureValue></ds:SignatureValue>
300              <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
301                <ds:X509Data>
302                  <ds:X509Certificate></ds:X509Certificate>
303                </ds:X509Data>
304              </KeyInfo>
305            </ds:Signature>
306          </Assertion>
307        </samlp:Response>
308      XML
309      subject = described_class.new(xml)
310      expect(subject).not_to be_signed
311      expect(subject.assertion).to be_signed
312    end
313
314    it 'returns true when the Response is signed' do
315      xml = <<-XML.strip_heredoc
316        <?xml version="1.0"?>
317        <samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
318          <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
319            <ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
320              <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
321              <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
322              <ds:Reference URI="##{id}">
323                <ds:Transforms>
324                  <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
325                  <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
326                </ds:Transforms>
327                <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
328                <ds:DigestValue></ds:DigestValue>
329              </ds:Reference>
330            </ds:SignedInfo>
331            <ds:SignatureValue></ds:SignatureValue>
332            <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
333              <ds:X509Data>
334                <ds:X509Certificate></ds:X509Certificate>
335              </ds:X509Data>
336            </KeyInfo>
337          </ds:Signature>
338          <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
339        </samlp:Response>
340      XML
341      subject = described_class.new(xml)
342      expect(subject).to be_signed
343    end
344
345    it 'returns false when there is no signature' do
346      xml = <<-XML.strip_heredoc
347        <?xml version="1.0"?>
348        <samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
349          <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
350        </samlp:Response>
351      XML
352      subject = described_class.new(xml)
353      expect(subject).not_to be_signed
354    end
355  end
356
357  describe '#certificate' do
358    let(:now) { Time.now.utc }
359    let(:id) { Xml::Kit::Id.generate }
360    let(:url) { FFaker::Internet.uri('https') }
361    let(:certificate) do
362      ::Xml::Kit::Certificate.new(
363        ::Xml::Kit::SelfSignedCertificate.new.create(passphrase: 'password')[0],
364        use: :signing
365      )
366    end
367
368    it 'returns the certificate when the Assertion is signed' do
369      xml = <<-XML.strip_heredoc
370        <?xml version="1.0"?>
371        <samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
372          <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0">
373            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
374              <ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
375                <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
376                <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
377                <ds:Reference URI="##{id}">
378                  <ds:Transforms>
379                    <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
380                    <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
381                  </ds:Transforms>
382                  <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
383                  <ds:DigestValue></ds:DigestValue>
384                </ds:Reference>
385              </ds:SignedInfo>
386              <ds:SignatureValue></ds:SignatureValue>
387              <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
388                <ds:X509Data>
389                  <ds:X509Certificate>#{certificate.stripped}</ds:X509Certificate>
390                </ds:X509Data>
391              </KeyInfo>
392            </ds:Signature>
393          </Assertion>
394        </samlp:Response>
395      XML
396      subject = described_class.new(xml)
397      expect(subject.signature).not_to be_present
398      expect(subject.assertion.signature).to be_present
399      expect(subject.assertion.signature.certificate.stripped).to eql(certificate.stripped)
400    end
401
402    it 'returns the certificate when the Response is signed' do
403      xml = <<-XML.strip_heredoc
404        <?xml version="1.0"?>
405        <samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
406          <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
407            <ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
408              <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
409              <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
410              <ds:Reference URI="##{id}">
411                <ds:Transforms>
412                  <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
413                  <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
414                </ds:Transforms>
415                <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
416                <ds:DigestValue></ds:DigestValue>
417              </ds:Reference>
418            </ds:SignedInfo>
419            <ds:SignatureValue></ds:SignatureValue>
420            <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
421              <ds:X509Data>
422                <ds:X509Certificate>#{certificate}</ds:X509Certificate>
423              </ds:X509Data>
424            </KeyInfo>
425          </ds:Signature>
426          <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
427        </samlp:Response>
428      XML
429      subject = described_class.new(xml)
430      expect(subject.signature.certificate).to eql(certificate)
431    end
432
433    it 'returns nil when there is no signature' do
434      xml = <<-XML.strip_heredoc
435        <?xml version="1.0"?>
436        <samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
437          <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
438        </samlp:Response>
439      XML
440      subject = described_class.new(xml)
441      expect(subject.signature).not_to be_present
442    end
443  end
444
445  describe 'encrypted assertion' do
446    let(:id) { Xml::Kit::Id.generate }
447    let(:now) { Time.now.utc }
448    let(:assertion_consumer_service_url) { FFaker::Internet.uri('https') }
449    let(:password) { FFaker::Movie.title }
450    let(:email) { FFaker::Internet.email }
451    let(:created_at) { Time.now }
452    let(:assertion) do
453      <<-XML.strip_heredoc
454        <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="2017-11-23T04:33:58Z" Version="2.0">
455         <Issuer>#{FFaker::Internet.uri('https')}</Issuer>
456         <Subject>
457           <NameID Format="#{Saml::Kit::Namespaces::PERSISTENT}">#{SecureRandom.uuid}</NameID>
458           <SubjectConfirmation Method="#{Saml::Kit::Namespaces::BEARER}">
459             <SubjectConfirmationData InResponseTo="#{SecureRandom.uuid}" NotOnOrAfter="2017-11-23T07:33:58Z" Recipient="https://westyundt.ca/acs"/>
460           </SubjectConfirmation>
461         </Subject>
462         <Conditions NotBefore="2017-11-23T04:33:58Z" NotOnOrAfter="2017-11-23T07:33:58Z">
463           <AudienceRestriction>
464             <Audience>American Wolves</Audience>
465           </AudienceRestriction>
466         </Conditions>
467         <AuthnStatement AuthnInstant="2017-11-23T04:33:58Z" SessionIndex="_11d39a7f-1b86-43ed-90d7-68090a857ca8" SessionNotOnOrAfter="2017-11-23T07:33:58Z">
468           <AuthnContext>
469             <AuthnContextClassRef>#{Saml::Kit::Namespaces::PASSWORD}</AuthnContextClassRef>
470           </AuthnContext>
471         </AuthnStatement>
472         <AttributeStatement>
473           <Attribute Name="email" NameFormat="#{Saml::Kit::Namespaces::URI}">
474             <AttributeValue>#{email}</AttributeValue>
475           </Attribute>
476           <Attribute Name="created_at" NameFormat="#{Saml::Kit::Namespaces::URI}">
477             <AttributeValue>#{created_at.iso8601}</AttributeValue>
478           </Attribute>
479         </AttributeStatement>
480        </Assertion>
481      XML
482    end
483
484    it 'parses the encrypted assertion' do
485      certificate_pem, private_key_pem = ::Xml::Kit::SelfSignedCertificate.new.create(passphrase: password)
486      public_key = OpenSSL::X509::Certificate.new(certificate_pem).public_key
487      private_key = OpenSSL::PKey::RSA.new(private_key_pem, password)
488
489      allow(Saml::Kit.configuration).to receive(:private_keys)
490        .with(hash_including(use: :encryption))
491        .and_return([private_key])
492
493      cipher = OpenSSL::Cipher.new('AES-128-CBC')
494      cipher.encrypt
495      key = cipher.random_key
496      iv = cipher.random_iv
497      encrypted = cipher.update(assertion) + cipher.final
498
499      xml = <<-XML.strip_heredoc
500        <samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" xmlns:saml="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{assertion_consumer_service_url}" InResponseTo="#{Xml::Kit::Id.generate}">
501          <saml:Issuer>#{FFaker::Internet.uri('https')}</saml:Issuer>
502          <samlp:Status>
503            <samlp:StatusCode Value="#{Saml::Kit::Namespaces::SUCCESS}"/>
504          </samlp:Status>
505          <saml:EncryptedAssertion>
506            <xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
507            <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
508            <dsig:KeyInfo xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
509              <xenc:EncryptedKey>
510                <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
511                <xenc:CipherData>
512                  <xenc:CipherValue>#{Base64.encode64(public_key.public_encrypt(key))}</xenc:CipherValue>
513                </xenc:CipherData>
514              </xenc:EncryptedKey>
515            </dsig:KeyInfo>
516            <xenc:CipherData>
517              <xenc:CipherValue>#{Base64.encode64(iv + encrypted)}</xenc:CipherValue>
518            </xenc:CipherData>
519            </xenc:EncryptedData>
520          </saml:EncryptedAssertion>
521        </samlp:Response>
522      XML
523
524      subject = described_class.new(xml)
525      expect(subject.attributes).to match_array([
526        ['created_at', created_at.iso8601],
527        ['email', email]
528      ])
529    end
530  end
531
532  describe 'parsing' do
533    let(:user) { User.new(attributes: attributes) }
534    let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: Xml::Kit::Id.generate, signed?: true, trusted?: true, provider: nil, assertion_consumer_service_url: FFaker::Internet.uri('https'), name_id_format: '', issuer: FFaker::Internet.uri('https')) }
535    let(:attributes) { { name: 'mo' } }
536
537    it 'returns the name id' do
538      subject = described_class.build(user, request)
539      expect(subject.name_id).to eql(user.name_id)
540    end
541
542    it 'excludes comments from the name id' do
543      user.name_id = 'shiro@voltron.com<!-- CVE-2017-11428 -->.evil.com'
544      subject = described_class.build(user, request)
545      expect(subject.name_id).to eql('shiro@voltron.com<!-- CVE-2017-11428 -->.evil.com')
546      expect(subject.name_id).not_to eql('shiro@voltron.com')
547    end
548
549    it 'parses the name id safely (CVE-2017-11428)' do
550      raw = IO.read('spec/fixtures/response_node_text_attack.xml.base64')
551      subject = Saml::Kit::Bindings::HttpPost.new(location: '').deserialize({ 'SAMLResponse' => raw })
552      expect(subject.name_id).to eql('support@onelogin.com')
553      expect(subject.attributes[:surname]).to eql('smith')
554    end
555
556    it 'returns the single attributes' do
557      subject = described_class.build(user, request)
558      expect(subject.attributes).to eql('name' => 'mo')
559    end
560
561    it 'returns the multiple attributes' do
562      attributes[:age] = 33
563      subject = described_class.build(user, request)
564      expect(subject.attributes).to eql('name' => 'mo', 'age' => '33')
565    end
566
567    it 'can parse an assertion without a name id' do
568      xml = expand_template('no_nameid.saml_response', issue_instant: Time.now)
569      subject = described_class.new(xml)
570      expect(subject.name_id).to be_nil
571    end
572
573    it 'parses a response with a status code of Requester' do
574      message = FFaker::Lorem.sentence
575      subject = described_class.new(expand_template('requester_error.saml_response', status_message: message))
576      expect(subject.status_code).to eql(Saml::Kit::Namespaces::REQUESTER_ERROR)
577      expect(subject.status_message).to eql(message)
578    end
579
580    it 'parses an array of attributes' do
581      attributes[:roles] = %i[admin user]
582      subject = described_class.build(user, request)
583      expect(subject.attributes[:roles]).to match_array(%w[admin user])
584    end
585  end
586
587  describe '#build' do
588    it 'can build a response without a request' do
589      configuration = Saml::Kit::Configuration.new do |config|
590        config.entity_id = FFaker::Internet.uri('https')
591      end
592      sp = Saml::Kit::Metadata.build(&:build_service_provider)
593      allow(configuration.registry).to receive(:metadata_for).with(configuration.entity_id).and_return(sp)
594      result = described_class.build(user, configuration: configuration)
595      expect(result).to be_instance_of(described_class)
596      expect(result).to be_valid
597    end
598
599    it 'can build a response without the need for the user to provide attributes' do
600      configuration = Saml::Kit::Configuration.new do |config|
601        config.entity_id = FFaker::Internet.uri('https')
602      end
603      sp = Saml::Kit::Metadata.build(&:build_service_provider)
604      allow(configuration.registry).to receive(:metadata_for).with(configuration.entity_id).and_return(sp)
605      user = UserWithoutAttributes.new
606
607      result = described_class.build(user, configuration: configuration)
608      expect(result).to be_instance_of(described_class)
609      expect(result).to be_valid
610      expect(result.attributes).to be_empty
611    end
612  end
613
614  describe '.new' do
615    let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
616    let(:metadata) { instance_double(Saml::Kit::IdentityProviderMetadata) }
617    let(:configuration) do
618      Saml::Kit::Configuration.new do |config|
619        config.entity_id = request.issuer
620        config.registry = registry
621        config.generate_key_pair_for(use: :signing)
622      end
623    end
624
625    it 'parses a raw response' do
626      allow(registry).to receive(:metadata_for).and_return(metadata)
627      allow(metadata).to receive(:matches?).and_return(true)
628
629      saml = described_class.build(user, request, configuration: configuration)
630      expect(described_class.new(saml.to_xml, configuration: configuration)).to be_valid
631    end
632  end
633end