devise_saml_authenticatable 1.3.2 → 1.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +0 -2
  3. data/.travis.yml +29 -22
  4. data/Gemfile +2 -2
  5. data/README.md +105 -32
  6. data/app/controllers/devise/saml_sessions_controller.rb +35 -7
  7. data/devise_saml_authenticatable.gemspec +2 -1
  8. data/lib/devise_saml_authenticatable.rb +27 -2
  9. data/lib/devise_saml_authenticatable/default_attribute_map_resolver.rb +26 -0
  10. data/lib/devise_saml_authenticatable/default_idp_entity_id_reader.rb +2 -0
  11. data/lib/devise_saml_authenticatable/exception.rb +1 -1
  12. data/lib/devise_saml_authenticatable/model.rb +16 -18
  13. data/lib/devise_saml_authenticatable/routes.rb +17 -6
  14. data/lib/devise_saml_authenticatable/saml_mapped_attributes.rb +15 -2
  15. data/lib/devise_saml_authenticatable/strategy.rb +1 -0
  16. data/lib/devise_saml_authenticatable/version.rb +1 -1
  17. data/spec/controllers/devise/saml_sessions_controller_spec.rb +118 -11
  18. data/spec/devise_saml_authenticatable/default_attribute_map_resolver_spec.rb +58 -0
  19. data/spec/devise_saml_authenticatable/model_spec.rb +68 -7
  20. data/spec/devise_saml_authenticatable/saml_mapped_attributes_spec.rb +50 -0
  21. data/spec/features/saml_authentication_spec.rb +45 -21
  22. data/spec/rails_helper.rb +6 -2
  23. data/spec/routes/routes_spec.rb +102 -0
  24. data/spec/spec_helper.rb +7 -0
  25. data/spec/support/Gemfile.rails4 +23 -6
  26. data/spec/support/Gemfile.rails5 +13 -2
  27. data/spec/support/Gemfile.rails5.1 +25 -0
  28. data/spec/support/Gemfile.rails5.2 +25 -0
  29. data/spec/support/attribute-map.yml +12 -0
  30. data/spec/support/attribute_map_resolver.rb.erb +14 -0
  31. data/spec/support/idp_settings_adapter.rb.erb +5 -5
  32. data/spec/support/idp_template.rb +6 -2
  33. data/spec/support/rails_app.rb +75 -17
  34. data/spec/support/saml_idp_controller.rb.erb +13 -6
  35. data/spec/support/sp_template.rb +45 -21
  36. metadata +25 -13
  37. data/spec/support/Gemfile.ruby-saml-1.3 +0 -24
@@ -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,13 +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
- ---
44
- "saml-email-format": email
45
- "saml-name-format": name
46
- ATTRIBUTEMAP
47
43
  end
48
44
 
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
54
+ }
55
+ let(:attributemap) { attribute_map_resolver.new(nil).attribute_map }
49
56
  let(:response) { double(:response, attributes: attributes, name_id: name_id) }
50
57
  let(:attributes) {
51
58
  OneLogin::RubySaml::Attributes.new(
@@ -97,6 +104,12 @@ describe Devise::Models::SamlAuthenticatable do
97
104
  expect(model.name).to eq('A User')
98
105
  expect(model.saved).to be(true)
99
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
100
113
  end
101
114
 
102
115
  context "when configured to update a user and the user is found" do
@@ -112,6 +125,13 @@ describe Devise::Models::SamlAuthenticatable do
112
125
  expect(model.name).to eq('A User')
113
126
  expect(model.saved).to be(true)
114
127
  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
115
135
  end
116
136
  end
117
137
 
@@ -179,8 +199,8 @@ describe Devise::Models::SamlAuthenticatable do
179
199
  end
180
200
  end
181
201
 
182
- context "when configured with a resource validator" do
183
- let(:validator_class) { double("validator_class") }
202
+ context "when configured with a resource validator class" do
203
+ let(:validator_class) { double("validator") }
184
204
  let(:validator) { double("validator") }
185
205
  let(:user) { Model.new(new_record: false) }
186
206
 
@@ -212,6 +232,41 @@ describe Devise::Models::SamlAuthenticatable do
212
232
  end
213
233
  end
214
234
 
235
+
236
+ context "when configured with a resource validator hook" do
237
+ let(:validator_hook) { double("validator_hook") }
238
+ let(:decorated_response) { ::SamlAuthenticatable::SamlResponse.new(response, attributemap) }
239
+ let(:user) { Model.new(new_record: false) }
240
+
241
+ before do
242
+ allow(Devise).to receive(:saml_resource_validator_hook).and_return(validator_hook)
243
+ allow(::SamlAuthenticatable::SamlResponse).to receive(:new).with(response, attributemap).and_return(decorated_response)
244
+ end
245
+
246
+ context "and sent a valid value" do
247
+ before do
248
+ expect(validator_hook).to receive(:call).with(user, decorated_response, 'user@example.com').and_return(true)
249
+ end
250
+
251
+ it "returns the user" do
252
+ expect(Model).to receive(:where).with(email: 'user@example.com').and_return([user])
253
+ expect(Model.authenticate_with_saml(response, nil)).to eq(user)
254
+ end
255
+ end
256
+
257
+ context "and sent an invalid value" do
258
+ before do
259
+ expect(validator_hook).to receive(:call).with(user, decorated_response, 'user@example.com').and_return(false)
260
+ end
261
+
262
+ it "returns nil" do
263
+ expect(Model).to receive(:where).with(email: 'user@example.com').and_return([user])
264
+ expect(Model.authenticate_with_saml(response, nil)).to be_nil
265
+ end
266
+ end
267
+ end
268
+
269
+
215
270
  context "when configured to use a custom update hook" do
216
271
  it "can replicate the default behaviour in a custom hook" do
217
272
  configure_hook do |user, saml_response|
@@ -330,4 +385,10 @@ describe Devise::Models::SamlAuthenticatable do
330
385
  allow(Devise).to receive(:saml_resource_locator).and_return(block)
331
386
  end
332
387
  end
388
+
389
+ describe "::attribute_map" do
390
+ it "returns the attribute map" do
391
+ expect(Model.attribute_map).to eq(attributemap)
392
+ end
393
+ end
333
394
  end
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+ require 'devise_saml_authenticatable/saml_mapped_attributes'
3
+
4
+ describe SamlAuthenticatable::SamlMappedAttributes do
5
+ let(:instance) { described_class.new(saml_attributes, attribute_map) }
6
+ let(:attribute_map_file) { File.join(File.dirname(__FILE__), '../support/attribute-map.yml') }
7
+ let(:attribute_map) { YAML.load(File.read(attribute_map_file)) }
8
+ let(:saml_attributes) do
9
+ {
10
+ "first_name" => ["John"],
11
+ "last_name"=>["Smith"],
12
+ "email"=>["john.smith@example.com"]
13
+ }
14
+ end
15
+
16
+ describe "#value_by_resource_key" do
17
+ RSpec.shared_examples "correctly maps the value of the resource key" do |saml_key, resource_key, expected_value|
18
+ subject(:perform) { instance.value_by_resource_key(resource_key) }
19
+
20
+ it "correctly maps the resource key, #{resource_key}, to the value of the '#{saml_key}' SAML key" do
21
+ saml_attributes[saml_key] = saml_attributes.delete(resource_key)
22
+ expect(perform).to eq(expected_value)
23
+ end
24
+ end
25
+
26
+ context "first_name" do
27
+ saml_keys = ['urn:mace:dir:attribute-def:first_name', 'first_name', 'firstName', 'firstname']
28
+
29
+ saml_keys.each do |saml_key|
30
+ include_examples 'correctly maps the value of the resource key', saml_key, 'first_name', ['John']
31
+ end
32
+ end
33
+
34
+ context 'last_name' do
35
+ saml_keys = ['urn:mace:dir:attribute-def:last_name', 'last_name', 'lastName', 'lastname']
36
+
37
+ saml_keys.each do |saml_key|
38
+ include_examples 'correctly maps the value of the resource key', saml_key, 'last_name', ['Smith']
39
+ end
40
+ end
41
+
42
+ context 'email' do
43
+ saml_keys = ['urn:mace:dir:attribute-def:email', 'email_address', 'emailAddress', 'email']
44
+
45
+ saml_keys.each do |saml_key|
46
+ include_examples 'correctly maps the value of the resource key', saml_key, 'email', ['john.smith@example.com']
47
+ end
48
+ end
49
+ end
50
+ end
@@ -5,6 +5,7 @@ require 'uri'
5
5
  require 'capybara/rspec'
6
6
  require 'capybara/poltergeist'
7
7
  Capybara.default_driver = :poltergeist
8
+ Capybara.server = :webrick
8
9
 
9
10
  describe "SAML Authentication", type: :feature do
10
11
  let(:idp_port) { 8009 }
@@ -56,7 +57,7 @@ describe "SAML Authentication", type: :feature do
56
57
  expect(current_url).to eq("http://localhost:8020/")
57
58
 
58
59
  click_on "Log out"
59
- #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
60
61
  expect(current_url).to match(%r(\Ahttp://localhost:8009/saml/auth\?SAMLRequest=))
61
62
 
62
63
  # prove user is now signed out
@@ -84,8 +85,8 @@ describe "SAML Authentication", type: :feature do
84
85
  @sp_pid = start_app('sp', sp_port)
85
86
  end
86
87
  after(:each) do
87
- stop_app(@idp_pid)
88
- stop_app(@sp_pid)
88
+ stop_app("idp", @idp_pid)
89
+ stop_app("sp", @sp_pid)
89
90
  end
90
91
 
91
92
  it_behaves_like "it authenticates and creates users"
@@ -99,8 +100,8 @@ describe "SAML Authentication", type: :feature do
99
100
  @sp_pid = start_app('sp', sp_port)
100
101
  end
101
102
  after(:each) do
102
- stop_app(@idp_pid)
103
- stop_app(@sp_pid)
103
+ stop_app("idp", @idp_pid)
104
+ stop_app("sp", @sp_pid)
104
105
  end
105
106
 
106
107
  it_behaves_like "it authenticates and creates users"
@@ -114,8 +115,8 @@ describe "SAML Authentication", type: :feature do
114
115
  @sp_pid = start_app('sp', sp_port)
115
116
  end
116
117
  after(:each) do
117
- stop_app(@idp_pid)
118
- stop_app(@sp_pid)
118
+ stop_app("idp", @idp_pid)
119
+ stop_app("sp", @sp_pid)
119
120
  end
120
121
 
121
122
  it_behaves_like "it authenticates and creates users"
@@ -130,33 +131,33 @@ describe "SAML Authentication", type: :feature do
130
131
  @sp_pid = start_app('sp', sp_port)
131
132
  end
132
133
  after(:each) do
133
- stop_app(@idp_pid)
134
- stop_app(@sp_pid)
134
+ stop_app("idp", @idp_pid)
135
+ stop_app("sp", @sp_pid)
135
136
  end
136
137
 
137
138
  it_behaves_like "it authenticates and creates users"
138
139
  end
139
140
 
140
141
  context "when the idp_settings_adapter key is set" do
141
-
142
142
  before(:each) do
143
143
  create_app('idp', 'INCLUDE_SUBJECT_IN_ATTRIBUTES' => "false")
144
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/\?SAMLRequest=))
160
+ expect(current_url).to match(%r(\Ahttp://localhost:8010/saml/auth\?SAMLRequest=))
160
161
  end
161
162
  end
162
163
 
@@ -171,8 +172,8 @@ describe "SAML Authentication", type: :feature do
171
172
  end
172
173
 
173
174
  after(:each) do
174
- stop_app(@idp_pid)
175
- stop_app(@sp_pid)
175
+ stop_app("idp", @idp_pid)
176
+ stop_app("sp", @sp_pid)
176
177
  end
177
178
 
178
179
  it_behaves_like "it authenticates and creates users"
@@ -187,24 +188,47 @@ describe "SAML Authentication", type: :feature do
187
188
  fill_in "Email", with: "you@example.com"
188
189
  fill_in "Password", with: "asdf"
189
190
  click_on "Sign in"
190
- expect(page).to have_content("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.")
191
192
  expect(current_url).to eq("http://www.example.com/")
192
193
  end
193
194
  end
194
195
  end
195
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
+
196
221
  def create_user(email)
197
222
  response = Net::HTTP.post_form(URI('http://localhost:8020/users'), email: email)
198
223
  expect(response.code).to eq('201')
199
224
  end
200
225
 
201
- def sign_in
202
- visit 'http://localhost:8020/'
203
- 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)}"
204
228
  fill_in "Email", with: "you@example.com"
205
229
  fill_in "Password", with: "asdf"
206
230
  click_on "Sign in"
207
- Timeout.timeout(Capybara.default_wait_time) do
231
+ Timeout.timeout(Capybara.default_max_wait_time) do
208
232
  loop do
209
233
  sleep 0.1
210
234
  break if current_url == "http://localhost:8020/"
@@ -3,12 +3,16 @@ ENV["RAILS_ENV"] ||= 'test'
3
3
  require 'spec_helper'
4
4
 
5
5
  create_app('sp', 'USE_SUBJECT_TO_AUTHENTICATE' => "false")
6
- require 'support/sp/config/environment'
6
+ require "#{working_directory}/sp/config/environment"
7
7
  require 'rspec/rails'
8
8
 
9
9
  ActiveRecord::Migration.verbose = false
10
10
  ActiveRecord::Base.logger = Logger.new(nil)
11
- ActiveRecord::Migrator.migrate(File.expand_path("../support/sp/db/migrate/", __FILE__))
11
+ if ActiveRecord::Base.connection.respond_to?(:migration_context)
12
+ ActiveRecord::Base.connection.migration_context.migrate
13
+ else
14
+ ActiveRecord::Migrator.migrate("#{working_directory}/sp/db/migrate/")
15
+ end
12
16
 
13
17
  RSpec.configure do |config|
14
18
  config.use_transactional_fixtures = true
@@ -0,0 +1,102 @@
1
+ require 'rails_helper'
2
+
3
+ describe 'SamlAuthenticatable Routes', type: :routing do
4
+ describe 'GET /users/saml/sign_in (login)' do
5
+ it 'routes to Devise::SamlSessionsController#new' do
6
+ expect(get: '/users/saml/sign_in').to route_to(controller: 'devise/saml_sessions', action: 'new')
7
+ expect(get: new_user_session_path).to route_to(controller: 'devise/saml_sessions', action: 'new')
8
+ end
9
+ end
10
+
11
+ describe 'POST /users/saml/auth (session creation)' do
12
+ it 'routes to Devise::SamlSessionsController#create' do
13
+ expect(post: '/users/saml/auth').to route_to(controller: 'devise/saml_sessions', action: 'create')
14
+ end
15
+ end
16
+
17
+ describe 'DELETE /users/sign_out (logout)' do
18
+ it 'routes to Devise::SamlSessionsController#destroy' do
19
+ expect(delete: '/users/sign_out').to route_to(controller: 'devise/saml_sessions', action: 'destroy')
20
+ expect(delete: destroy_user_session_path).to route_to(controller: 'devise/saml_sessions', action: 'destroy')
21
+ end
22
+ end
23
+
24
+ describe 'GET /users/saml/metadata' do
25
+ it 'routes to Devise::SamlSessionsController#metadata' do
26
+ expect(get: '/users/saml/metadata').to route_to(controller: 'devise/saml_sessions', action: 'metadata')
27
+ end
28
+ end
29
+
30
+ describe 'GET /users/saml/idp_sign_out (IdP-initiated logout)' do
31
+ it 'routes to Devise::SamlSessionsController#idp_sign_out' do
32
+ expect(get: '/users/saml/idp_sign_out').to route_to(controller: 'devise/saml_sessions', action: 'idp_sign_out')
33
+ end
34
+ end
35
+
36
+ describe 'POST /users/saml/idp_sign_out (IdP-initiated logout)' do
37
+ it 'routes to Devise::SamlSessionsController#idp_sign_out' do
38
+ expect(post: '/users/saml/idp_sign_out').to route_to(controller: 'devise/saml_sessions', action: 'idp_sign_out')
39
+ end
40
+ end
41
+
42
+ context 'when saml_route_helper_prefix is "sso"' do
43
+ before(:all) do
44
+ ::Devise.saml_route_helper_prefix = 'sso'
45
+
46
+ # A very simple Rails engine
47
+ module SamlRouteHelperPrefixEngine
48
+ class Engine < ::Rails::Engine
49
+ isolate_namespace SamlRouteHelperPrefixEngine
50
+ end
51
+
52
+ Engine.routes.draw do
53
+ devise_for :users, module: :devise
54
+ end
55
+ end
56
+ end
57
+ after(:all) do
58
+ ::Devise.saml_route_helper_prefix = nil
59
+ end
60
+ routes { SamlRouteHelperPrefixEngine::Engine.routes }
61
+
62
+ describe 'GET /users/saml/sign_in (login)' do
63
+ it 'routes to Devise::SamlSessionsController#new' do
64
+ expect(get: '/users/saml/sign_in').to route_to(controller: 'devise/saml_sessions', action: 'new')
65
+ expect(get: new_sso_user_session_path).to route_to(controller: 'devise/saml_sessions', action: 'new')
66
+ end
67
+ end
68
+
69
+ describe 'POST /users/saml/auth (session creation)' do
70
+ it 'routes to Devise::SamlSessionsController#create' do
71
+ expect(post: '/users/saml/auth').to route_to(controller: 'devise/saml_sessions', action: 'create')
72
+ end
73
+ end
74
+
75
+ describe 'DELETE /users/sign_out (logout)' do
76
+ it 'routes to Devise::SamlSessionsController#destroy' do
77
+ expect(delete: '/users/sign_out').to route_to(controller: 'devise/saml_sessions', action: 'destroy')
78
+ expect(delete: destroy_sso_user_session_path).to route_to(controller: 'devise/saml_sessions', action: 'destroy')
79
+ end
80
+ end
81
+
82
+ describe 'GET /users/saml/metadata' do
83
+ it 'routes to Devise::SamlSessionsController#metadata' do
84
+ expect(get: '/users/saml/metadata').to route_to(controller: 'devise/saml_sessions', action: 'metadata')
85
+ end
86
+ end
87
+
88
+ describe 'GET /users/saml/idp_sign_out (IdP-initiated logout)' do
89
+ it 'routes to Devise::SamlSessionsController#idp_sign_out' do
90
+ expect(get: '/users/saml/idp_sign_out').to route_to(controller: 'devise/saml_sessions', action: 'idp_sign_out')
91
+ expect(get: idp_destroy_sso_user_session_path).to route_to(controller: 'devise/saml_sessions', action: 'idp_sign_out')
92
+ end
93
+ end
94
+
95
+ describe 'POST /users/saml/idp_sign_out (IdP-initiated logout)' do
96
+ it 'routes to Devise::SamlSessionsController#idp_sign_out' do
97
+ expect(post: '/users/saml/idp_sign_out').to route_to(controller: 'devise/saml_sessions', action: 'idp_sign_out')
98
+ expect(post: idp_destroy_sso_user_session_path).to route_to(controller: 'devise/saml_sessions', action: 'idp_sign_out')
99
+ end
100
+ end
101
+ end
102
+ end
@@ -1,3 +1,5 @@
1
+ require "fileutils"
2
+
1
3
  RSpec.configure do |config|
2
4
  config.run_all_when_everything_filtered = true
3
5
  config.filter_run :focus
@@ -28,8 +30,13 @@ RSpec.configure do |config|
28
30
  Devise.saml_session_index_key = @original_saml_session_index_key
29
31
  Devise.idp_settings_adapter = nil
30
32
  end
33
+
34
+ config.after :suite do
35
+ FileUtils.rm_rf($working_directory) if $working_directory
36
+ end
31
37
  end
32
38
 
33
39
  require 'support/rails_app'
34
40
 
41
+ require "action_controller" # https://github.com/heartcombo/responders/pull/95
35
42
  require 'devise_saml_authenticatable'