main
 1# frozen_string_literal: true
 2
 3RSpec.describe Saml::Kit::Bindings::UrlBuilder do
 4  describe '#build' do
 5    let(:xml) { '<xml></xml>' }
 6    let(:destination) { FFaker::Internet.http_url }
 7    let(:relay_state) { FFaker::Movie.title }
 8
 9    [
10      [Saml::Kit::AuthenticationRequest, 'SAMLRequest'],
11      [Saml::Kit::LogoutRequest, 'SAMLRequest'],
12      [Saml::Kit::Response, 'SAMLResponse'],
13      [Saml::Kit::LogoutResponse, 'SAMLResponse'],
14    ].each do |(response_type, query_string_parameter)|
15      describe response_type.to_s do
16        subject { described_class.new(configuration: configuration) }
17
18        let(:configuration) do
19          Saml::Kit::Configuration.new do |config|
20            config.generate_key_pair_for(use: :signing)
21          end
22        end
23
24        let(:response) { instance_double(response_type, destination: destination, to_xml: xml, query_string_parameter: query_string_parameter) }
25
26        def to_query_params(url)
27          Hash[URI.parse(url).query.split('&').map { |x| x.split('=', 2) }]
28        end
29
30        it 'returns a url containing the target location' do
31          result_uri = URI.parse(subject.build(response))
32          expect(result_uri.scheme).to eql('http')
33          expect(result_uri.host).to eql(URI.parse(destination).host)
34        end
35
36        it 'includes the message deflated (without header and checksum), base64-encoded, and URL-encoded' do
37          result = subject.build(response, relay_state: relay_state)
38          query_params = to_query_params(result)
39          level = Zlib::BEST_COMPRESSION
40          expected = CGI.escape(Base64.encode64(Zlib::Deflate.deflate(xml, level)[2..-5]).delete("\n"))
41          expect(result).to include("#{query_string_parameter}=#{expected}")
42          expect(query_params[query_string_parameter]).to eql(expected)
43        end
44
45        it 'includes the relay state' do
46          result = subject.build(response, relay_state: relay_state)
47          query_params = to_query_params(result)
48          expect(query_params['RelayState']).to eql(CGI.escape(relay_state))
49          expect(result).to include("RelayState=#{CGI.escape(relay_state)}")
50        end
51
52        it 'excludes the relay state' do
53          query_params = to_query_params(subject.build(response))
54          expect(query_params['RelayState']).to be_nil
55        end
56
57        it 'includes a signature' do
58          result = subject.build(response, relay_state: relay_state)
59          query_params = to_query_params(result)
60          expect(query_params['SigAlg']).to eql(CGI.escape(::Xml::Kit::Namespaces::SHA256))
61
62          payload = "#{query_string_parameter}=#{query_params[query_string_parameter]}" \
63                    "&RelayState=#{query_params['RelayState']}" \
64                    "&SigAlg=#{query_params['SigAlg']}"
65          private_key = configuration.private_keys(use: :signing).last
66          expected_signature = Base64.strict_encode64(private_key.sign(OpenSSL::Digest::SHA256.new, payload))
67          expect(query_params['Signature']).to eql(expected_signature)
68        end
69
70        it 'generates the signature correctly when the relay state is absent' do
71          result = subject.build(response)
72          query_params = to_query_params(result)
73          expect(query_params['SigAlg']).to eql(CGI.escape(::Xml::Kit::Namespaces::SHA256))
74
75          payload = "#{query_string_parameter}=#{query_params[query_string_parameter]}" \
76                    "&SigAlg=#{query_params['SigAlg']}"
77          private_key = configuration.private_keys(use: :signing).last
78          expected_signature = Base64.strict_encode64(private_key.sign(OpenSSL::Digest::SHA256.new, payload))
79          expect(query_params['Signature']).to eql(expected_signature)
80        end
81      end
82    end
83  end
84end