devise_saml_authenticatable 1.5.0 → 1.6.3

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.
@@ -8,7 +8,7 @@ module Devise
8
8
  if params[:SAMLResponse]
9
9
  OneLogin::RubySaml::Response.new(
10
10
  params[:SAMLResponse],
11
- settings: Devise.saml_config,
11
+ settings: saml_config(get_idp_entity_id(params)),
12
12
  allowed_clock_drift: Devise.allowed_clock_drift_in_seconds,
13
13
  )
14
14
  else
@@ -1,3 +1,3 @@
1
1
  module DeviseSamlAuthenticatable
2
- VERSION = "1.5.0"
2
+ VERSION = "1.6.3"
3
3
  end
@@ -1,31 +1,38 @@
1
1
  require 'rails_helper'
2
2
 
3
- class Devise::SessionsController < ActionController::Base
4
- # The important parts from devise
3
+ # The important parts from devise
4
+ class DeviseController < ApplicationController
5
+ attr_accessor :current_user
6
+
5
7
  def resource_class
6
8
  User
7
9
  end
8
10
 
11
+ def require_no_authentication
12
+ end
13
+ end
14
+ class Devise::SessionsController < DeviseController
9
15
  def destroy
10
16
  sign_out
11
17
  redirect_to after_sign_out_path_for(:user)
12
18
  end
13
19
 
14
- def require_no_authentication
20
+ def verify_signed_out_user
21
+ # no-op for these tests
15
22
  end
16
23
  end
17
24
 
18
25
  require_relative '../../../app/controllers/devise/saml_sessions_controller'
19
26
 
20
27
  describe Devise::SamlSessionsController, type: :controller do
21
- let(:saml_config) { Devise.saml_config }
22
28
  let(:idp_providers_adapter) { spy("Stub IDPSettings Adaptor") }
23
29
 
24
30
  before do
31
+ @request.env["devise.mapping"] = Devise.mappings[:user]
25
32
  allow(idp_providers_adapter).to receive(:settings).and_return({
26
33
  assertion_consumer_service_url: "acs_url",
27
34
  assertion_consumer_service_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
28
- name_identifier_format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
35
+ name_identifier_format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
29
36
  issuer: "sp_issuer",
30
37
  idp_entity_id: "http://www.example.com",
31
38
  authn_context: "",
@@ -35,6 +42,20 @@ describe Devise::SamlSessionsController, type: :controller do
35
42
  })
36
43
  end
37
44
 
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
56
+ end
57
+ end
58
+
38
59
  describe '#new' do
39
60
  let(:saml_response) { File.read(File.join(File.dirname(__FILE__), '../../support', 'response_encrypted_nameid.xml.base64')) }
40
61
 
@@ -113,6 +134,8 @@ describe Devise::SamlSessionsController, type: :controller do
113
134
  end
114
135
 
115
136
  describe '#metadata' do
137
+ let(:saml_config) { Devise.saml_config.dup }
138
+
116
139
  context "with the default configuration" do
117
140
  it 'generates metadata' do
118
141
  get :metadata
@@ -132,7 +155,7 @@ describe Devise::SamlSessionsController, type: :controller do
132
155
  Devise.saml_configure do |settings|
133
156
  settings.assertion_consumer_service_url = "http://localhost:3000/users/saml/auth"
134
157
  settings.assertion_consumer_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
135
- settings.name_identifier_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
158
+ settings.name_identifier_format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
136
159
  settings.issuer = "http://localhost:3000"
137
160
  end
138
161
  end
@@ -149,23 +172,47 @@ describe Devise::SamlSessionsController, type: :controller do
149
172
  end
150
173
 
151
174
  describe '#destroy' do
175
+ before do
176
+ allow(controller).to receive(:sign_out)
177
+ end
178
+
152
179
  context "when using the default saml config" do
153
- it 'signs out and redirects to the IdP' do
154
- expect(controller).to receive(:sign_out)
180
+ it "signs out and redirects to the IdP" do
155
181
  delete :destroy
182
+ expect(controller).to have_received(:sign_out)
156
183
  expect(response).to redirect_to(%r(\Ahttp://localhost:8009/saml/logout\?SAMLRequest=))
157
184
  end
158
185
  end
159
186
 
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")
190
+ end
191
+
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")
194
+
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"
199
+ end
200
+
201
+ delete :destroy
202
+ expect(actual_settings.name_identifier_value).to eq("user@example.com")
203
+ expect(actual_settings.sessionindex).to eq("sessionindex")
204
+ end
205
+ end
206
+
160
207
  context "with a specified idp" do
161
208
  before do
162
209
  Devise.idp_settings_adapter = idp_providers_adapter
163
210
  end
164
211
 
165
212
  it "redirects to the associated IdP SSO target url" do
166
- expect(controller).to receive(:sign_out)
167
213
  expect(DeviseSamlAuthenticatable::DefaultIdpEntityIdReader).to receive(:entity_id)
168
214
  delete :destroy
215
+ expect(controller).to have_received(:sign_out)
169
216
  expect(response).to redirect_to(%r(\Ahttp://idp_slo_url\?SAMLRequest=))
170
217
  end
171
218
 
@@ -194,8 +241,8 @@ describe Devise::SamlSessionsController, type: :controller do
194
241
  end
195
242
 
196
243
  it "redirects to the associated IdP SLO target url" do
197
- expect(controller).to receive(:sign_out)
198
244
  do_delete
245
+ expect(controller).to have_received(:sign_out)
199
246
  expect(idp_providers_adapter).to have_received(:settings).with("http://www.example.com")
200
247
  expect(response).to redirect_to(%r(\Ahttp://idp_slo_url\?SAMLRequest=))
201
248
  end
@@ -263,12 +310,13 @@ describe Devise::SamlSessionsController, type: :controller do
263
310
  let(:name_id) { '12312312' }
264
311
  before do
265
312
  allow(OneLogin::RubySaml::SloLogoutrequest).to receive(:new).and_return(saml_request)
313
+ allow(User).to receive(:reset_session_key_for)
266
314
  end
267
315
 
268
316
  it 'direct the resource to reset the session key' do
269
- expect(User).to receive(:reset_session_key_for).with(name_id)
270
317
  do_post
271
318
  expect(response).to redirect_to response_url
319
+ expect(User).to have_received(:reset_session_key_for).with(name_id)
272
320
  end
273
321
 
274
322
  context "with a specified idp" do
@@ -285,6 +333,16 @@ describe Devise::SamlSessionsController, type: :controller do
285
333
  end
286
334
  end
287
335
 
336
+ context "with a relay_state lambda defined" do
337
+ let(:relay_state) { ->(request) { "123" } }
338
+
339
+ it "includes the RelayState param in the request to the IdP" do
340
+ expect(Devise).to receive(:saml_relay_state).at_least(:once).and_return(relay_state)
341
+ do_post
342
+ expect(saml_response).to have_received(:create).with(Devise.saml_config, saml_request.id, nil, {RelayState: "123"})
343
+ end
344
+ end
345
+
288
346
  context 'when saml_session_index_key is not configured' do
289
347
  before do
290
348
  Devise.saml_session_index_key = nil
@@ -0,0 +1,58 @@
1
+ require "spec_helper"
2
+ require "devise_saml_authenticatable/default_attribute_map_resolver"
3
+
4
+ describe DeviseSamlAuthenticatable::DefaultAttributeMapResolver do
5
+ let!(:rails) { class_double("Rails", env: "test", logger: logger, root: rails_root).as_stubbed_const }
6
+ let(:logger) { instance_double("Logger", info: nil) }
7
+ let(:rails_root) { Pathname.new("tmp") }
8
+
9
+ let(:saml_response) { instance_double("OneLogin::RubySaml::Response") }
10
+ let(:file_contents) {
11
+ <<YAML
12
+ ---
13
+ firstname: first_name
14
+ lastname: last_name
15
+ YAML
16
+ }
17
+ before do
18
+ allow(File).to receive(:exist?).and_return(true)
19
+ allow(File).to receive(:read).and_return(file_contents)
20
+ end
21
+
22
+ describe "#attribute_map" do
23
+ it "reads the attribute map from the config file" do
24
+ expect(described_class.new(saml_response).attribute_map).to eq(
25
+ "firstname" => "first_name",
26
+ "lastname" => "last_name",
27
+ )
28
+ expect(File).to have_received(:read).with(Pathname.new("tmp").join("config", "attribute-map.yml"))
29
+ end
30
+
31
+ context "when the attribute map is broken down by environment" do
32
+ let(:file_contents) {
33
+ <<YAML
34
+ ---
35
+ test:
36
+ first: first_name
37
+ last: last_name
38
+ YAML
39
+ }
40
+ it "reads the attribute map from the environment key" do
41
+ expect(described_class.new(saml_response).attribute_map).to eq(
42
+ "first" => "first_name",
43
+ "last" => "last_name",
44
+ )
45
+ end
46
+ end
47
+
48
+ context "when the config file does not exist" do
49
+ before do
50
+ allow(File).to receive(:exist?).and_return(false)
51
+ end
52
+
53
+ it "is an empty hash" do
54
+ expect(described_class.new(saml_response).attribute_map).to eq({})
55
+ end
56
+ end
57
+ end
58
+ end
@@ -32,6 +32,7 @@ describe Devise::Models::SamlAuthenticatable do
32
32
  end
33
33
 
34
34
  before do
35
+ allow(Devise).to receive(:saml_attribute_map_resolver).and_return(attribute_map_resolver)
35
36
  allow(Devise).to receive(:saml_default_user_key).and_return(:email)
36
37
  allow(Devise).to receive(:saml_create_user).and_return(false)
37
38
  allow(Devise).to receive(:saml_use_subject).and_return(false)
@@ -39,15 +40,19 @@ describe Devise::Models::SamlAuthenticatable do
39
40
 
40
41
  before do
41
42
  allow(Rails).to receive(:root).and_return("/railsroot")
42
- allow(File).to receive(:read).with("/railsroot/config/attribute-map.yml").and_return(attributemap)
43
43
  end
44
44
 
45
- let(:attributemap) {<<-ATTRIBUTEMAP
46
- ---
47
- "saml-email-format": email
48
- "saml-name-format": name
49
- ATTRIBUTEMAP
45
+ let(:attribute_map_resolver) {
46
+ Class.new(::DeviseSamlAuthenticatable::DefaultAttributeMapResolver) do
47
+ def attribute_map
48
+ {
49
+ "saml-email-format" => "email",
50
+ "saml-name-format" => "name",
51
+ }
52
+ end
53
+ end
50
54
  }
55
+ let(:attributemap) { attribute_map_resolver.new(nil).attribute_map }
51
56
  let(:response) { double(:response, attributes: attributes, name_id: name_id) }
52
57
  let(:attributes) {
53
58
  OneLogin::RubySaml::Attributes.new(
@@ -217,12 +222,12 @@ describe Devise::Models::SamlAuthenticatable do
217
222
 
218
223
  context "when configured with a resource validator hook" do
219
224
  let(:validator_hook) { double("validator_hook") }
220
- let(:decorated_response) { ::SamlAuthenticatable::SamlResponse.new(response, YAML.load(attributemap)) }
225
+ let(:decorated_response) { ::SamlAuthenticatable::SamlResponse.new(response, attributemap) }
221
226
  let(:user) { Model.new(new_record: false) }
222
227
 
223
228
  before do
224
229
  allow(Devise).to receive(:saml_resource_validator_hook).and_return(validator_hook)
225
- allow(::SamlAuthenticatable::SamlResponse).to receive(:new).with(response, YAML.load(attributemap)).and_return(decorated_response)
230
+ allow(::SamlAuthenticatable::SamlResponse).to receive(:new).with(response, attributemap).and_return(decorated_response)
226
231
  end
227
232
 
228
233
  context "and sent a valid value" do
@@ -367,4 +372,10 @@ describe Devise::Models::SamlAuthenticatable do
367
372
  allow(Devise).to receive(:saml_resource_locator).and_return(block)
368
373
  end
369
374
  end
375
+
376
+ describe "::attribute_map" do
377
+ it "returns the attribute map" do
378
+ expect(Model.attribute_map).to eq(attributemap)
379
+ end
380
+ end
370
381
  end
@@ -57,7 +57,7 @@ describe "SAML Authentication", type: :feature do
57
57
  expect(current_url).to eq("http://localhost:8020/")
58
58
 
59
59
  click_on "Log out"
60
- #confirm the logout response redirected to the SP which in turn attempted to sign th e
60
+ # confirm the logout response redirected to the SP which in turn attempted to sign the user back in
61
61
  expect(current_url).to match(%r(\Ahttp://localhost:8009/saml/auth\?SAMLRequest=))
62
62
 
63
63
  # prove user is now signed out
@@ -85,8 +85,8 @@ describe "SAML Authentication", type: :feature do
85
85
  @sp_pid = start_app('sp', sp_port)
86
86
  end
87
87
  after(:each) do
88
- stop_app(@idp_pid)
89
- stop_app(@sp_pid)
88
+ stop_app("idp", @idp_pid)
89
+ stop_app("sp", @sp_pid)
90
90
  end
91
91
 
92
92
  it_behaves_like "it authenticates and creates users"
@@ -100,8 +100,8 @@ describe "SAML Authentication", type: :feature do
100
100
  @sp_pid = start_app('sp', sp_port)
101
101
  end
102
102
  after(:each) do
103
- stop_app(@idp_pid)
104
- stop_app(@sp_pid)
103
+ stop_app("idp", @idp_pid)
104
+ stop_app("sp", @sp_pid)
105
105
  end
106
106
 
107
107
  it_behaves_like "it authenticates and creates users"
@@ -115,8 +115,8 @@ describe "SAML Authentication", type: :feature do
115
115
  @sp_pid = start_app('sp', sp_port)
116
116
  end
117
117
  after(:each) do
118
- stop_app(@idp_pid)
119
- stop_app(@sp_pid)
118
+ stop_app("idp", @idp_pid)
119
+ stop_app("sp", @sp_pid)
120
120
  end
121
121
 
122
122
  it_behaves_like "it authenticates and creates users"
@@ -131,8 +131,8 @@ describe "SAML Authentication", type: :feature do
131
131
  @sp_pid = start_app('sp', sp_port)
132
132
  end
133
133
  after(:each) do
134
- stop_app(@idp_pid)
135
- stop_app(@sp_pid)
134
+ stop_app("idp", @idp_pid)
135
+ stop_app("sp", @sp_pid)
136
136
  end
137
137
 
138
138
  it_behaves_like "it authenticates and creates users"
@@ -141,39 +141,23 @@ describe "SAML Authentication", type: :feature do
141
141
  context "when the idp_settings_adapter key is set" do
142
142
  before(:each) do
143
143
  create_app('idp', 'INCLUDE_SUBJECT_IN_ATTRIBUTES' => "false")
144
- create_app('sp', 'USE_SUBJECT_TO_AUTHENTICATE' => "true", 'IDP_SETTINGS_ADAPTER' => "IdpSettingsAdapter", 'IDP_ENTITY_ID_READER' => "OurEntityIdReader")
144
+ create_app('sp', 'USE_SUBJECT_TO_AUTHENTICATE' => "true", 'IDP_SETTINGS_ADAPTER' => '"IdpSettingsAdapter"', 'IDP_ENTITY_ID_READER' => '"OurEntityIdReader"')
145
145
 
146
- @idp_pid = start_app('idp', idp_port)
146
+ # use a different port for this entity ID; configured in spec/support/idp_settings_adapter.rb.erb
147
+ @idp_pid = start_app('idp', 8010)
147
148
  @sp_pid = start_app('sp', sp_port)
148
149
  end
149
150
 
150
151
  after(:each) do
151
- stop_app(@idp_pid)
152
- stop_app(@sp_pid)
152
+ stop_app("idp", @idp_pid)
153
+ stop_app("sp", @sp_pid)
153
154
  end
154
155
 
155
156
  it "authenticates an existing user on a SP via an IdP" do
156
157
  create_user("you@example.com")
157
158
 
158
159
  visit 'http://localhost:8020/users/saml/sign_in/?entity_id=http%3A%2F%2Flocalhost%3A8020%2Fsaml%2Fmetadata'
159
- expect(current_url).to match(%r(\Ahttp://www.example.com/sso\?SAMLRequest=))
160
- end
161
-
162
- it "logs a user out of the IdP via the SP" do
163
- sign_in
164
-
165
- # prove user is still signed in
166
- visit 'http://localhost:8020/'
167
- expect(page).to have_content("you@example.com")
168
- expect(current_url).to eq("http://localhost:8020/")
169
-
170
- click_on "Log out"
171
- #confirm the logout response redirected to the SP which in turn attempted to sign th e
172
- expect(current_url).to match(%r(\Ahttp://www.example.com/slo\?SAMLRequest=))
173
-
174
- # prove user is now signed out
175
- visit 'http://localhost:8020/users/saml/sign_in/?entity_id=http%3A%2F%2Flocalhost%3A8020%2Fsaml%2Fmetadata'
176
- expect(current_url).to match(%r(\Ahttp://www.example.com/sso\?SAMLRequest=))
160
+ expect(current_url).to match(%r(\Ahttp://localhost:8010/saml/auth\?SAMLRequest=))
177
161
  end
178
162
  end
179
163
 
@@ -188,8 +172,8 @@ describe "SAML Authentication", type: :feature do
188
172
  end
189
173
 
190
174
  after(:each) do
191
- stop_app(@idp_pid)
192
- stop_app(@sp_pid)
175
+ stop_app("idp", @idp_pid)
176
+ stop_app("sp", @sp_pid)
193
177
  end
194
178
 
195
179
  it_behaves_like "it authenticates and creates users"
@@ -204,20 +188,43 @@ describe "SAML Authentication", type: :feature do
204
188
  fill_in "Email", with: "you@example.com"
205
189
  fill_in "Password", with: "asdf"
206
190
  click_on "Sign in"
207
- expect(page).to have_content(:all, "Example Domain This domain is established to be used for illustrative examples in documents. You may use this domain in examples without prior coordination or asking for permission.")
191
+ expect(page).to have_content(:all, "Example Domain This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission.")
208
192
  expect(current_url).to eq("http://www.example.com/")
209
193
  end
210
194
  end
211
195
  end
212
196
 
197
+ context "when the saml_attribute_map is set" do
198
+ before(:each) do
199
+ create_app(
200
+ "idp",
201
+ "EMAIL_ADDRESS_ATTRIBUTE_KEY" => "myemailaddress",
202
+ "NAME_ATTRIBUTE_KEY" => "myname",
203
+ "INCLUDE_SUBJECT_IN_ATTRIBUTES" => "false",
204
+ )
205
+ create_app(
206
+ "sp",
207
+ "ATTRIBUTE_MAP_RESOLVER" => '"AttributeMapResolver"',
208
+ "USE_SUBJECT_TO_AUTHENTICATE" => "true",
209
+ )
210
+ @idp_pid = start_app("idp", idp_port)
211
+ @sp_pid = start_app("sp", sp_port)
212
+ end
213
+ after(:each) do
214
+ stop_app("idp", @idp_pid)
215
+ stop_app("sp", @sp_pid)
216
+ end
217
+
218
+ it_behaves_like "it authenticates and creates users"
219
+ end
220
+
213
221
  def create_user(email)
214
222
  response = Net::HTTP.post_form(URI('http://localhost:8020/users'), email: email)
215
223
  expect(response.code).to eq('201')
216
224
  end
217
225
 
218
- def sign_in
219
- visit 'http://localhost:8020/'
220
- expect(current_url).to match(%r(\Ahttp://localhost:8009/saml/auth\?SAMLRequest=))
226
+ def sign_in(entity_id: "")
227
+ visit "http://localhost:8020/users/saml/sign_in/?entity_id=#{URI.escape(entity_id)}"
221
228
  fill_in "Email", with: "you@example.com"
222
229
  fill_in "Password", with: "asdf"
223
230
  click_on "Sign in"