@@ -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 :lowercase_url_encoding security option
468+ settings . security [ :lowercase_url_encoding ] = true
469+ logout_request_force_downcasing_test = OneLogin ::RubySaml ::SloLogoutrequest . new (
470+ params [ 'SAMLRequest' ] , get_params : params , settings : settings
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
0 commit comments