main
1# frozen_string_literal: true
2
3RSpec.describe Saml::Kit::Assertion do
4 subject do
5 described_class.build(user, request) do |x|
6 x.issuer = entity_id
7 end
8 end
9
10 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) }
11 let(:user) { User.new(name_id: SecureRandom.uuid, attributes: { id: SecureRandom.uuid }) }
12 let(:entity_id) { FFaker::Internet.uri('https') }
13
14 specify { expect(subject.issuer).to eql(entity_id) }
15 specify { expect(subject.name_id).to eql(user.name_id) }
16 specify { expect(subject.name_id_format).to eql(Saml::Kit::Namespaces::PERSISTENT) }
17 specify { expect(subject.started_at.to_i).to eql(Time.now.utc.to_i) }
18 specify { expect(subject.expired_at.to_i).to eql(Saml::Kit.configuration.session_timeout.since(Time.now).utc.to_i) }
19 specify { expect(subject.attributes).to eql('id' => user.attributes[:id]) }
20 specify { expect(subject.audiences).to match_array([request.issuer]) }
21
22 describe '#active?' do
23 let(:configuration) do
24 Saml::Kit::Configuration.new do |config|
25 config.session_timeout = 30.minutes
26 config.clock_drift = 30.seconds
27 end
28 end
29
30 it 'is valid after a valid session window + drift' do
31 now = Time.current
32 travel_to now
33 not_on_or_after = configuration.session_timeout.since(now).iso8601
34 xml = <<-XML.strip_heredoc
35 <Response xmlns="#{Saml::Kit::Namespaces::PROTOCOL}">
36 <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{Xml::Kit::Id.generate}" IssueInstant="#{now.iso8601}" Version="2.0">
37 <Issuer>#{FFaker::Internet.uri('https')}</Issuer>
38 <Subject>
39 <NameID Format="#{Saml::Kit::Namespaces::PERSISTENT}">#{SecureRandom.uuid}</NameID>
40 <SubjectConfirmation Method="#{Saml::Kit::Namespaces::BEARER}">
41 <SubjectConfirmationData InResponseTo="#{SecureRandom.uuid}" NotOnOrAfter="#{not_on_or_after}" Recipient="#{FFaker::Internet.uri('https')}"/>
42 </SubjectConfirmation>
43 </Subject>
44 <Conditions NotBefore="#{now.utc.iso8601}" NotOnOrAfter="#{not_on_or_after}">
45 <AudienceRestriction>
46 <Audience>#{FFaker::Internet.uri('https')}</Audience>
47 </AudienceRestriction>
48 </Conditions>
49 <AuthnStatement AuthnInstant="#{now.utc.iso8601}" SessionIndex="#{Xml::Kit::Id.generate}" SessionNotOnOrAfter="#{not_on_or_after}">
50 <AuthnContext>
51 <AuthnContextClassRef>#{Saml::Kit::Namespaces::PASSWORD}</AuthnContextClassRef>
52 </AuthnContext>
53 </AuthnStatement>
54 </Assertion>
55 </Response>
56 XML
57 document = Nokogiri::XML(xml)
58 node = document.at_xpath('//saml:Assertion', 'saml' => Saml::Kit::Namespaces::ASSERTION)
59 subject = described_class.new(node, configuration: configuration)
60 travel_to((configuration.clock_drift - 1.second).before(now))
61 expect(subject).to be_active
62 expect(subject).not_to be_expired
63 end
64
65 it 'interprets integers correctly' do
66 configuration.clock_drift = 30
67 now = Time.current
68 travel_to now
69 not_before = now.utc.iso8601
70 not_after = configuration.session_timeout.since(now).iso8601
71 xml = <<-XML.strip_heredoc
72 <Response xmlns="#{Saml::Kit::Namespaces::PROTOCOL}">
73 <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{Xml::Kit::Id.generate}" IssueInstant="#{now.iso8601}" Version="2.0">
74 <Issuer>#{FFaker::Internet.uri('https')}</Issuer>
75 <Subject>
76 <NameID Format="#{Saml::Kit::Namespaces::PERSISTENT}">#{SecureRandom.uuid}</NameID>
77 <SubjectConfirmation Method="#{Saml::Kit::Namespaces::BEARER}">
78 <SubjectConfirmationData InResponseTo="#{SecureRandom.uuid}" NotOnOrAfter="#{not_after}" Recipient="#{FFaker::Internet.uri('https')}"/>
79 </SubjectConfirmation>
80 </Subject>
81 <Conditions NotBefore="#{not_before}" NotOnOrAfter="#{not_after}">
82 <AudienceRestriction>
83 <Audience>#{FFaker::Internet.uri('https')}</Audience>
84 </AudienceRestriction>
85 </Conditions>
86 <AuthnStatement AuthnInstant="#{now.utc.iso8601}" SessionIndex="#{Xml::Kit::Id.generate}" SessionNotOnOrAfter="#{not_after}">
87 <AuthnContext>
88 <AuthnContextClassRef>#{Saml::Kit::Namespaces::PASSWORD}</AuthnContextClassRef>
89 </AuthnContext>
90 </AuthnStatement>
91 </Assertion>
92 </Response>
93 XML
94 document = Nokogiri::XML(xml)
95 node = document.at_xpath('//saml:Assertion', 'saml' => Saml::Kit::Namespaces::ASSERTION)
96 subject = described_class.new(node, configuration: configuration)
97 expect(subject).to be_active
98 expect(subject).not_to be_expired
99 end
100 end
101
102 describe '#present?' do
103 it 'returns false when the assertion is empty' do
104 subject = described_class.new(nil)
105 expect(subject).not_to be_present
106 end
107
108 it 'returns true when the assertion is present' do
109 not_before = Time.now.utc.iso8601
110 not_after = 10.minutes.from_now.iso8601
111 xml = <<-XML.strip_heredoc
112 <Response>
113 <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{Xml::Kit::Id.generate}" IssueInstant="#{Time.now.iso8601}" Version="2.0">
114 <Issuer>#{FFaker::Internet.uri('https')}</Issuer>
115 <Subject>
116 <NameID Format="#{Saml::Kit::Namespaces::PERSISTENT}">#{SecureRandom.uuid}</NameID>
117 <SubjectConfirmation Method="#{Saml::Kit::Namespaces::BEARER}">
118 <SubjectConfirmationData InResponseTo="#{SecureRandom.uuid}" NotOnOrAfter="#{not_after}" Recipient="#{FFaker::Internet.uri('https')}"/>
119 </SubjectConfirmation>
120 </Subject>
121 <Conditions NotBefore="#{not_before}" NotOnOrAfter="#{not_after}">
122 <AudienceRestriction>
123 <Audience>#{FFaker::Internet.uri('https')}</Audience>
124 </AudienceRestriction>
125 </Conditions>
126 <AuthnStatement AuthnInstant="#{Time.now.utc.iso8601}" SessionIndex="#{Xml::Kit::Id.generate}" SessionNotOnOrAfter="#{not_after}">
127 <AuthnContext>
128 <AuthnContextClassRef>#{Saml::Kit::Namespaces::PASSWORD}</AuthnContextClassRef>
129 </AuthnContext>
130 </AuthnStatement>
131 </Assertion>
132 </Response>
133 XML
134 subject = described_class.new(Nokogiri::XML(xml))
135 expect(subject).to be_present
136 end
137 end
138
139 describe '#signed?' do
140 let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: ::Xml::Kit::Id.generate, issuer: FFaker::Internet.http_url, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, provider: nil, signed?: true, trusted?: true) }
141 let(:user) { User.new(attributes: { id: SecureRandom.uuid }) }
142
143 it 'detects a signature in an encrypted assertion' do
144 encryption_key_pair = Xml::Kit::KeyPair.generate(use: :encryption)
145 response = Saml::Kit::Response.build(user, request) do |x|
146 x.sign_with(Xml::Kit::KeyPair.generate(use: :signing))
147 x.encrypt_with(encryption_key_pair)
148 end
149 assertion = response.assertion([encryption_key_pair.private_key])
150 expect(response).to be_signed
151 expect(assertion).to be_signed
152 end
153 end
154
155 describe '#encrypted?' do
156 it 'returns true when encrypted' do
157 key_pair = Xml::Kit::KeyPair.generate(use: :encryption)
158 response = Saml::Kit::Response.build(user, request) do |x|
159 x.encrypt_with(key_pair)
160 end
161 subject = response.assertion([key_pair.private_key])
162 expect(subject).to be_encrypted
163 end
164
165 it 'returns false when not encrypted' do
166 expect(subject).not_to be_encrypted
167 end
168 end
169
170 describe '#to_xml' do
171 let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: ::Xml::Kit::Id.generate, issuer: FFaker::Internet.http_url, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, provider: nil, signed?: true, trusted?: true) }
172 let(:user) { User.new(attributes: { id: SecureRandom.uuid }) }
173
174 it 'returns the decrypted xml' do
175 encryption_key_pair = Xml::Kit::KeyPair.generate(use: :encryption)
176 response = Saml::Kit::Response.build(user, request) do |x|
177 x.sign_with(Xml::Kit::KeyPair.generate(use: :signing))
178 x.encrypt_with(encryption_key_pair)
179 end
180 assertion = response.assertion([encryption_key_pair.private_key])
181 expect(assertion.to_xml).not_to include('EncryptedAssertion')
182 expect(assertion.to_xml).to include('Assertion')
183 end
184 end
185
186 describe '#valid?' do
187 let(:entity_id) { FFaker::Internet.uri('https') }
188 let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: ::Xml::Kit::Id.generate, issuer: entity_id, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, provider: nil, signed?: true, trusted?: true) }
189 let(:name_id) { SecureRandom.uuid }
190 let(:user) { User.new(name_id: name_id, attributes: { id: SecureRandom.uuid }) }
191 let(:registry) { instance_double(Saml::Kit::DefaultRegistry, metadata_for: idp) }
192 let(:idp) { Saml::Kit::IdentityProviderMetadata.build(configuration: configuration) }
193 let(:configuration) do
194 Saml::Kit::Configuration.new do |x|
195 x.entity_id = entity_id
196 x.generate_key_pair_for(use: :signing)
197 end
198 end
199
200 before do
201 allow(configuration.registry).to receive(:metadata_for).with(entity_id).and_return(idp)
202 end
203
204 it 'is invalid when the encrypted signature is invalid' do
205 xml = Saml::Kit::Response.build_xml(user, request, configuration: configuration)
206 altered = xml.gsub(name_id, 'altered')
207 document = Nokogiri::XML(altered)
208 assertion = document.at_xpath('/samlp:Response/saml:Assertion', Saml::Kit::Document::NAMESPACES)
209 key_pair = Xml::Kit::KeyPair.generate(use: :encryption)
210 encrypted = Xml::Kit::EncryptedData.new(assertion.to_xml, asymmetric_cipher: Xml::Kit::Crypto::RsaCipher.new('', key_pair.private_key)).to_xml
211 response = Saml::Kit::Response.new(encrypted, configuration: configuration)
212 expect(response.assertion([key_pair.private_key])).to be_invalid
213 end
214
215 it 'is valid when the encrypted signature is valid' do
216 key_pair = Xml::Kit::KeyPair.generate(use: :encryption)
217 response = Saml::Kit::Response.build(user, request, configuration: configuration) do |x|
218 x.encrypt_with(key_pair)
219 end
220 expect(response.assertion([key_pair.private_key])).to be_valid
221 end
222
223 it 'is invalid when the assertion signature is invalid' do
224 xml = Saml::Kit::Response.build_xml(user, request, configuration: configuration)
225 altered = xml.gsub(name_id, 'altered')
226 response = Saml::Kit::Response.new(altered, configuration: configuration)
227 expect(response.assertion).to be_invalid
228 expect(response.assertion.errors[:digest_value]).to match_array(['is invalid.'])
229 end
230
231 it 'is invalid when the response signature is invalid' do
232 xml = Saml::Kit::Response.build_xml(user, request, configuration: configuration)
233 altered = xml.gsub('StatusCode', 'ALTERED')
234 response = Saml::Kit::Response.new(altered, configuration: configuration)
235 expect(response).to be_invalid
236 end
237
238 it 'is valid' do
239 response = Saml::Kit::Response.build(user, request, configuration: configuration)
240 expect(response.assertion).to be_valid
241 end
242 end
243
244 describe '.new' do
245 let(:user) { instance_double(User, name_id_for: SecureRandom.uuid, assertion_attributes_for: {}) }
246 let(:saml_request) { instance_double(Saml::Kit::AuthenticationRequest, id: Xml::Kit::Id.generate, issuer: configuration.entity_id) }
247 let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
248 let(:configuration) do
249 Saml::Kit::Configuration.new do |x|
250 x.entity_id = FFaker::Internet.uri('https')
251 x.registry = registry
252 x.generate_key_pair_for(use: :signing)
253 end
254 end
255 let(:metadata) do
256 Saml::Kit::Metadata.build(configuration: configuration, &:build_identity_provider)
257 end
258
259 before { allow(registry).to receive(:metadata_for).with(configuration.entity_id).and_return(metadata) }
260
261 it 'parses a raw xml assertion' do
262 saml = described_class.build_xml(user, saml_request, configuration: configuration)
263 expect(described_class.new(saml, configuration: configuration)).to be_valid
264 end
265 end
266end