main
  1# frozen_string_literal: true
  2
  3RSpec.describe Saml::Kit::Builders::Response do
  4  subject { described_class.new(user, request, configuration: configuration) }
  5
  6  let(:configuration) do
  7    Saml::Kit::Configuration.new do |config|
  8      config.entity_id = issuer
  9      config.generate_key_pair_for(use: :signing)
 10      config.generate_key_pair_for(use: :encryption)
 11    end
 12  end
 13  let(:email) { FFaker::Internet.email }
 14  let(:assertion_consumer_service_url) { FFaker::Internet.uri('https') }
 15  let(:user) { User.new(attributes: { email: email, created_at: Time.now.utc.iso8601, roles: %w[husband father programmer] }) }
 16  let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: Xml::Kit::Id.generate, assertion_consumer_service_url: assertion_consumer_service_url, issuer: issuer, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: provider, trusted?: true, signed?: true) }
 17  let(:provider) { instance_double(Saml::Kit::ServiceProviderMetadata, want_assertions_signed: false, encryption_certificates: [configuration.certificates(use: :encryption).last]) }
 18  let(:issuer) { FFaker::Internet.uri('https') }
 19
 20  describe '#build' do
 21    it 'builds a response with the request_id' do
 22      expect(subject.build.request_id).to eql(request.id)
 23    end
 24
 25    it 'builds a valid encrypted assertion' do
 26      allow(configuration.registry).to receive(:metadata_for).with(issuer).and_return(provider)
 27      allow(provider).to receive(:matches?).and_return(true)
 28
 29      subject.embed_signature = true
 30      subject.encrypt = true
 31      result = subject.build
 32      expect(result).to be_valid
 33    end
 34
 35    it 'builds an encrypted assertion with a custom default nameid format' do
 36      allow(configuration.registry).to receive(:metadata_for).with(issuer).and_return(provider)
 37      allow(provider).to receive(:matches?).and_return(true)
 38      allow(request).to receive(:name_id_format).and_return(nil)
 39
 40      subject.assertion.default_name_id_format = Saml::Kit::Namespaces::TRANSIENT
 41      subject.embed_signature = true
 42      subject.encrypt = true
 43
 44      result = Hash.from_xml(subject.to_xml)
 45      expect(result['Response']['EncryptedAssertion']).to be_present
 46      encrypted_assertion = result['Response']['EncryptedAssertion']
 47      decrypted_assertion = Xml::Kit::Decryption.new(private_keys: configuration.private_keys(use: :encryption)).decrypt_hash(encrypted_assertion)
 48      document = Saml::Kit::Document.new(decrypted_assertion, name: 'Assertion')
 49      expect(document.at_xpath('//saml:NameID/@Format').value).to eql(Saml::Kit::Namespaces::TRANSIENT)
 50    end
 51
 52    it 'includes the issuer' do
 53      subject.encrypt = false
 54      result = subject.build
 55      expect(result.issuer).to eql(issuer)
 56      expect(result.assertion.issuer).to eql(issuer)
 57    end
 58
 59    it 'builds a response with a status code' do
 60      subject.status_code = Saml::Kit::Namespaces::REQUESTER_ERROR
 61      subject.status_message = 'Invalid message signature'
 62      result = subject.build
 63      expect(result.status_message).to eql('Invalid message signature')
 64    end
 65
 66    it 'builds a response without an assertion' do
 67      subject.assertion = nil
 68      result = subject.build
 69      expect(result.assertion).not_to be_present
 70    end
 71  end
 72
 73  describe '#to_xml' do
 74    it 'returns a proper response for the user' do
 75      travel_to 1.second.from_now
 76      allow(Saml::Kit.configuration).to receive(:entity_id).and_return(issuer)
 77      subject.destination = assertion_consumer_service_url
 78      subject.encrypt = false
 79      subject.encrypt = true
 80      subject.encrypt = false
 81      hash = Hash.from_xml(subject.to_xml)
 82
 83      expect(hash['Response']['ID']).to be_present
 84      expect(hash['Response']['Version']).to eql('2.0')
 85      expect(hash['Response']['IssueInstant']).to eql(Time.now.utc.iso8601)
 86      expect(hash['Response']['Destination']).to eql(assertion_consumer_service_url)
 87      expect(hash['Response']['InResponseTo']).to eql(request.id)
 88      expect(hash['Response']['Issuer']).to eql(issuer)
 89      expect(hash['Response']['Status']['StatusCode']['Value']).to eql('urn:oasis:names:tc:SAML:2.0:status:Success')
 90
 91      expect(hash['Response']['Assertion']['ID']).to be_present
 92      expect(hash['Response']['Assertion']['IssueInstant']).to eql(Time.now.utc.iso8601)
 93      expect(hash['Response']['Assertion']['Version']).to eql('2.0')
 94      expect(hash['Response']['Assertion']['Issuer']).to eql(issuer)
 95
 96      expect(hash['Response']['Assertion']['Subject']['NameID']).to eql(user.name_id)
 97      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['Method']).to eql('urn:oasis:names:tc:SAML:2.0:cm:bearer')
 98      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['NotOnOrAfter']).to eql(5.minutes.from_now.utc.iso8601)
 99      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['Recipient']).to eql(assertion_consumer_service_url)
100      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['InResponseTo']).to eql(request.id)
101
102      expect(hash['Response']['Assertion']['Conditions']['NotBefore']).to eql(0.seconds.ago.utc.iso8601)
103      expect(hash['Response']['Assertion']['Conditions']['NotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
104      expect(hash['Response']['Assertion']['Conditions']['AudienceRestriction']['Audience']).to eql(request.issuer)
105
106      expect(hash['Response']['Assertion']['AuthnStatement']['AuthnInstant']).to eql(Time.now.utc.iso8601)
107      expect(hash['Response']['Assertion']['AuthnStatement']['SessionIndex']).to eql(hash['Response']['Assertion']['ID'])
108      expect(hash['Response']['Assertion']['AuthnStatement']['AuthnContext']['AuthnContextClassRef']).to eql('urn:oasis:names:tc:SAML:2.0:ac:classes:Password')
109
110      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['Name']).to eql('email')
111      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['AttributeValue']).to eql(email)
112
113      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['Name']).to eql('created_at')
114      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['AttributeValue']).to be_present
115
116      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][2]['Name']).to eql('roles')
117      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][2]['AttributeValue']).to match_array(%w[husband father programmer])
118    end
119
120    it 'does not add a signature when the SP does not want assertions signed' do
121      builder = Saml::Kit::Builders::ServiceProviderMetadata.new
122      builder.want_assertions_signed = false
123      metadata = builder.build
124      allow(request).to receive(:provider).and_return(metadata)
125
126      subject = described_class.new(user, request)
127      hash = Hash.from_xml(subject.to_xml)
128      expect(hash['Response']['Signature']).to be_nil
129    end
130
131    it 'generates an EncryptedAssertion' do
132      subject.encrypt = true
133      result = Hash.from_xml(subject.to_xml)
134      expect(result['Response']['EncryptedAssertion']).to be_present
135      encrypted_assertion = result['Response']['EncryptedAssertion']
136      decrypted_assertion = Xml::Kit::Decryption.new(private_keys: configuration.private_keys(use: :encryption)).decrypt_hash(encrypted_assertion)
137      decrypted_hash = Hash.from_xml(decrypted_assertion)
138      expect(decrypted_hash['Assertion']).to be_present
139      expect(decrypted_hash['Assertion']['Issuer']).to be_present
140      expect(decrypted_hash['Assertion']['Subject']).to be_present
141      expect(decrypted_hash['Assertion']['Subject']['NameID']).to be_present
142      expect(decrypted_hash['Assertion']['Subject']['SubjectConfirmation']).to be_present
143      expect(decrypted_hash['Assertion']['Conditions']).to be_present
144      expect(decrypted_hash['Assertion']['Conditions']['AudienceRestriction']).to be_present
145      expect(decrypted_hash['Assertion']['AuthnStatement']).to be_present
146      expect(decrypted_hash['Assertion']['AuthnStatement']['AuthnContext']).to be_present
147      expect(decrypted_hash['Assertion']['AuthnStatement']['AuthnContext']['AuthnContextClassRef']).to be_present
148    end
149
150    it 'generates a signed response and encrypted assertion' do
151      subject.encrypt = true
152      subject.embed_signature = true
153      result = Hash.from_xml(subject.to_xml)
154      expect(result['Response']['Signature']).to be_present
155      expect(result['Response']['EncryptedAssertion']).to be_present
156    end
157
158    it 'generates a signed response and assertion' do
159      subject.encrypt = false
160      subject.embed_signature = true
161      result = Hash.from_xml(subject.to_xml)
162      expect(result['Response']['Signature']).to be_present
163      expect(result['Response']['Assertion']['Signature']).to be_present
164    end
165
166    it 'generates a signed response and signed and encrypted assertion' do
167      subject.encrypt = true
168      subject.embed_signature = true
169
170      result = Saml::Kit::Response.new(subject.to_xml, configuration: configuration)
171      expect(result).to be_signed
172      expect(result.assertion).to be_signed
173      expect(result.assertion).to be_encrypted
174    end
175
176    it 'generates an encrypted assertion' do
177      subject.encrypt = true
178      subject.embed_signature = false
179
180      result = Saml::Kit::Response.new(subject.to_xml, configuration: configuration)
181      expect(result).not_to be_signed
182      expect(result.assertion).not_to be_signed
183      expect(result.assertion).to be_encrypted
184    end
185
186    it 'excludes the nameid format when the request does not specify a nameid format in the nameid policy' do
187      xml = <<-XML.strip_heredoc
188        <samlp:AuthnRequest Version="2.0" ID="I_RzVGR.ktLi_wpo3IbsgwVJ2r8" IssueInstant="#{Time.now.iso8601}" Destination="#{FFaker::Internet.uri('https')}" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
189          <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">#{FFaker::Name.first_name}</saml:Issuer>
190          <samlp:NameIDPolicy AllowCreate="true" />
191          <samlp:RequestedAuthnContext Comparison="exact">
192            <saml:AuthnContextClassRef xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
193          </samlp:RequestedAuthnContext>
194        </samlp:AuthnRequest>
195      XML
196      authnrequest = Saml::Kit::AuthenticationRequest.new(xml)
197      user = User.new(name_id: FFaker::Internet.email)
198      result = Saml::Kit::Response.build(user, authnrequest)
199      expect(result.assertion.name_id_format).to eql(Saml::Kit::Namespaces::UNSPECIFIED_NAMEID)
200    end
201  end
202
203  describe '.build' do
204    let(:configuration) do
205      Saml::Kit::Configuration.new do |config|
206        config.entity_id = issuer
207        config.generate_key_pair_for(use: :signing)
208        config.generate_key_pair_for(use: :signing)
209        config.generate_key_pair_for(use: :signing)
210      end
211    end
212
213    it 'signs the response with a specific certificate' do
214      key_pair = configuration.key_pairs(use: :signing)[1]
215      subject.embed_signature = true
216      subject.sign_with(key_pair)
217
218      result = subject.build
219
220      expect(result).to be_signed
221      expect(result.signature.certificate).to eql(key_pair.certificate)
222    end
223
224    it 'specifies the recipient on the subject confirmation data' do
225      acs_url = FFaker::Internet.uri('https')
226      subject.assertion # force memoization the assertion
227      subject.destination = acs_url
228      result = subject.build
229      expect(result.assertion.at_xpath('/samlp:Response/saml:Assertion/saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData').attribute('Recipient').value).to eql(acs_url)
230    end
231
232    it 'uses the updated issuer on the assertion' do
233      issuer = FFaker::Internet.uri('https')
234      subject.assertion # force memoization the assertion
235      subject.issuer = issuer
236      result = subject.build
237      expect(result.assertion.issuer).to eql(issuer)
238    end
239
240    it 'uses the `now` on the assertion' do
241      now = 5.minutes.from_now
242      subject.assertion # force memoization the assertion
243      subject.now = now
244      result = subject.build
245      expect(result.assertion.started_at.to_i).to eql(now.to_i)
246    end
247
248    it 'does not embed the signature' do
249      subject.assertion # force memoization the assertion
250      subject.embed_signature = false
251      result = subject.build
252      expect(result.assertion).not_to be_signed
253    end
254  end
255end