omniauth-saml 1.1.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of omniauth-saml might be problematic. Click here for more details.

@@ -6,8 +6,8 @@ RSpec::Matchers.define :fail_with do |message|
6
6
  end
7
7
  end
8
8
 
9
- def post_xml(xml=:example_response)
10
- post "/auth/saml/callback", {'SAMLResponse' => load_xml(xml)}
9
+ def post_xml(xml = :example_response, opts = {})
10
+ post "/auth/saml/callback", opts.merge({'SAMLResponse' => load_xml(xml)})
11
11
  end
12
12
 
13
13
  describe OmniAuth::Strategies::SAML, :type => :strategy do
@@ -16,42 +16,103 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
16
16
  let(:auth_hash){ last_request.env['omniauth.auth'] }
17
17
  let(:saml_options) do
18
18
  {
19
- :assertion_consumer_service_url => "http://localhost:3000/auth/saml/callback",
20
- :issuer => "https://saml.issuer.url/issuers/29490",
21
- :idp_sso_target_url => "https://idp.sso.target_url/signon/29490",
19
+ :assertion_consumer_service_url => "http://localhost:9080/auth/saml/callback",
20
+ :single_logout_service_url => "http://localhost:9080/auth/saml/slo",
21
+ :idp_sso_service_url => "https://idp.sso.example.com/signon/29490",
22
+ :idp_slo_service_url => "https://idp.sso.example.com/signoff/29490",
22
23
  :idp_cert_fingerprint => "C1:59:74:2B:E8:0C:6C:A9:41:0F:6E:83:F6:D1:52:25:45:58:89:FB",
23
- :idp_sso_target_url_runtime_params => {:original_param_key => :mapped_param_key},
24
- :name_identifier_format => "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
24
+ :idp_sso_service_url_runtime_params => {:original_param_key => :mapped_param_key},
25
+ :name_identifier_format => "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
26
+ :request_attributes => [
27
+ { :name => 'email', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Email address' },
28
+ { :name => 'name', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Full name' },
29
+ { :name => 'first_name', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Given name' },
30
+ { :name => 'last_name', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Family name' }
31
+ ],
32
+ :attribute_service_name => 'Required attributes'
25
33
  }
26
34
  end
27
35
  let(:strategy) { [OmniAuth::Strategies::SAML, saml_options] }
28
36
 
29
- describe 'GET /auth/saml' do
37
+ describe 'POST /auth/saml' do
30
38
  context 'without idp runtime params present' do
31
39
  before do
32
- get '/auth/saml'
40
+ post '/auth/saml'
33
41
  end
34
42
 
35
43
  it 'should get authentication page' do
36
- last_response.should be_redirect
37
- last_response.location.should match /https:\/\/idp.sso.target_url\/signon\/29490/
38
- last_response.location.should match /\?SAMLRequest=/
39
- last_response.location.should_not match /mapped_param_key/
40
- last_response.location.should_not match /original_param_key/
44
+ expect(last_response).to be_redirect
45
+ expect(last_response.location).to match /https:\/\/idp.sso.example.com\/signon\/29490/
46
+ expect(last_response.location).to match /\?SAMLRequest=/
47
+ expect(last_response.location).not_to match /mapped_param_key/
48
+ expect(last_response.location).not_to match /original_param_key/
41
49
  end
42
50
  end
43
51
 
44
52
  context 'with idp runtime params' do
45
53
  before do
46
- get '/auth/saml', 'original_param_key' => 'original_param_value', 'mapped_param_key' => 'mapped_param_value'
54
+ post '/auth/saml', 'original_param_key' => 'original_param_value', 'mapped_param_key' => 'mapped_param_value'
47
55
  end
48
56
 
49
57
  it 'should get authentication page' do
50
- last_response.should be_redirect
51
- last_response.location.should match /https:\/\/idp.sso.target_url\/signon\/29490/
52
- last_response.location.should match /\?SAMLRequest=/
53
- last_response.location.should match /\&mapped_param_key=original_param_value/
54
- last_response.location.should_not match /original_param_key/
58
+ expect(last_response).to be_redirect
59
+ expect(last_response.location).to match /https:\/\/idp.sso.example.com\/signon\/29490/
60
+ expect(last_response.location).to match /\?SAMLRequest=/
61
+ expect(last_response.location).to match /\&mapped_param_key=original_param_value/
62
+ expect(last_response.location).not_to match /original_param_key/
63
+ end
64
+ end
65
+
66
+ context "when the assertion_consumer_service_url is the default" do
67
+ before :each do
68
+ saml_options[:compress_request] = false
69
+ saml_options.delete(:assertion_consumer_service_url)
70
+ end
71
+
72
+ it 'should send the current callback_url as the assertion_consumer_service_url' do
73
+ %w(foo.example.com bar.example.com).each do |host|
74
+ post "https://#{host}/auth/saml"
75
+
76
+ expect(last_response).to be_redirect
77
+
78
+ location = URI.parse(last_response.location)
79
+ query = Rack::Utils.parse_query location.query
80
+ expect(query).to have_key('SAMLRequest')
81
+
82
+ request = REXML::Document.new(Base64.decode64(query['SAMLRequest']))
83
+ expect(request.root).not_to be_nil
84
+
85
+ acs = request.root.attributes.get_attribute('AssertionConsumerServiceURL')
86
+ expect(acs.to_s).to eq "https://#{host}/auth/saml/callback"
87
+ end
88
+ end
89
+ end
90
+
91
+ context 'when authn request signing is requested' do
92
+ subject { post '/auth/saml' }
93
+
94
+ let(:private_key) { OpenSSL::PKey::RSA.new 2048 }
95
+
96
+ before do
97
+ saml_options[:compress_request] = false
98
+
99
+ saml_options[:private_key] = private_key.to_pem
100
+ saml_options[:security] = {
101
+ authn_requests_signed: true,
102
+ signature_method: XMLSecurity::Document::RSA_SHA256
103
+ }
104
+ end
105
+
106
+ it 'should sign the request' do
107
+ is_expected.to be_redirect
108
+
109
+ location = URI.parse(last_response.location)
110
+ query = Rack::Utils.parse_query location.query
111
+ expect(query).to have_key('SAMLRequest')
112
+ expect(query).to have_key('Signature')
113
+ expect(query).to have_key('SigAlg')
114
+
115
+ expect(query['SigAlg']).to eq XMLSecurity::Document::RSA_SHA256
55
116
  end
56
117
  end
57
118
  end
@@ -62,7 +123,7 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
62
123
  let(:xml) { :example_response }
63
124
 
64
125
  before :each do
65
- Time.stub(:now).and_return(Time.new(2012, 11, 8, 20, 40, 00, 0))
126
+ allow(Time).to receive(:now).and_return(Time.utc(2012, 11, 8, 20, 40, 00))
66
127
  end
67
128
 
68
129
  context "when the response is valid" do
@@ -71,33 +132,84 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
71
132
  end
72
133
 
73
134
  it "should set the uid to the nameID in the SAML response" do
74
- auth_hash['uid'].should == '_1f6fcf6be5e13b08b1e3610e7ff59f205fbd814f23'
135
+ expect(auth_hash['uid']).to eq '_1f6fcf6be5e13b08b1e3610e7ff59f205fbd814f23'
75
136
  end
76
137
 
77
138
  it "should set the raw info to all attributes" do
78
- auth_hash['extra']['raw_info'].to_hash.should == {
79
- 'first_name' => 'Rajiv',
80
- 'last_name' => 'Manglani',
81
- 'email' => 'user@example.com',
82
- 'company_name' => 'Example Company'
83
- }
139
+ expect(auth_hash['extra']['raw_info'].all.to_hash).to eq(
140
+ 'first_name' => ['Rajiv'],
141
+ 'last_name' => ['Manglani'],
142
+ 'email' => ['user@example.com'],
143
+ 'company_name' => ['Example Company'],
144
+ 'fingerprint' => saml_options[:idp_cert_fingerprint]
145
+ )
146
+ end
147
+
148
+ it "should set the response_object to the response object from ruby_saml response" do
149
+ expect(auth_hash['extra']['response_object']).to be_kind_of(OneLogin::RubySaml::Response)
150
+ end
151
+ end
152
+
153
+ context "when fingerprint is empty and there's a fingerprint validator" do
154
+ before :each do
155
+ saml_options.delete(:idp_cert_fingerprint)
156
+ saml_options[:idp_cert_fingerprint_validator] = fingerprint_validator
157
+ end
158
+
159
+ let(:fingerprint_validator) { lambda { |_| "C1:59:74:2B:E8:0C:6C:A9:41:0F:6E:83:F6:D1:52:25:45:58:89:FB" } }
160
+
161
+ context "when the fingerprint validator returns a truthy value" do
162
+ before { post_xml }
163
+
164
+ it "should set the uid to the nameID in the SAML response" do
165
+ expect(auth_hash['uid']).to eq '_1f6fcf6be5e13b08b1e3610e7ff59f205fbd814f23'
166
+ end
167
+
168
+ it "should set the raw info to all attributes" do
169
+ expect(auth_hash['extra']['raw_info'].all.to_hash).to eq(
170
+ 'first_name' => ['Rajiv'],
171
+ 'last_name' => ['Manglani'],
172
+ 'email' => ['user@example.com'],
173
+ 'company_name' => ['Example Company'],
174
+ 'fingerprint' => 'C1:59:74:2B:E8:0C:6C:A9:41:0F:6E:83:F6:D1:52:25:45:58:89:FB'
175
+ )
176
+ end
177
+ end
178
+
179
+ context "when the fingerprint validator returns false" do
180
+ let(:fingerprint_validator) { lambda { |_| false } }
181
+
182
+ before { post_xml }
183
+
184
+ it { is_expected.to fail_with(:invalid_ticket) }
84
185
  end
85
186
  end
86
187
 
188
+ context "when the assertion_consumer_service_url is the default" do
189
+ before :each do
190
+ saml_options.delete(:assertion_consumer_service_url)
191
+ OmniAuth.config.full_host = 'http://localhost:9080'
192
+ post_xml
193
+ end
194
+
195
+ it { is_expected.not_to fail_with(:invalid_ticket) }
196
+ end
197
+
87
198
  context "when there is no SAMLResponse parameter" do
88
199
  before :each do
89
200
  post '/auth/saml/callback'
90
201
  end
91
202
 
92
- it { should fail_with(:invalid_ticket) }
203
+ it { is_expected.to fail_with(:invalid_ticket) }
93
204
  end
94
205
 
95
206
  context "when there is no name id in the XML" do
96
207
  before :each do
208
+ allow(Time).to receive(:now).and_return(Time.utc(2012, 11, 8, 23, 55, 00))
97
209
  post_xml :no_name_id
98
210
  end
99
211
 
100
- it { should fail_with(:invalid_ticket) }
212
+ it { is_expected.to fail_with(:invalid_ticket) }
101
213
  end
102
214
 
103
215
  context "when the fingerprint is invalid" do
@@ -106,7 +218,7 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
106
218
  post_xml
107
219
  end
108
220
 
109
- it { should fail_with(:invalid_ticket) }
221
+ it { is_expected.to fail_with(:invalid_ticket) }
110
222
  end
111
223
 
112
224
  context "when the digest is invalid" do
@@ -114,7 +226,7 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
114
226
  post_xml :digest_mismatch
115
227
  end
116
228
 
117
- it { should fail_with(:invalid_ticket) }
229
+ it { is_expected.to fail_with(:invalid_ticket) }
118
230
  end
119
231
 
120
232
  context "when the signature is invalid" do
@@ -122,7 +234,220 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
122
234
  post_xml :invalid_signature
123
235
  end
124
236
 
125
- it { should fail_with(:invalid_ticket) }
237
+ it { is_expected.to fail_with(:invalid_ticket) }
238
+ end
239
+
240
+ context "when the response is stale" do
241
+ before :each do
242
+ allow(Time).to receive(:now).and_return(Time.utc(2012, 11, 8, 20, 45, 00))
243
+ end
244
+
245
+ context "without :allowed_clock_drift option" do
246
+ before { post_xml :example_response }
247
+
248
+ it { is_expected.to fail_with(:invalid_ticket) }
249
+ end
250
+
251
+ context "with :allowed_clock_drift option" do
252
+ before :each do
253
+ saml_options[:allowed_clock_drift] = 60
254
+ post_xml :example_response
255
+ end
256
+
257
+ it { is_expected.to_not fail_with(:invalid_ticket) }
258
+ end
259
+ end
260
+
261
+ context "when response has custom attributes" do
262
+ before :each do
263
+ saml_options[:idp_cert_fingerprint] = "3B:82:F1:F5:54:FC:A8:FF:12:B8:4B:B8:16:61:1D:E4:8E:9B:E2:3C"
264
+ saml_options[:attribute_statements] = {
265
+ email: ["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"],
266
+ first_name: ["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"],
267
+ last_name: ["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"]
268
+ }
269
+ post_xml :custom_attributes
270
+ end
271
+
272
+ it "should obey attribute statements mapping" do
273
+ expect(auth_hash[:info]).to eq(
274
+ 'first_name' => 'Rajiv',
275
+ 'last_name' => 'Manglani',
276
+ 'email' => 'user@example.com',
277
+ 'name' => nil
278
+ )
279
+ end
280
+ end
281
+
282
+ context "when using custom user id attribute" do
283
+ before :each do
284
+ saml_options[:idp_cert_fingerprint] = "3B:82:F1:F5:54:FC:A8:FF:12:B8:4B:B8:16:61:1D:E4:8E:9B:E2:3C"
285
+ saml_options[:uid_attribute] = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
286
+ post_xml :custom_attributes
287
+ end
288
+
289
+ it "should return user id attribute" do
290
+ expect(auth_hash[:uid]).to eq("user@example.com")
291
+ end
292
+ end
293
+
294
+ context "when using custom user id attribute, but it is missing" do
295
+ before :each do
296
+ saml_options[:uid_attribute] = "missing_attribute"
297
+ post_xml
298
+ end
299
+
300
+ it "should fail to authenticate" do
301
+ should fail_with(:invalid_ticket)
302
+ expect(last_request.env['omniauth.error']).to be_instance_of(OmniAuth::Strategies::SAML::ValidationError)
303
+ expect(last_request.env['omniauth.error'].message).to eq("SAML response missing 'missing_attribute' attribute")
304
+ end
305
+ end
306
+
307
+ context "when response is a logout response" do
308
+ before :each do
309
+ saml_options[:sp_entity_id] = "https://idp.sso.example.com/metadata/29490"
310
+
311
+ post "/auth/saml/slo", {
312
+ SAMLResponse: load_xml(:example_logout_response),
313
+ RelayState: "https://example.com/",
314
+ }, "rack.session" => {"saml_transaction_id" => "_3fef1069-d0c6-418a-b68d-6f008a4787e9"}
315
+ end
316
+ it "should redirect to relaystate" do
317
+ expect(last_response).to be_redirect
318
+ expect(last_response.location).to match /https:\/\/example.com\//
319
+ end
320
+ end
321
+
322
+ context "when request is a logout request" do
323
+ subject { post "/auth/saml/slo", params, "rack.session" => { "saml_uid" => "username@example.com" } }
324
+
325
+ before :each do
326
+ saml_options[:sp_entity_id] = "https://idp.sso.example.com/metadata/29490"
327
+ end
328
+
329
+ let(:params) do
330
+ {
331
+ "SAMLRequest" => load_xml(:example_logout_request),
332
+ "RelayState" => "https://example.com/",
333
+ }
334
+ end
335
+
336
+ context "when logout request is valid" do
337
+ before { subject }
338
+
339
+ it "should redirect to logout response" do
340
+ expect(last_response).to be_redirect
341
+ expect(last_response.location).to match /https:\/\/idp.sso.example.com\/signoff\/29490/
342
+ expect(last_response.location).to match /RelayState=https%3A%2F%2Fexample.com%2F/
343
+ end
344
+ end
345
+
346
+ context "when request is an invalid logout request" do
347
+ before :each do
348
+ allow_any_instance_of(OneLogin::RubySaml::SloLogoutrequest).to receive(:is_valid?).and_return(false)
349
+ end
350
+
351
+ # TODO: Maybe this should not raise an exception, but return some 4xx error instead?
352
+ it "should raise an exception" do
353
+ expect { subject }.
354
+ to raise_error(OmniAuth::Strategies::SAML::ValidationError, 'SAML failed to process LogoutRequest')
355
+ end
356
+ end
357
+
358
+ context "when request is a logout request but the request param is missing" do
359
+ let(:params) { {} }
360
+
361
+ # TODO: Maybe this should not raise an exception, but return a 422 error instead?
362
+ it 'should raise an exception' do
363
+ expect { subject }.
364
+ to raise_error(OmniAuth::Strategies::SAML::ValidationError, 'SAML logout response/request missing')
365
+ end
366
+ end
367
+ end
368
+
369
+ context "when sp initiated SLO" do
370
+ def test_default_relay_state(static_default_relay_state = nil, &block_default_relay_state)
371
+ saml_options["slo_default_relay_state"] = static_default_relay_state || block_default_relay_state
372
+ post "/auth/saml/spslo"
373
+
374
+ expect(last_response).to be_redirect
375
+ expect(last_response.location).to match /https:\/\/idp.sso.example.com\/signoff\/29490/
376
+ expect(last_response.location).to match /RelayState=https%3A%2F%2Fexample.com%2F/
377
+ end
378
+
379
+ it "should redirect to logout request" do
380
+ test_default_relay_state("https://example.com/")
381
+ end
382
+
383
+ it "should redirect to logout request with a block" do
384
+ test_default_relay_state do
385
+ "https://example.com/"
386
+ end
387
+ end
388
+
389
+ it "should redirect to logout request with a block with a request parameter" do
390
+ test_default_relay_state do |request|
391
+ "https://example.com/"
392
+ end
393
+ end
394
+
395
+ it "should give not implemented without an idp_slo_service_url" do
396
+ saml_options.delete(:idp_slo_service_url)
397
+ post "/auth/saml/spslo"
398
+
399
+ expect(last_response.status).to eq 501
400
+ expect(last_response.body).to match /Not Implemented/
401
+ end
402
+ end
403
+ end
404
+
405
+ describe 'POST /auth/saml/metadata' do
406
+ before do
407
+ saml_options[:sp_entity_id] = 'http://example.com/SAML'
408
+ post '/auth/saml/metadata'
409
+ end
410
+
411
+ it 'should get SP metadata page' do
412
+ expect(last_response.status).to eq 200
413
+ expect(last_response.header["Content-Type"]).to eq "application/xml"
414
+ end
415
+
416
+ it 'should configure attributes consuming service' do
417
+ expect(last_response.body).to match /AttributeConsumingService/
418
+ expect(last_response.body).to match /first_name/
419
+ expect(last_response.body).to match /last_name/
420
+ expect(last_response.body).to match /Required attributes/
421
+ expect(last_response.body).to match /entityID/
422
+ expect(last_response.body).to match /http:\/\/example.com\/SAML/
423
+ end
424
+ end
425
+
426
+ context 'when hitting an unknown route in our sub path' do
427
+ before { post '/auth/saml/unknown' }
428
+
429
+ specify { expect(last_response.status).to eql 404 }
430
+ end
431
+
432
+ context 'when hitting a completely unknown route' do
433
+ before { post '/unknown' }
434
+
435
+ specify { expect(last_response.status).to eql 404 }
436
+ end
437
+
438
+ context 'when hitting a route that contains a substring match for the strategy name' do
439
+ before { post '/auth/saml2/metadata' }
440
+
441
+ it 'should not set the strategy' do
442
+ expect(last_request.env['omniauth.strategy']).to be_nil
443
+ expect(last_response.status).to eql 404
444
+ end
445
+ end
446
+
447
+ describe 'subclass behavior' do
448
+ it 'registers subclasses in OmniAuth.strategies' do
449
+ subclass = Class.new(described_class)
450
+ expect(OmniAuth.strategies).to include(described_class, subclass)
126
451
  end
127
452
  end
128
453
  end
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,10 @@
1
1
  require 'simplecov'
2
+
3
+ if ENV['TRAVIS']
4
+ require 'coveralls'
5
+ Coveralls.wear!
6
+ end
7
+
2
8
  SimpleCov.start
3
9
 
4
10
  require 'omniauth-saml'
@@ -7,6 +13,11 @@ require 'rexml/document'
7
13
  require 'rexml/xpath'
8
14
  require 'base64'
9
15
 
16
+ TEST_LOGGER = Logger.new(StringIO.new)
17
+ OneLogin::RubySaml::Logging.logger = TEST_LOGGER
18
+ OmniAuth.config.logger = TEST_LOGGER
19
+ OmniAuth.config.request_validation_phase = proc {}
20
+
10
21
  RSpec.configure do |config|
11
22
  config.include Rack::Test::Methods
12
23
  end