Commit 341d0b3

mo <mo.khan@gmail.com>
2017-11-04 20:37:46
add validation for Response.
1 parent 77f623a
lib/saml/kit/authentication_request.rb
@@ -3,13 +3,13 @@ module Saml
     class AuthenticationRequest
       PROTOCOL_XSD = File.expand_path("./xsd/saml-schema-protocol-2.0.xsd", File.dirname(__FILE__)).freeze
       include XsdValidatable
-
       include ActiveModel::Validations
+
       validates_presence_of :content
       validates_presence_of :acs_url, if: :login_request?
       validate :must_be_request
       validate :must_have_valid_signature
-      validate :must_be_registered_service_provider
+      validate :must_be_registered
       validate :must_match_xsd
 
       attr_reader :content, :name
@@ -51,11 +51,11 @@ module Saml
       private
 
       def registered_acs_url
-        acs_urls = service_provider.assertion_consumer_services
+        acs_urls = provider.assertion_consumer_services
         return acs_urls.first[:location] if acs_urls.any?
       end
 
-      def service_provider
+      def provider
         registry.metadata_for(issuer)
       end
 
@@ -63,9 +63,9 @@ module Saml
         Saml::Kit.configuration.registry
       end
 
-      def must_be_registered_service_provider
+      def must_be_registered
         return unless login_request?
-        return if service_provider.matches?(fingerprint, use: "signing")
+        return if provider.matches?(fingerprint, use: "signing")
 
         errors[:base] << error_message(:invalid)
       end
lib/saml/kit/metadata.rb
@@ -62,6 +62,15 @@ module Saml
         end
       end
 
+      def matches?(fingerprint, use: :signing)
+        if :signing == use
+          sha256 = fingerprint.algorithm(OpenSSL::Digest::SHA256)
+          signing_certificates.find do |signing_certificate|
+            sha256 == signing_certificate[:fingerprint]
+          end
+        end
+      end
+
       def to_xml
         @xml
       end
lib/saml/kit/response.rb
@@ -1,13 +1,29 @@
 module Saml
   module Kit
     class Response
+      PROTOCOL_XSD = File.expand_path("./xsd/saml-schema-protocol-2.0.xsd", File.dirname(__FILE__)).freeze
+      include ActiveModel::Validations
+      include XsdValidatable
+
+      attr_reader :content, :name
+      validates_presence_of :content
+      validate :must_have_valid_signature
+      validate :must_be_response
+      validate :must_be_registered
+      validate :must_match_xsd
+
       def initialize(xml)
-        @xml = xml
+        @content = xml
         @xml_hash = Hash.from_xml(xml)
+        @name = 'Response'
       end
 
       def name_id
-        @xml_hash['Response']['Assertion']['Subject']['NameID']
+        @xml_hash[name]['Assertion']['Subject']['NameID']
+      end
+
+      def issuer
+        @xml_hash[name]['Issuer']
       end
 
       def [](key)
@@ -15,29 +31,80 @@ module Saml
       end
 
       def attributes
-        @attributes ||= Hash[@xml_hash['Response']['Assertion']['AttributeStatement']['Attribute'].map do |item|
+        @attributes ||= Hash[@xml_hash[name]['Assertion']['AttributeStatement']['Attribute'].map do |item|
           [item['Name'].to_sym, item['AttributeValue']]
         end].with_indifferent_access
       end
 
       def acs_url
-        @xml_hash['Response']['Destination']
+        @xml_hash[name]['Destination']
       end
 
       def to_xml
-        @xml
+        content
       end
 
       def encode
         Base64.strict_encode64(to_xml)
       end
 
+      def certificate
+        @xml_hash[name]['Signature']['KeyInfo']['X509Data']['X509Certificate']
+      end
+
+      def fingerprint
+        Fingerprint.new(certificate)
+      end
+
       def self.parse(saml_response)
         new(Base64.decode64(saml_response))
       end
 
+      private
+
+      def provider
+        registry.metadata_for(issuer)
+      end
+
+      def registry
+        Saml::Kit.configuration.registry
+      end
+
+      def must_have_valid_signature
+        return if to_xml.blank?
+
+        xml = Saml::Kit::Xml.new(to_xml)
+        xml.valid?
+        xml.errors.each do |error|
+          errors[:base] << error
+        end
+      end
+
+      def must_be_response
+        return if to_xml.blank?
+
+        errors[:base] << error_message(:invalid) unless login_response?
+      end
+
+      def must_be_registered
+        return unless login_response?
+        return if provider.present? && provider.matches?(fingerprint, use: "signing")
+
+        errors[:base] << error_message(:invalid)
+      end
+
+      def must_match_xsd
+        matches_xsd?(PROTOCOL_XSD)
+      end
+
+      def login_response?
+        return false if to_xml.blank?
+        @xml_hash[name].present?
+      end
+
       class Builder
-        attr_reader :user, :request, :id, :reference_id, :now, :name_id_format
+        attr_reader :user, :request
+        attr_accessor :id, :reference_id, :now, :name_id_format
 
         def initialize(user, request)
           @user = user
@@ -51,11 +118,11 @@ module Saml
         def to_xml
           signature = Signature.new(id)
           xml = ::Builder::XmlMarkup.new
-          xml.tag!("samlp:Response", response_options) do
-            signature.template(xml)
+          xml.Response response_options do
             xml.Issuer(configuration.issuer, xmlns: Namespaces::ASSERTION)
-            xml.tag!("samlp:Status") do
-              xml.tag!('samlp:StatusCode', Value: Namespaces::SUCCESS)
+            signature.template(xml)
+            xml.Status do
+              xml.StatusCode Value: Namespaces::SUCCESS
             end
             xml.Assertion(assertion_options) do
               xml.Issuer configuration.issuer
@@ -105,7 +172,7 @@ module Saml
             Destination: request.acs_url,
             Consent: Namespaces::UNSPECIFIED,
             InResponseTo: request.id,
-            "xmlns:samlp" => Namespaces::PROTOCOL,
+            xmlns: Namespaces::PROTOCOL,
           }
         end
 
@@ -114,6 +181,7 @@ module Saml
             ID: "_#{reference_id}",
             IssueInstant: now.iso8601,
             Version: "2.0",
+            xmlns: Namespaces::ASSERTION,
           }
         end
 
lib/saml/kit/service_provider_metadata.rb
@@ -14,15 +14,6 @@ module Saml
         end
       end
 
-      def matches?(fingerprint, use: :signing)
-        if :signing == use
-          sha256 = fingerprint.algorithm(OpenSSL::Digest::SHA256)
-          signing_certificates.find do |signing_certificate|
-            sha256 == signing_certificate[:fingerprint]
-          end
-        end
-      end
-
       private
 
       class Builder
spec/saml/response_spec.rb
@@ -137,4 +137,62 @@ RSpec.describe Saml::Kit::Response do
       expect(result['Response']['Assertion']['AttributeStatement']['Attribute'][0]['AttributeValue']).to eql("ea64c235-e18d-4b9a-8672-06ef84dabdec")
     end
   end
+
+  describe "#valid?" do
+    let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: "_#{SecureRandom.uuid}", issuer: FFaker::Internet.http_url, acs_url: FFaker::Internet.http_url) }
+    let(:user) { double(:user, uuid: SecureRandom.uuid, assertion_attributes_for: { id: SecureRandom.uuid }) }
+    let(:builder) { described_class::Builder.new(user, request) }
+    let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
+    let(:metadata) { instance_double(Saml::Kit::IdentityProviderMetadata) }
+
+    before :each do
+      allow(Saml::Kit.configuration).to receive(:registry).and_return(registry)
+    end
+
+    it 'is valid' do
+      allow(registry).to receive(:metadata_for).and_return(metadata)
+      allow(metadata).to receive(:matches?).and_return(true)
+      expect(builder.build).to be_valid
+    end
+
+    it 'is invalid when blank' do
+      expect(described_class.new("")).to be_invalid
+    end
+
+    it 'is invalid if the document has been tampered with' do
+      allow(registry).to receive(:metadata_for).and_return(metadata)
+      allow(metadata).to receive(:matches?).and_return(true)
+      name_id_format = Saml::Kit::Namespaces::PERSISTENT
+      builder.name_id_format = name_id_format
+      subject = described_class.new(builder.to_xml.gsub(name_id_format, Saml::Kit::Namespaces::EMAIL_ADDRESS))
+      expect(subject).to_not be_valid
+    end
+
+    it 'is invalid when not a Response' do
+      xml = Saml::Kit::IdentityProviderMetadata::Builder.new.to_xml
+      expect(described_class.new(xml)).to be_invalid
+    end
+
+    it 'is invalid when the fingerprint of the certificate does not match the registered fingerprint' do
+      allow(registry).to receive(:metadata_for).and_return(metadata)
+      allow(metadata).to receive(:matches?).and_return(false)
+      expect(described_class.new(builder.to_xml)).to be_invalid
+    end
+
+    it 'validates the schema of the response' do
+      allow(registry).to receive(:metadata_for).and_return(metadata)
+      allow(metadata).to receive(:matches?).and_return(true)
+      xml = ::Builder::XmlMarkup.new
+      id = SecureRandom.uuid
+      options = { "xmlns:samlp" => Saml::Kit::Namespaces::PROTOCOL, ID: "_#{id}", }
+      signature = Saml::Kit::Signature.new(id)
+      xml.tag!("samlp:Response", options) do
+        signature.template(xml)
+        xml.Fake do
+          xml.NotAllowed "Huh?"
+        end
+      end
+      expect(described_class.new(signature.finalize(xml))).to be_invalid
+    end
+  end
 end