Skip to content

Commit 5eafa99

Browse files
Merge pull request #16 from gerardo-navarro/gerardo-navarro-complete-shared-examples-for-test-cases
Streamline RelayState shared example matchers
2 parents 270fec1 + 03ee7c1 commit 5eafa99

File tree

1 file changed

+75
-165
lines changed

1 file changed

+75
-165
lines changed

spec/omniauth/strategies/saml_spec.rb

Lines changed: 75 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,55 @@
3030
end
3131
let(:strategy) { [OmniAuth::Strategies::SAML, saml_options] }
3232

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+
3382
describe 'POST /auth/saml' do
3483
context 'without idp runtime params present' do
3584
before do
@@ -280,17 +329,12 @@ def post_xml(xml = :example_response, opts = {})
280329
{ "rack.session" => { "saml_transaction_id" => "_3fef1069-d0c6-418a-b68d-6f008a4787e9" } }
281330
end
282331

283-
let(:params) do
284-
{
285-
SAMLResponse: load_xml(:example_logout_response),
286-
RelayState: relay_state,
287-
}
288-
end
332+
let(:params) { { SAMLResponse: load_xml(:example_logout_response) } }
289333

290334
subject(:post_slo_response) { post "/auth/saml/slo", params, opts }
291335

292336
context "when relay state is relative" do
293-
let(:relay_state) { "/signed-out" }
337+
let(:params) {super().merge(RelayState: "/signed-out")}
294338

295339
it "redirects to the relaystate" do
296340
post_slo_response
@@ -301,7 +345,7 @@ def post_xml(xml = :example_response, opts = {})
301345
end
302346

303347
context "when relay state is an absolute https URL" do
304-
let(:relay_state) { "https://example.com/" }
348+
let(:params) {super().merge(RelayState: "https://example.com/")}
305349

306350
it "redirects without a location header" do
307351
post_slo_response
@@ -314,35 +358,32 @@ def post_xml(xml = :example_response, opts = {})
314358
context 'when slo_default_relay_state is present' do
315359
let(:saml_options) { super().merge(slo_default_relay_state: '/signed-out') }
316360

361+
context "when response relay state is valid" do
362+
let(:params) {super().merge(RelayState: "/safe/logout")}
363+
364+
it {is_expected.to be_redirect.and have_attributes(location: '/safe/logout') }
365+
end
366+
317367
context "when response relay state is invalid" do
318-
let(:relay_state) { "https://example.com/" }
319-
320-
[
321-
"//attacker.test",
322-
"javascript:alert(1)",
323-
"https://example.com/logout",
324-
].each do |unsafe_relay_state|
325-
context "#{unsafe_relay_state}" do
326-
let(:relay_state) { unsafe_relay_state }
327-
328-
it { is_expected.to be_redirect.and have_attributes(location: "/signed-out") }
329-
end
330-
end
368+
let(:params) {super().merge(RelayState: "javascript:alert(1)")}
369+
370+
it {is_expected.to be_redirect.and have_attributes(location: '/signed-out') }
331371
end
332372
end
333373

334374
context 'when slo_default_relay_state is blank' do
335375
let(:saml_options) { super().merge(slo_default_relay_state: nil) }
336376

337-
context "when response relay state is invalid" do
338-
let(:relay_state) { "javascript:alert(1)" }
377+
context "when response relay state is valid" do
378+
let(:params) {super().merge(RelayState: "/safe/logout")}
339379

340-
it "redirects without a location header" do
341-
post_slo_response
380+
it {is_expected.to be_redirect.and have_attributes(location: '/safe/logout') }
381+
end
342382

343-
expect(last_response).to be_redirect
344-
expect(last_response.headers.fetch("Location")).to be_nil
345-
end
383+
context "when response relay state is invalid" do
384+
let(:params) {super().merge(RelayState: "javascript:alert(1)")}
385+
386+
it {is_expected.to be_redirect.and have_attributes(location: nil) }
346387
end
347388
end
348389
end
@@ -374,78 +415,7 @@ def post_xml(xml = :example_response, opts = {})
374415
context 'when slo_default_relay_state is present' do
375416
let(:saml_options) { super().merge(slo_default_relay_state: '/signed-out') }
376417

377-
context "when request relay state is invalid" do
378-
let(:relay_state) { "https://example.com/" }
379-
380-
let(:params) do
381-
{
382-
"SAMLRequest" => load_xml(:example_logout_request),
383-
"RelayState" => relay_state,
384-
}
385-
end
386-
387-
[
388-
"//attacker.test",
389-
"javascript:alert(1)",
390-
"https://example.com/logout",
391-
].each do |unsafe_relay_state|
392-
context "when the relay state is #{unsafe_relay_state}" do
393-
let(:relay_state) { unsafe_relay_state }
394-
395-
it { is_expected.to be_redirect.and have_attributes(location: a_string_matching(/RelayState=%2Fsigned-out/)) }
396-
end
397-
end
398-
399-
context "when the validator rejects the default relay state" do
400-
let(:relay_state) { "javascript:alert(1)" }
401-
let(:saml_options) do
402-
super().merge(slo_relay_state_validator: proc { |value| value.start_with?("https://") })
403-
end
404-
405-
it { is_expected.to be_redirect.and have_attributes(location: a_string_matching(/RelayState=%2Fsigned-out/)) }
406-
end
407-
408-
end
409-
410-
context "with validators that resolve to falsy" do
411-
let(:params) { super().merge("RelayState" => "javascript:alert(1)") }
412-
413-
context "when the validator option is nil" do
414-
let(:saml_options) { super().merge(slo_relay_state_validator: nil) }
415-
416-
it { is_expected.to have_attributes(location: a_string_matching(/RelayState=%2Fsigned-out/)) }
417-
end
418-
419-
context "when the validator option is false" do
420-
let(:saml_options) { super().merge(slo_relay_state_validator: false) }
421-
422-
it { is_expected.to have_attributes(location: a_string_matching(/RelayState=%2Fsigned-out/)) }
423-
end
424-
425-
context "when the validator option is true" do
426-
let(:saml_options) { super().merge(slo_relay_state_validator: true) }
427-
428-
it { is_expected.to have_attributes(location: a_string_matching(/RelayState=javascript%3Aalert%281%29/)) }
429-
end
430-
431-
context "when the validator returns false" do
432-
let(:saml_options) do
433-
super().merge(slo_relay_state_validator: proc { |relay_state| relay_state == "/signed-out" })
434-
end
435-
436-
it { is_expected.to have_attributes(location: a_string_matching(/RelayState=%2Fsigned-out/)) }
437-
end
438-
439-
context "when the validator returns nil" do
440-
let(:saml_options) do
441-
super().merge(
442-
slo_relay_state_validator: proc { |relay_state| relay_state == "/signed-out" ? true : nil },
443-
)
444-
end
445-
446-
it { is_expected.to have_attributes(location: a_string_matching(/RelayState=%2Fsigned-out/)) }
447-
end
448-
end
418+
it_behaves_like 'validating RelayState param'
449419
end
450420

451421
context 'when slo_default_relay_state is blank' do
@@ -546,72 +516,12 @@ def test_default_relay_state(static_default_relay_state = nil, &block_default_re
546516
end
547517
end
548518

549-
context 'when slo_default_relay_state is present' do
550-
let(:saml_options) { super().merge(slo_default_relay_state: '/signed-out') }
551-
let(:params) { {} }
552-
subject { post "/auth/saml/spslo", params }
553-
554-
context 'with an https relay state' do
555-
let(:params) { { RelayState: "https://example.com/logout" } }
556-
557-
it { is_expected.to be_redirect.and have_attributes(location: a_string_matching(/RelayState=%2Fsigned-out/)) }
558-
end
559-
560-
context 'with a protocol relative relay state' do
561-
let(:params) { { RelayState: "//attacker.test" } }
562-
563-
it { is_expected.to be_redirect.and have_attributes(location: a_string_matching(/RelayState=%2Fsigned-out/)) }
564-
end
565-
566-
context 'with a javascript relay state' do
567-
let(:params) { { RelayState: "javascript:alert(1)" } }
568-
569-
it { is_expected.to be_redirect.and have_attributes(location: a_string_matching(/RelayState=%2Fsigned-out/)) }
570-
571-
context 'when the validator would reject the default' do
572-
let(:saml_options) do
573-
super().merge(slo_relay_state_validator: proc { |value| value.start_with?("https://") })
574-
end
575-
576-
it { is_expected.to be_redirect.and have_attributes(location: a_string_matching(/RelayState=%2Fsigned-out/)) }
577-
end
578-
579-
context 'when the validator is nil' do
580-
let(:saml_options) { super().merge(slo_relay_state_validator: nil) }
581-
582-
it { is_expected.to be_redirect.and have_attributes(location: a_string_matching(/RelayState=%2Fsigned-out/)) }
583-
end
584-
585-
context 'when the validator is false' do
586-
let(:saml_options) { super().merge(slo_relay_state_validator: false) }
587-
588-
it { is_expected.to be_redirect.and have_attributes(location: a_string_matching(/RelayState=%2Fsigned-out/)) }
589-
end
590-
591-
context 'when the validator is true' do
592-
let(:saml_options) { super().merge(slo_relay_state_validator: true) }
593-
594-
it { is_expected.to be_redirect.and have_attributes(location: a_string_matching(/RelayState=javascript%3Aalert%281%29/)) }
595-
end
596-
597-
context 'when the validator returns false' do
598-
let(:saml_options) do
599-
super().merge(slo_relay_state_validator: proc { |state| state == "/signed-out" })
600-
end
601-
602-
it { is_expected.to be_redirect.and have_attributes(location: a_string_matching(/RelayState=%2Fsigned-out/)) }
603-
end
604-
605-
context 'when the validator returns nil' do
606-
let(:saml_options) do
607-
super().merge(
608-
slo_relay_state_validator: proc { |state| state == "/signed-out" ? true : nil },
609-
)
610-
end
519+
context 'when slo_default_relay_state is present' do
520+
let(:saml_options) { super().merge(slo_default_relay_state: '/signed-out') }
521+
let(:params) { {} }
522+
subject { post "/auth/saml/spslo", params }
611523

612-
it { is_expected.to be_redirect.and have_attributes(location: a_string_matching(/RelayState=%2Fsigned-out/)) }
613-
end
614-
end
524+
it_behaves_like 'validating RelayState param'
615525

616526
context 'when using a custom default relay state' do
617527
let(:saml_options) do

0 commit comments

Comments
 (0)