devise_saml_authenticatable 1.6.1 → 1.8.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.
@@ -1,4 +1,5 @@
1
1
  require 'rails_helper'
2
+ require 'support/ruby_saml_support'
2
3
 
3
4
  # The important parts from devise
4
5
  class DeviseController < ApplicationController
@@ -8,112 +9,122 @@ class DeviseController < ApplicationController
8
9
  User
9
10
  end
10
11
 
11
- def require_no_authentication
12
+ def resource_name
13
+ 'users'
14
+ end
15
+
16
+ def require_no_authentication; end
17
+
18
+ def set_flash_message!(key, kind, _options = {})
19
+ flash[key] = I18n.t("devise.sessions.#{kind}")
12
20
  end
13
21
  end
22
+
14
23
  class Devise::SessionsController < DeviseController
15
24
  def destroy
16
25
  sign_out
17
- redirect_to after_sign_out_path_for(:user)
18
- end
19
-
20
- def verify_signed_out_user
21
- # no-op for these tests
26
+ redirect_to after_sign_out_path_for(:user), allow_other_host: true
22
27
  end
23
28
  end
24
29
 
25
30
  require_relative '../../../app/controllers/devise/saml_sessions_controller'
26
31
 
27
32
  describe Devise::SamlSessionsController, type: :controller do
28
- let(:idp_providers_adapter) { spy("Stub IDPSettings Adaptor") }
33
+ include RubySamlSupport
34
+
35
+ let(:idp_providers_adapter) { spy('Stub IDPSettings Adaptor') }
29
36
 
30
37
  before do
31
- @request.env["devise.mapping"] = Devise.mappings[:user]
32
- allow(idp_providers_adapter).to receive(:settings).and_return({
33
- assertion_consumer_service_url: "acs_url",
34
- assertion_consumer_service_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
35
- name_identifier_format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
36
- issuer: "sp_issuer",
37
- idp_entity_id: "http://www.example.com",
38
- authn_context: "",
39
- idp_slo_target_url: "http://idp_slo_url",
40
- idp_sso_target_url: "http://idp_sso_url",
41
- idp_cert: "idp_cert"
38
+ @request.env['devise.mapping'] = Devise.mappings[:user]
39
+ settings = {
40
+ assertion_consumer_service_url: 'acs_url',
41
+ assertion_consumer_service_binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
42
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient',
43
+ issuer: 'sp_issuer',
44
+ idp_entity_id: 'http://www.example.com',
45
+ authn_context: '',
46
+ idp_cert: 'idp_cert'
47
+ }
48
+ with_ruby_saml_1_12_or_greater(proc {
49
+ settings.merge!(
50
+ idp_slo_service_url: 'http://idp_slo_url',
51
+ idp_sso_service_url: 'http://idp_sso_url'
52
+ )
53
+ }, else_do: proc {
54
+ settings.merge!(
55
+ idp_slo_target_url: 'http://idp_slo_url',
56
+ idp_sso_target_url: 'http://idp_sso_url'
57
+ )
42
58
  })
59
+ allow(idp_providers_adapter).to receive(:settings).and_return(settings)
43
60
  end
44
61
 
45
- before do
46
- if Rails::VERSION::MAJOR < 5 && Gem::Version.new(RUBY_VERSION) > Gem::Version.new("2.6")
47
- # we still want to support Rails 4
48
- # patch tests using snippet from https://github.com/rails/rails/issues/34790#issuecomment-483607370
49
- class ActionController::TestResponse < ActionDispatch::TestResponse
50
- def recycle!
51
- @mon_mutex_owner_object_id = nil
52
- @mon_mutex = nil
53
- initialize
54
- end
55
- end
62
+ describe '#new' do
63
+ let(:saml_response) do
64
+ File.read(File.join(File.dirname(__FILE__), '../../support', 'response_encrypted_nameid.xml.base64'))
56
65
  end
57
- end
58
66
 
59
- describe '#new' do
60
- let(:saml_response) { File.read(File.join(File.dirname(__FILE__), '../../support', 'response_encrypted_nameid.xml.base64')) }
67
+ subject(:do_get) do
68
+ get :new, params: { 'SAMLResponse' => saml_response }
69
+ end
61
70
 
62
- subject(:do_get) {
63
- if Rails::VERSION::MAJOR > 4
64
- get :new, params: {"SAMLResponse" => saml_response}
65
- else
66
- get :new, "SAMLResponse" => saml_response
71
+ context 'when using the default saml config' do
72
+ it 'redirects to the IdP SSO target url' do
73
+ do_get
74
+ expect(response).to redirect_to(%r{\Ahttp://localhost:8009/saml/auth\?SAMLRequest=})
67
75
  end
68
- }
69
76
 
70
- context "when using the default saml config" do
71
- it "redirects to the IdP SSO target url" do
77
+ it 'stores saml_transaction_id in the session' do
72
78
  do_get
73
- expect(response).to redirect_to(%r(\Ahttp://localhost:8009/saml/auth\?SAMLRequest=))
79
+ if OneLogin::RubySaml::Authrequest.public_instance_methods.include?(:request_id)
80
+ expect(session[:saml_transaction_id]).to be_present
81
+ end
74
82
  end
75
83
  end
76
84
 
77
- context "with a specified idp" do
85
+ context 'with a specified idp' do
78
86
  before do
79
87
  Devise.idp_settings_adapter = idp_providers_adapter
80
88
  end
81
89
 
82
- it "redirects to the associated IdP SSO target url" do
90
+ it 'redirects to the associated IdP SSO target url' do
83
91
  do_get
84
- expect(response).to redirect_to(%r(\Ahttp://idp_sso_url\?SAMLRequest=))
92
+ expect(response).to redirect_to(%r{\Ahttp://idp_sso_url\?SAMLRequest=})
85
93
  end
86
94
 
87
- it "uses the DefaultIdpEntityIdReader" do
95
+ it 'stores saml_transaction_id in the session' do
96
+ do_get
97
+ if OneLogin::RubySaml::Authrequest.public_instance_methods.include?(:request_id)
98
+ expect(session[:saml_transaction_id]).to be_present
99
+ end
100
+ end
101
+
102
+ it 'uses the DefaultIdpEntityIdReader' do
88
103
  expect(DeviseSamlAuthenticatable::DefaultIdpEntityIdReader).to receive(:entity_id)
89
104
  do_get
90
105
  expect(idp_providers_adapter).to have_received(:settings).with(nil)
91
106
  end
92
107
 
93
- context "with a relay_state lambda defined" do
94
- let(:relay_state) { ->(request) { "123" } }
108
+ context 'with a relay_state lambda defined' do
109
+ let(:relay_state) { ->(_request) { '123' } }
95
110
 
96
- it "includes the RelayState param in the request to the IdP" do
111
+ it 'includes the RelayState param in the request to the IdP' do
97
112
  expect(Devise).to receive(:saml_relay_state).at_least(:once).and_return(relay_state)
98
113
  do_get
99
- expect(response).to redirect_to(%r(\Ahttp://idp_sso_url\?SAMLRequest=.*&RelayState=123))
114
+ expect(response).to redirect_to(%r{\Ahttp://idp_sso_url\?SAMLRequest=.*&RelayState=123})
100
115
  end
101
116
  end
102
117
 
103
- context "with a specified idp entity id reader" do
118
+ context 'with a specified idp entity id reader' do
104
119
  class OurIdpEntityIdReader
105
120
  def self.entity_id(params)
106
121
  params[:entity_id]
107
122
  end
108
123
  end
109
124
 
110
- subject(:do_get) {
111
- if Rails::VERSION::MAJOR > 4
112
- get :new, params: {entity_id: "http://www.example.com"}
113
- else
114
- get :new, entity_id: "http://www.example.com"
115
- end
116
- }
125
+ subject(:do_get) do
126
+ get :new, params: { entity_id: 'http://www.example.com' }
127
+ end
117
128
 
118
129
  before do
119
130
  @default_reader = Devise.idp_entity_id_reader
@@ -124,10 +135,10 @@ describe Devise::SamlSessionsController, type: :controller do
124
135
  Devise.idp_entity_id_reader = @default_reader
125
136
  end
126
137
 
127
- it "redirects to the associated IdP SSO target url" do
138
+ it 'redirects to the associated IdP SSO target url' do
128
139
  do_get
129
- expect(idp_providers_adapter).to have_received(:settings).with("http://www.example.com")
130
- expect(response).to redirect_to(%r(\Ahttp://idp_sso_url\?SAMLRequest=))
140
+ expect(idp_providers_adapter).to have_received(:settings).with('http://www.example.com')
141
+ expect(response).to redirect_to(%r{\Ahttp://idp_sso_url\?SAMLRequest=})
131
142
  end
132
143
  end
133
144
  end
@@ -136,7 +147,7 @@ describe Devise::SamlSessionsController, type: :controller do
136
147
  describe '#metadata' do
137
148
  let(:saml_config) { Devise.saml_config.dup }
138
149
 
139
- context "with the default configuration" do
150
+ context 'with the default configuration' do
140
151
  it 'generates metadata' do
141
152
  get :metadata
142
153
 
@@ -147,20 +158,20 @@ describe Devise::SamlSessionsController, type: :controller do
147
158
  end
148
159
  end
149
160
 
150
- context "with a specified IDP" do
151
- let(:saml_config) { controller.saml_config("anything") }
161
+ context 'with a specified IDP' do
162
+ let(:saml_config) { controller.saml_config('anything') }
152
163
 
153
164
  before do
154
165
  Devise.idp_settings_adapter = idp_providers_adapter
155
166
  Devise.saml_configure do |settings|
156
- settings.assertion_consumer_service_url = "http://localhost:3000/users/saml/auth"
157
- settings.assertion_consumer_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
158
- settings.name_identifier_format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
159
- settings.issuer = "http://localhost:3000"
167
+ settings.assertion_consumer_service_url = 'http://localhost:3000/users/saml/auth'
168
+ settings.assertion_consumer_service_binding = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
169
+ settings.name_identifier_format = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
170
+ settings.issuer = 'http://localhost:3000'
160
171
  end
161
172
  end
162
173
 
163
- it "generates the same service metadata" do
174
+ it 'generates the same service metadata' do
164
175
  get :metadata
165
176
 
166
177
  # Remove ID that can vary across requests
@@ -172,79 +183,131 @@ describe Devise::SamlSessionsController, type: :controller do
172
183
  end
173
184
 
174
185
  describe '#destroy' do
175
- before do
176
- allow(controller).to receive(:sign_out)
177
- end
186
+ subject { delete :destroy }
178
187
 
179
- context "when using the default saml config" do
180
- it "signs out and redirects to the IdP" do
181
- delete :destroy
182
- expect(controller).to have_received(:sign_out)
183
- expect(response).to redirect_to(%r(\Ahttp://localhost:8009/saml/logout\?SAMLRequest=))
188
+ context 'when user is signed out' do
189
+ before do
190
+ class Devise::SessionsController < DeviseController
191
+ def all_signed_out?
192
+ true
193
+ end
194
+ end
184
195
  end
185
- end
186
196
 
187
- context "when configured to use a non-transient name identifier" do
188
- before do
189
- allow(Devise.saml_config).to receive(:name_identifier_format).and_return("urn:oasis:names:tc:SAML:2.0:nameid-format:persistent")
197
+ shared_examples 'not create SP initiated logout request' do
198
+ it do
199
+ expect(OneLogin::RubySaml::Logoutrequest).not_to receive(:new)
200
+ subject
201
+ end
202
+ end
203
+
204
+ context 'when Devise.saml_sign_out_success_url is set' do
205
+ before do
206
+ allow(Devise).to receive(:saml_sign_out_success_url).and_return('http://localhost:8009/logged_out')
207
+ end
208
+
209
+ it 'redirect to saml_sign_out_success_url' do
210
+ is_expected.to redirect_to 'http://localhost:8009/logged_out'
211
+ expect(flash[:notice]).to eq I18n.t('devise.sessions.already_signed_out')
212
+ end
213
+
214
+ it_behaves_like 'not create SP initiated logout request'
190
215
  end
191
216
 
192
- it "includes a LogoutRequest with the name identifier and session index", :aggregate_failures do
193
- controller.current_user = Struct.new(:email, :session_index).new("user@example.com", "sessionindex")
217
+ context 'when Devise.saml_sign_out_success_url is not set' do
218
+ before do
219
+ class Devise::SessionsController < DeviseController
220
+ def after_sign_out_path_for(_)
221
+ 'http://localhost:8009/logged_out'
222
+ end
223
+ end
224
+ end
194
225
 
195
- actual_settings = nil
196
- expect_any_instance_of(OneLogin::RubySaml::Logoutrequest).to receive(:create) do |_, settings|
197
- actual_settings = settings
198
- "http://localhost:8009/saml/logout"
226
+ it "redirect to devise's after sign out path" do
227
+ is_expected.to redirect_to 'http://localhost:8009/logged_out'
228
+ expect(flash[:notice]).to eq I18n.t('devise.sessions.already_signed_out')
199
229
  end
200
230
 
201
- delete :destroy
202
- expect(actual_settings.name_identifier_value).to eq("user@example.com")
203
- expect(actual_settings.sessionindex).to eq("sessionindex")
231
+ it_behaves_like 'not create SP initiated logout request'
204
232
  end
205
233
  end
206
234
 
207
- context "with a specified idp" do
235
+ context 'when user is not signed out' do
208
236
  before do
209
- Devise.idp_settings_adapter = idp_providers_adapter
237
+ class Devise::SessionsController < DeviseController
238
+ def all_signed_out?
239
+ false
240
+ end
241
+ end
242
+ allow(controller).to receive(:sign_out)
210
243
  end
211
244
 
212
- it "redirects to the associated IdP SSO target url" do
213
- expect(DeviseSamlAuthenticatable::DefaultIdpEntityIdReader).to receive(:entity_id)
214
- delete :destroy
215
- expect(controller).to have_received(:sign_out)
216
- expect(response).to redirect_to(%r(\Ahttp://idp_slo_url\?SAMLRequest=))
245
+ context 'when using the default saml config' do
246
+ it 'signs out and redirects to the IdP' do
247
+ delete :destroy
248
+ expect(controller).to have_received(:sign_out)
249
+ expect(response).to redirect_to(%r{\Ahttp://localhost:8009/saml/logout\?SAMLRequest=})
250
+ end
217
251
  end
218
252
 
219
- context "with a specified idp entity id reader" do
220
- class OurIdpEntityIdReader
221
- def self.entity_id(params)
222
- params[:entity_id]
223
- end
253
+ context 'when configured to use a non-transient name identifier' do
254
+ before do
255
+ allow(Devise.saml_config).to receive(:name_identifier_format).and_return('urn:oasis:names:tc:SAML:2.0:nameid-format:persistent')
224
256
  end
225
257
 
226
- subject(:do_delete) {
227
- if Rails::VERSION::MAJOR > 4
228
- delete :destroy, params: {entity_id: "http://www.example.com"}
229
- else
230
- delete :destroy, entity_id: "http://www.example.com"
258
+ it 'includes a LogoutRequest with the name identifier and session index', :aggregate_failures do
259
+ controller.current_user = Struct.new(:email, :session_index).new('user@example.com', 'sessionindex')
260
+
261
+ actual_settings = nil
262
+ expect_any_instance_of(OneLogin::RubySaml::Logoutrequest).to receive(:create) do |_, settings|
263
+ actual_settings = settings
264
+ 'http://localhost:8009/saml/logout'
231
265
  end
232
- }
233
266
 
234
- before do
235
- @default_reader = Devise.idp_entity_id_reader
236
- Devise.idp_entity_id_reader = OurIdpEntityIdReader # which will have some different behavior
267
+ delete :destroy
268
+ expect(actual_settings.name_identifier_value).to eq('user@example.com')
269
+ expect(actual_settings.sessionindex).to eq('sessionindex')
237
270
  end
271
+ end
238
272
 
239
- after do
240
- Devise.idp_entity_id_reader = @default_reader
273
+ context 'with a specified idp' do
274
+ before do
275
+ Devise.idp_settings_adapter = idp_providers_adapter
241
276
  end
242
277
 
243
- it "redirects to the associated IdP SLO target url" do
244
- do_delete
278
+ it 'redirects to the associated IdP SSO target url' do
279
+ expect(DeviseSamlAuthenticatable::DefaultIdpEntityIdReader).to receive(:entity_id)
280
+ delete :destroy
245
281
  expect(controller).to have_received(:sign_out)
246
- expect(idp_providers_adapter).to have_received(:settings).with("http://www.example.com")
247
- expect(response).to redirect_to(%r(\Ahttp://idp_slo_url\?SAMLRequest=))
282
+ expect(response).to redirect_to(%r{\Ahttp://idp_slo_url\?SAMLRequest=})
283
+ end
284
+
285
+ context 'with a specified idp entity id reader' do
286
+ class OurIdpEntityIdReader
287
+ def self.entity_id(params)
288
+ params[:entity_id]
289
+ end
290
+ end
291
+
292
+ subject(:do_delete) do
293
+ delete :destroy, params: { entity_id: 'http://www.example.com' }
294
+ end
295
+
296
+ before do
297
+ @default_reader = Devise.idp_entity_id_reader
298
+ Devise.idp_entity_id_reader = OurIdpEntityIdReader # which will have some different behavior
299
+ end
300
+
301
+ after do
302
+ Devise.idp_entity_id_reader = @default_reader
303
+ end
304
+
305
+ it 'redirects to the associated IdP SLO target url' do
306
+ do_delete
307
+ expect(controller).to have_received(:sign_out)
308
+ expect(idp_providers_adapter).to have_received(:settings).with('http://www.example.com')
309
+ expect(response).to redirect_to(%r{\Ahttp://idp_slo_url\?SAMLRequest=})
310
+ end
248
311
  end
249
312
  end
250
313
  end
@@ -264,14 +327,10 @@ describe Devise::SamlSessionsController, type: :controller do
264
327
  expect(response.status).to eq 500
265
328
  end
266
329
 
267
- context "when receiving a logout response from the IdP after redirecting an SP logout request" do
268
- subject(:do_post) {
269
- if Rails::VERSION::MAJOR > 4
270
- post :idp_sign_out, params: {SAMLResponse: "stubbed_response"}
271
- else
272
- post :idp_sign_out, SAMLResponse: "stubbed_response"
273
- end
274
- }
330
+ context 'when receiving a logout response from the IdP after redirecting an SP logout request' do
331
+ subject(:do_post) do
332
+ post :idp_sign_out, params: { SAMLResponse: 'stubbed_response' }
333
+ end
275
334
 
276
335
  it 'accepts a LogoutResponse and redirects sign_in' do
277
336
  do_post
@@ -293,20 +352,18 @@ describe Devise::SamlSessionsController, type: :controller do
293
352
  end
294
353
  end
295
354
 
296
- context "when receiving an IdP logout request" do
297
- subject(:do_post) {
298
- if Rails::VERSION::MAJOR > 4
299
- post :idp_sign_out, params: {SAMLRequest: "stubbed_logout_request"}
300
- else
301
- post :idp_sign_out, SAMLRequest: "stubbed_logout_request"
302
- end
303
- }
355
+ context 'when receiving an IdP logout request' do
356
+ subject(:do_post) do
357
+ post :idp_sign_out, params: { SAMLRequest: 'stubbed_logout_request' }
358
+ end
304
359
 
305
- let(:saml_request) { double(:slo_logoutrequest, {
306
- id: 42,
307
- name_id: name_id,
308
- issuer: "http://www.example.com"
309
- }) }
360
+ let(:saml_request) do
361
+ double(:slo_logoutrequest, {
362
+ id: 42,
363
+ name_id: name_id,
364
+ issuer: 'http://www.example.com'
365
+ })
366
+ end
310
367
  let(:name_id) { '12312312' }
311
368
  before do
312
369
  allow(OneLogin::RubySaml::SloLogoutrequest).to receive(:new).and_return(saml_request)
@@ -319,27 +376,28 @@ describe Devise::SamlSessionsController, type: :controller do
319
376
  expect(User).to have_received(:reset_session_key_for).with(name_id)
320
377
  end
321
378
 
322
- context "with a specified idp" do
323
- let(:idp_entity_id) { "http://www.example.com" }
379
+ context 'with a specified idp' do
380
+ let(:idp_entity_id) { 'http://www.example.com' }
324
381
  before do
325
382
  Devise.idp_settings_adapter = idp_providers_adapter
326
383
  end
327
384
 
328
- it "accepts a LogoutResponse for the associated slo_target_url and redirects to sign_in" do
385
+ it 'accepts a LogoutResponse for the associated slo_target_url and redirects to sign_in' do
329
386
  do_post
330
387
  expect(response.status).to eq 302
331
388
  expect(idp_providers_adapter).to have_received(:settings).with(idp_entity_id)
332
- expect(response).to redirect_to "http://localhost/logout_response"
389
+ expect(response).to redirect_to 'http://localhost/logout_response'
333
390
  end
334
391
  end
335
392
 
336
- context "with a relay_state lambda defined" do
337
- let(:relay_state) { ->(request) { "123" } }
393
+ context 'with a relay_state lambda defined' do
394
+ let(:relay_state) { ->(_request) { '123' } }
338
395
 
339
- it "includes the RelayState param in the request to the IdP" do
396
+ it 'includes the RelayState param in the request to the IdP' do
340
397
  expect(Devise).to receive(:saml_relay_state).at_least(:once).and_return(relay_state)
341
398
  do_post
342
- expect(saml_response).to have_received(:create).with(Devise.saml_config, saml_request.id, nil, {RelayState: "123"})
399
+ expect(saml_response).to have_received(:create).with(Devise.saml_config, saml_request.id, nil,
400
+ { RelayState: '123' })
343
401
  end
344
402
  end
345
403
 
@@ -104,12 +104,6 @@ describe Devise::Models::SamlAuthenticatable do
104
104
  expect(model.name).to eq('A User')
105
105
  expect(model.saved).to be(true)
106
106
  end
107
-
108
- it "returns nil if it fails to create a user" do
109
- expect(Model).to receive(:where).with(email: 'user@example.com').and_return([])
110
- expect(Devise).to receive(:saml_update_resource_hook).and_raise(StandardError.new)
111
- expect(Model.authenticate_with_saml(response, nil)).to be_nil
112
- end
113
107
  end
114
108
 
115
109
  context "when configured to update a user and the user is found" do
@@ -125,13 +119,6 @@ describe Devise::Models::SamlAuthenticatable do
125
119
  expect(model.name).to eq('A User')
126
120
  expect(model.saved).to be(true)
127
121
  end
128
-
129
- it "returns nil if it fails to update a user" do
130
- user = Model.new(new_record: false)
131
- expect(Model).to receive(:where).with(email: 'user@example.com').and_return([user])
132
- expect(Devise).to receive(:saml_update_resource_hook).and_raise(StandardError.new)
133
- expect(Model.authenticate_with_saml(response, nil)).to be_nil
134
- end
135
122
  end
136
123
  end
137
124