devise_saml_authenticatable 1.8.0 → 1.9.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: 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