devise_saml_authenticatable 1.6.1 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3ca38cfe51cdf71fc8a264de868fc285bfa413307aa4798e097fbf88e85020d
4
- data.tar.gz: 2371ef1371e29547b4eb9681066fa43f0b6d39236b7df8676236bf13678e2d01
3
+ metadata.gz: c2b6dd7d4f718cf0df20aff218f90f1eac720279e4ff5afe6aedef20f84a14fd
4
+ data.tar.gz: 5efc5fa9d89ee10eb6328261b6b870ce580dbe7cd48cedbe8dd609786c5c9f84
5
5
  SHA512:
6
- metadata.gz: 872bfb214a00504735d9183d169e4119ebb9c6ae3129de729be642f39c4675b57040bb1c9d46899e73d534aa82d7ac9291c425109c10eaeb290831480e7976ab
7
- data.tar.gz: 3269ef293937aef443afa9a325acc0addf027c3ad7421865a9ebe1554d7215461767ffe9e34cd4cc2bf8edbae831e7e342dd3c47245d57209d8ddd820a128212
6
+ metadata.gz: 70c0b6c4e5f6ec2b7f4a421c898c493cb34aef837c119e126d1b557640f685c1c35ad7cddaf94de3598601fe691563fa2984297010b4ac96f539609c8fa55f95
7
+ data.tar.gz: ca3d854ab1bd6b84d3a7d2225feb926f9fbc2d6df5c546c975d5773e8bdd8254d5ce544dd08f6c32a0db29e15f9f4aa3bbc38bee1dbdf491d9e92826b00c760b
@@ -0,0 +1,52 @@
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.1"
16
+ - "3.0"
17
+ - "2.7"
18
+ - "2.6"
19
+ gemfile:
20
+ - Gemfile
21
+ - spec/support/Gemfile.rails6.1
22
+ - spec/support/Gemfile.rails6
23
+ - spec/support/Gemfile.rails5.2
24
+ bundler:
25
+ - "2"
26
+ exclude:
27
+ - ruby: "2.6"
28
+ gemfile: Gemfile
29
+ bundler: "2"
30
+ - ruby: "3.0"
31
+ gemfile: spec/support/Gemfile.rails5.2
32
+ bundler: "2"
33
+ - ruby: "3.0"
34
+ gemfile: spec/support/Gemfile.rails6
35
+ bundler: "2"
36
+ - ruby: "3.1"
37
+ gemfile: spec/support/Gemfile.rails5.2
38
+ bundler: "2"
39
+ - ruby: "3.1"
40
+ gemfile: spec/support/Gemfile.rails6
41
+ bundler: "2"
42
+ runs-on: ubuntu-latest
43
+ env:
44
+ BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfile }}
45
+ steps:
46
+ - uses: actions/checkout@v2
47
+ - uses: ruby/setup-ruby@v1
48
+ with:
49
+ bundler: ${{ matrix.bundler }}
50
+ ruby-version: ${{ matrix.ruby }}
51
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
52
+ - 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
 
@@ -85,21 +85,21 @@ In `config/initializers/devise.rb`:
85
85
  # for the user's session to facilitate an IDP initiated logout request.
86
86
  config.saml_session_index_key = :session_index
87
87
 
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.
88
+ # You can set this value to use Subject or SAML assertion as info to which email will be compared.
89
+ # If you don't set it then email will be extracted from SAML assertion attributes.
90
90
  config.saml_use_subject = true
91
91
 
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
92
+ # You can support multiple IdPs by setting this value to the name of a class that implements a ::settings method
93
+ # which takes an IdP entity id as an argument and returns a hash of idp settings for the corresponding IdP.
94
+ # config.idp_settings_adapter = "MyIdPSettingsAdapter"
95
95
 
96
96
  # 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
97
+ # by setting this to the name of a custom reader class, or use the default.
98
+ # config.idp_entity_id_reader = "DeviseSamlAuthenticatable::DefaultIdpEntityIdReader"
99
99
 
100
- # You can set a handler object that takes the response for a failed SAML request and the strategy,
100
+ # You can set the name of a class that takes the response for a failed SAML request and the strategy,
101
101
  # and implements a #handle method. This method can then redirect the user, return error messages, etc.
102
- # config.saml_failed_callback = nil
102
+ # config.saml_failed_callback = "MySamlFailedCallbacksHandler"
103
103
 
104
104
  # You can customize the named routes generated in case of named route collisions with
105
105
  # other Devise modules or libraries. Set the saml_route_helper_prefix to a string that will
@@ -111,16 +111,19 @@ In `config/initializers/devise.rb`:
111
111
  # This is a time in seconds.
112
112
  # config.allowed_clock_drift_in_seconds = 0
113
113
 
114
+ # In SAML responses, validate that the identity provider has included an InResponseTo
115
+ # header that matches the ID of the SAML request. (Default is false)
116
+ # config.saml_validate_in_response_to = false
117
+
114
118
  # Configure with your SAML settings (see ruby-saml's README for more information: https://github.com/onelogin/ruby-saml).
115
119
  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
120
  settings.assertion_consumer_service_url = "http://localhost:3000/users/saml/auth"
118
121
  settings.assertion_consumer_service_binding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
119
122
  settings.name_identifier_format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
120
123
  settings.issuer = "http://localhost:3000/saml/metadata"
121
124
  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"
125
+ settings.idp_slo_service_url = "http://localhost/simplesaml/www/saml2/idp/SingleLogoutService.php"
126
+ settings.idp_sso_service_url = "http://localhost/simplesaml/www/saml2/idp/SSOService.php"
124
127
  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
128
  settings.idp_cert_fingerprint_algorithm = "http://www.w3.org/2000/09/xmldsig#sha1"
126
129
  end
@@ -169,7 +172,7 @@ If you only have one IdP, you can use the config file above, or just return a si
169
172
  ...
170
173
  # ==> Configuration for :saml_authenticatable
171
174
 
172
- config.saml_attribute_map_resolver = MyAttributeMapResolver
175
+ config.saml_attribute_map_resolver = "MyAttributeMapResolver"
173
176
  end
174
177
  ```
175
178
 
@@ -207,8 +210,8 @@ class IdPSettingsAdapter
207
210
  issuer: "http://localhost:3000/saml/metadata",
208
211
  idp_entity_id: "http://www.example_idp_entity_id.com",
209
212
  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",
213
+ idp_slo_service_url: "http://example_idp_slo_service_url.com",
214
+ idp_sso_service_url: "http://example_idp_sso_service_url.com",
212
215
  idp_cert: "example_idp_cert"
213
216
  }
214
217
  when "http://www.another_idp_entity_id.biz"
@@ -219,8 +222,8 @@ class IdPSettingsAdapter
219
222
  issuer: "http://localhost:3000/saml/metadata",
220
223
  idp_entity_id: "http://www.another_idp_entity_id.biz",
221
224
  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",
225
+ idp_slo_service_url: "http://another_idp_slo_service_url.com",
226
+ idp_sso_service_url: "http://another_idp_sso_service_url.com",
224
227
  idp_cert: "another_idp_cert"
225
228
  }
226
229
  else
@@ -1,28 +1,24 @@
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
11
  request = OneLogin::RubySaml::Authrequest.new
17
12
  auth_params = { RelayState: relay_state } if relay_state
18
13
  action = request.create(saml_config(idp_entity_id), auth_params || {})
19
- redirect_to action
14
+ session[:saml_transaction_id] = request.request_id if 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))
26
22
  end
27
23
 
28
24
  def idp_sign_out
@@ -31,7 +27,7 @@ class Devise::SamlSessionsController < Devise::SessionsController
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,18 +45,19 @@ 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
@@ -79,12 +76,21 @@ class Devise::SamlSessionsController < Devise::SessionsController
79
76
  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
@@ -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
@@ -66,12 +66,7 @@ module Devise
66
66
  end
67
67
 
68
68
  if Devise.saml_update_user || (resource.new_record? && Devise.saml_create_user)
69
- begin
70
- Devise.saml_update_resource_hook.call(resource, decorated_response, auth_value)
71
- rescue
72
- logger.info("User(#{auth_value}) failed to create or update.")
73
- return nil
74
- end
69
+ Devise.saml_update_resource_hook.call(resource, decorated_response, auth_value)
75
70
  end
76
71
 
77
72
  resource
@@ -87,7 +82,15 @@ module Devise
87
82
  end
88
83
 
89
84
  def attribute_map(saml_response = nil)
90
- Devise.saml_attribute_map_resolver.new(saml_response).attribute_map
85
+ attribute_map_resolver.new(saml_response).attribute_map
86
+ end
87
+
88
+ def attribute_map_resolver
89
+ if Devise.saml_attribute_map_resolver.respond_to?(:new)
90
+ Devise.saml_attribute_map_resolver
91
+ else
92
+ Devise.saml_attribute_map_resolver.constantize
93
+ end
91
94
  end
92
95
  end
93
96
  end
@@ -22,7 +22,7 @@ module DeviseSamlAuthenticatable
22
22
  def adapter_based_config(idp_entity_id)
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
+ idp_settings_adapter.settings(idp_entity_id).each do |k,v|
26
26
  acc = "#{k.to_s}=".to_sym
27
27
 
28
28
  if config.respond_to? acc
@@ -33,7 +33,23 @@ module DeviseSamlAuthenticatable
33
33
  end
34
34
 
35
35
  def get_idp_entity_id(params)
36
- Devise.idp_entity_id_reader.entity_id(params)
36
+ idp_entity_id_reader.entity_id(params)
37
+ end
38
+
39
+ def idp_entity_id_reader
40
+ if Devise.idp_entity_id_reader.respond_to?(:entity_id)
41
+ Devise.idp_entity_id_reader
42
+ else
43
+ @idp_entity_id_reader ||= Devise.idp_entity_id_reader.constantize
44
+ end
45
+ end
46
+
47
+ def idp_settings_adapter
48
+ if Devise.idp_settings_adapter.respond_to?(:settings)
49
+ Devise.idp_settings_adapter
50
+ else
51
+ @idp_settings_adapter ||= Devise.idp_settings_adapter.constantize
52
+ end
37
53
  end
38
54
  end
39
55
  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)),
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.1"
2
+ VERSION = "1.8.0"
3
3
  end
@@ -56,7 +56,7 @@ module Devise
56
56
 
57
57
  # Reader that can parse entity id from a SAMLMessage
58
58
  mattr_accessor :idp_entity_id_reader
59
- @@idp_entity_id_reader ||= ::DeviseSamlAuthenticatable::DefaultIdpEntityIdReader
59
+ @@idp_entity_id_reader ||= "::DeviseSamlAuthenticatable::DefaultIdpEntityIdReader"
60
60
 
61
61
  # Implements a #handle method that takes the response and strategy as an argument
62
62
  mattr_accessor :saml_failed_callback
@@ -67,9 +67,13 @@ module Devise
67
67
  mattr_accessor :saml_relay_state
68
68
  @@saml_relay_state
69
69
 
70
+ # Validate that the InResponseTo header in SAML responses matches the ID of the request.
71
+ mattr_accessor :saml_validate_in_response_to
72
+ @@saml_validate_in_response_to = false
73
+
70
74
  # Instead of storing the attribute_map in attribute-map.yml, store it in the database, or set it programatically
71
75
  mattr_accessor :saml_attribute_map_resolver
72
- @@saml_attribute_map_resolver ||= ::DeviseSamlAuthenticatable::DefaultAttributeMapResolver
76
+ @@saml_attribute_map_resolver ||= "::DeviseSamlAuthenticatable::DefaultAttributeMapResolver"
73
77
 
74
78
  # Implements a #validate method that takes the retrieved resource and response right after retrieval,
75
79
  # and returns true if it's valid. False will cause authentication to fail.