devise_saml_authenticatable 1.3.0 → 1.3.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/.gitignore +1 -1
- data/.travis.yml +23 -4
- data/Gemfile +3 -2
- data/README.md +1 -0
- data/app/controllers/devise/saml_sessions_controller.rb +9 -2
- data/lib/devise_saml_authenticatable.rb +5 -0
- data/lib/devise_saml_authenticatable/model.rb +3 -2
- data/lib/devise_saml_authenticatable/strategy.rb +27 -9
- data/lib/devise_saml_authenticatable/version.rb +1 -1
- data/spec/controllers/devise/saml_sessions_controller_spec.rb +97 -49
- data/spec/devise_saml_authenticatable/model_spec.rb +33 -16
- data/spec/devise_saml_authenticatable/strategy_spec.rb +35 -4
- data/spec/features/saml_authentication_spec.rb +2 -2
- data/spec/support/Gemfile.rails4 +23 -0
- data/spec/support/Gemfile.ruby-saml-1.3 +23 -0
- data/spec/support/idp_template.rb +0 -3
- data/spec/support/rails_app.rb +4 -6
- data/spec/support/sp_template.rb +4 -4
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eeabad37a5473557499db0b8de604afb7740608d
|
4
|
+
data.tar.gz: 952108992254613cdf27f72aedbf6e2a772f95a3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 720d7b21c40596b762a85c5c0df2f9dc3d1af7fdce86a7c8562428b866e79eab6a768e070c648fd4c657415bca175102d4eaa0dff6866c7cc8b14025d1bf5101
|
7
|
+
data.tar.gz: c3368984264f54a99a37ce9e4b546b7bbd2460a834bc57300dfaf242232619bad50f51bb66adc4d2e1e41d287ce4f3f7f85923697835a06bf14f73968a31f1ed
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -2,11 +2,30 @@ language: ruby
|
|
2
2
|
rvm:
|
3
3
|
- "1.9.3"
|
4
4
|
- "2.0.0"
|
5
|
-
- "2.1.
|
6
|
-
- "2.2.
|
5
|
+
- "2.1.9"
|
6
|
+
- "2.2.5"
|
7
|
+
- "2.3.1"
|
8
|
+
gemfile:
|
9
|
+
- Gemfile
|
10
|
+
- spec/support/Gemfile.rails4
|
11
|
+
- spec/support/Gemfile.ruby-saml-1.3
|
12
|
+
matrix:
|
13
|
+
allow_failures:
|
14
|
+
- rvm: "1.9.3"
|
15
|
+
gemfile: Gemfile
|
16
|
+
- rvm: "1.9.3"
|
17
|
+
gemfile: spec/support/Gemfile.ruby-saml-1.3
|
18
|
+
- rvm: "2.0.0"
|
19
|
+
gemfile: Gemfile
|
20
|
+
- rvm: "2.0.0"
|
21
|
+
gemfile: spec/support/Gemfile.ruby-saml-1.3
|
22
|
+
- rvm: "2.1.9"
|
23
|
+
gemfile: Gemfile
|
24
|
+
- rvm: "2.1.9"
|
25
|
+
gemfile: spec/support/Gemfile.ruby-saml-1.3
|
7
26
|
|
8
27
|
script:
|
9
|
-
-
|
28
|
+
- bundle exec rake
|
10
29
|
|
11
30
|
notifications:
|
12
31
|
hipchat:
|
@@ -15,4 +34,4 @@ notifications:
|
|
15
34
|
template:
|
16
35
|
- '%{repository}<a href="%{build_url}">#%{build_number}</a> (%{branch} - <a href="%{compare_url}">%{commit}</a> : %{author}): %{message}'
|
17
36
|
format: html
|
18
|
-
on_pull_requests:
|
37
|
+
on_pull_requests: true
|
data/Gemfile
CHANGED
@@ -6,10 +6,11 @@ gemspec
|
|
6
6
|
group :test do
|
7
7
|
gem 'rake'
|
8
8
|
gem 'rspec', '~> 3.0'
|
9
|
-
gem 'rails', '~>
|
9
|
+
gem 'rails', '~> 5.0'
|
10
10
|
gem 'rspec-rails'
|
11
11
|
gem 'sqlite3'
|
12
|
-
gem 'capybara
|
12
|
+
gem 'capybara'
|
13
|
+
gem 'poltergeist'
|
13
14
|
|
14
15
|
# Lock down versions of gems for older versions of Ruby
|
15
16
|
if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.0")
|
data/README.md
CHANGED
@@ -160,6 +160,7 @@ end
|
|
160
160
|
Detecting the entity ID passed to the `settings` method is done by `config.idp_entity_id_reader`.
|
161
161
|
By default this will find the `Issuer` in the SAML request.
|
162
162
|
You can support more use cases by writing your own and implementing the `.entity_id` method.
|
163
|
+
If you use encrypted assertions, your entity ID reader will need to understand how to decrypt the response from each of the possible IdPs.
|
163
164
|
|
164
165
|
## Identity Provider
|
165
166
|
|
@@ -3,12 +3,13 @@ require "ruby-saml"
|
|
3
3
|
class Devise::SamlSessionsController < Devise::SessionsController
|
4
4
|
include DeviseSamlAuthenticatable::SamlConfig
|
5
5
|
unloadable if Rails::VERSION::MAJOR < 4
|
6
|
-
skip_before_filter :verify_authenticity_token
|
6
|
+
skip_before_filter :verify_authenticity_token, raise: false
|
7
7
|
|
8
8
|
def new
|
9
9
|
idp_entity_id = get_idp_entity_id(params)
|
10
10
|
request = OneLogin::RubySaml::Authrequest.new
|
11
|
-
|
11
|
+
auth_params = { RelayState: relay_state } if relay_state
|
12
|
+
action = request.create(saml_config(idp_entity_id), auth_params || {})
|
12
13
|
redirect_to action
|
13
14
|
end
|
14
15
|
|
@@ -40,6 +41,12 @@ class Devise::SamlSessionsController < Devise::SessionsController
|
|
40
41
|
|
41
42
|
protected
|
42
43
|
|
44
|
+
def relay_state
|
45
|
+
@relay_state ||= if Devise.saml_relay_state.present?
|
46
|
+
Devise.saml_relay_state.call(request)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
43
50
|
# Override devise to send user to IdP logout for SLO
|
44
51
|
def after_sign_out_path_for(_)
|
45
52
|
request = OneLogin::RubySaml::Logoutrequest.new
|
@@ -57,6 +57,11 @@ module Devise
|
|
57
57
|
mattr_accessor :saml_failed_callback
|
58
58
|
@@saml_failed_callback
|
59
59
|
|
60
|
+
# lambda that generates the RelayState param for the SAML AuthRequest, takes request
|
61
|
+
# from SamlSessionsController#new action as an argument
|
62
|
+
mattr_accessor :saml_relay_state
|
63
|
+
@@saml_relay_state
|
64
|
+
|
60
65
|
mattr_accessor :saml_config
|
61
66
|
@@saml_config = OneLogin::RubySaml::Settings.new
|
62
67
|
def self.saml_configure
|
@@ -28,7 +28,7 @@ module Devise
|
|
28
28
|
end
|
29
29
|
|
30
30
|
module ClassMethods
|
31
|
-
def authenticate_with_saml(saml_response)
|
31
|
+
def authenticate_with_saml(saml_response, relay_state)
|
32
32
|
key = Devise.saml_default_user_key
|
33
33
|
attributes = saml_response.attributes
|
34
34
|
if (Devise.saml_use_subject)
|
@@ -36,8 +36,9 @@ module Devise
|
|
36
36
|
else
|
37
37
|
inv_attr = attribute_map.invert
|
38
38
|
auth_value = attributes[inv_attr[key.to_s]]
|
39
|
-
auth_value.try(:downcase!) if Devise.case_insensitive_keys.include?(key)
|
40
39
|
end
|
40
|
+
auth_value.try(:downcase!) if Devise.case_insensitive_keys.include?(key)
|
41
|
+
|
41
42
|
resource = where(key => auth_value).first
|
42
43
|
|
43
44
|
if resource.nil?
|
@@ -13,14 +13,11 @@ module Devise
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def authenticate!
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
resource.after_saml_authentication(@response.sessionindex)
|
20
|
-
success!(resource)
|
21
|
-
else
|
22
|
-
fail!(:invalid)
|
23
|
-
Devise.saml_failed_callback.new.handle(@response, self) if Devise.saml_failed_callback
|
16
|
+
parse_saml_response
|
17
|
+
retrieve_resource unless self.halted?
|
18
|
+
unless self.halted?
|
19
|
+
@resource.after_saml_authentication(@response.sessionindex)
|
20
|
+
success!(@resource)
|
24
21
|
end
|
25
22
|
end
|
26
23
|
|
@@ -28,7 +25,28 @@ module Devise
|
|
28
25
|
# Any known way on how to let the IdP send the CSRF token along with the SAMLResponse ?
|
29
26
|
# Please let me know!
|
30
27
|
def store?
|
31
|
-
|
28
|
+
!mapping.to.skip_session_storage.include?(:saml_auth)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def parse_saml_response
|
33
|
+
@response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], settings: saml_config(get_idp_entity_id(params)))
|
34
|
+
unless @response.is_valid?
|
35
|
+
failed_auth("Auth errors: #{@response.errors.join(', ')}")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def retrieve_resource
|
40
|
+
@resource = mapping.to.authenticate_with_saml(@response, params[:RelayState])
|
41
|
+
if @resource.nil?
|
42
|
+
failed_auth("Resource could not be found")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def failed_auth(msg)
|
47
|
+
DeviseSamlAuthenticatable::Logger.send(msg)
|
48
|
+
fail!(:invalid)
|
49
|
+
Devise.saml_failed_callback.new.handle(@response, self) if Devise.saml_failed_callback
|
32
50
|
end
|
33
51
|
|
34
52
|
end
|
@@ -38,9 +38,17 @@ describe Devise::SamlSessionsController, type: :controller do
|
|
38
38
|
describe '#new' do
|
39
39
|
let(:saml_response) { File.read(File.join(File.dirname(__FILE__), '../../support', 'response_encrypted_nameid.xml.base64')) }
|
40
40
|
|
41
|
+
subject(:do_get) {
|
42
|
+
if Rails::VERSION::MAJOR > 4
|
43
|
+
get :new, params: {"SAMLResponse" => saml_response}
|
44
|
+
else
|
45
|
+
get :new, "SAMLResponse" => saml_response
|
46
|
+
end
|
47
|
+
}
|
48
|
+
|
41
49
|
context "when using the default saml config" do
|
42
50
|
it "redirects to the IdP SSO target url" do
|
43
|
-
|
51
|
+
do_get
|
44
52
|
expect(response).to redirect_to(%r(\Ahttp://localhost:8009/saml/auth\?SAMLRequest=))
|
45
53
|
end
|
46
54
|
end
|
@@ -51,13 +59,23 @@ describe Devise::SamlSessionsController, type: :controller do
|
|
51
59
|
end
|
52
60
|
|
53
61
|
it "redirects to the associated IdP SSO target url" do
|
54
|
-
|
62
|
+
do_get
|
55
63
|
expect(response).to redirect_to(%r(\Ahttp://idp_sso_url\?SAMLRequest=))
|
56
64
|
end
|
57
65
|
|
58
66
|
it "uses the DefaultIdpEntityIdReader" do
|
59
67
|
expect(DeviseSamlAuthenticatable::DefaultIdpEntityIdReader).to receive(:entity_id)
|
60
|
-
|
68
|
+
do_get
|
69
|
+
end
|
70
|
+
|
71
|
+
context "with a relay_state lambda defined" do
|
72
|
+
let(:relay_state) { ->(request) { "123" } }
|
73
|
+
|
74
|
+
it "includes the RelayState param in the request to the IdP" do
|
75
|
+
expect(Devise).to receive(:saml_relay_state).at_least(:once).and_return(relay_state)
|
76
|
+
do_get
|
77
|
+
expect(response).to redirect_to(%r(\Ahttp://idp_sso_url\?SAMLRequest=.*&RelayState=123))
|
78
|
+
end
|
61
79
|
end
|
62
80
|
|
63
81
|
context "with a specified idp entity id reader" do
|
@@ -67,6 +85,14 @@ describe Devise::SamlSessionsController, type: :controller do
|
|
67
85
|
end
|
68
86
|
end
|
69
87
|
|
88
|
+
subject(:do_get) {
|
89
|
+
if Rails::VERSION::MAJOR > 4
|
90
|
+
get :new, params: {entity_id: "http://www.example.com"}
|
91
|
+
else
|
92
|
+
get :new, entity_id: "http://www.example.com"
|
93
|
+
end
|
94
|
+
}
|
95
|
+
|
70
96
|
before do
|
71
97
|
@default_reader = Devise.idp_entity_id_reader
|
72
98
|
Devise.idp_entity_id_reader = OurIdpEntityIdReader # which will have some different behavior
|
@@ -77,7 +103,7 @@ describe Devise::SamlSessionsController, type: :controller do
|
|
77
103
|
end
|
78
104
|
|
79
105
|
it "redirects to the associated IdP SSO target url" do
|
80
|
-
|
106
|
+
do_get
|
81
107
|
expect(response).to redirect_to(%r(\Ahttp://idp_sso_url\?SAMLRequest=))
|
82
108
|
end
|
83
109
|
end
|
@@ -129,76 +155,98 @@ describe Devise::SamlSessionsController, type: :controller do
|
|
129
155
|
end
|
130
156
|
|
131
157
|
describe '#idp_sign_out' do
|
132
|
-
let(:name_id) { '12312312' }
|
133
|
-
let(:saml_request) { double(:slo_logoutrequest, {
|
134
|
-
id: 42,
|
135
|
-
name_id: name_id,
|
136
|
-
issuer: "http://www.example.com"
|
137
|
-
}) }
|
138
158
|
let(:saml_response) { double(:slo_logoutresponse) }
|
139
159
|
let(:response_url) { 'http://localhost/logout_response' }
|
140
|
-
|
141
160
|
before do
|
142
|
-
allow(OneLogin::RubySaml::SloLogoutrequest).to receive(:new).and_return(saml_request)
|
143
161
|
allow(OneLogin::RubySaml::SloLogoutresponse).to receive(:new).and_return(saml_response)
|
144
162
|
allow(saml_response).to receive(:create).and_return(response_url)
|
145
163
|
end
|
146
164
|
|
147
|
-
it 'returns invalid request if SAMLRequest is not passed' do
|
148
|
-
expect(User).not_to receive(:reset_session_key_for)
|
165
|
+
it 'returns invalid request if SAMLRequest or SAMLResponse is not passed' do
|
166
|
+
expect(User).not_to receive(:reset_session_key_for)
|
149
167
|
post :idp_sign_out
|
150
168
|
expect(response.status).to eq 500
|
151
169
|
end
|
152
170
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
171
|
+
context "when receiving a logout response from the IdP after redirecting an SP logout request" do
|
172
|
+
subject(:do_post) {
|
173
|
+
if Rails::VERSION::MAJOR > 4
|
174
|
+
post :idp_sign_out, params: {SAMLResponse: "stubbed_response"}
|
175
|
+
else
|
176
|
+
post :idp_sign_out, SAMLResponse: "stubbed_response"
|
177
|
+
end
|
178
|
+
}
|
158
179
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
180
|
+
it 'accepts a LogoutResponse and redirects sign_in' do
|
181
|
+
do_post
|
182
|
+
expect(response.status).to eq 302
|
183
|
+
expect(response).to redirect_to '/users/saml/sign_in'
|
163
184
|
end
|
164
185
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
186
|
+
context 'when saml_sign_out_success_url is configured' do
|
187
|
+
let(:test_url) { '/test/url' }
|
188
|
+
before do
|
189
|
+
Devise.saml_sign_out_success_url = test_url
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'accepts a LogoutResponse and returns success' do
|
193
|
+
do_post
|
194
|
+
expect(response.status).to eq 302
|
195
|
+
expect(response).to redirect_to test_url
|
196
|
+
end
|
170
197
|
end
|
171
198
|
end
|
172
199
|
|
173
|
-
context
|
174
|
-
|
200
|
+
context "when receiving an IdP logout request" do
|
201
|
+
subject(:do_post) {
|
202
|
+
if Rails::VERSION::MAJOR > 4
|
203
|
+
post :idp_sign_out, params: {SAMLRequest: "stubbed_logout_request"}
|
204
|
+
else
|
205
|
+
post :idp_sign_out, SAMLRequest: "stubbed_logout_request"
|
206
|
+
end
|
207
|
+
}
|
208
|
+
|
209
|
+
let(:saml_request) { double(:slo_logoutrequest, {
|
210
|
+
id: 42,
|
211
|
+
name_id: name_id,
|
212
|
+
issuer: "http://www.example.com"
|
213
|
+
}) }
|
214
|
+
let(:name_id) { '12312312' }
|
175
215
|
before do
|
176
|
-
|
216
|
+
allow(OneLogin::RubySaml::SloLogoutrequest).to receive(:new).and_return(saml_request)
|
177
217
|
end
|
178
218
|
|
179
|
-
it '
|
180
|
-
|
181
|
-
|
182
|
-
expect(response).to redirect_to
|
219
|
+
it 'direct the resource to reset the session key' do
|
220
|
+
expect(User).to receive(:reset_session_key_for).with(name_id)
|
221
|
+
do_post
|
222
|
+
expect(response).to redirect_to response_url
|
183
223
|
end
|
184
|
-
end
|
185
224
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
225
|
+
context "with a specified idp" do
|
226
|
+
let(:idp_entity_id) { "http://www.example.com" }
|
227
|
+
before do
|
228
|
+
Devise.idp_settings_adapter = idp_providers_adapter
|
229
|
+
end
|
190
230
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
231
|
+
it "accepts a LogoutResponse for the associated slo_target_url and redirects to sign_in" do
|
232
|
+
do_post
|
233
|
+
expect(response.status).to eq 302
|
234
|
+
expect(idp_providers_adapter).to have_received(:settings).with(idp_entity_id)
|
235
|
+
expect(response).to redirect_to "http://localhost/logout_response"
|
236
|
+
end
|
195
237
|
end
|
196
|
-
end
|
197
238
|
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
239
|
+
context 'when saml_session_index_key is not configured' do
|
240
|
+
before do
|
241
|
+
Devise.saml_session_index_key = nil
|
242
|
+
end
|
243
|
+
|
244
|
+
it 'returns invalid request' do
|
245
|
+
expect(User).not_to receive(:reset_session_key_for).with(name_id)
|
246
|
+
do_post
|
247
|
+
expect(response.status).to eq 500
|
248
|
+
end
|
249
|
+
end
|
202
250
|
end
|
203
251
|
end
|
204
252
|
end
|
@@ -58,12 +58,12 @@ describe Devise::Models::SamlAuthenticatable do
|
|
58
58
|
it "looks up the user by the configured default user key" do
|
59
59
|
user = Model.new(new_record: false)
|
60
60
|
expect(Model).to receive(:where).with(email: 'user@example.com').and_return([user])
|
61
|
-
expect(Model.authenticate_with_saml(response)).to eq(user)
|
61
|
+
expect(Model.authenticate_with_saml(response, nil)).to eq(user)
|
62
62
|
end
|
63
63
|
|
64
64
|
it "returns nil if it cannot find a user" do
|
65
65
|
expect(Model).to receive(:where).with(email: 'user@example.com').and_return([])
|
66
|
-
expect(Model.authenticate_with_saml(response)).to be_nil
|
66
|
+
expect(Model.authenticate_with_saml(response, nil)).to be_nil
|
67
67
|
end
|
68
68
|
|
69
69
|
context "when configured to use the subject" do
|
@@ -77,12 +77,12 @@ describe Devise::Models::SamlAuthenticatable do
|
|
77
77
|
it "looks up the user by the configured default user key" do
|
78
78
|
user = Model.new(new_record: false)
|
79
79
|
expect(Model).to receive(:where).with(email: 'user@example.com').and_return([user])
|
80
|
-
expect(Model.authenticate_with_saml(response)).to eq(user)
|
80
|
+
expect(Model.authenticate_with_saml(response, nil)).to eq(user)
|
81
81
|
end
|
82
82
|
|
83
83
|
it "returns nil if it cannot find a user" do
|
84
84
|
expect(Model).to receive(:where).with(email: 'user@example.com').and_return([])
|
85
|
-
expect(Model.authenticate_with_saml(response)).to be_nil
|
85
|
+
expect(Model.authenticate_with_saml(response, nil)).to be_nil
|
86
86
|
end
|
87
87
|
|
88
88
|
context "when configured to create a user and the user is not found" do
|
@@ -92,7 +92,7 @@ describe Devise::Models::SamlAuthenticatable do
|
|
92
92
|
|
93
93
|
it "creates and returns a new user with the name identifier and given attributes" do
|
94
94
|
expect(Model).to receive(:where).with(email: 'user@example.com').and_return([])
|
95
|
-
model = Model.authenticate_with_saml(response)
|
95
|
+
model = Model.authenticate_with_saml(response, nil)
|
96
96
|
expect(model.email).to eq('user@example.com')
|
97
97
|
expect(model.name).to eq('A User')
|
98
98
|
expect(model.saved).to be(true)
|
@@ -107,7 +107,7 @@ describe Devise::Models::SamlAuthenticatable do
|
|
107
107
|
it "creates and returns a new user with the name identifier and given attributes" do
|
108
108
|
user = Model.new(email: "old_mail@mail.com", name: "old name", new_record: false)
|
109
109
|
expect(Model).to receive(:where).with(email: 'user@example.com').and_return([user])
|
110
|
-
model = Model.authenticate_with_saml(response)
|
110
|
+
model = Model.authenticate_with_saml(response, nil)
|
111
111
|
expect(model.email).to eq('user@example.com')
|
112
112
|
expect(model.name).to eq('A User')
|
113
113
|
expect(model.saved).to be(true)
|
@@ -122,7 +122,7 @@ describe Devise::Models::SamlAuthenticatable do
|
|
122
122
|
|
123
123
|
it "creates and returns a new user with the given attributes" do
|
124
124
|
expect(Model).to receive(:where).with(email: 'user@example.com').and_return([])
|
125
|
-
model = Model.authenticate_with_saml(response)
|
125
|
+
model = Model.authenticate_with_saml(response, nil)
|
126
126
|
expect(model.email).to eq('user@example.com')
|
127
127
|
expect(model.name).to eq('A User')
|
128
128
|
expect(model.saved).to be(true)
|
@@ -136,29 +136,46 @@ describe Devise::Models::SamlAuthenticatable do
|
|
136
136
|
|
137
137
|
it "returns nil if the user is not found" do
|
138
138
|
expect(Model).to receive(:where).with(email: 'user@example.com').and_return([])
|
139
|
-
expect(Model.authenticate_with_saml(response)).to be_nil
|
139
|
+
expect(Model.authenticate_with_saml(response, nil)).to be_nil
|
140
140
|
end
|
141
141
|
|
142
142
|
it "updates the attributes if the user is found" do
|
143
143
|
user = Model.new(email: "old_mail@mail.com", name: "old name", new_record: false)
|
144
144
|
expect(Model).to receive(:where).with(email: 'user@example.com').and_return([user])
|
145
|
-
model = Model.authenticate_with_saml(response)
|
145
|
+
model = Model.authenticate_with_saml(response, nil)
|
146
146
|
expect(model.email).to eq('user@example.com')
|
147
147
|
expect(model.name).to eq('A User')
|
148
148
|
expect(model.saved).to be(true)
|
149
149
|
end
|
150
150
|
end
|
151
151
|
|
152
|
-
|
153
152
|
context "when configured with a case-insensitive key" do
|
154
|
-
|
155
|
-
|
153
|
+
shared_examples "correct downcasing" do
|
154
|
+
before do
|
155
|
+
allow(Devise).to receive(:case_insensitive_keys).and_return([:email])
|
156
|
+
end
|
157
|
+
|
158
|
+
it "looks up the user with a downcased value" do
|
159
|
+
user = Model.new(new_record: false)
|
160
|
+
expect(Model).to receive(:where).with(email: 'upper@example.com').and_return([user])
|
161
|
+
expect(Model.authenticate_with_saml(response, nil)).to eq(user)
|
162
|
+
end
|
156
163
|
end
|
157
164
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
165
|
+
context "when configured to use the subject" do
|
166
|
+
let(:name_id) { 'UPPER@example.com' }
|
167
|
+
|
168
|
+
before do
|
169
|
+
allow(Devise).to receive(:saml_use_subject).and_return(true)
|
170
|
+
end
|
171
|
+
|
172
|
+
include_examples "correct downcasing"
|
173
|
+
end
|
174
|
+
|
175
|
+
context "when using default user key" do
|
176
|
+
let(:attributes) { OneLogin::RubySaml::Attributes.new('saml-email-format' => ['UPPER@example.com']) }
|
177
|
+
|
178
|
+
include_examples "correct downcasing"
|
162
179
|
end
|
163
180
|
end
|
164
181
|
end
|
@@ -3,15 +3,16 @@ require 'rails_helper'
|
|
3
3
|
describe Devise::Strategies::SamlAuthenticatable do
|
4
4
|
subject(:strategy) { described_class.new(env, :user) }
|
5
5
|
let(:env) { {} }
|
6
|
+
let(:errors) { ["Test1", "Test2"] }
|
6
7
|
|
7
|
-
let(:response) { double(:response, issuers: [idp_entity_id], :settings= => nil, is_valid?: true, sessionindex: '123123123') }
|
8
|
+
let(:response) { double(:response, issuers: [idp_entity_id], :settings= => nil, is_valid?: true, sessionindex: '123123123', errors: errors) }
|
8
9
|
let(:idp_entity_id) { "https://test/saml/metadata/123123" }
|
9
10
|
before do
|
10
11
|
allow(OneLogin::RubySaml::Response).to receive(:new).and_return(response)
|
11
12
|
end
|
12
13
|
|
13
14
|
let(:mapping) { double(:mapping, to: user_class) }
|
14
|
-
let(:user_class) { double(:user_class, authenticate_with_saml: user) }
|
15
|
+
let(:user_class) { double(:user_class, authenticate_with_saml: user, skip_session_storage: []) }
|
15
16
|
let(:user) { double(:user) }
|
16
17
|
before do
|
17
18
|
allow(strategy).to receive(:mapping).and_return(mapping)
|
@@ -32,13 +33,23 @@ describe Devise::Strategies::SamlAuthenticatable do
|
|
32
33
|
|
33
34
|
it "authenticates with the response" do
|
34
35
|
expect(OneLogin::RubySaml::Response).to receive(:new).with(params[:SAMLResponse], anything)
|
35
|
-
expect(user_class).to receive(:authenticate_with_saml).with(response)
|
36
|
+
expect(user_class).to receive(:authenticate_with_saml).with(response, nil)
|
36
37
|
expect(user).to receive(:after_saml_authentication).with(response.sessionindex)
|
37
38
|
|
38
39
|
expect(strategy).to receive(:success!).with(user)
|
39
40
|
strategy.authenticate!
|
40
41
|
end
|
41
42
|
|
43
|
+
context "and a RelayState parameter" do
|
44
|
+
let(:params) { super().merge(RelayState: "foo") }
|
45
|
+
it "authenticates with the response" do
|
46
|
+
expect(user_class).to receive(:authenticate_with_saml).with(response, params[:RelayState])
|
47
|
+
|
48
|
+
expect(strategy).to receive(:success!).with(user)
|
49
|
+
strategy.authenticate!
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
42
53
|
context "when saml config uses an idp_adapter" do
|
43
54
|
let(:idp_providers_adapter) {
|
44
55
|
Class.new {
|
@@ -70,7 +81,7 @@ describe Devise::Strategies::SamlAuthenticatable do
|
|
70
81
|
it "authenticates with the response for the corresponding idp" do
|
71
82
|
expect(OneLogin::RubySaml::Response).to receive(:new).with(params[:SAMLResponse], anything)
|
72
83
|
expect(idp_providers_adapter).to receive(:settings).with(idp_entity_id)
|
73
|
-
expect(user_class).to receive(:authenticate_with_saml).with(response)
|
84
|
+
expect(user_class).to receive(:authenticate_with_saml).with(response, params[:RelayState])
|
74
85
|
expect(user).to receive(:after_saml_authentication).with(response.sessionindex)
|
75
86
|
|
76
87
|
expect(strategy).to receive(:success!).with(user)
|
@@ -85,6 +96,11 @@ describe Devise::Strategies::SamlAuthenticatable do
|
|
85
96
|
expect(strategy).to receive(:fail!).with(:invalid)
|
86
97
|
strategy.authenticate!
|
87
98
|
end
|
99
|
+
|
100
|
+
it 'logs the error' do
|
101
|
+
expect(DeviseSamlAuthenticatable::Logger).to receive(:send).with('Resource could not be found')
|
102
|
+
strategy.authenticate!
|
103
|
+
end
|
88
104
|
end
|
89
105
|
|
90
106
|
context "and the SAML response is not valid" do
|
@@ -103,6 +119,11 @@ describe Devise::Strategies::SamlAuthenticatable do
|
|
103
119
|
Devise.saml_failed_callback = @saml_failed_login
|
104
120
|
end
|
105
121
|
|
122
|
+
it 'logs the error' do
|
123
|
+
expect(DeviseSamlAuthenticatable::Logger).to receive(:send).with('Auth errors: Test1, Test2')
|
124
|
+
strategy.authenticate!
|
125
|
+
end
|
126
|
+
|
106
127
|
it "fails to authenticate" do
|
107
128
|
expect(strategy).to receive(:fail!).with(:invalid)
|
108
129
|
strategy.authenticate!
|
@@ -122,4 +143,14 @@ describe Devise::Strategies::SamlAuthenticatable do
|
|
122
143
|
it "is permanent" do
|
123
144
|
expect(strategy).to be_store
|
124
145
|
end
|
146
|
+
|
147
|
+
context "when the user should not be stored in the session" do
|
148
|
+
before do
|
149
|
+
allow(user_class).to receive(:skip_session_storage).and_return([:saml_auth])
|
150
|
+
end
|
151
|
+
|
152
|
+
it "is not stored" do
|
153
|
+
expect(strategy).not_to be_store
|
154
|
+
end
|
155
|
+
end
|
125
156
|
end
|
@@ -3,8 +3,8 @@ require 'net/http'
|
|
3
3
|
require 'timeout'
|
4
4
|
require 'uri'
|
5
5
|
require 'capybara/rspec'
|
6
|
-
require 'capybara/
|
7
|
-
Capybara.default_driver = :
|
6
|
+
require 'capybara/poltergeist'
|
7
|
+
Capybara.default_driver = :poltergeist
|
8
8
|
|
9
9
|
describe "SAML Authentication", type: :feature do
|
10
10
|
let(:idp_port) { 8009 }
|
@@ -0,0 +1,23 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in devise_saml_authenticatable.gemspec
|
4
|
+
gemspec path: '../..'
|
5
|
+
|
6
|
+
group :test do
|
7
|
+
gem 'rake'
|
8
|
+
gem 'rspec', '~> 3.0'
|
9
|
+
gem 'rails', '~> 4.0'
|
10
|
+
gem 'rspec-rails'
|
11
|
+
gem 'sqlite3'
|
12
|
+
gem 'capybara'
|
13
|
+
gem 'poltergeist'
|
14
|
+
|
15
|
+
# Lock down versions of gems for older versions of Ruby
|
16
|
+
if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.0")
|
17
|
+
gem 'addressable', '~> 2.4.0'
|
18
|
+
gem 'mime-types', '~> 2.99'
|
19
|
+
end
|
20
|
+
if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
|
21
|
+
gem 'devise', '~> 3.5'
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in devise_saml_authenticatable.gemspec
|
4
|
+
gemspec path: '../..'
|
5
|
+
|
6
|
+
group :test do
|
7
|
+
gem 'rake'
|
8
|
+
gem 'rspec', '~> 3.0'
|
9
|
+
gem 'rails', '~> 5.0'
|
10
|
+
gem 'rspec-rails'
|
11
|
+
gem 'ruby-saml', '~> 1.3.0'
|
12
|
+
gem 'sqlite3'
|
13
|
+
gem 'capybara'
|
14
|
+
gem 'poltergeist'
|
15
|
+
|
16
|
+
# Lock down versions of gems for older versions of Ruby
|
17
|
+
if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.0")
|
18
|
+
gem 'mime-types', '~> 2.99'
|
19
|
+
end
|
20
|
+
if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
|
21
|
+
gem 'devise', '~> 3.5'
|
22
|
+
end
|
23
|
+
end
|
@@ -9,9 +9,6 @@ gem 'thin'
|
|
9
9
|
insert_into_file('Gemfile', after: /\z/) {
|
10
10
|
<<-GEMFILE
|
11
11
|
# Lock down versions of gems for older versions of Ruby
|
12
|
-
if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.0")
|
13
|
-
gem 'mime-types', '~> 2.99'
|
14
|
-
end
|
15
12
|
if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
|
16
13
|
gem 'devise', '~> 3.5'
|
17
14
|
end
|
data/spec/support/rails_app.rb
CHANGED
@@ -12,13 +12,11 @@ def app_ready?(pid, port)
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def create_app(name, env = {})
|
15
|
-
rails_new_options = %w(-T -J -S --skip-spring)
|
15
|
+
rails_new_options = %w(-T -J -S --skip-spring --skip-listen)
|
16
16
|
rails_new_options << "-O" if name == 'idp'
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
system(env, "rails", "new", name, *rails_new_options, "-m", "#{name}_template.rb")
|
21
|
-
end
|
17
|
+
Dir.chdir(File.expand_path('../../support', __FILE__)) do
|
18
|
+
FileUtils.rm_rf(name)
|
19
|
+
system(env, "rails", "new", name, *rails_new_options, "-m", "#{name}_template.rb")
|
22
20
|
end
|
23
21
|
end
|
24
22
|
|
data/spec/support/sp_template.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# Set up a SAML Service Provider
|
2
2
|
|
3
|
+
require "onelogin/ruby-saml/version"
|
4
|
+
|
3
5
|
saml_session_index_key = ENV.fetch('SAML_SESSION_INDEX_KEY', ":session_index")
|
4
6
|
use_subject_to_authenticate = ENV.fetch('USE_SUBJECT_TO_AUTHENTICATE')
|
5
7
|
idp_settings_adapter = ENV.fetch('IDP_SETTINGS_ADAPTER', "nil")
|
@@ -7,14 +9,12 @@ idp_entity_id_reader = ENV.fetch('IDP_ENTITY_ID_READER', "DeviseSamlAuthenticata
|
|
7
9
|
saml_failed_callback = ENV.fetch('SAML_FAILED_CALLBACK', "nil")
|
8
10
|
|
9
11
|
gem 'devise_saml_authenticatable', path: '../../..'
|
12
|
+
gem 'ruby-saml', OneLogin::RubySaml::VERSION
|
10
13
|
gem 'thin'
|
11
14
|
|
12
15
|
insert_into_file('Gemfile', after: /\z/) {
|
13
16
|
<<-GEMFILE
|
14
17
|
# Lock down versions of gems for older versions of Ruby
|
15
|
-
if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.0")
|
16
|
-
gem 'mime-types', '~> 2.99'
|
17
|
-
end
|
18
18
|
if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new("2.1")
|
19
19
|
gem 'devise', '~> 3.5'
|
20
20
|
end
|
@@ -100,7 +100,7 @@ end
|
|
100
100
|
route "resources :users, only: [:create]"
|
101
101
|
create_file('app/controllers/users_controller.rb', <<-USERS)
|
102
102
|
class UsersController < ApplicationController
|
103
|
-
|
103
|
+
skip_before_action :verify_authenticity_token
|
104
104
|
def create
|
105
105
|
User.create!(email: params[:email])
|
106
106
|
render nothing: true, status: 201
|
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.3.
|
4
|
+
version: 1.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josef Sauter
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-11-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: devise
|
@@ -72,6 +72,8 @@ files:
|
|
72
72
|
- spec/features/saml_authentication_spec.rb
|
73
73
|
- spec/rails_helper.rb
|
74
74
|
- spec/spec_helper.rb
|
75
|
+
- spec/support/Gemfile.rails4
|
76
|
+
- spec/support/Gemfile.ruby-saml-1.3
|
75
77
|
- spec/support/idp_settings_adapter.rb.erb
|
76
78
|
- spec/support/idp_template.rb
|
77
79
|
- spec/support/rails_app.rb
|
@@ -111,6 +113,8 @@ test_files:
|
|
111
113
|
- spec/features/saml_authentication_spec.rb
|
112
114
|
- spec/rails_helper.rb
|
113
115
|
- spec/spec_helper.rb
|
116
|
+
- spec/support/Gemfile.rails4
|
117
|
+
- spec/support/Gemfile.ruby-saml-1.3
|
114
118
|
- spec/support/idp_settings_adapter.rb.erb
|
115
119
|
- spec/support/idp_template.rb
|
116
120
|
- spec/support/rails_app.rb
|