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