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