omniauth-saml 2.2.3 → 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: 74e6f8ffd71deca8c0cf5a47561df7c878b200af810dbf893f89677eb49da313
4
- data.tar.gz: 217c895d6d946983062dc1f66f362f98aa394c94a8f0267e69e2af9f1555cbc4
3
+ metadata.gz: 7d346f41f4069110547ddd73e4d9f7b23196d8519871da44f0eb42f2176c5fe6
4
+ data.tar.gz: 7451c766a513d52948fc07e0ac971e6e5d1b4392d822991444db38d72a66c835
5
5
  SHA512:
6
- metadata.gz: '0825de571d12121384accff0a106c4d76420308d007698632dd2661f030942cc4ed570e649f8b455ddcf2340d9109d26422de44ec3eb36f06978b732023409b4'
7
- data.tar.gz: 5229a183ad1d335f01b9de5111293925d5be4d8e461de89e2d5f4924a14939eb4286fe4d307af7c7e5db2b126a0ff495a0c3c9ba064f6d430624f207c25119df
6
+ metadata.gz: f93c043e99ccd8521877c45d2627cc87a150c42fb086b394172be4a12d02c28b816fec390810f934bdf10b07f68049ee222d15dc2a1e50e623e155b90951fca1
7
+ data.tar.gz: ae4440bfcb758760cd1f15b797d2f9b0b339a4ecfebd16e80834359c247cb335c15459fd509acf71364971ed5ef9d8d7009224f2996c3fbfd1ad54a92b26282d
data/CHANGELOG.md CHANGED
@@ -1,10 +1,28 @@
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
+
10
+ <a name="v2.2.4"></a>
11
+ ### v2.2.4 (2025-05-14)
12
+
13
+
14
+ #### Bug Fixes
15
+
16
+ * remove :idp_cert_fingerprint_validator ([c573690](/../../commit/c573690))
17
+ * Fix GHSA-cgp2-2cmh-pf7x
18
+
1
19
  <a name="v2.2.3"></a>
2
20
  ### v2.2.3 (2025-03-12)
3
21
 
4
22
 
5
23
  #### Features
6
24
 
7
- * new release 2.2.3 ([0d06a3c](/../../commit/0d06a3c))
25
+ * new release 2.2.3 ([34eb354](/../../commit/34eb354))
8
26
 
9
27
 
10
28
  #### Bug Fixes
data/README.md CHANGED
@@ -39,7 +39,6 @@ use OmniAuth::Strategies::SAML,
39
39
  :encryption => []
40
40
  },
41
41
  :idp_cert_fingerprint => "E7:91:B2:E1:...",
42
- :idp_cert_fingerprint_validator => lambda { |fingerprint| fingerprint },
43
42
  :name_identifier_format => "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
44
43
  ```
45
44
 
@@ -66,7 +65,6 @@ Rails.application.config.middleware.use OmniAuth::Builder do
66
65
  :encryption => []
67
66
  },
68
67
  :idp_cert_fingerprint => "E7:91:B2:E1:...",
69
- :idp_cert_fingerprint_validator => lambda { |fingerprint| fingerprint },
70
68
  :name_identifier_format => "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
71
69
  end
72
70
  ```
@@ -103,7 +101,18 @@ Note that when [integrating with Devise](#devise-integration), the URL path will
103
101
  * `:slo_default_relay_state` - The value to use as default `RelayState` for single log outs. The
104
102
  value can be a string, or a `Proc` (or other object responding to `call`). The `request`
105
103
  instance will be passed to this callable if it has an arity of 1. If the value is a string,
106
- 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.
107
116
 
108
117
  * `:idp_sso_service_url_runtime_params` - A dynamic mapping of request params that exist
109
118
  during the request phase of OmniAuth that should to be sent to the IdP after a specific
@@ -112,20 +121,16 @@ Note that when [integrating with Devise](#devise-integration), the URL path will
112
121
  `original_param_value`. Optional.
113
122
 
114
123
  * `:idp_cert` - The identity provider's certificate in PEM format. Takes precedence
115
- over the fingerprint option below. This option or `:idp_cert_multi` or `:idp_cert_fingerprint` or `:idp_cert_fingerprint_validator` must
124
+ over the fingerprint option below. This option or `:idp_cert_multi` or `:idp_cert_fingerprint` must
116
125
  be present.
117
-
126
+
118
127
  * `:idp_cert_multi` - Multiple identity provider certificates in PEM format. Takes precedence
119
- over the fingerprint option below. This option `:idp_cert` or `:idp_cert_fingerprint` or `:idp_cert_fingerprint_validator` must
128
+ over the fingerprint option below. This option `:idp_cert` or `:idp_cert_fingerprint` must
120
129
  be present.
121
130
 
122
131
  * `:idp_cert_fingerprint` - The SHA1 fingerprint of the certificate, e.g.
123
132
  "90:CC:16:F0:8D:...". This is provided from the identity provider when setting up
124
- the relationship. This option or `:idp_cert` or `:idp_cert_multi` or `:idp_cert_fingerprint_validator` MUST be present.
125
-
126
- * `:idp_cert_fingerprint_validator` - A lambda that MUST accept one parameter
127
- (the fingerprint), verify if it is valid and return it if successful. This option
128
- or `:idp_cert` or `:idp_cert_multi` or `:idp_cert_fingerprint` MUST be present.
133
+ the relationship. This option or `:idp_cert` or `:idp_cert_multi` MUST be present.
129
134
 
130
135
  * `:name_identifier_format` - Used during SP-initiated SSO. Describes the format of
131
136
  the username required by this application. If you need the email address, use
@@ -198,7 +203,9 @@ Single Logout can be Service Provider initiated or Identity Provider initiated.
198
203
  For SP initiated logout, the `idp_slo_service_url` option must be set to the logout url on the IdP,
199
204
  and users directed to `user_saml_omniauth_authorize_path + '/spslo'` after logging out locally. For
200
205
  IdP initiated logout, logout requests from the IdP should go to `/auth/saml/slo` (this can be
201
- 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.
202
209
 
203
210
  When using Devise as an authentication solution, the SP initiated flow can be integrated
204
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
 
@@ -43,9 +63,6 @@ module OmniAuth
43
63
  raise OmniAuth::Strategies::SAML::ValidationError.new("SAML response missing") unless request.params["SAMLResponse"]
44
64
 
45
65
  with_settings do |settings|
46
- # Call a fingerprint validation method if there's one
47
- validate_fingerprint(settings) if options.idp_cert_fingerprint_validator
48
-
49
66
  handle_response(request.params["SAMLResponse"], options_for_response_object, settings) do
50
67
  super
51
68
  end
@@ -76,8 +93,12 @@ module OmniAuth
76
93
  if on_subpath?(:metadata)
77
94
  other_phase_for_metadata
78
95
  elsif on_subpath?(:slo)
96
+ return slo_disabled_response unless slo_enabled?
97
+
79
98
  other_phase_for_slo
80
99
  elsif on_subpath?(:spslo)
100
+ return slo_disabled_response unless slo_enabled?
101
+
81
102
  other_phase_for_spslo
82
103
  else
83
104
  call_app!
@@ -118,6 +139,22 @@ module OmniAuth
118
139
  nil
119
140
  end
120
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
+
121
158
  private
122
159
 
123
160
  def request_path_pattern
@@ -146,19 +183,34 @@ module OmniAuth
146
183
 
147
184
  def slo_relay_state
148
185
  if request.params.has_key?("RelayState") && request.params["RelayState"] != ""
149
- request.params["RelayState"]
150
- else
151
- slo_default_relay_state = options.slo_default_relay_state
152
- if slo_default_relay_state.respond_to?(:call)
153
- if slo_default_relay_state.arity == 1
154
- slo_default_relay_state.call(request)
155
- else
156
- slo_default_relay_state.call
157
- end
158
- else
159
- slo_default_relay_state
160
- end
186
+ relay_state = request.params["RelayState"]
187
+
188
+ return relay_state if valid_slo_relay_state?(relay_state)
161
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)
162
214
  end
163
215
 
164
216
  def handle_logout_response(raw_response, settings)
@@ -218,17 +270,6 @@ module OmniAuth
218
270
  yield OneLogin::RubySaml::Settings.new(options)
219
271
  end
220
272
 
221
- def validate_fingerprint(settings)
222
- fingerprint_exists = options.idp_cert_fingerprint_validator[response_fingerprint]
223
-
224
- unless fingerprint_exists
225
- raise OmniAuth::Strategies::SAML::ValidationError.new("Non-existent fingerprint")
226
- end
227
-
228
- # id_cert_fingerprint becomes the given fingerprint if it exists
229
- settings.idp_cert_fingerprint = fingerprint_exists
230
- end
231
-
232
273
  def options_for_response_object
233
274
  # filter options to select only extra parameters
234
275
  opts = options.select {|k,_| RUBYSAML_RESPONSE_OPTIONS.include?(k.to_sym)}
@@ -273,6 +314,14 @@ module OmniAuth
273
314
  end
274
315
  end
275
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
+
276
325
  def add_request_attributes_to(settings)
277
326
  settings.attribute_consuming_service.service_name options.attribute_service_name
278
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.3'
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,42 +221,9 @@ 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
149
- expect(auth_hash['extra']['response_object']).to be_kind_of(OneLogin::RubySaml::Response)
150
- end
151
- end
152
-
153
- context "when fingerprint is empty and there's a fingerprint validator" do
154
- before :each do
155
- saml_options.delete(:idp_cert_fingerprint)
156
- saml_options[:idp_cert_fingerprint_validator] = fingerprint_validator
157
- end
158
-
159
- let(:fingerprint_validator) { lambda { |_| "C1:59:74:2B:E8:0C:6C:A9:41:0F:6E:83:F6:D1:52:25:45:58:89:FB" } }
160
-
161
- context "when the fingerprint validator returns a truthy value" do
162
- before { post_xml }
163
-
164
- it "should set the uid to the nameID in the SAML response" do
165
- expect(auth_hash['uid']).to eq '_1f6fcf6be5e13b08b1e3610e7ff59f205fbd814f23'
166
- end
167
-
168
- it "should set the raw info to all attributes" do
169
- expect(auth_hash['extra']['raw_info'].all.to_hash).to eq(
170
- 'first_name' => ['Rajiv'],
171
- 'last_name' => ['Manglani'],
172
- 'email' => ['user@example.com'],
173
- 'company_name' => ['Example Company'],
174
- 'fingerprint' => 'C1:59:74:2B:E8:0C:6C:A9:41:0F:6E:83:F6:D1:52:25:45:58:89:FB'
175
- )
176
- end
177
- end
178
-
179
- context "when the fingerprint validator returns false" do
180
- let(:fingerprint_validator) { lambda { |_| false } }
181
-
182
- before { post_xml }
224
+ post_callback_response
183
225
 
184
- it { is_expected.to fail_with(:invalid_ticket) }
226
+ expect(auth_hash['extra']['response_object']).to be_kind_of(OneLogin::RubySaml::Response)
185
227
  end
186
228
  end
187
229
 
@@ -189,24 +231,22 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
189
231
  before :each do
190
232
  saml_options.delete(:assertion_consumer_service_url)
191
233
  OmniAuth.config.full_host = 'http://localhost:9080'
192
- post_xml
193
234
  end
194
235
 
195
236
  it { is_expected.not_to fail_with(:invalid_ticket) }
196
237
  end
197
238
 
198
239
  context "when there is no SAMLResponse parameter" do
199
- before :each do
200
- post '/auth/saml/callback'
201
- end
240
+ let(:params) { {} }
202
241
 
203
242
  it { is_expected.to fail_with(:invalid_ticket) }
204
243
  end
205
244
 
206
245
  context "when there is no name id in the XML" do
246
+ let(:xml) { :no_name_id }
247
+
207
248
  before :each do
208
249
  allow(Time).to receive(:now).and_return(Time.utc(2012, 11, 8, 23, 55, 00))
209
- post_xml :no_name_id
210
250
  end
211
251
 
212
252
  it { is_expected.to fail_with(:invalid_ticket) }
@@ -215,43 +255,37 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
215
255
  context "when the fingerprint is invalid" do
216
256
  before :each do
217
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"
218
- post_xml
219
258
  end
220
259
 
221
260
  it { is_expected.to fail_with(:invalid_ticket) }
222
261
  end
223
262
 
224
263
  context "when the digest is invalid" do
225
- before :each do
226
- post_xml :digest_mismatch
227
- end
264
+ let(:xml) { :digest_mismatch }
228
265
 
229
266
  it { is_expected.to fail_with(:invalid_ticket) }
230
267
  end
231
268
 
232
269
  context "when the signature is invalid" do
233
- before :each do
234
- post_xml :invalid_signature
235
- end
270
+ let(:xml) { :invalid_signature }
236
271
 
237
272
  it { is_expected.to fail_with(:invalid_ticket) }
238
273
  end
239
274
 
240
275
  context "when the response is stale" do
276
+ let(:xml) { :example_response }
277
+
241
278
  before :each do
242
279
  allow(Time).to receive(:now).and_return(Time.utc(2012, 11, 8, 20, 45, 00))
243
280
  end
244
281
 
245
282
  context "without :allowed_clock_drift option" do
246
- before { post_xml :example_response }
247
-
248
283
  it { is_expected.to fail_with(:invalid_ticket) }
249
284
  end
250
285
 
251
286
  context "with :allowed_clock_drift option" do
252
287
  before :each do
253
288
  saml_options[:allowed_clock_drift] = 60
254
- post_xml :example_response
255
289
  end
256
290
 
257
291
  it { is_expected.to_not fail_with(:invalid_ticket) }
@@ -259,14 +293,16 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
259
293
  end
260
294
 
261
295
  context "when response has custom attributes" do
296
+ let(:xml) { :custom_attributes }
297
+
262
298
  before :each do
263
- 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"
264
299
  saml_options[:attribute_statements] = {
265
300
  email: ["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"],
266
301
  first_name: ["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"],
267
302
  last_name: ["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"]
268
303
  }
269
- post_xml :custom_attributes
304
+
305
+ post_callback_response
270
306
  end
271
307
 
272
308
  it "should obey attribute statements mapping" do
@@ -280,10 +316,12 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
280
316
  end
281
317
 
282
318
  context "when using custom user id attribute" do
319
+ let(:xml) { :custom_attributes }
320
+
283
321
  before :each do
284
- 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"
285
322
  saml_options[:uid_attribute] = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
286
- post_xml :custom_attributes
323
+
324
+ post_callback_response
287
325
  end
288
326
 
289
327
  it "should return user id attribute" do
@@ -294,55 +332,146 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
294
332
  context "when using custom user id attribute, but it is missing" do
295
333
  before :each do
296
334
  saml_options[:uid_attribute] = "missing_attribute"
297
- post_xml
298
335
  end
299
336
 
300
337
  it "should fail to authenticate" do
301
- should fail_with(:invalid_ticket)
338
+ expect(post_callback_response).to fail_with(:invalid_ticket)
302
339
  expect(last_request.env['omniauth.error']).to be_instance_of(OmniAuth::Strategies::SAML::ValidationError)
303
340
  expect(last_request.env['omniauth.error'].message).to eq("SAML response missing 'missing_attribute' attribute")
304
341
  end
305
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
306
349
 
307
350
  context "when response is a logout response" do
308
- before :each do
309
- 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
310
354
 
311
- post "/auth/saml/slo", {
312
- SAMLResponse: load_xml(:example_logout_response),
313
- RelayState: "https://example.com/",
314
- }, "rack.session" => {"saml_transaction_id" => "_3fef1069-d0c6-418a-b68d-6f008a4787e9"}
355
+ let(:params) { { SAMLResponse: load_xml(:example_logout_response) } }
356
+
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
315
368
  end
316
- it "should redirect to relaystate" do
317
- expect(last_response).to be_redirect
318
- 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
319
411
  end
320
412
  end
321
413
 
322
414
  context "when request is a logout request" do
323
415
  subject { post "/auth/saml/slo", params, "rack.session" => { "saml_uid" => "username@example.com" } }
324
416
 
325
- before :each do
326
- saml_options[:sp_entity_id] = "https://idp.sso.example.com/metadata/29490"
327
- end
417
+ let(:relay_state) { "https://example.com/" }
328
418
 
329
419
  let(:params) do
330
420
  {
331
421
  "SAMLRequest" => load_xml(:example_logout_request),
332
- "RelayState" => "https://example.com/",
422
+ "RelayState" => relay_state,
333
423
  }
334
424
  end
335
425
 
336
426
  context "when logout request is valid" do
427
+ let(:relay_state) { "/logout" }
428
+
337
429
  before { subject }
338
430
 
339
431
  it "should redirect to logout response" do
340
432
  expect(last_response).to be_redirect
341
433
  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/
434
+ expect(last_response.location).to match /RelayState=%2Flogout/
435
+ end
436
+ end
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
343
458
  end
344
459
  end
345
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
+
346
475
  context "when request is an invalid logout request" do
347
476
  before :each do
348
477
  allow_any_instance_of(OneLogin::RubySaml::SloLogoutrequest).to receive(:is_valid?).and_return(false)
@@ -367,38 +496,80 @@ describe OmniAuth::Strategies::SAML, :type => :strategy do
367
496
  end
368
497
  end
369
498
 
370
- context "when sp initiated SLO" do
371
- def test_default_relay_state(static_default_relay_state = nil, &block_default_relay_state)
372
- saml_options["slo_default_relay_state"] = static_default_relay_state || block_default_relay_state
373
- 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
374
504
 
375
- expect(last_response).to be_redirect
376
- expect(last_response.location).to match /https:\/\/idp.sso.example.com\/signoff\/29490/
377
- 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"
378
508
  end
509
+ end
510
+ end
511
+
512
+ describe 'POST /auth/saml/spslo' do
513
+ let(:params) { {} }
514
+ subject { post "/auth/saml/spslo", params }
379
515
 
380
- it "should redirect to logout request" do
381
- test_default_relay_state("https://example.com/")
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
528
+
529
+ it "should redirect to logout request with a block" do
530
+ test_default_relay_state do
531
+ "https://example.com/"
382
532
  end
533
+ end
383
534
 
384
- it "should redirect to logout request with a block" do
385
- test_default_relay_state do
386
- "https://example.com/"
387
- 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/"
388
538
  end
539
+ end
389
540
 
390
- it "should redirect to logout request with a block with a request parameter" do
391
- test_default_relay_state do |request|
392
- "https://example.com/"
393
- 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=/)
394
553
  end
554
+ end
555
+
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"
395
559
 
396
- it "should give not implemented without an idp_slo_service_url" do
397
- saml_options.delete(:idp_slo_service_url)
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
398
567
  post "/auth/saml/spslo"
568
+ end
399
569
 
570
+ it "should return not implemented" do
400
571
  expect(last_response.status).to eq 501
401
- expect(last_response.body).to match /Not Implemented/
572
+ expect(last_response.body).to eq "Not Implemented"
402
573
  end
403
574
  end
404
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.3
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-03-12 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: