devise_saml_authenticatable 1.4.0 → 1.6.2

Sign up to get free protection for your applications and to get access to all the features.
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