devise_saml_authenticatable 1.3.0 → 1.3.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|