main
1# frozen_string_literal: true
2
3RSpec.describe Saml::Kit::AuthenticationRequest do
4 subject { described_class.new(raw_xml, configuration: configuration) }
5
6 let(:id) { Xml::Kit::Id.generate }
7 let(:assertion_consumer_service_url) { "https://#{FFaker::Internet.domain_name}/acs" }
8 let(:issuer) { FFaker::Movie.title }
9 let(:destination) { FFaker::Internet.http_url }
10 let(:name_id_format) { Saml::Kit::Namespaces::EMAIL_ADDRESS }
11 let(:raw_xml) do
12 described_class.build_xml(configuration: configuration) do |builder|
13 builder.id = id
14 builder.now = Time.now.utc
15 builder.issuer = issuer
16 builder.assertion_consumer_service_url = assertion_consumer_service_url
17 builder.name_id_format = name_id_format
18 builder.destination = destination
19 end
20 end
21 let(:configuration) do
22 Saml::Kit::Configuration.new do |config|
23 config.generate_key_pair_for(use: :signing)
24 end
25 end
26
27 specify { expect(subject.name).to eql('AuthnRequest') }
28 specify { expect(subject.issuer).to eql(issuer) }
29 specify { expect(subject.id).to eql(id) }
30 specify { expect(subject.assertion_consumer_service_url).to eql(assertion_consumer_service_url) }
31 specify { expect(subject.name_id_format).to eql(name_id_format) }
32 specify { expect(subject.destination).to eql(destination) }
33 specify { expect(subject.force_authn).to be(false) }
34
35 describe '#valid?' do
36 let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
37 let(:metadata) { Saml::Kit::ServiceProviderMetadata.build(configuration: configuration) }
38
39 before do
40 allow(configuration).to receive(:registry).and_return(registry)
41 allow(registry).to receive(:metadata_for).and_return(metadata)
42 end
43
44 it 'is valid when left untampered' do
45 subject = described_class.new(raw_xml, configuration: configuration)
46 expect(subject).to be_valid
47 end
48
49 it 'is invalid if the document has been tampered with' do
50 raw_xml.gsub!(issuer, 'corrupt')
51 subject = described_class.new(raw_xml)
52 expect(subject).to be_invalid
53 end
54
55 it 'is invalid when blank' do
56 subject = described_class.new('')
57 expect(subject).to be_invalid
58 expect(subject.errors[:content]).to be_present
59 end
60
61 it 'is invalid when not an AuthnRequest' do
62 xml = Saml::Kit::IdentityProviderMetadata.build.to_xml
63 subject = described_class.new(xml)
64 expect(subject).to be_invalid
65 expect(subject.errors[:base]).to include(subject.error_message(:invalid))
66 end
67
68 it 'is invalid when the fingerprint of the certificate does not match the registered fingerprint' do
69 allow(metadata).to receive(:matches?).and_return(false)
70 subject = described_class.build do |builder|
71 builder.issuer = issuer
72 builder.assertion_consumer_service_url = assertion_consumer_service_url
73 end
74
75 expect(subject).to be_invalid
76 expect(subject.errors[:fingerprint]).to be_present
77 end
78
79 it 'is invalid when the service provider is not known' do
80 allow(registry).to receive(:metadata_for).and_return(nil)
81 subject = described_class.build
82 expect(subject).to be_invalid
83 expect(subject.errors[:provider]).to be_present
84 end
85
86 it 'validates the schema of the request' do
87 id = Xml::Kit::Id.generate
88 key_pair = ::Xml::Kit::KeyPair.generate(use: :signing)
89 signed_xml = ::Xml::Kit::Signatures.sign(key_pair: key_pair) do |xml, signature|
90 xml.tag!('samlp:AuthnRequest', 'xmlns:samlp' => Saml::Kit::Namespaces::PROTOCOL, AssertionConsumerServiceURL: assertion_consumer_service_url, ID: id) do
91 signature.template(id)
92 xml.Fake do
93 xml.NotAllowed 'Huh?'
94 end
95 end
96 end
97 expect(described_class.new(signed_xml)).to be_invalid
98 end
99
100 it 'validates a request without a signature' do
101 now = Time.now.utc
102 raw_xml = <<-XML.strip_heredoc
103 <samlp:AuthnRequest AssertionConsumerServiceURL='#{assertion_consumer_service_url}' ID='#{Xml::Kit::Id.generate}' IssueInstant='#{now.iso8601}' Version='2.0' xmlns:saml='#{Saml::Kit::Namespaces::ASSERTION}' xmlns:samlp='#{Saml::Kit::Namespaces::PROTOCOL}'>
104 <saml:Issuer>#{issuer}</saml:Issuer>
105 <samlp:NameIDPolicy AllowCreate='true' Format='#{Saml::Kit::Namespaces::EMAIL_ADDRESS}'/>
106 </samlp:AuthnRequest>
107 XML
108
109 subject = described_class.new(raw_xml, configuration: configuration)
110 subject.signature_verified!
111 expect(subject).to be_valid
112 end
113
114 it 'is valid when there is no signature, and the issuer is registered' do
115 now = Time.now.utc
116 raw_xml = <<-XML.strip_heredoc
117 <samlp:AuthnRequest AssertionConsumerServiceURL='#{assertion_consumer_service_url}' ID='#{Xml::Kit::Id.generate}' IssueInstant='#{now.iso8601}' Version='2.0' xmlns:saml='#{Saml::Kit::Namespaces::ASSERTION}' xmlns:samlp='#{Saml::Kit::Namespaces::PROTOCOL}'>
118 <saml:Issuer>#{issuer}</saml:Issuer>
119 <samlp:NameIDPolicy AllowCreate='true' Format='#{Saml::Kit::Namespaces::PERSISTENT}'/>
120 </samlp:AuthnRequest>
121 XML
122
123 allow(registry).to receive(:metadata_for).with(issuer).and_return(metadata)
124 subject = described_class.new(raw_xml, configuration: configuration)
125 expect(subject).to be_valid
126 end
127
128 it 'is invalid when there is no signature, and the issuer is not registered' do
129 now = Time.now.utc
130 raw_xml = <<-XML.strip_heredoc
131 <samlp:AuthnRequest AssertionConsumerServiceURL='#{assertion_consumer_service_url}' ID='#{Xml::Kit::Id.generate}' IssueInstant='#{now.iso8601}' Version='2.0' xmlns:saml='#{Saml::Kit::Namespaces::ASSERTION}' xmlns:samlp='#{Saml::Kit::Namespaces::PROTOCOL}'>
132 <saml:Issuer>#{issuer}</saml:Issuer>
133 <samlp:NameIDPolicy AllowCreate='true' Format='#{Saml::Kit::Namespaces::PERSISTENT}'/>
134 </samlp:AuthnRequest>
135 XML
136
137 allow(registry).to receive(:metadata_for).with(issuer).and_return(nil)
138 subject = described_class.new(raw_xml, configuration: configuration)
139 expect(subject).to be_invalid
140 end
141
142 context 'when the certificate is expired' do
143 let(:expired_certificate) do
144 certificate = OpenSSL::X509::Certificate.new
145 certificate.public_key = private_key.public_key
146 certificate.not_before = 1.day.ago
147 certificate.not_after = 1.second.ago
148 certificate
149 end
150 let(:private_key) { OpenSSL::PKey::RSA.new(2048) }
151 let(:digest_algorithm) { OpenSSL::Digest::SHA256.new }
152
153 before do
154 expired_certificate.sign(private_key, digest_algorithm)
155 end
156
157 it 'is invalid' do
158 document = described_class.build do |x|
159 x.embed_signature = true
160 certificate = ::Xml::Kit::Certificate.new(expired_certificate)
161 x.sign_with(certificate.to_key_pair(private_key))
162 end
163 subject = described_class.new(document.to_xml)
164 expect(subject).to be_invalid
165 expect(subject.errors[:certificate]).to be_present
166 end
167 end
168 end
169
170 describe '#assertion_consumer_service_url' do
171 let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
172 let(:metadata) { instance_double(Saml::Kit::ServiceProviderMetadata) }
173
174 it 'returns the ACS in the request' do
175 subject = described_class.build do |builder|
176 builder.assertion_consumer_service_url = assertion_consumer_service_url
177 end
178 expect(subject.assertion_consumer_service_url).to eql(assertion_consumer_service_url)
179 end
180
181 it 'returns nil' do
182 subject = described_class.build do |builder|
183 builder.assertion_consumer_service_url = nil
184 end
185
186 expect(subject.assertion_consumer_service_url).to be_nil
187 end
188 end
189
190 describe '#force_authn' do
191 context 'when set to true' do
192 subject { described_class.build { |x| x.force_authn = true } }
193
194 specify { expect(subject.force_authn).to be(true) }
195 end
196
197 context 'when set to false' do
198 subject { described_class.build { |x| x.force_authn = false } }
199
200 specify { expect(subject.force_authn).to be(false) }
201 end
202
203 context 'when not specified' do
204 subject { described_class.build { |x| x.force_authn = nil } }
205
206 specify { expect(subject.force_authn).to be(false) }
207 end
208 end
209
210 describe '.build' do
211 let(:url) { FFaker::Internet.uri('https') }
212 let(:entity_id) { FFaker::Internet.uri('https') }
213
214 it 'provides a nice API for building a request' do
215 result = described_class.build do |builder|
216 builder.issuer = entity_id
217 builder.assertion_consumer_service_url = url
218 end
219
220 expect(result).to be_instance_of(described_class)
221 expect(result.issuer).to eql(entity_id)
222 expect(result.assertion_consumer_service_url).to eql(url)
223 end
224
225 it 'can build a authnrequest without a nameid policy' do
226 result = described_class.build do |x|
227 x.issuer = entity_id
228 x.assertion_consumer_service_url = url
229 x.name_id_format = nil
230 end
231 expect(result).to be_instance_of(described_class)
232 result.registry = instance_double(Saml::Kit::DefaultRegistry, metadata_for: Saml::Kit::ServiceProviderMetadata.build)
233 expect(result).to be_valid
234 expect(result.to_xml).not_to include('NameIDPolicy')
235 end
236 end
237
238 describe '#response_for' do
239 let(:user) { User.new }
240 let(:provider) do
241 Saml::Kit::ServiceProviderMetadata.build do |x|
242 x.add_assertion_consumer_service(FFaker::Internet.uri('https'), binding: :http_post)
243 end
244 end
245
246 it 'serializes a response' do
247 allow(subject).to receive(:provider).and_return(provider)
248 _url, saml_params = subject.response_for(user, binding: :http_post, relay_state: FFaker::Movie.title)
249
250 response = provider.assertion_consumer_service_for(binding: :http_post).deserialize(saml_params)
251 expect(response).to be_instance_of(Saml::Kit::Response)
252 end
253
254 it 'serializes a response with the specified signing certificate' do
255 allow(subject).to receive(:provider).and_return(provider)
256 configuration = Saml::Kit::Configuration.new do |config|
257 config.generate_key_pair_for(use: :signing)
258 end
259 key_pair = configuration.key_pairs(use: :signing).first
260 _url, saml_params = subject.response_for(user, binding: :http_post, configuration: configuration) do |builder|
261 builder.sign_with(key_pair)
262 end
263
264 response = provider.assertion_consumer_service_for(binding: :http_post).deserialize(saml_params)
265 expect(response).to be_instance_of(Saml::Kit::Response)
266 end
267 end
268
269 describe '#signature.valid?' do
270 it 'returns true when the signature is valid' do
271 expect(subject.signature).to be_valid
272 end
273
274 it 'returns false when the signature does not match' do
275 raw_xml.gsub!(issuer, 'corrupt')
276 subject = described_class.new(raw_xml, configuration: configuration)
277 expect(subject.signature).not_to be_valid
278 end
279 end
280end