omniauth-saml 2.2.4 → 2.2.5

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: 0ee62f105d63c2d5778122c04beb5c36a4fa8c8768beaa46d822d24ef40659ad
4
- data.tar.gz: b9e3d10fa4c3bdae5ba7144e0ba8cd8e3ad98a7a16e022458cacbe2e267dd7f4
3
+ metadata.gz: 7d346f41f4069110547ddd73e4d9f7b23196d8519871da44f0eb42f2176c5fe6
4
+ data.tar.gz: 7451c766a513d52948fc07e0ac971e6e5d1b4392d822991444db38d72a66c835
5
5
  SHA512:
6
- metadata.gz: 33702588126fa0a198d286e58a4c4d7e55830582d9ed7188db51f8e428718e79c9399af1fa7d5271f804560afa45b8118182b49f6a66252200b2435c86c9e926
7
- data.tar.gz: b550692fca027b90ef1f29a89a97eac2b22dfbdce6591c7b5b5cbda731b4ec807bbdfd63ef7b5397f64319b6cbea8076aa39220eb3fb7bc7491ab29668e95b59
6
+ metadata.gz: f93c043e99ccd8521877c45d2627cc87a150c42fb086b394172be4a12d02c28b816fec390810f934bdf10b07f68049ee222d15dc2a1e50e623e155b90951fca1
7
+ data.tar.gz: ae4440bfcb758760cd1f15b797d2f9b0b339a4ecfebd16e80834359c247cb335c15459fd509acf71364971ed5ef9d8d7009224f2996c3fbfd1ad54a92b26282d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ <a name="v2.2.5"></a>
2
+ ### v2.2.5 (2023-07-25)
3
+
4
+
5
+ #### Features
6
+
7
+ * Support RelayState binding by default during SSO ([a508436](/../../commit/a508436))
8
+
9
+
1
10
  <a name="v2.2.4"></a>
2
11
  ### v2.2.4 (2025-05-14)
3
12
 
data/README.md CHANGED
@@ -101,7 +101,18 @@ Note that when [integrating with Devise](#devise-integration), the URL path will
101
101
  * `:slo_default_relay_state` - The value to use as default `RelayState` for single log outs. The
102
102
  value can be a string, or a `Proc` (or other object responding to `call`). The `request`
103
103
  instance will be passed to this callable if it has an arity of 1. If the value is a string,
104
- the string will be returned, when the `RelayState` is called. Optional.
104
+ the string will be returned, when the `RelayState` is called.
105
+ The value is assumed to be safe and is not validated by `:slo_relay_state_validator`.
106
+ Optional.
107
+
108
+ * `:slo_enabled` - Enables or disables Single Logout (SLO). Set to `false` to disable SLO. Defaults to `true`. Optional.
109
+
110
+ * `:slo_relay_state_validator` - A callable used to validate any `RelayState` before performing the redirect
111
+ in Single Logout flows. The callable receives the RelayState value and the current Rack request.
112
+ If unset, the default validator is used. The default validator allows only relative paths beginning
113
+ with `/` and rejects absolute URLs, invalid URIs, protocol-relative URLs, and other schemes.
114
+ If the given `RelayState` is considered invalid then the `slo_default_relay_state` value is used for the SLO redirect.
115
+ Optional.
105
116
 
106
117
  * `:idp_sso_service_url_runtime_params` - A dynamic mapping of request params that exist
107
118
  during the request phase of OmniAuth that should to be sent to the IdP after a specific
@@ -112,7 +123,7 @@ Note that when [integrating with Devise](#devise-integration), the URL path will
112
123
  * `:idp_cert` - The identity provider's certificate in PEM format. Takes precedence
113
124
  over the fingerprint option below. This option or `:idp_cert_multi` or `:idp_cert_fingerprint` must
114
125
  be present.
115
-
126
+
116
127
  * `:idp_cert_multi` - Multiple identity provider certificates in PEM format. Takes precedence
117
128
  over the fingerprint option below. This option `:idp_cert` or `:idp_cert_fingerprint` must
118
129
  be present.
@@ -192,7 +203,9 @@ Single Logout can be Service Provider initiated or Identity Provider initiated.
192
203
  For SP initiated logout, the `idp_slo_service_url` option must be set to the logout url on the IdP,
193
204
  and users directed to `user_saml_omniauth_authorize_path + '/spslo'` after logging out locally. For
194
205
  IdP initiated logout, logout requests from the IdP should go to `/auth/saml/slo` (this can be
195
- advertised in metadata by setting the `single_logout_service_url` config option).
206
+ advertised in metadata by setting the `single_logout_service_url` config option). If you wish to
207
+ disable Single Logout entirely (both SP and IdP initiated), set `:slo_enabled => false`; the `/auth/saml/slo`
208
+ and `/auth/saml/spslo` endpoints will then respond with HTTP 501 Not Implemented.
196
209
 
197
210
  When using Devise as an authentication solution, the SP initiated flow can be integrated
198
211
  in the `SessionsController#destroy` action.
@@ -1,5 +1,6 @@
1
1
  require 'omniauth'
2
2
  require 'ruby-saml'
3
+ require 'uri'
3
4
 
4
5
  module OmniAuth
5
6
  module Strategies
@@ -13,7 +14,7 @@ module OmniAuth
13
14
  RUBYSAML_RESPONSE_OPTIONS = OneLogin::RubySaml::Response::AVAILABLE_OPTIONS
14
15
 
15
16
  option :name_identifier_format, nil
16
- option :idp_sso_service_url_runtime_params, {}
17
+ option :idp_sso_service_url_runtime_params, { RelayState: 'RelayState' }
17
18
  option :request_attributes, [
18
19
  { :name => 'email', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Email address' },
19
20
  { :name => 'name', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Full name' },
@@ -27,7 +28,26 @@ module OmniAuth
27
28
  first_name: ["first_name", "firstname", "firstName"],
28
29
  last_name: ["last_name", "lastname", "lastName"]
29
30
  }
31
+ DEFAULT_SLO_RELAY_STATE_VALIDATOR = lambda do |relay_state, _request|
32
+ return true if relay_state.nil? || relay_state == ""
33
+
34
+ return false if relay_state.start_with?("//")
35
+
36
+ begin
37
+ uri = URI.parse(relay_state)
38
+ rescue URI::Error
39
+ return false
40
+ end
41
+
42
+ return false unless uri.relative?
43
+
44
+ path = uri.path
45
+ path && path.start_with?("/")
46
+ end
47
+
30
48
  option :slo_default_relay_state
49
+ option :slo_enabled, true
50
+ option :slo_relay_state_validator, DEFAULT_SLO_RELAY_STATE_VALIDATOR
31
51
  option :uid_attribute
32
52
  option :idp_slo_session_destroy, proc { |_env, session| session.clear }
33
53
 
@@ -73,8 +93,12 @@ module OmniAuth
73
93
  if on_subpath?(:metadata)
74
94
  other_phase_for_metadata
75
95
  elsif on_subpath?(:slo)
96
+ return slo_disabled_response unless slo_enabled?
97
+
76
98
  other_phase_for_slo
77
99
  elsif on_subpath?(:spslo)
100
+ return slo_disabled_response unless slo_enabled?
101
+
78
102
  other_phase_for_spslo
79
103
  else
80
104
  call_app!
@@ -115,6 +139,22 @@ module OmniAuth
115
139
  nil
116
140
  end
117
141
 
142
+ def mock_request_call
143
+ # Per SAML 2.0, if a RelayState param is passed, IDPs "MUST place the exact RelayState
144
+ # data it received with the request into the corresponding RelayState parameter in the response."
145
+ #
146
+ # By default, the "mock" `OmniAuth::Strategy` implementation will forward along any URL params,
147
+ # so we can in turn take any POSTed RelayState params and put them in the GET query string:
148
+ query_hash = request.GET.merge!(additional_params_for_authn_request.slice('RelayState'))
149
+ query_string = Rack::Utils.build_query(query_hash)
150
+
151
+ request.set_header(Rack::QUERY_STRING, query_string)
152
+ request.set_header(Rack::RACK_REQUEST_QUERY_STRING, query_string)
153
+ request.set_header(Rack::RACK_REQUEST_QUERY_HASH, query_hash)
154
+
155
+ super
156
+ end
157
+
118
158
  private
119
159
 
120
160
  def request_path_pattern
@@ -143,19 +183,34 @@ module OmniAuth
143
183
 
144
184
  def slo_relay_state
145
185
  if request.params.has_key?("RelayState") && request.params["RelayState"] != ""
146
- request.params["RelayState"]
147
- else
148
- slo_default_relay_state = options.slo_default_relay_state
149
- if slo_default_relay_state.respond_to?(:call)
150
- if slo_default_relay_state.arity == 1
151
- slo_default_relay_state.call(request)
152
- else
153
- slo_default_relay_state.call
154
- end
155
- else
156
- slo_default_relay_state
157
- end
186
+ relay_state = request.params["RelayState"]
187
+
188
+ return relay_state if valid_slo_relay_state?(relay_state)
158
189
  end
190
+
191
+ default_slo_relay_state
192
+ end
193
+
194
+ def valid_slo_relay_state?(relay_state)
195
+ validator = options.slo_relay_state_validator
196
+
197
+ return !!call_slo_relay_state_validator(validator, relay_state) if validator.respond_to?(:call)
198
+
199
+ !!validator
200
+ end
201
+
202
+ def call_slo_relay_state_validator(validator, relay_state)
203
+ return validator.call if validator.arity.zero?
204
+ return validator.call(relay_state) if validator.arity == 1
205
+ validator.call(relay_state, request)
206
+ end
207
+
208
+ def default_slo_relay_state
209
+ slo_default_relay_state = options.slo_default_relay_state
210
+
211
+ return slo_default_relay_state unless slo_default_relay_state.respond_to?(:call)
212
+ return slo_default_relay_state.call if slo_default_relay_state.arity.zero?
213
+ slo_default_relay_state.call(request)
159
214
  end
160
215
 
161
216
  def handle_logout_response(raw_response, settings)
@@ -259,6 +314,14 @@ module OmniAuth
259
314
  end
260
315
  end
261
316
 
317
+ def slo_enabled?
318
+ !!options[:slo_enabled]
319
+ end
320
+
321
+ def slo_disabled_response
322
+ Rack::Response.new("Not Implemented", 501, { "Content-Type" => "text/html" }).finish
323
+ end
324
+
262
325
  def add_request_attributes_to(settings)
263
326
  settings.attribute_consuming_service.service_name options.attribute_service_name
264
327
  settings.sp_entity_id = options.sp_entity_id
@@ -1,5 +1,5 @@
1
1
  module OmniAuth
2
2
  module SAML
3
- VERSION = '2.2.4'
3
+ VERSION = '2.2.5'
4
4
  end
5
5
  end
@@ -6,10 +6,6 @@ RSpec::Matchers.define :fail_with do |message|
6
6
  end
7
7
  end
8
8
 
9
- def post_xml(xml = :example_response, opts = {})
10
- post "/auth/saml/callback", opts.merge({'SAMLResponse' => load_xml(xml)})
11
- end
12
-
13
9
  describe OmniAuth::Strategies::SAML, :type => :strategy do
14
10
  include OmniAuth::Test::StrategyTestCase
15
11
 
@@ -34,6 +30,55 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
34
30
  end
35
31
  let(:strategy) { [OmniAuth::Strategies::SAML, saml_options] }
36
32
 
33
+ shared_examples 'validating RelayState param' do
34
+ context 'when slo_relay_state_validator is not defined and default' do
35
+ [
36
+ ['/signed-out', '//attacker.test', '%2Fsigned-out'],
37
+ ['/signed-out', 'javascript:alert(1)', '%2Fsigned-out'],
38
+ ['/signed-out', 'https://example.com/logout', '%2Fsigned-out'],
39
+ ['/signed-out', 'https://example.com/logout?param=1&two=two', '%2Fsigned-out'],
40
+ ['/signed-out', '/', '%2F'],
41
+ ['', '//attacker.test', ''],
42
+ ['', '/team/logout', '%2Fteam%2Flogout'],
43
+ ].each do |slo_default_relay_state, relay_state_param, expected_relay_state|
44
+ context "when slo_default_relay_state: #{slo_default_relay_state.inspect}, relay_state_param: #{relay_state_param.inspect}" do
45
+ let(:saml_options) { super().merge(slo_default_relay_state: slo_default_relay_state) }
46
+ let(:params) { super().merge('RelayState' => relay_state_param) }
47
+
48
+ it { is_expected.to be_redirect.and have_attributes(location: a_string_including("RelayState=#{expected_relay_state}")) }
49
+ end
50
+ end
51
+ end
52
+
53
+ context 'when slo_relay_state_validator is overridden' do
54
+ [
55
+ ['/signed-out', proc { |state| state.start_with?('https://trusted.example.com') }, 'https://trusted.example.com/logout', 'https%3A%2F%2Ftrusted.example.com%2Flogout'],
56
+ ['/signed-out', proc { |state| state.start_with?('https://trusted.example.com') }, 'https://attacker.test/logout', '%2Fsigned-out'],
57
+ ['/signed-out', proc { |state| state.start_with?('https://trusted.example.com') }, '/safe/path', '%2Fsigned-out'],
58
+ ['/signed-out', proc { |state, req| state == req.params['RelayState'] }, '/team/logout', '%2Fteam%2Flogout'],
59
+ ['/signed-out', nil, '//attacker.test', '%2Fsigned-out'],
60
+ ['/signed-out', false, '//attacker.test', '%2Fsigned-out'],
61
+ ['/signed-out', proc { |_| false }, '//attacker.test', '%2Fsigned-out'],
62
+ ['/signed-out', proc { |_| true }, 'javascript:alert(1)', 'javascript%3Aalert%281%29'],
63
+ [nil, true, 'https://example.com/logout', 'https%3A%2F%2Fexample.com%2Flogout'],
64
+ [nil, true, 'javascript:alert(1)', 'javascript%3Aalert%281%29'],
65
+ [nil, true, '/', '%2F'],
66
+ ].each do |slo_default_relay_state, slo_relay_state_validator, relay_state_param, expected_relay_state|
67
+ context "when slo_default_relay_state: #{slo_default_relay_state.inspect}, slo_relay_state_validator: #{slo_relay_state_validator.inspect}, relay_state_param: #{relay_state_param.inspect}" do
68
+ let(:saml_options) do
69
+ super().merge(
70
+ slo_default_relay_state: slo_default_relay_state,
71
+ slo_relay_state_validator: slo_relay_state_validator,
72
+ )
73
+ end
74
+ let(:params) { super().merge('RelayState' => relay_state_param) }
75
+
76
+ it { is_expected.to be_redirect.and have_attributes(location: a_string_including("RelayState=#{expected_relay_state}")) }
77
+ end
78
+ end
79
+ end
80
+ end
81
+
37
82
  describe 'POST /auth/saml' do
38
83
  context 'without idp runtime params present' do
39
84
  before do
@@ -63,6 +108,33 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
63
108
  end
64
109
  end
65
110
 
111
+ context 'with RelayState param' do
112
+ before do
113
+ post '/auth/saml', 'RelayState' => 'RELAY_STATE_VALUE'
114
+ end
115
+
116
+ it 'should get authentication page' do
117
+ expect(last_response).to be_redirect
118
+ expect(last_response.location).to match(
119
+ /\Ahttps:\/\/idp.sso.example.com\/signon\/29490\?SAMLRequest=.*&RelayState=RELAY_STATE_VALUE\z/,
120
+ )
121
+ end
122
+
123
+ context 'when test_mode is enabled' do
124
+ around do |example|
125
+ OmniAuth.config.test_mode = true
126
+ example.run
127
+ ensure
128
+ OmniAuth.config.test_mode = false
129
+ end
130
+
131
+ it 'should redirect to local saml callback page' do
132
+ expect(last_response).to be_redirect
133
+ expect(last_response.location).to eq('http://example.org/auth/saml/callback?RelayState=RELAY_STATE_VALUE')
134
+ end
135
+ end
136
+ end
137
+
66
138
  context "when the assertion_consumer_service_url is the default" do
67
139
  before :each do
68
140
  saml_options[:compress_request] = false
@@ -118,24 +190,27 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
118
190
  end
119
191
 
120
192
  describe 'POST /auth/saml/callback' do
121
- subject { last_response }
122
-
123
193
  let(:xml) { :example_response }
194
+ let(:params) { { 'SAMLResponse' => load_xml(xml) } }
195
+
196
+ subject(:post_callback_response) do
197
+ post "/auth/saml/callback", params
198
+ end
124
199
 
125
200
  before :each do
126
201
  allow(Time).to receive(:now).and_return(Time.utc(2012, 11, 8, 20, 40, 00))
127
202
  end
128
203
 
129
204
  context "when the response is valid" do
130
- before :each do
131
- post_xml
132
- end
133
-
134
205
  it "should set the uid to the nameID in the SAML response" do
206
+ post_callback_response
207
+
135
208
  expect(auth_hash['uid']).to eq '_1f6fcf6be5e13b08b1e3610e7ff59f205fbd814f23'
136
209
  end
137
210
 
138
211
  it "should set the raw info to all attributes" do
212
+ post_callback_response
213
+
139
214
  expect(auth_hash['extra']['raw_info'].all.to_hash).to eq(
140
215
  'first_name' => ['Rajiv'],
141
216
  'last_name' => ['Manglani'],
@@ -146,6 +221,8 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
146
221
  end
147
222
 
148
223
  it "should set the response_object to the response object from ruby_saml response" do
224
+ post_callback_response
225
+
149
226
  expect(auth_hash['extra']['response_object']).to be_kind_of(OneLogin::RubySaml::Response)
150
227
  end
151
228
  end
@@ -154,24 +231,22 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
154
231
  before :each do
155
232
  saml_options.delete(:assertion_consumer_service_url)
156
233
  OmniAuth.config.full_host = 'http://localhost:9080'
157
- post_xml
158
234
  end
159
235
 
160
236
  it { is_expected.not_to fail_with(:invalid_ticket) }
161
237
  end
162
238
 
163
239
  context "when there is no SAMLResponse parameter" do
164
- before :each do
165
- post '/auth/saml/callback'
166
- end
240
+ let(:params) { {} }
167
241
 
168
242
  it { is_expected.to fail_with(:invalid_ticket) }
169
243
  end
170
244
 
171
245
  context "when there is no name id in the XML" do
246
+ let(:xml) { :no_name_id }
247
+
172
248
  before :each do
173
249
  allow(Time).to receive(:now).and_return(Time.utc(2012, 11, 8, 23, 55, 00))
174
- post_xml :no_name_id
175
250
  end
176
251
 
177
252
  it { is_expected.to fail_with(:invalid_ticket) }
@@ -180,43 +255,37 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
180
255
  context "when the fingerprint is invalid" do
181
256
  before :each do
182
257
  saml_options[:idp_cert_fingerprint] = "00:00:00:00:00:0C:6C:A9:41:0F:6E:83:F6:D1:52:25:45:58:89:FB"
183
- post_xml
184
258
  end
185
259
 
186
260
  it { is_expected.to fail_with(:invalid_ticket) }
187
261
  end
188
262
 
189
263
  context "when the digest is invalid" do
190
- before :each do
191
- post_xml :digest_mismatch
192
- end
264
+ let(:xml) { :digest_mismatch }
193
265
 
194
266
  it { is_expected.to fail_with(:invalid_ticket) }
195
267
  end
196
268
 
197
269
  context "when the signature is invalid" do
198
- before :each do
199
- post_xml :invalid_signature
200
- end
270
+ let(:xml) { :invalid_signature }
201
271
 
202
272
  it { is_expected.to fail_with(:invalid_ticket) }
203
273
  end
204
274
 
205
275
  context "when the response is stale" do
276
+ let(:xml) { :example_response }
277
+
206
278
  before :each do
207
279
  allow(Time).to receive(:now).and_return(Time.utc(2012, 11, 8, 20, 45, 00))
208
280
  end
209
281
 
210
282
  context "without :allowed_clock_drift option" do
211
- before { post_xml :example_response }
212
-
213
283
  it { is_expected.to fail_with(:invalid_ticket) }
214
284
  end
215
285
 
216
286
  context "with :allowed_clock_drift option" do
217
287
  before :each do
218
288
  saml_options[:allowed_clock_drift] = 60
219
- post_xml :example_response
220
289
  end
221
290
 
222
291
  it { is_expected.to_not fail_with(:invalid_ticket) }
@@ -224,14 +293,16 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
224
293
  end
225
294
 
226
295
  context "when response has custom attributes" do
296
+ let(:xml) { :custom_attributes }
297
+
227
298
  before :each do
228
- saml_options[:idp_cert_fingerprint] = "3B:82:F1:F5:54:FC:A8:FF:12:B8:4B:B8:16:61:1D:E4:8E:9B:E2:3C"
229
299
  saml_options[:attribute_statements] = {
230
300
  email: ["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"],
231
301
  first_name: ["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"],
232
302
  last_name: ["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"]
233
303
  }
234
- post_xml :custom_attributes
304
+
305
+ post_callback_response
235
306
  end
236
307
 
237
308
  it "should obey attribute statements mapping" do
@@ -245,10 +316,12 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
245
316
  end
246
317
 
247
318
  context "when using custom user id attribute" do
319
+ let(:xml) { :custom_attributes }
320
+
248
321
  before :each do
249
- saml_options[:idp_cert_fingerprint] = "3B:82:F1:F5:54:FC:A8:FF:12:B8:4B:B8:16:61:1D:E4:8E:9B:E2:3C"
250
322
  saml_options[:uid_attribute] = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
251
- post_xml :custom_attributes
323
+
324
+ post_callback_response
252
325
  end
253
326
 
254
327
  it "should return user id attribute" do
@@ -259,55 +332,146 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
259
332
  context "when using custom user id attribute, but it is missing" do
260
333
  before :each do
261
334
  saml_options[:uid_attribute] = "missing_attribute"
262
- post_xml
263
335
  end
264
336
 
265
337
  it "should fail to authenticate" do
266
- should fail_with(:invalid_ticket)
338
+ expect(post_callback_response).to fail_with(:invalid_ticket)
267
339
  expect(last_request.env['omniauth.error']).to be_instance_of(OmniAuth::Strategies::SAML::ValidationError)
268
340
  expect(last_request.env['omniauth.error'].message).to eq("SAML response missing 'missing_attribute' attribute")
269
341
  end
270
342
  end
343
+ end
344
+
345
+ describe 'POST /auth/saml/slo' do
346
+ before do
347
+ saml_options[:sp_entity_id] = "https://idp.sso.example.com/metadata/29490"
348
+ end
271
349
 
272
350
  context "when response is a logout response" do
273
- before :each do
274
- saml_options[:sp_entity_id] = "https://idp.sso.example.com/metadata/29490"
351
+ let(:opts) do
352
+ { "rack.session" => { "saml_transaction_id" => "_3fef1069-d0c6-418a-b68d-6f008a4787e9" } }
353
+ end
354
+
355
+ let(:params) { { SAMLResponse: load_xml(:example_logout_response) } }
275
356
 
276
- post "/auth/saml/slo", {
277
- SAMLResponse: load_xml(:example_logout_response),
278
- RelayState: "https://example.com/",
279
- }, "rack.session" => {"saml_transaction_id" => "_3fef1069-d0c6-418a-b68d-6f008a4787e9"}
357
+ subject(:post_slo_response) { post "/auth/saml/slo", params, opts }
358
+
359
+ context "when relay state is relative" do
360
+ let(:params) { super().merge(RelayState: "/signed-out") }
361
+
362
+ it "redirects to the relaystate" do
363
+ post_slo_response
364
+
365
+ expect(last_response).to be_redirect
366
+ expect(last_response.location).to eq "/signed-out"
367
+ end
280
368
  end
281
- it "should redirect to relaystate" do
282
- expect(last_response).to be_redirect
283
- expect(last_response.location).to match /https:\/\/example.com\//
369
+
370
+ context "when relay state is an absolute https URL" do
371
+ let(:params) { super().merge(RelayState: "https://example.com/") }
372
+
373
+ it "redirects without a location header" do
374
+ post_slo_response
375
+
376
+ expect(last_response).to be_redirect
377
+ expect(last_response.headers.fetch("Location")).to be_nil
378
+ end
379
+ end
380
+
381
+ context 'when slo_default_relay_state is present' do
382
+ let(:saml_options) { super().merge(slo_default_relay_state: '/signed-out') }
383
+
384
+ context "when response relay state is valid" do
385
+ let(:params) { super().merge(RelayState: "/safe/logout") }
386
+
387
+ it { is_expected.to be_redirect.and have_attributes(location: '/safe/logout') }
388
+ end
389
+
390
+ context "when response relay state is invalid" do
391
+ let(:params) { super().merge(RelayState: "javascript:alert(1)") }
392
+
393
+ it { is_expected.to be_redirect.and have_attributes(location: '/signed-out') }
394
+ end
395
+ end
396
+
397
+ context 'when slo_default_relay_state is blank' do
398
+ let(:saml_options) { super().merge(slo_default_relay_state: nil) }
399
+
400
+ context "when response relay state is valid" do
401
+ let(:params) { super().merge(RelayState: "/safe/logout") }
402
+
403
+ it { is_expected.to be_redirect.and have_attributes(location: '/safe/logout') }
404
+ end
405
+
406
+ context "when response relay state is invalid" do
407
+ let(:params) { super().merge(RelayState: "javascript:alert(1)") }
408
+
409
+ it { is_expected.to be_redirect.and have_attributes(location: nil) }
410
+ end
284
411
  end
285
412
  end
286
413
 
287
414
  context "when request is a logout request" do
288
415
  subject { post "/auth/saml/slo", params, "rack.session" => { "saml_uid" => "username@example.com" } }
289
416
 
290
- before :each do
291
- saml_options[:sp_entity_id] = "https://idp.sso.example.com/metadata/29490"
292
- end
417
+ let(:relay_state) { "https://example.com/" }
293
418
 
294
419
  let(:params) do
295
420
  {
296
421
  "SAMLRequest" => load_xml(:example_logout_request),
297
- "RelayState" => "https://example.com/",
422
+ "RelayState" => relay_state,
298
423
  }
299
424
  end
300
425
 
301
426
  context "when logout request is valid" do
427
+ let(:relay_state) { "/logout" }
428
+
302
429
  before { subject }
303
430
 
304
431
  it "should redirect to logout response" do
305
432
  expect(last_response).to be_redirect
306
433
  expect(last_response.location).to match /https:\/\/idp.sso.example.com\/signoff\/29490/
307
- expect(last_response.location).to match /RelayState=https%3A%2F%2Fexample.com%2F/
434
+ expect(last_response.location).to match /RelayState=%2Flogout/
308
435
  end
309
436
  end
310
437
 
438
+ it_behaves_like 'validating RelayState param'
439
+
440
+ context 'when slo_default_relay_state is blank' do
441
+ let(:saml_options) { super().merge(slo_default_relay_state: nil) }
442
+
443
+ context "when request relay state is invalid" do
444
+ let(:params) do
445
+ {
446
+ "SAMLRequest" => load_xml(:example_logout_request),
447
+ "RelayState" => "javascript:alert(1)",
448
+ }
449
+ end
450
+
451
+ it "redirects without including a RelayState parameter" do
452
+ subject
453
+
454
+ expect(last_response).to be_redirect
455
+ expect(last_response.location).to match %r{https://idp\.sso\.example\.com/signoff/29490}
456
+ expect(last_response.location).not_to match(/RelayState=/)
457
+ end
458
+ end
459
+ end
460
+
461
+ context "with a custom relay state validator" do
462
+ let(:saml_options) do
463
+ super().merge(
464
+ slo_relay_state_validator: proc do |relay_state, rack_request|
465
+ expect(rack_request).to respond_to(:params)
466
+ relay_state == "custom-state"
467
+ end,
468
+ )
469
+ end
470
+ let(:params) { super().merge("RelayState" => "custom-state") }
471
+
472
+ it { is_expected.to be_redirect.and have_attributes(location: a_string_matching(/RelayState=custom-state/)) }
473
+ end
474
+
311
475
  context "when request is an invalid logout request" do
312
476
  before :each do
313
477
  allow_any_instance_of(OneLogin::RubySaml::SloLogoutrequest).to receive(:is_valid?).and_return(false)
@@ -332,38 +496,80 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
332
496
  end
333
497
  end
334
498
 
335
- context "when sp initiated SLO" do
336
- def test_default_relay_state(static_default_relay_state = nil, &block_default_relay_state)
337
- saml_options["slo_default_relay_state"] = static_default_relay_state || block_default_relay_state
338
- post "/auth/saml/spslo"
499
+ context "when SLO is disabled" do
500
+ before do
501
+ saml_options[:slo_enabled] = false
502
+ post "/auth/saml/slo"
503
+ end
339
504
 
340
- expect(last_response).to be_redirect
341
- expect(last_response.location).to match /https:\/\/idp.sso.example.com\/signoff\/29490/
342
- expect(last_response.location).to match /RelayState=https%3A%2F%2Fexample.com%2F/
505
+ it "should return not implemented" do
506
+ expect(last_response.status).to eq 501
507
+ expect(last_response.body).to eq "Not Implemented"
343
508
  end
509
+ end
510
+ end
511
+
512
+ describe 'POST /auth/saml/spslo' do
513
+ let(:params) { {} }
514
+ subject { post "/auth/saml/spslo", params }
515
+
516
+ def test_default_relay_state(static_default_relay_state = nil, &block_default_relay_state)
517
+ saml_options["slo_default_relay_state"] = static_default_relay_state || block_default_relay_state
518
+ post "/auth/saml/spslo"
519
+
520
+ expect(last_response).to be_redirect
521
+ expect(last_response.location).to match /https:\/\/idp.sso.example.com\/signoff\/29490/
522
+ expect(last_response.location).to match /RelayState=https%3A%2F%2Fexample.com%2F/
523
+ end
524
+
525
+ it "should redirect to logout request" do
526
+ test_default_relay_state("https://example.com/")
527
+ end
344
528
 
345
- it "should redirect to logout request" do
346
- test_default_relay_state("https://example.com/")
529
+ it "should redirect to logout request with a block" do
530
+ test_default_relay_state do
531
+ "https://example.com/"
347
532
  end
533
+ end
348
534
 
349
- it "should redirect to logout request with a block" do
350
- test_default_relay_state do
351
- "https://example.com/"
352
- end
535
+ it "should redirect to logout request with a block with a request parameter" do
536
+ test_default_relay_state do |request|
537
+ "https://example.com/"
353
538
  end
539
+ end
354
540
 
355
- it "should redirect to logout request with a block with a request parameter" do
356
- test_default_relay_state do |request|
357
- "https://example.com/"
358
- end
541
+ it_behaves_like 'validating RelayState param'
542
+
543
+ context 'when slo_default_relay_state is blank' do
544
+ let(:saml_options) { super().merge(slo_default_relay_state: nil) }
545
+ let(:params) { { RelayState: "//example.com" } }
546
+
547
+ it "redirects without including a RelayState parameter" do
548
+ subject
549
+
550
+ expect(last_response).to be_redirect
551
+ expect(last_response.location).to match %r{https://idp\.sso\.example\.com/signoff/29490}
552
+ expect(last_response.location).not_to match(/RelayState=/)
359
553
  end
554
+ end
360
555
 
361
- it "should give not implemented without an idp_slo_service_url" do
362
- saml_options.delete(:idp_slo_service_url)
556
+ it "should give not implemented without an idp_slo_service_url" do
557
+ saml_options.delete(:idp_slo_service_url)
558
+ post "/auth/saml/spslo"
559
+
560
+ expect(last_response.status).to eq 501
561
+ expect(last_response.body).to match /Not Implemented/
562
+ end
563
+
564
+ context "when SLO is disabled" do
565
+ before do
566
+ saml_options[:slo_enabled] = false
363
567
  post "/auth/saml/spslo"
568
+ end
364
569
 
570
+ it "should return not implemented" do
365
571
  expect(last_response.status).to eq 501
366
- expect(last_response.body).to match /Not Implemented/
572
+ expect(last_response.body).to eq "Not Implemented"
367
573
  end
368
574
  end
369
575
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omniauth-saml
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.4
4
+ version: 2.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Raecoo Cao
@@ -11,10 +11,9 @@ authors:
11
11
  - Nikos Dimitrakopoulos
12
12
  - Rudolf Vriend
13
13
  - Bruno Pedro
14
- autorequire:
15
14
  bindir: bin
16
15
  cert_chain: []
17
- date: 2025-05-27 00:00:00.000000000 Z
16
+ date: 1980-01-02 00:00:00.000000000 Z
18
17
  dependencies:
19
18
  - !ruby/object:Gem::Dependency
20
19
  name: omniauth
@@ -129,7 +128,6 @@ dependencies:
129
128
  - !ruby/object:Gem::Version
130
129
  version: '0.8'
131
130
  description: A generic SAML strategy for OmniAuth.
132
- email:
133
131
  executables: []
134
132
  extensions: []
135
133
  extra_rdoc_files: []
@@ -147,7 +145,6 @@ homepage: https://github.com/omniauth/omniauth-saml
147
145
  licenses:
148
146
  - MIT
149
147
  metadata: {}
150
- post_install_message:
151
148
  rdoc_options: []
152
149
  require_paths:
153
150
  - lib
@@ -162,8 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
162
159
  - !ruby/object:Gem::Version
163
160
  version: '0'
164
161
  requirements: []
165
- rubygems_version: 3.4.19
166
- signing_key:
162
+ rubygems_version: 3.6.9
167
163
  specification_version: 4
168
164
  summary: A generic SAML strategy for OmniAuth.
169
165
  test_files: