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