devise_saml_authenticatable 1.6.2 → 1.9.1
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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +62 -0
- data/.gitignore +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +12 -2
- data/README.md +62 -26
- data/app/controllers/devise/saml_sessions_controller.rb +33 -27
- data/devise_saml_authenticatable.gemspec +2 -1
- data/lib/devise_saml_authenticatable/logger.rb +2 -2
- data/lib/devise_saml_authenticatable/model.rb +17 -3
- data/lib/devise_saml_authenticatable/saml_config.rb +28 -6
- data/lib/devise_saml_authenticatable/strategy.rb +23 -5
- data/lib/devise_saml_authenticatable/version.rb +1 -1
- data/lib/devise_saml_authenticatable.rb +16 -2
- data/spec/controllers/devise/saml_sessions_controller_spec.rb +205 -147
- data/spec/devise_saml_authenticatable/model_spec.rb +137 -19
- data/spec/devise_saml_authenticatable/saml_config_spec.rb +69 -22
- data/spec/devise_saml_authenticatable/strategy_spec.rb +58 -9
- data/spec/features/saml_authentication_spec.rb +19 -6
- data/spec/support/Gemfile.rails5.2 +2 -13
- data/spec/support/Gemfile.rails6 +18 -0
- data/spec/support/Gemfile.rails6.1 +24 -0
- data/spec/support/idp_settings_adapter.rb.erb +19 -9
- data/spec/support/idp_template.rb +5 -13
- data/spec/support/rails_app.rb +6 -7
- data/spec/support/ruby_saml_support.rb +10 -0
- data/spec/support/saml_idp_controller.rb.erb +1 -6
- data/spec/support/sp_template.rb +22 -19
- metadata +14 -12
- data/.travis.yml +0 -52
- data/spec/support/Gemfile.rails4 +0 -41
- data/spec/support/Gemfile.rails5 +0 -25
- data/spec/support/Gemfile.rails5.1 +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d503c9931a5af5182f1f6910dcfc548d692fcc3e45ad2fb464b3931c4791ac59
|
4
|
+
data.tar.gz: 6da638f28754c2a8f9d44d38a8a61f0796b04e023e0dba8d845fb60bc004bebe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1d44c5c95a396f22008c33b4636fd201c6d6ec71a5193909bb6cc1aa59f660301831cada23e589b05ed52260d8a1a2e42a7442a72757f821ef6ede752a26554e
|
7
|
+
data.tar.gz: '0603740818f257bc90e63f4732c59c6d8a686e0d28c9dedb6cf8ce877a06234e51d39376602d105660e3e25744dfe0163dec931b436ec3bfbe9aabbf06167e36'
|
@@ -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
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', '~>
|
9
|
+
gem 'rails', '~> 7.0.0'
|
10
10
|
gem 'rspec-rails'
|
11
11
|
gem 'sqlite3', '~> 1.4.0'
|
12
12
|
gem 'capybara'
|
13
|
-
gem '
|
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 (`
|
61
|
-
- SLO endpoint (`
|
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
|
89
|
-
# If you don't set it then email will be extracted from SAML
|
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
|
93
|
-
# an IdP entity id as
|
94
|
-
config.idp_settings_adapter =
|
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
|
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 =
|
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.
|
154
|
+
settings.sp_entity_id = "http://localhost:3000/saml/metadata"
|
121
155
|
settings.authn_context = ""
|
122
|
-
settings.
|
123
|
-
settings.
|
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
|
-
##
|
228
|
+
## IdP Settings Adapter
|
195
229
|
|
196
|
-
|
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: "
|
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
|
-
|
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
|
-
|
211
|
-
|
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
|
-
|
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
|
-
|
223
|
-
|
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
|
1
|
+
require 'ruby-saml'
|
2
2
|
|
3
3
|
class Devise::SamlSessionsController < Devise::SessionsController
|
4
4
|
include DeviseSamlAuthenticatable::SamlConfig
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
+
auth_request = OneLogin::RubySaml::Authrequest.new
|
17
12
|
auth_params = { RelayState: relay_state } if relay_state
|
18
|
-
action =
|
19
|
-
|
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 :
|
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 ==
|
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
|
-
|
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
|
-
|
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
|
-
|
76
|
+
logout_request.create(saml_settings)
|
80
77
|
end
|
81
78
|
|
82
|
-
|
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
|
-
|
85
|
-
|
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
|
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
|
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
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
@@ -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.
|