devise_saml_authenticatable 1.6.2 → 1.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +6 -0
  3. data/.github/workflows/ci.yml +62 -0
  4. data/.gitignore +1 -0
  5. data/.ruby-version +1 -0
  6. data/Gemfile +12 -2
  7. data/README.md +62 -26
  8. data/app/controllers/devise/saml_sessions_controller.rb +33 -27
  9. data/devise_saml_authenticatable.gemspec +2 -1
  10. data/lib/devise_saml_authenticatable/logger.rb +2 -2
  11. data/lib/devise_saml_authenticatable/model.rb +17 -3
  12. data/lib/devise_saml_authenticatable/saml_config.rb +28 -6
  13. data/lib/devise_saml_authenticatable/strategy.rb +23 -5
  14. data/lib/devise_saml_authenticatable/version.rb +1 -1
  15. data/lib/devise_saml_authenticatable.rb +16 -2
  16. data/spec/controllers/devise/saml_sessions_controller_spec.rb +205 -147
  17. data/spec/devise_saml_authenticatable/model_spec.rb +137 -19
  18. data/spec/devise_saml_authenticatable/saml_config_spec.rb +69 -22
  19. data/spec/devise_saml_authenticatable/strategy_spec.rb +58 -9
  20. data/spec/features/saml_authentication_spec.rb +19 -6
  21. data/spec/support/Gemfile.rails5.2 +2 -13
  22. data/spec/support/Gemfile.rails6 +18 -0
  23. data/spec/support/Gemfile.rails6.1 +24 -0
  24. data/spec/support/idp_settings_adapter.rb.erb +19 -9
  25. data/spec/support/idp_template.rb +5 -13
  26. data/spec/support/rails_app.rb +6 -7
  27. data/spec/support/ruby_saml_support.rb +10 -0
  28. data/spec/support/saml_idp_controller.rb.erb +1 -6
  29. data/spec/support/sp_template.rb +22 -19
  30. metadata +14 -12
  31. data/.travis.yml +0 -52
  32. data/spec/support/Gemfile.rails4 +0 -41
  33. data/spec/support/Gemfile.rails5 +0 -25
  34. data/spec/support/Gemfile.rails5.1 +0 -25
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0d1e1f1df121795583d9fc25cab40edac01bb61f8adbdb4fac7c0c15bfe4b0c
4
- data.tar.gz: 0d7770994b0c119da178a3b19ced6d52a88364d561279c5bd759ce5580fb5fe5
3
+ metadata.gz: d503c9931a5af5182f1f6910dcfc548d692fcc3e45ad2fb464b3931c4791ac59
4
+ data.tar.gz: 6da638f28754c2a8f9d44d38a8a61f0796b04e023e0dba8d845fb60bc004bebe
5
5
  SHA512:
6
- metadata.gz: d849dc5dff4bd09d0dd034449ed3e68494f0f1c82c8e31c708b7d6e0d3a366a2fd119fb24704d0e5a946b6fc206dbe072fadbf7218489abffdbe1b3644d41419
7
- data.tar.gz: e64b35109ed35b61e804fa5e06d3b3c299210cf75804f706e299147d02a7ddb929246c1a7fdb65aee12f5ceed5348267d3c6e4657544288e3a45741474d5e30d
6
+ metadata.gz: 1d44c5c95a396f22008c33b4636fd201c6d6ec71a5193909bb6cc1aa59f660301831cada23e589b05ed52260d8a1a2e42a7442a72757f821ef6ede752a26554e
7
+ data.tar.gz: '0603740818f257bc90e63f4732c59c6d8a686e0d28c9dedb6cf8ce877a06234e51d39376602d105660e3e25744dfe0163dec931b436ec3bfbe9aabbf06167e36'
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
@@ -0,0 +1,62 @@
1
+ name: ci
2
+ on:
3
+ push:
4
+ branches:
5
+ - master
6
+ pull_request:
7
+ branches:
8
+ - master
9
+ jobs:
10
+ test:
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ ruby:
15
+ - "3.2"
16
+ - "3.1"
17
+ - "3.0"
18
+ - "2.7"
19
+ - "2.6"
20
+ gemfile:
21
+ - Gemfile
22
+ - spec/support/Gemfile.rails6.1
23
+ - spec/support/Gemfile.rails6
24
+ - spec/support/Gemfile.rails5.2
25
+ bundler:
26
+ - "2"
27
+ exclude:
28
+ - ruby: "2.6"
29
+ gemfile: Gemfile
30
+ bundler: "2"
31
+ - ruby: "3.0"
32
+ gemfile: spec/support/Gemfile.rails5.2
33
+ bundler: "2"
34
+ - ruby: "3.0"
35
+ gemfile: spec/support/Gemfile.rails6
36
+ bundler: "2"
37
+ - ruby: "3.1"
38
+ gemfile: spec/support/Gemfile.rails5.2
39
+ bundler: "2"
40
+ - ruby: "3.1"
41
+ gemfile: spec/support/Gemfile.rails6
42
+ bundler: "2"
43
+ - ruby: "3.2"
44
+ gemfile: spec/support/Gemfile.rails5.2
45
+ bundler: "2"
46
+ - ruby: "3.2"
47
+ gemfile: spec/support/Gemfile.rails6
48
+ bundler: "2"
49
+ - ruby: "3.2"
50
+ gemfile: spec/support/Gemfile.rails6.1
51
+ bundler: "2"
52
+ runs-on: ubuntu-latest
53
+ env:
54
+ BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}
55
+ steps:
56
+ - uses: actions/checkout@v3
57
+ - uses: ruby/setup-ruby@v1
58
+ with:
59
+ bundler: ${{ matrix.bundler }}
60
+ ruby-version: ${{ matrix.ruby }}
61
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
62
+ - run: bundle exec rake
data/.gitignore CHANGED
@@ -13,4 +13,5 @@ lib/bundler/man
13
13
  pkg
14
14
  rdoc
15
15
  spec/reports
16
+ spec/support/bin/*
16
17
  tmp
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.1.0
data/Gemfile CHANGED
@@ -6,9 +6,19 @@ gemspec
6
6
  group :test do
7
7
  gem 'rake'
8
8
  gem 'rspec', '~> 3.0'
9
- gem 'rails', '~> 6.0'
9
+ gem 'rails', '~> 7.0.0'
10
10
  gem 'rspec-rails'
11
11
  gem 'sqlite3', '~> 1.4.0'
12
12
  gem 'capybara'
13
- gem 'poltergeist'
13
+ gem 'selenium-webdriver'
14
+
15
+ if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new("3.0")
16
+ gem 'webrick'
17
+ end
18
+
19
+ if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new("3.1")
20
+ gem 'net-smtp', require: false
21
+ gem 'net-imap', require: false
22
+ gem 'net-pop', require: false
23
+ end
14
24
  end
data/README.md CHANGED
@@ -57,8 +57,8 @@ An extra step in SAML SSO setup is adding your application to your identity prov
57
57
  Your IdP should give you some information you need to configure in [ruby-saml](https://github.com/onelogin/ruby-saml), as in the next section:
58
58
 
59
59
  - Issuer (`idp_entity_id`)
60
- - SSO endpoint (`idp_sso_target_url`)
61
- - SLO endpoint (`idp_slo_target_url`)
60
+ - SSO endpoint (`idp_sso_service_url`)
61
+ - SLO endpoint (`idp_slo_service_url`)
62
62
  - Certificate fingerprint (`idp_cert_fingerprint`) and algorithm (`idp_cert_fingerprint_algorithm`)
63
63
  - Or the certificate itself (`idp_cert`)
64
64
 
@@ -72,11 +72,42 @@ In `config/initializers/devise.rb`:
72
72
  # ==> Configuration for :saml_authenticatable
73
73
 
74
74
  # Create user if the user does not exist. (Default is false)
75
+ # Can also accept a proc, for ex:
76
+ # Devise.saml_create_user = Proc.new do |model_class, saml_response, auth_value|
77
+ # model_class == Admin
78
+ # end
75
79
  config.saml_create_user = true
76
80
 
77
81
  # Update the attributes of the user after a successful login. (Default is false)
82
+ # Can also accept a proc, for ex:
83
+ # Devise.saml_update_user = Proc.new do |model_class, saml_response, auth_value|
84
+ # model_class == Admin
85
+ # end
78
86
  config.saml_update_user = true
79
87
 
88
+ # Lambda that is called if Devise.saml_update_user and/or Devise.saml_create_user are true.
89
+ # Receives the model object, saml_response and auth_value, and defines how the object's values are
90
+ # updated with regards to the SAML response.
91
+ # config.saml_update_resource_hook = -> (user, saml_response, auth_value) {
92
+ # saml_response.attributes.resource_keys.each do |key|
93
+ # user.send "#{key}=", saml_response.attribute_value_by_resource_key(key)
94
+ # end
95
+ #
96
+ # if (Devise.saml_use_subject)
97
+ # user.send "#{Devise.saml_default_user_key}=", auth_value
98
+ # end
99
+ #
100
+ # user.save!
101
+ # }
102
+
103
+ # Lambda that is called to resolve the saml_response and auth_value into the correct user object.
104
+ # Receives a copy of the ActiveRecord::Model, saml_response and auth_value. Is expected to return
105
+ # one instance of the provided model that is the matched account, or nil if none exists.
106
+ # config.saml_resource_locator = -> (model, saml_response, auth_value) {
107
+ # model.where(Devise.saml_default_user_key => auth_value).first
108
+ # }
109
+
110
+
80
111
  # Set the default user key. The user will be looked up by this key. Make
81
112
  # sure that the Authentication Response includes the attribute.
82
113
  config.saml_default_user_key = :email
@@ -85,21 +116,21 @@ In `config/initializers/devise.rb`:
85
116
  # for the user's session to facilitate an IDP initiated logout request.
86
117
  config.saml_session_index_key = :session_index
87
118
 
88
- # You can set this value to use Subject or SAML assertation as info to which email will be compared.
89
- # If you don't set it then email will be extracted from SAML assertation attributes.
119
+ # You can set this value to use Subject or SAML assertion as info to which email will be compared.
120
+ # If you don't set it then email will be extracted from SAML assertion attributes.
90
121
  config.saml_use_subject = true
91
122
 
92
- # You can support multiple IdPs by setting this value to a class that implements a #settings method which takes
93
- # an IdP entity id as an argument and returns a hash of idp settings for the corresponding IdP.
94
- config.idp_settings_adapter = nil
123
+ # You can implement IdP settings with the options to support multiple IdPs and use the request object by setting this value to the name of a class that implements a ::settings method
124
+ # which takes an IdP entity id and a request object as arguments and returns a hash of idp settings for the corresponding IdP.
125
+ # config.idp_settings_adapter = "MyIdPSettingsAdapter"
95
126
 
96
127
  # You provide you own method to find the idp_entity_id in a SAML message in the case of multiple IdPs
97
- # by setting this to a custom reader class, or use the default.
98
- # config.idp_entity_id_reader = DeviseSamlAuthenticatable::DefaultIdpEntityIdReader
128
+ # by setting this to the name of a custom reader class, or use the default.
129
+ # config.idp_entity_id_reader = "DeviseSamlAuthenticatable::DefaultIdpEntityIdReader"
99
130
 
100
- # You can set a handler object that takes the response for a failed SAML request and the strategy,
131
+ # You can set the name of a class that takes the response for a failed SAML request and the strategy,
101
132
  # and implements a #handle method. This method can then redirect the user, return error messages, etc.
102
- # config.saml_failed_callback = nil
133
+ # config.saml_failed_callback = "MySamlFailedCallbacksHandler"
103
134
 
104
135
  # You can customize the named routes generated in case of named route collisions with
105
136
  # other Devise modules or libraries. Set the saml_route_helper_prefix to a string that will
@@ -111,16 +142,19 @@ In `config/initializers/devise.rb`:
111
142
  # This is a time in seconds.
112
143
  # config.allowed_clock_drift_in_seconds = 0
113
144
 
145
+ # In SAML responses, validate that the identity provider has included an InResponseTo
146
+ # header that matches the ID of the SAML request. (Default is false)
147
+ # config.saml_validate_in_response_to = false
148
+
114
149
  # Configure with your SAML settings (see ruby-saml's README for more information: https://github.com/onelogin/ruby-saml).
115
150
  config.saml_configure do |settings|
116
- # assertion_consumer_service_url is required starting with ruby-saml 1.4.3: https://github.com/onelogin/ruby-saml#updating-from-142-to-143
117
151
  settings.assertion_consumer_service_url = "http://localhost:3000/users/saml/auth"
118
152
  settings.assertion_consumer_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
119
153
  settings.name_identifier_format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
120
- settings.issuer = "http://localhost:3000/saml/metadata"
154
+ settings.sp_entity_id = "http://localhost:3000/saml/metadata"
121
155
  settings.authn_context = ""
122
- settings.idp_slo_target_url = "http://localhost/simplesaml/www/saml2/idp/SingleLogoutService.php"
123
- settings.idp_sso_target_url = "http://localhost/simplesaml/www/saml2/idp/SSOService.php"
156
+ settings.idp_slo_service_url = "http://localhost/simplesaml/www/saml2/idp/SingleLogoutService.php"
157
+ settings.idp_sso_service_url = "http://localhost/simplesaml/www/saml2/idp/SSOService.php"
124
158
  settings.idp_cert_fingerprint = "00:A1:2B:3C:44:55:6F:A7:88:CC:DD:EE:22:33:44:55:D6:77:8F:99"
125
159
  settings.idp_cert_fingerprint_algorithm = "http://www.w3.org/2000/09/xmldsig#sha1"
126
160
  end
@@ -169,7 +203,7 @@ If you only have one IdP, you can use the config file above, or just return a si
169
203
  ...
170
204
  # ==> Configuration for :saml_authenticatable
171
205
 
172
- config.saml_attribute_map_resolver = MyAttributeMapResolver
206
+ config.saml_attribute_map_resolver = "MyAttributeMapResolver"
173
207
  end
174
208
  ```
175
209
 
@@ -191,24 +225,26 @@ If you only have one IdP, you can use the config file above, or just return a si
191
225
  end
192
226
  ```
193
227
 
194
- ## Supporting Multiple IdPs
228
+ ## IdP Settings Adapter
195
229
 
196
- If you must support multiple Identity Providers you can implement an adapter class with a `#settings` method that takes an IdP entity id and returns a hash of settings for the corresponding IdP. The `config.idp_settings_adapter` then must be set to point to your adapter in `config/initializers/devise.rb`. The implementation of the adapter is up to you. A simple example may look like this:
230
+ Implementing a custom settings adapter allows you to support multiple Identity Providers, and dynamic application domains with the request object.
231
+
232
+ You can implement an adapter class with a `#settings` method. It must take two arguments (idp_entity_id, request) and return a hash of settings for the corresponding IdP. The `config.idp_settings_adapter` then must be set to point to your adapter in `config/initializers/devise.rb`. The implementation of the adapter is up to you. A simple example may look like this:
197
233
 
198
234
  ```ruby
199
235
  class IdPSettingsAdapter
200
- def self.settings(idp_entity_id)
236
+ def self.settings(idp_entity_id, request)
201
237
  case idp_entity_id
202
238
  when "http://www.example_idp_entity_id.com"
203
239
  {
204
- assertion_consumer_service_url: "http://localhost:3000/users/saml/auth",
240
+ assertion_consumer_service_url: "#{request.protocol}#{request.host_with_port}/users/saml/auth",
205
241
  assertion_consumer_service_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
206
242
  name_identifier_format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
207
- issuer: "http://localhost:3000/saml/metadata",
243
+ sp_entity_id: "#{request.protocol}#{request.host_with_port}/saml/metadata",
208
244
  idp_entity_id: "http://www.example_idp_entity_id.com",
209
245
  authn_context: "",
210
- idp_slo_target_url: "http://example_idp_slo_target_url.com",
211
- idp_sso_target_url: "http://example_idp_sso_target_url.com",
246
+ idp_slo_service_url: "http://example_idp_slo_service_url.com",
247
+ idp_sso_service_url: "http://example_idp_sso_service_url.com",
212
248
  idp_cert: "example_idp_cert"
213
249
  }
214
250
  when "http://www.another_idp_entity_id.biz"
@@ -216,11 +252,11 @@ class IdPSettingsAdapter
216
252
  assertion_consumer_service_url: "http://localhost:3000/users/saml/auth",
217
253
  assertion_consumer_service_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
218
254
  name_identifier_format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
219
- issuer: "http://localhost:3000/saml/metadata",
255
+ sp_entity_id: "http://localhost:3000/saml/metadata",
220
256
  idp_entity_id: "http://www.another_idp_entity_id.biz",
221
257
  authn_context: "",
222
- idp_slo_target_url: "http://another_idp_slo_target_url.com",
223
- idp_sso_target_url: "http://another_idp_sso_target_url.com",
258
+ idp_slo_service_url: "http://another_idp_slo_service_url.com",
259
+ idp_sso_service_url: "http://another_idp_sso_service_url.com",
224
260
  idp_cert: "another_idp_cert"
225
261
  }
226
262
  else
@@ -1,37 +1,33 @@
1
- require "ruby-saml"
1
+ require 'ruby-saml'
2
2
 
3
3
  class Devise::SamlSessionsController < Devise::SessionsController
4
4
  include DeviseSamlAuthenticatable::SamlConfig
5
- unloadable if Rails::VERSION::MAJOR < 4
6
- if Rails::VERSION::MAJOR < 5
7
- skip_before_filter :verify_authenticity_token
8
- prepend_before_filter :store_info_for_sp_initiated_logout, only: :destroy
9
- else
10
- skip_before_action :verify_authenticity_token, raise: false
11
- prepend_before_action :store_info_for_sp_initiated_logout, only: :destroy
12
- end
5
+
6
+ skip_before_action :verify_authenticity_token, raise: false
7
+ prepend_before_action :verify_signed_out_user, :store_info_for_sp_initiated_logout, only: :destroy
13
8
 
14
9
  def new
15
10
  idp_entity_id = get_idp_entity_id(params)
16
- request = OneLogin::RubySaml::Authrequest.new
11
+ auth_request = OneLogin::RubySaml::Authrequest.new
17
12
  auth_params = { RelayState: relay_state } if relay_state
18
- action = request.create(saml_config(idp_entity_id), auth_params || {})
19
- redirect_to action
13
+ action = auth_request.create(saml_config(idp_entity_id, request), auth_params || {})
14
+ session[:saml_transaction_id] = auth_request.request_id if auth_request.respond_to?(:request_id)
15
+ redirect_to action, allow_other_host: true
20
16
  end
21
17
 
22
18
  def metadata
23
19
  idp_entity_id = params[:idp_entity_id]
24
20
  meta = OneLogin::RubySaml::Metadata.new
25
- render :xml => meta.generate(saml_config(idp_entity_id))
21
+ render xml: meta.generate(saml_config(idp_entity_id, request))
26
22
  end
27
23
 
28
24
  def idp_sign_out
29
25
  if params[:SAMLRequest] && Devise.saml_session_index_key
30
- saml_config = saml_config(get_idp_entity_id(params))
26
+ saml_config = saml_config(get_idp_entity_id(params), request)
31
27
  logout_request = OneLogin::RubySaml::SloLogoutrequest.new(params[:SAMLRequest], settings: saml_config)
32
28
  resource_class.reset_session_key_for(logout_request.name_id)
33
29
 
34
- redirect_to generate_idp_logout_response(saml_config, logout_request.id)
30
+ redirect_to generate_idp_logout_response(saml_config, logout_request.id), allow_other_host: true
35
31
  elsif params[:SAMLResponse]
36
32
  # Currently Devise handles the session invalidation when the request is made.
37
33
  # To support a true SP initiated logout response, the request ID would have to be tracked and session invalidated
@@ -49,25 +45,26 @@ class Devise::SamlSessionsController < Devise::SessionsController
49
45
  protected
50
46
 
51
47
  def relay_state
52
- @relay_state ||= if Devise.saml_relay_state.present?
53
- Devise.saml_relay_state.call(request)
54
- end
48
+ @relay_state ||= (Devise.saml_relay_state.call(request) if Devise.saml_relay_state.present?)
55
49
  end
56
50
 
57
51
  # For non transient name ID, save info to identify user for logout purpose
58
52
  # before that user's session got destroyed. These info are used in the
59
53
  # `after_sign_out_path_for` method below.
60
54
  def store_info_for_sp_initiated_logout
61
- return if Devise.saml_config.name_identifier_format == "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
55
+ return if Devise.saml_config.name_identifier_format == 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
56
+
62
57
  @name_identifier_value_for_sp_initiated_logout = Devise.saml_name_identifier_retriever.call(current_user)
63
- @sessionindex_for_sp_initiated_logout = current_user.public_send(Devise.saml_session_index_key) if Devise.saml_session_index_key
58
+ if Devise.saml_session_index_key
59
+ @sessionindex_for_sp_initiated_logout = current_user.public_send(Devise.saml_session_index_key)
60
+ end
64
61
  end
65
62
 
66
63
  # Override devise to send user to IdP logout for SLO
67
64
  def after_sign_out_path_for(_)
68
65
  idp_entity_id = get_idp_entity_id(params)
69
- request = OneLogin::RubySaml::Logoutrequest.new
70
- saml_settings = saml_config(idp_entity_id).dup
66
+ logout_request = OneLogin::RubySaml::Logoutrequest.new
67
+ saml_settings = saml_config(idp_entity_id, request).dup
71
68
 
72
69
  # Add attributes to saml_settings which will later be used to create the SP
73
70
  # initiated logout request
@@ -76,15 +73,24 @@ class Devise::SamlSessionsController < Devise::SessionsController
76
73
  saml_settings.sessionindex = @sessionindex_for_sp_initiated_logout
77
74
  end
78
75
 
79
- request.create(saml_settings)
76
+ logout_request.create(saml_settings)
80
77
  end
81
78
 
82
- def generate_idp_logout_response(saml_config, logout_request_id)
79
+ # Overried devise: if user is signed out, not create the SP initiated logout request,
80
+ # redirect to saml_sign_out_success_url,
81
+ # or devise's after_sign_out_path_for
82
+ def verify_signed_out_user
83
+ if all_signed_out?
84
+ set_flash_message! :notice, :already_signed_out
83
85
 
84
- params = {}
85
- if relay_state
86
- params[:RelayState] = relay_state
86
+ redirect_to (Devise.saml_sign_out_success_url.presence ||
87
+ Devise::SessionsController.new.after_sign_out_path_for(resource_name)), allow_other_host: true
87
88
  end
89
+ end
90
+
91
+ def generate_idp_logout_response(saml_config, logout_request_id)
92
+ params = {}
93
+ params[:RelayState] = relay_state if relay_state
88
94
 
89
95
  OneLogin::RubySaml::SloLogoutresponse.new.create(saml_config, logout_request_id, nil, params)
90
96
  end
@@ -6,7 +6,7 @@ Gem::Specification.new do |gem|
6
6
  gem.email = ["Josef.Sauter@gmail.com"]
7
7
  gem.description = %q{SAML Authentication for devise}
8
8
  gem.summary = %q{SAML Authentication for devise }
9
- gem.homepage = ""
9
+ gem.homepage = "https://github.com/apokalipto/devise_saml_authenticatable"
10
10
  gem.license = "MIT"
11
11
 
12
12
  gem.files = `git ls-files`.split($\)
@@ -16,6 +16,7 @@ Gem::Specification.new do |gem|
16
16
  gem.require_paths = ["lib"]
17
17
  gem.version = DeviseSamlAuthenticatable::VERSION
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
+ gem.required_ruby_version = ">= 2.6.0"
19
20
 
20
21
  gem.add_dependency("devise","> 2.0.0")
21
22
  gem.add_dependency("ruby-saml","~> 1.7")
@@ -1,9 +1,9 @@
1
1
  module DeviseSamlAuthenticatable
2
2
 
3
3
  class Logger
4
- def self.send(message, logger = Rails.logger)
4
+ def self.send(message, log_level = ::Logger::INFO, logger = Rails.logger)
5
5
  if ::Devise.saml_logger
6
- logger.add 0, " \e[36msaml:\e[0m #{message}"
6
+ logger.add log_level, " \e[36msaml:\e[0m #{message}"
7
7
  end
8
8
  end
9
9
  end
@@ -55,8 +55,11 @@ module Devise
55
55
  end
56
56
  end
57
57
 
58
+ create_user = if Devise.saml_create_user.respond_to?(:call) then Devise.saml_create_user.call(self, decorated_response, auth_value)
59
+ else Devise.saml_create_user
60
+ end
58
61
  if resource.nil?
59
- if Devise.saml_create_user
62
+ if create_user
60
63
  logger.info("Creating user(#{auth_value}).")
61
64
  resource = new
62
65
  else
@@ -65,7 +68,10 @@ module Devise
65
68
  end
66
69
  end
67
70
 
68
- if Devise.saml_update_user || (resource.new_record? && Devise.saml_create_user)
71
+ update_user = if Devise.saml_update_user.respond_to?(:call) then Devise.saml_update_user.call(self, decorated_response, auth_value)
72
+ else Devise.saml_update_user
73
+ end
74
+ if update_user || (resource.new_record? && create_user)
69
75
  Devise.saml_update_resource_hook.call(resource, decorated_response, auth_value)
70
76
  end
71
77
 
@@ -82,7 +88,15 @@ module Devise
82
88
  end
83
89
 
84
90
  def attribute_map(saml_response = nil)
85
- Devise.saml_attribute_map_resolver.new(saml_response).attribute_map
91
+ attribute_map_resolver.new(saml_response).attribute_map
92
+ end
93
+
94
+ def attribute_map_resolver
95
+ if Devise.saml_attribute_map_resolver.respond_to?(:new)
96
+ Devise.saml_attribute_map_resolver
97
+ else
98
+ Devise.saml_attribute_map_resolver.constantize
99
+ end
86
100
  end
87
101
  end
88
102
  end
@@ -1,9 +1,9 @@
1
1
  require 'ruby-saml'
2
2
  module DeviseSamlAuthenticatable
3
3
  module SamlConfig
4
- def saml_config(idp_entity_id = nil)
4
+ def saml_config(idp_entity_id = nil, request = nil)
5
5
  return file_based_config if file_based_config
6
- return adapter_based_config(idp_entity_id) if Devise.idp_settings_adapter
6
+ return adapter_based_config(idp_entity_id, request) if Devise.idp_settings_adapter
7
7
 
8
8
  Devise.saml_config
9
9
  end
@@ -14,15 +14,21 @@ module DeviseSamlAuthenticatable
14
14
  return @file_based_config if @file_based_config
15
15
  idp_config_path = "#{Rails.root}/config/idp.yml"
16
16
 
17
- if File.exists?(idp_config_path)
17
+ if File.exist?(idp_config_path)
18
18
  @file_based_config ||= OneLogin::RubySaml::Settings.new(YAML.load(File.read(idp_config_path))[Rails.env])
19
19
  end
20
20
  end
21
21
 
22
- def adapter_based_config(idp_entity_id)
22
+ def adapter_based_config(idp_entity_id, request)
23
23
  config = Marshal.load(Marshal.dump(Devise.saml_config))
24
24
 
25
- Devise.idp_settings_adapter.settings(idp_entity_id).each do |k,v|
25
+ if idp_settings_adapter.method(:settings).parameters.length == 1
26
+ settings = idp_settings_adapter.settings(idp_entity_id)
27
+ else
28
+ settings = idp_settings_adapter.settings(idp_entity_id, request)
29
+ end
30
+
31
+ settings.each do |k,v|
26
32
  acc = "#{k.to_s}=".to_sym
27
33
 
28
34
  if config.respond_to? acc
@@ -33,7 +39,23 @@ module DeviseSamlAuthenticatable
33
39
  end
34
40
 
35
41
  def get_idp_entity_id(params)
36
- Devise.idp_entity_id_reader.entity_id(params)
42
+ idp_entity_id_reader.entity_id(params)
43
+ end
44
+
45
+ def idp_entity_id_reader
46
+ if Devise.idp_entity_id_reader.respond_to?(:entity_id)
47
+ Devise.idp_entity_id_reader
48
+ else
49
+ @idp_entity_id_reader ||= Devise.idp_entity_id_reader.constantize
50
+ end
51
+ end
52
+
53
+ def idp_settings_adapter
54
+ if Devise.idp_settings_adapter.respond_to?(:settings)
55
+ Devise.idp_settings_adapter
56
+ else
57
+ @idp_settings_adapter ||= Devise.idp_settings_adapter.constantize
58
+ end
37
59
  end
38
60
  end
39
61
  end
@@ -8,8 +8,7 @@ module Devise
8
8
  if params[:SAMLResponse]
9
9
  OneLogin::RubySaml::Response.new(
10
10
  params[:SAMLResponse],
11
- settings: saml_config(get_idp_entity_id(params)),
12
- allowed_clock_drift: Devise.allowed_clock_drift_in_seconds,
11
+ response_options,
13
12
  )
14
13
  else
15
14
  false
@@ -36,8 +35,7 @@ module Devise
36
35
  def parse_saml_response
37
36
  @response = OneLogin::RubySaml::Response.new(
38
37
  params[:SAMLResponse],
39
- settings: saml_config(get_idp_entity_id(params)),
40
- allowed_clock_drift: Devise.allowed_clock_drift_in_seconds,
38
+ response_options,
41
39
  )
42
40
  unless @response.is_valid?
43
41
  failed_auth("Auth errors: #{@response.errors.join(', ')}")
@@ -54,9 +52,29 @@ module Devise
54
52
  def failed_auth(msg)
55
53
  DeviseSamlAuthenticatable::Logger.send(msg)
56
54
  fail!(:invalid)
57
- Devise.saml_failed_callback.new.handle(@response, self) if Devise.saml_failed_callback
55
+ failed_callback.new.handle(@response, self) if Devise.saml_failed_callback
56
+ end
57
+
58
+ def failed_callback
59
+ if Devise.saml_failed_callback.respond_to?(:new)
60
+ Devise.saml_failed_callback
61
+ else
62
+ Devise.saml_failed_callback.constantize
63
+ end
58
64
  end
59
65
 
66
+ def response_options
67
+ options = {
68
+ settings: saml_config(get_idp_entity_id(params), request),
69
+ allowed_clock_drift: Devise.allowed_clock_drift_in_seconds,
70
+ }
71
+
72
+ if Devise.saml_validate_in_response_to
73
+ options[:matches_request_id] = request.session[:saml_transaction_id] || "ID_MISSING"
74
+ end
75
+
76
+ options
77
+ end
60
78
  end
61
79
  end
62
80
  end
@@ -1,3 +1,3 @@
1
1
  module DeviseSamlAuthenticatable
2
- VERSION = "1.6.2"
2
+ VERSION = "1.9.1"
3
3
  end
@@ -29,10 +29,20 @@ module Devise
29
29
  @@saml_logger = true
30
30
 
31
31
  # Add valid users to database
32
+ # Can accept a Boolean value or a Proc that is called with the model class, the saml_response and auth_value
33
+ # Ex:
34
+ # Devise.saml_create_user = Proc.new do |model_class, saml_response, auth_value|
35
+ # model_class == Admin
36
+ # end
32
37
  mattr_accessor :saml_create_user
33
38
  @@saml_create_user = false
34
39
 
35
40
  # Update user attributes after login
41
+ # Can accept a Boolean value or a Proc that is called with the model class, the saml_response and auth_value
42
+ # Ex:
43
+ # Devise.saml_update_user = Proc.new do |model_class, saml_response, auth_value|
44
+ # model_class == User
45
+ # end
36
46
  mattr_accessor :saml_update_user
37
47
  @@saml_update_user = false
38
48
 
@@ -56,7 +66,7 @@ module Devise
56
66
 
57
67
  # Reader that can parse entity id from a SAMLMessage
58
68
  mattr_accessor :idp_entity_id_reader
59
- @@idp_entity_id_reader ||= ::DeviseSamlAuthenticatable::DefaultIdpEntityIdReader
69
+ @@idp_entity_id_reader ||= "::DeviseSamlAuthenticatable::DefaultIdpEntityIdReader"
60
70
 
61
71
  # Implements a #handle method that takes the response and strategy as an argument
62
72
  mattr_accessor :saml_failed_callback
@@ -67,9 +77,13 @@ module Devise
67
77
  mattr_accessor :saml_relay_state
68
78
  @@saml_relay_state
69
79
 
80
+ # Validate that the InResponseTo header in SAML responses matches the ID of the request.
81
+ mattr_accessor :saml_validate_in_response_to
82
+ @@saml_validate_in_response_to = false
83
+
70
84
  # Instead of storing the attribute_map in attribute-map.yml, store it in the database, or set it programatically
71
85
  mattr_accessor :saml_attribute_map_resolver
72
- @@saml_attribute_map_resolver ||= ::DeviseSamlAuthenticatable::DefaultAttributeMapResolver
86
+ @@saml_attribute_map_resolver ||= "::DeviseSamlAuthenticatable::DefaultAttributeMapResolver"
73
87
 
74
88
  # Implements a #validate method that takes the retrieved resource and response right after retrieval,
75
89
  # and returns true if it's valid. False will cause authentication to fail.