devise_saml_authenticatable 1.4.0 → 1.6.2

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.
Files changed (36) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +0 -2
  3. data/.travis.yml +27 -24
  4. data/Gemfile +2 -2
  5. data/README.md +99 -30
  6. data/app/controllers/devise/saml_sessions_controller.rb +34 -7
  7. data/devise_saml_authenticatable.gemspec +1 -1
  8. data/lib/devise_saml_authenticatable.rb +25 -0
  9. data/lib/devise_saml_authenticatable/default_attribute_map_resolver.rb +26 -0
  10. data/lib/devise_saml_authenticatable/exception.rb +1 -1
  11. data/lib/devise_saml_authenticatable/model.rb +10 -17
  12. data/lib/devise_saml_authenticatable/routes.rb +17 -6
  13. data/lib/devise_saml_authenticatable/saml_mapped_attributes.rb +15 -2
  14. data/lib/devise_saml_authenticatable/strategy.rb +1 -1
  15. data/lib/devise_saml_authenticatable/version.rb +1 -1
  16. data/spec/controllers/devise/saml_sessions_controller_spec.rb +69 -11
  17. data/spec/devise_saml_authenticatable/default_attribute_map_resolver_spec.rb +58 -0
  18. data/spec/devise_saml_authenticatable/model_spec.rb +55 -7
  19. data/spec/devise_saml_authenticatable/saml_mapped_attributes_spec.rb +50 -0
  20. data/spec/features/saml_authentication_spec.rb +45 -37
  21. data/spec/rails_helper.rb +6 -2
  22. data/spec/routes/routes_spec.rb +102 -0
  23. data/spec/spec_helper.rb +7 -0
  24. data/spec/support/Gemfile.rails4 +20 -10
  25. data/spec/support/Gemfile.rails5 +13 -2
  26. data/spec/support/Gemfile.rails5.1 +25 -0
  27. data/spec/support/Gemfile.rails5.2 +25 -0
  28. data/spec/support/attribute-map.yml +12 -0
  29. data/spec/support/attribute_map_resolver.rb.erb +14 -0
  30. data/spec/support/idp_settings_adapter.rb.erb +5 -5
  31. data/spec/support/idp_template.rb +6 -2
  32. data/spec/support/rails_app.rb +75 -17
  33. data/spec/support/saml_idp_controller.rb.erb +13 -6
  34. data/spec/support/sp_template.rb +45 -21
  35. metadata +23 -12
  36. data/spec/support/Gemfile.ruby-saml-1.3 +0 -22
@@ -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,8 +131,8 @@ 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"
@@ -142,37 +143,21 @@ describe "SAML Authentication", type: :feature do
142
143
  create_app('idp', 'INCLUDE_SUBJECT_IN_ATTRIBUTES' => "false")
143
144
  create_app('sp', 'USE_SUBJECT_TO_AUTHENTICATE' => "true", 'IDP_SETTINGS_ADAPTER' => "IdpSettingsAdapter", 'IDP_ENTITY_ID_READER' => "OurEntityIdReader")
144
145
 
145
- @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)
146
148
  @sp_pid = start_app('sp', sp_port)
147
149
  end
148
150
 
149
151
  after(:each) do
150
- stop_app(@idp_pid)
151
- stop_app(@sp_pid)
152
+ stop_app("idp", @idp_pid)
153
+ stop_app("sp", @sp_pid)
152
154
  end
153
155
 
154
156
  it "authenticates an existing user on a SP via an IdP" do
155
157
  create_user("you@example.com")
156
158
 
157
159
  visit 'http://localhost:8020/users/saml/sign_in/?entity_id=http%3A%2F%2Flocalhost%3A8020%2Fsaml%2Fmetadata'
158
- expect(current_url).to match(%r(\Ahttp://www.example.com/sso\?SAMLRequest=))
159
- end
160
-
161
- it "logs a user out of the IdP via the SP" do
162
- sign_in
163
-
164
- # prove user is still signed in
165
- visit 'http://localhost:8020/'
166
- expect(page).to have_content("you@example.com")
167
- expect(current_url).to eq("http://localhost:8020/")
168
-
169
- click_on "Log out"
170
- #confirm the logout response redirected to the SP which in turn attempted to sign th e
171
- expect(current_url).to match(%r(\Ahttp://www.example.com/slo\?SAMLRequest=))
172
-
173
- # prove user is now signed out
174
- visit 'http://localhost:8020/users/saml/sign_in/?entity_id=http%3A%2F%2Flocalhost%3A8020%2Fsaml%2Fmetadata'
175
- 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=))
176
161
  end
177
162
  end
178
163
 
@@ -187,8 +172,8 @@ describe "SAML Authentication", type: :feature do
187
172
  end
188
173
 
189
174
  after(:each) do
190
- stop_app(@idp_pid)
191
- stop_app(@sp_pid)
175
+ stop_app("idp", @idp_pid)
176
+ stop_app("sp", @sp_pid)
192
177
  end
193
178
 
194
179
  it_behaves_like "it authenticates and creates users"
@@ -203,24 +188,47 @@ describe "SAML Authentication", type: :feature do
203
188
  fill_in "Email", with: "you@example.com"
204
189
  fill_in "Password", with: "asdf"
205
190
  click_on "Sign in"
206
- 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.")
207
192
  expect(current_url).to eq("http://www.example.com/")
208
193
  end
209
194
  end
210
195
  end
211
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
+
212
221
  def create_user(email)
213
222
  response = Net::HTTP.post_form(URI('http://localhost:8020/users'), email: email)
214
223
  expect(response.code).to eq('201')
215
224
  end
216
225
 
217
- def sign_in
218
- visit 'http://localhost:8020/'
219
- 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)}"
220
228
  fill_in "Email", with: "you@example.com"
221
229
  fill_in "Password", with: "asdf"
222
230
  click_on "Sign in"
223
- Timeout.timeout(Capybara.default_wait_time) do
231
+ Timeout.timeout(Capybara.default_max_wait_time) do
224
232
  loop do
225
233
  sleep 0.1
226
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'
@@ -6,26 +6,36 @@ gemspec path: '../..'
6
6
  group :test do
7
7
  gem 'rspec', '~> 3.0'
8
8
  gem 'rails', '~> 4.0'
9
- gem 'rspec-rails'
10
- gem 'sqlite3'
9
+ gem 'rspec-rails', '~> 3.9'
10
+ gem 'sqlite3', '~> 1.3.6'
11
11
  gem 'capybara'
12
12
  gem 'poltergeist'
13
13
 
14
14
  # Lock down versions of gems for older versions of Ruby
15
- if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.0")
16
- gem 'addressable', '~> 2.4.0'
17
- gem 'mime-types', '~> 2.99'
18
- gem 'public_suffix', '~> 1.4.6'
19
- gem 'rake', '~> 12.2.0'
20
- elsif Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
21
- gem 'public_suffix', '~> 2.0.5'
22
- gem 'rake'
15
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
16
+ gem 'rake', '~> 12.2'
23
17
  else
24
18
  gem 'rake'
25
19
  end
26
20
 
27
21
  if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
28
22
  gem 'devise', '~> 3.5'
23
+ gem 'minitest', '~> 5.11.0'
29
24
  gem 'nokogiri', '~> 1.6.8'
25
+ gem 'public_suffix', '~> 2.0.5'
26
+ end
27
+
28
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
29
+ gem 'responders', '~> 1.0'
30
+ elsif Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
31
+ gem 'responders', '~> 2.0'
32
+ end
33
+
34
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.2")
35
+ gem 'byebug', '~> 9.0'
36
+ elsif Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.3")
37
+ gem 'byebug', '~> 10.0'
38
+ elsif Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
39
+ gem 'byebug', '~> 11.0.0'
30
40
  end
31
41
  end
@@ -7,8 +7,19 @@ group :test do
7
7
  gem 'rake'
8
8
  gem 'rspec', '~> 3.0'
9
9
  gem 'rails', '~> 5.0.0'
10
- gem 'rspec-rails'
11
- gem 'sqlite3'
10
+ gem 'rspec-rails', '~> 3.9'
11
+ gem 'sqlite3', '~> 1.3.6'
12
12
  gem 'capybara'
13
13
  gem 'poltergeist'
14
+
15
+ # Lock down versions of gems for older versions of Ruby
16
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
17
+ gem 'responders', '~> 2.4'
18
+ end
19
+
20
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.3")
21
+ gem 'byebug', '~> 10.0'
22
+ elsif Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
23
+ gem 'byebug', '~> 11.0.0'
24
+ end
14
25
  end
@@ -0,0 +1,25 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in devise_saml_authenticatable.gemspec
4
+ gemspec path: '../..'
5
+
6
+ group :test do
7
+ gem 'rake'
8
+ gem 'rspec', '~> 3.0'
9
+ gem 'rails', '~> 5.1.0'
10
+ gem 'rspec-rails', '~> 3.9'
11
+ gem 'sqlite3', '~> 1.3.6'
12
+ gem 'capybara'
13
+ gem 'poltergeist'
14
+
15
+ # Lock down versions of gems for older versions of Ruby
16
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
17
+ gem 'responders', '~> 2.4'
18
+ end
19
+
20
+ if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.3")
21
+ gem 'byebug', '~> 10.0'
22
+ elsif Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.4")
23
+ gem 'byebug', '~> 11.0.0'
24
+ end
25
+ end