Skip to content

Commit d239c0e

Browse files
committed
Handles Azure AD downcased request encoding
1 parent d9e7635 commit d239c0e

File tree

2 files changed

+51
-0
lines changed

2 files changed

+51
-0
lines changed

test/slo_logoutrequest_test.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,52 @@ class RubySamlTest < Minitest::Test
425425
logout_request_sign_test = OneLogin::RubySaml::SloLogoutrequest.new(params['SAMLRequest'], options)
426426
assert logout_request_sign_test.send(:validate_signature)
427427
end
428+
429+
it "handles Azure AD downcased request encoding" do
430+
# Use Logoutrequest only to build the SAMLRequest parameter.
431+
settings.security[:signature_method] = XMLSecurity::Document::RSA_SHA256
432+
settings.soft = false
433+
434+
# Creating the query manually to tweak it later instead of using
435+
# OneLogin::RubySaml::Utils.build_query
436+
request_doc = OneLogin::RubySaml::Logoutrequest.new.create_logout_request_xml_doc(settings)
437+
request = Zlib::Deflate.deflate(request_doc.to_s, 9)[2..-5]
438+
base64_request = Base64.encode64(request).gsub(/\n/, "")
439+
# The original request received from Azure AD comes with downcased
440+
# encoded characters, like %2f instead of %2F, and the signature they
441+
# send is based on this base64 request.
442+
params = {
443+
'SAMLRequest' => downcased_escape(base64_request),
444+
'SigAlg' => downcased_escape(settings.security[:signature_method]),
445+
}
446+
# Assemble query string.
447+
query = "SAMLRequest=#{params['SAMLRequest']}&SigAlg=#{params['SigAlg']}"
448+
# Make normalised signature based on our modified params.
449+
sign_algorithm = XMLSecurity::BaseDocument.new.algorithm(
450+
settings.security[:signature_method]
451+
)
452+
signature = settings.get_sp_key.sign(sign_algorithm.new, query)
453+
params['Signature'] = downcased_escape(Base64.encode64(signature).gsub(/\n/, ""))
454+
455+
# Then parameters are usually unescaped, like we manage them in rails
456+
params = params.map { |k, v| [k, CGI.unescape(v)] }.to_h
457+
# Construct SloLogoutrequest and ask it to validate the signature.
458+
# It will fail because the signature is based on the downcased request
459+
logout_request_downcased_test = OneLogin::RubySaml::SloLogoutrequest.new(
460+
params['SAMLRequest'], get_params: params, settings: settings,
461+
)
462+
assert_raises(OneLogin::RubySaml::ValidationError, "Invalid Signature on Logout Request") do
463+
logout_request_downcased_test.send(:validate_signature)
464+
end
465+
466+
# For this case, the parameters will be forced to be downcased after
467+
# being escaped with :force_escape_downcasing option
468+
logout_request_force_downcasing_test = OneLogin::RubySaml::SloLogoutrequest.new(
469+
params['SAMLRequest'], get_params: params, settings: settings,
470+
force_escape_downcasing: true
471+
)
472+
assert logout_request_force_downcasing_test.send(:validate_signature)
473+
end
428474
end
429475

430476
describe "#validate_signature with multiple idp certs" do

test/test_helper.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,4 +346,9 @@ def validate_xml!(document, schema)
346346
end
347347
end
348348
end
349+
350+
# Allows to emulate Azure AD request behavior
351+
def downcased_escape(str)
352+
CGI.escape(str).gsub(/%[A-Fa-f0-9]{2}/) { |match| match.downcase }
353+
end
349354
end

0 commit comments

Comments
 (0)