lib/saml/bindings/http_redirect.rb
module Saml
  module Bindings
    class HTTPRedirect
      include Saml::Notification

      class << self
        def create_url(request_or_response, options = {})
          options[:signature_algorithm] ||= 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' unless options[:exclude_signature]
          new(request_or_response, options).create_url
        end

        def receive_message(http_request, options = {})
          options[:signature]           = Saml::Encoding.decode_64(http_request.params["Signature"] || "")
          options[:signature_algorithm] = http_request.params["SigAlg"]
          options[:relay_state]         = http_request.params["RelayState"]

          receive_xml = http_request.params["SAMLRequest"] || http_request.params["SAMLResponse"]
          if receive_xml.nil?
            raise Saml::Errors::InvalidParams, 'require params `SAMLRequest` or `SAMLResponse`'
          end
          
          request_or_response = parse_request_or_response(options.delete(:type), http_request.params)

          redirect_binding = new(request_or_response, options)
          query_string     = URI.parse(http_request.url).query

          if http_request.params["Signature"].present?
            redirect_binding.verify_signature(query_string)
          else
            raise Saml::Errors::SignatureMissing.new('Signature missing, but provider requires a signature') if request_or_response.provider.authn_requests_signed?
          end

          request_or_response.actual_destination = http_request.url
          request_or_response
        end

        private

        def parse_request_or_response(type, params)
          message = notify('receive_message', decode_message(params["SAMLRequest"] || params["SAMLResponse"]))

          Saml.parse_message(message, type)
        end

        def decode_message(message)
          Saml::Encoding.decode_gzip(Saml::Encoding.decode_64(message))
        end
      end

      attr_accessor :request_or_response, :signature_algorithm, :relay_state, :signature, :exclude_signature

      def initialize(request_or_response, options = {})
        @request_or_response = request_or_response
        @signature_algorithm = options[:signature_algorithm]
        @relay_state         = options[:relay_state]
        @signature           = options[:signature]
        @exclude_signature   = options[:exclude_signature]
      end

      def verify_signature(query)
        unless request_or_response.provider.verify(signature_algorithm, signature, parse_signature_params(query))
          raise Saml::Errors::SignatureInvalid.new
        end
      end

      def create_url
        url = request_or_response.destination
        delimiter = url.include?('?') ? '&' : '?'

        [url, exclude_signature ? unsigned_params : signed_params].join(delimiter)
      end

      private

      def param_key
        request_or_response.is_a?(Saml::ComplexTypes::StatusResponseType) ? "SAMLResponse" : "SAMLRequest"
      end

      def parse_signature_params(query)
        params = {}
        query.split(/[&;]/).each do |pairs|
          key, value  = pairs.split('=', 2)
          params[key] = value
        end

        relay_state = params["RelayState"] ? "&RelayState=#{params['RelayState']}" : ""
        "#{param_key}=#{params[param_key]}#{relay_state}&SigAlg=#{params['SigAlg']}"
      end

      def encoded_message
        Saml::Encoding.encode_64(Saml::Encoding.encode_gzip(notify('create_message', request_or_response.to_xml)))
      end

      def encoded_params
        params.collect do |key, value|
          "#{key}=#{CGI.escape(value)}"
        end.join('&')
      end

      def params
        params = {}

        params[param_key] = encoded_message
        params["RelayState"] = relay_state if relay_state
        params["SigAlg"] = signature_algorithm if signature_algorithm

        params
      end

      def signed_params
        signature = request_or_response.provider.sign(signature_algorithm, encoded_params)

        encoded_signature = CGI.escape(Saml::Encoding.encode_64(signature))

        "#{encoded_params}&Signature=#{encoded_signature}"
      end

      def unsigned_params
        encoded_params.to_s
      end
    end
  end
end
