devise_saml_authenticatable 1.8.0 → 1.9.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2b6dd7d4f718cf0df20aff218f90f1eac720279e4ff5afe6aedef20f84a14fd
4
- data.tar.gz: 5efc5fa9d89ee10eb6328261b6b870ce580dbe7cd48cedbe8dd609786c5c9f84
3
+ metadata.gz: f5dd2cc3931480caf617a4a22266968ea550c52c4980353a5e295652dc25ce4d
4
+ data.tar.gz: b712dd20efd0c4ddd9c8a1321dc6e2cbf8876053689c36c94d272be5445f8fd7
5
5
  SHA512:
6
- metadata.gz: 70c0b6c4e5f6ec2b7f4a421c898c493cb34aef837c119e126d1b557640f685c1c35ad7cddaf94de3598601fe691563fa2984297010b4ac96f539609c8fa55f95
7
- data.tar.gz: ca3d854ab1bd6b84d3a7d2225feb926f9fbc2d6df5c546c975d5773e8bdd8254d5ce544dd08f6c32a0db29e15f9f4aa3bbc38bee1dbdf491d9e92826b00c760b
6
+ metadata.gz: 697615b8dfb2f798ae0fd71b796359825633b783fbebaf66cc947bd33d78ded081a862609b55b6407d87495a7a3b3df4ae9dfce1dd2143d7ce54e2c811727d1e
7
+ data.tar.gz: 74037754bbe52f5036aed75ad1d96cd0a44677f2963f496a5986a8f6be0542f92645f6a3c8fa67e738a9656d2d9697566f41dfbe6f5aa4b8306114e99a182864
data/README.md CHANGED
@@ -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
@@ -89,8 +120,8 @@ In `config/initializers/devise.rb`:
89
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 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.
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.
94
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
@@ -194,20 +225,22 @@ If you only have one IdP, you can use the config file above, or just return a si
194
225
  end
195
226
  ```
196
227
 
197
- ## Supporting Multiple IdPs
228
+ ## IdP Settings Adapter
229
+
230
+ Implementing a custom settings adapter allows you to support multiple Identity Providers, and dynamic application domains with the request object.
198
231
 
199
- 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:
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:
200
233
 
201
234
  ```ruby
202
235
  class IdPSettingsAdapter
203
- def self.settings(idp_entity_id)
236
+ def self.settings(idp_entity_id, request)
204
237
  case idp_entity_id
205
238
  when "http://www.example_idp_entity_id.com"
206
239
  {
207
- assertion_consumer_service_url: "http://localhost:3000/users/saml/auth",
240
+ assertion_consumer_service_url: "#{request.protocol}#{request.host_with_port}/users/saml/auth",
208
241
  assertion_consumer_service_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
209
242
  name_identifier_format: "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
210
- issuer: "http://localhost:3000/saml/metadata",
243
+ issuer: "#{request.protocol}#{request.host_with_port}/saml/metadata",
211
244
  idp_entity_id: "http://www.example_idp_entity_id.com",
212
245
  authn_context: "",
213
246
  idp_slo_service_url: "http://example_idp_slo_service_url.com",
@@ -8,22 +8,22 @@ class Devise::SamlSessionsController < Devise::SessionsController
8
8
 
9
9
  def new
10
10
  idp_entity_id = get_idp_entity_id(params)
11
- request = OneLogin::RubySaml::Authrequest.new
11
+ auth_request = OneLogin::RubySaml::Authrequest.new
12
12
  auth_params = { RelayState: relay_state } if relay_state
13
- action = request.create(saml_config(idp_entity_id), auth_params || {})
14
- session[:saml_transaction_id] = request.request_id if request.respond_to?(:request_id)
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
15
  redirect_to action, allow_other_host: true
16
16
  end
17
17
 
18
18
  def metadata
19
19
  idp_entity_id = params[:idp_entity_id]
20
20
  meta = OneLogin::RubySaml::Metadata.new
21
- render xml: meta.generate(saml_config(idp_entity_id))
21
+ render xml: meta.generate(saml_config(idp_entity_id, request))
22
22
  end
23
23
 
24
24
  def idp_sign_out
25
25
  if params[:SAMLRequest] && Devise.saml_session_index_key
26
- saml_config = saml_config(get_idp_entity_id(params))
26
+ saml_config = saml_config(get_idp_entity_id(params), request)
27
27
  logout_request = OneLogin::RubySaml::SloLogoutrequest.new(params[:SAMLRequest], settings: saml_config)
28
28
  resource_class.reset_session_key_for(logout_request.name_id)
29
29
 
@@ -63,8 +63,8 @@ class Devise::SamlSessionsController < Devise::SessionsController
63
63
  # Override devise to send user to IdP logout for SLO
64
64
  def after_sign_out_path_for(_)
65
65
  idp_entity_id = get_idp_entity_id(params)
66
- request = OneLogin::RubySaml::Logoutrequest.new
67
- 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
68
68
 
69
69
  # Add attributes to saml_settings which will later be used to create the SP
70
70
  # initiated logout request
@@ -73,7 +73,7 @@ class Devise::SamlSessionsController < Devise::SessionsController
73
73
  saml_settings.sessionindex = @sessionindex_for_sp_initiated_logout
74
74
  end
75
75
 
76
- request.create(saml_settings)
76
+ logout_request.create(saml_settings)
77
77
  end
78
78
 
79
79
  # Overried devise: if user is signed out, not create the SP initiated logout request,
@@ -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
 
@@ -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
@@ -19,10 +19,16 @@ module DeviseSamlAuthenticatable
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
- 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
@@ -65,7 +65,7 @@ module Devise
65
65
 
66
66
  def response_options
67
67
  options = {
68
- settings: saml_config(get_idp_entity_id(params)),
68
+ settings: saml_config(get_idp_entity_id(params), request),
69
69
  allowed_clock_drift: Devise.allowed_clock_drift_in_seconds,
70
70
  }
71
71
 
@@ -1,3 +1,3 @@
1
1
  module DeviseSamlAuthenticatable
2
- VERSION = "1.8.0"
2
+ VERSION = "1.9.0"
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
 
@@ -102,7 +102,7 @@ describe Devise::SamlSessionsController, type: :controller do
102
102
  it 'uses the DefaultIdpEntityIdReader' do
103
103
  expect(DeviseSamlAuthenticatable::DefaultIdpEntityIdReader).to receive(:entity_id)
104
104
  do_get
105
- expect(idp_providers_adapter).to have_received(:settings).with(nil)
105
+ expect(idp_providers_adapter).to have_received(:settings).with(nil, request)
106
106
  end
107
107
 
108
108
  context 'with a relay_state lambda defined' do
@@ -137,7 +137,7 @@ describe Devise::SamlSessionsController, type: :controller do
137
137
 
138
138
  it 'redirects to the associated IdP SSO target url' do
139
139
  do_get
140
- expect(idp_providers_adapter).to have_received(:settings).with('http://www.example.com')
140
+ expect(idp_providers_adapter).to have_received(:settings).with('http://www.example.com', request)
141
141
  expect(response).to redirect_to(%r{\Ahttp://idp_sso_url\?SAMLRequest=})
142
142
  end
143
143
  end
@@ -305,7 +305,7 @@ describe Devise::SamlSessionsController, type: :controller do
305
305
  it 'redirects to the associated IdP SLO target url' do
306
306
  do_delete
307
307
  expect(controller).to have_received(:sign_out)
308
- expect(idp_providers_adapter).to have_received(:settings).with('http://www.example.com')
308
+ expect(idp_providers_adapter).to have_received(:settings).with('http://www.example.com', request)
309
309
  expect(response).to redirect_to(%r{\Ahttp://idp_slo_url\?SAMLRequest=})
310
310
  end
311
311
  end
@@ -385,7 +385,7 @@ describe Devise::SamlSessionsController, type: :controller do
385
385
  it 'accepts a LogoutResponse for the associated slo_target_url and redirects to sign_in' do
386
386
  do_post
387
387
  expect(response.status).to eq 302
388
- expect(idp_providers_adapter).to have_received(:settings).with(idp_entity_id)
388
+ expect(idp_providers_adapter).to have_received(:settings).with(idp_entity_id, request)
389
389
  expect(response).to redirect_to 'http://localhost/logout_response'
390
390
  end
391
391
  end
@@ -106,6 +106,32 @@ describe Devise::Models::SamlAuthenticatable do
106
106
  end
107
107
  end
108
108
 
109
+ context "when configured to create a user by a proc and the user is not found" do
110
+ before do
111
+ create_user_proc = -> (model_class, _saml_response, auth_value) { model_class == Model && auth_value == 'user@example.com' }
112
+ allow(Devise).to receive(:saml_create_user).and_return(create_user_proc)
113
+ end
114
+
115
+ context "when the proc returns true" do
116
+ it "creates and returns a new user with the name identifier and given attributes" do
117
+ expect(Model).to receive(:where).with(email: name_id).and_return([])
118
+ model = Model.authenticate_with_saml(response, nil)
119
+ expect(model.email).to eq('user@example.com')
120
+ expect(model.name).to eq('A User')
121
+ expect(model.saved).to be(true)
122
+ end
123
+ end
124
+
125
+ context "when the proc returns false" do
126
+ let(:name_id) { 'do_not_create@example.com' }
127
+
128
+ it "does not creates new user" do
129
+ expect(Model).to receive(:where).with(email: name_id).and_return([])
130
+ expect(Model.authenticate_with_saml(response, nil)).to be_nil
131
+ end
132
+ end
133
+ end
134
+
109
135
  context "when configured to update a user and the user is found" do
110
136
  before do
111
137
  allow(Devise).to receive(:saml_update_user).and_return(true)
@@ -120,8 +146,39 @@ describe Devise::Models::SamlAuthenticatable do
120
146
  expect(model.saved).to be(true)
121
147
  end
122
148
  end
149
+
150
+ context "when configured to update a user by a proc and the user is found" do
151
+ let(:user) { Model.new(email: 'old_mail@mail.com', name: 'old name', new_record: false) }
152
+
153
+ before do
154
+ update_user_proc = -> (model_class, _saml_response, auth_value) { model_class == Model && auth_value == 'user@example.com' }
155
+ allow(Devise).to receive(:saml_update_user).and_return(update_user_proc)
156
+ end
157
+
158
+ context "when the proc returns true" do
159
+ it "updates user with given attributes" do
160
+ expect(Model).to receive(:where).with(email: name_id).and_return([user])
161
+ model = Model.authenticate_with_saml(response, nil)
162
+ expect(model.email).to eq('user@example.com')
163
+ expect(model.name).to eq('A User')
164
+ expect(model.saved).to be(true)
165
+ end
166
+ end
167
+
168
+ context "when the proc returns false" do
169
+ let(:name_id) { 'do_not_update@example.com' }
170
+
171
+ it "does not update user" do
172
+ expect(Model).to receive(:where).with(email: name_id).and_return([user])
173
+ model = Model.authenticate_with_saml(response, nil)
174
+ expect(model.email).to eq('old_mail@mail.com')
175
+ expect(model.name).to eq('old name')
176
+ end
177
+ end
178
+ end
123
179
  end
124
180
 
181
+
125
182
  context "when configured to create an user and the user is not found" do
126
183
  before do
127
184
  allow(Devise).to receive(:saml_create_user).and_return(true)
@@ -136,6 +193,35 @@ describe Devise::Models::SamlAuthenticatable do
136
193
  end
137
194
  end
138
195
 
196
+ context "when configured to create a user by a proc and the user is not found" do
197
+ let(:create_user_proc) { -> (_model_class, saml_response, _auth_value) { saml_response.raw_response.issuers.first == 'to_create_idp' } }
198
+
199
+ before do
200
+ allow(Devise).to receive(:saml_create_user).and_return(create_user_proc)
201
+ end
202
+
203
+ context "when the proc returns true" do
204
+ let(:response) { double(:response, issuers: ['to_create_idp'], attributes: attributes, name_id: name_id) }
205
+
206
+ it "creates and returns a new user with the name identifier and given attributes" do
207
+ expect(Model).to receive(:where).with(email: 'user@example.com').and_return([])
208
+ model = Model.authenticate_with_saml(response, nil)
209
+ expect(model.email).to eq('user@example.com')
210
+ expect(model.name).to eq('A User')
211
+ expect(model.saved).to be(true)
212
+ end
213
+ end
214
+
215
+ context "when the proc returns false" do
216
+ let(:response) { double(:response, issuers: ['do_not_create_idp'], attributes: attributes, name_id: name_id) }
217
+
218
+ it "does not creates new user" do
219
+ expect(Model).to receive(:where).with(email: 'user@example.com').and_return([])
220
+ expect(Model.authenticate_with_saml(response, nil)).to be_nil
221
+ end
222
+ end
223
+ end
224
+
139
225
  context "when configured to update an user" do
140
226
  before do
141
227
  allow(Devise).to receive(:saml_update_user).and_return(true)
@@ -156,6 +242,38 @@ describe Devise::Models::SamlAuthenticatable do
156
242
  end
157
243
  end
158
244
 
245
+ context "when configured to update a user by a proc and the user is found" do
246
+ let(:user) { Model.new(email: 'old_mail@mail.com', name: 'old name', new_record: false) }
247
+ let(:update_user_proc) { -> (_model_class, saml_response, _auth_value) { saml_response.raw_response.issuers.first == 'to_update_idp' } }
248
+
249
+ before do
250
+ allow(Devise).to receive(:saml_update_user).and_return(update_user_proc)
251
+ end
252
+
253
+ context "when the proc returns true" do
254
+ let(:response) { double(:response, issuers: ['to_update_idp'], attributes: attributes, name_id: name_id) }
255
+
256
+ it "updates user with given attributes" do
257
+ expect(Model).to receive(:where).with(email: 'user@example.com').and_return([user])
258
+ model = Model.authenticate_with_saml(response, nil)
259
+ expect(model.email).to eq('user@example.com')
260
+ expect(model.name).to eq('A User')
261
+ expect(model.saved).to be(true)
262
+ end
263
+ end
264
+
265
+ context "when the proc returns false" do
266
+ let(:response) { double(:response, issuers: ['do_not_update_idp'], attributes: attributes, name_id: name_id) }
267
+
268
+ it "does not update user" do
269
+ expect(Model).to receive(:where).with(email: 'user@example.com').and_return([user])
270
+ model = Model.authenticate_with_saml(response, nil)
271
+ expect(model.email).to eq('old_mail@mail.com')
272
+ expect(model.name).to eq('old name')
273
+ end
274
+ end
275
+ end
276
+
159
277
  context "when configured with a case-insensitive key" do
160
278
  shared_examples "correct downcasing" do
161
279
  before do
@@ -56,9 +56,9 @@ describe Devise::Strategies::SamlAuthenticatable do
56
56
  context "when saml config uses an idp_adapter" do
57
57
  let(:idp_providers_adapter) {
58
58
  Class.new {
59
- def self.settings(idp_entity_id)
59
+ def self.settings(idp_entity_id, request)
60
60
  base = {
61
- assertion_consumer_service_url: "acs_url",
61
+ assertion_consumer_service_url: "acs url",
62
62
  assertion_consumer_service_binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
63
63
  name_identifier_format: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
64
64
  issuer: "sp_issuer",
@@ -93,7 +93,7 @@ describe Devise::Strategies::SamlAuthenticatable do
93
93
 
94
94
  it "authenticates with the response for the corresponding idp" do
95
95
  expect(OneLogin::RubySaml::Response).to receive(:new).with(params[:SAMLResponse], anything)
96
- expect(idp_providers_adapter).to receive(:settings).with(idp_entity_id)
96
+ expect(idp_providers_adapter).to receive(:settings).with(idp_entity_id, anything)
97
97
  expect(user_class).to receive(:authenticate_with_saml).with(response, params[:RelayState])
98
98
  expect(user).to receive(:after_saml_authentication).with(response.sessionindex)
99
99
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devise_saml_authenticatable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.0
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josef Sauter
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-19 00:00:00.000000000 Z
11
+ date: 2022-04-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: devise