Commit 1f71e11

mo <mo@mokhan.ca>
2017-12-19 23:14:15
validate that there is only a single assertion.
1 parent ab1c5ed
Changed files (3)
lib
saml
spec
lib/saml/kit/locales/en.yml
@@ -5,6 +5,7 @@ en:
       Assertion:
         expired: "must not be expired."
         must_match_issuer: "must match entityId."
+        must_contain_single_assertion: "must contain single Assertion."
       AuthnRequest:
         invalid: "must contain AuthnRequest."
         invalid_fingerprint: "does not match."
lib/saml/kit/assertion.rb
@@ -70,12 +70,16 @@ module Saml
       private
 
       def assertion
-        if encrypted?
+        @assertion ||= if encrypted?
           decrypted = XmlDecryption.new(configuration: @configuration).decrypt(@xml_hash['Response']['EncryptedAssertion'])
           Saml::Kit.logger.debug(decrypted)
           Hash.from_xml(decrypted)['Assertion']
         else
-          @xml_hash.fetch('Response', {}).fetch('Assertion', {})
+          result = @xml_hash.fetch('Response', {}).fetch('Assertion', {})
+          return result if result.is_a?(Hash)
+
+          errors[:assertion] << error_message(:must_contain_single_assertion)
+          {}
         end
       end
 
spec/saml/response_spec.rb
@@ -160,6 +160,80 @@ RSpec.describe Saml::Kit::Response do
       subject = described_class.new(raw_xml)
       expect(subject).to be_invalid
     end
+
+    it 'is invalid when there are 2 assertions' do
+      id = Saml::Kit::Id.generate
+      issuer = FFaker::Internet.uri("https")
+      configuration = Saml::Kit::Configuration.new do |config|
+        config.generate_key_pair_for(use: :signing)
+      end
+      response_options = {
+        ID: id,
+        Version: "2.0",
+        IssueInstant: Time.now.iso8601,
+        Consent: Saml::Kit::Namespaces::UNSPECIFIED,
+        InResponseTo: request.id,
+        xmlns: Saml::Kit::Namespaces::PROTOCOL,
+      }
+      assertion_options = {
+        ID: Saml::Kit::Id.generate,
+        IssueInstant: Time.now.iso8601,
+        Version: "2.0",
+        xmlns: Saml::Kit::Namespaces::ASSERTION,
+      }
+      xml = Saml::Kit::Signatures.sign(configuration: configuration) do |xml, signature|
+        xml.instruct!
+        xml.Response response_options do
+          xml.Issuer(issuer, xmlns: Saml::Kit::Namespaces::ASSERTION)
+          xml.Status do
+            xml.StatusCode Value: Saml::Kit::Namespaces::SUCCESS
+          end
+          xml.Assertion(assertion_options) do
+            xml.Issuer issuer
+            signature.template(assertion_options[:ID])
+            xml.Subject do
+              xml.NameID FFaker::Internet.email, Format: Saml::Kit::Namespaces::EMAIL_ADDRESS
+              xml.SubjectConfirmation Method: Saml::Kit::Namespaces::BEARER do
+                xml.SubjectConfirmationData "", InResponseTo: request.id, NotOnOrAfter: 3.hours.from_now.utc.iso8601, Recipient: FFaker::Internet.uri("https")
+              end
+            end
+            xml.Conditions NotBefore: Time.now.utc.iso8601, NotOnOrAfter: 3.hours.from_now.utc.iso8601 do
+              xml.AudienceRestriction do
+                xml.Audience request.issuer
+              end
+            end
+            xml.AuthnStatement AuthnInstant: Time.now.iso8601, SessionIndex: assertion_options[:ID], SessionNotOnOrAfter: 3.hours.from_now.utc.iso8601 do
+              xml.AuthnContext do
+                xml.AuthnContextClassRef Saml::Kit::Namespaces::PASSWORD
+              end
+            end
+          end
+          new_options = assertion_options.merge(ID: Saml::Kit::Id.generate)
+          xml.Assertion(new_options) do
+            xml.Issuer issuer
+            xml.Subject do
+              xml.NameID FFaker::Internet.email, Format: Saml::Kit::Namespaces::EMAIL_ADDRESS
+              xml.SubjectConfirmation Method: Saml::Kit::Namespaces::BEARER do
+                xml.SubjectConfirmationData "", InResponseTo: request.id, NotOnOrAfter: 3.hours.from_now.utc.iso8601, Recipient: FFaker::Internet.uri("https")
+              end
+            end
+            xml.Conditions NotBefore: Time.now.utc.iso8601, NotOnOrAfter: 3.hours.from_now.utc.iso8601 do
+              xml.AudienceRestriction do
+                xml.Audience request.issuer
+              end
+            end
+            xml.AuthnStatement AuthnInstant: Time.now.iso8601, SessionIndex: new_options[:ID], SessionNotOnOrAfter: 3.hours.from_now.utc.iso8601 do
+              xml.AuthnContext do
+                xml.AuthnContextClassRef Saml::Kit::Namespaces::PASSWORD
+              end
+            end
+          end
+        end
+      end
+      subject = described_class.new(xml)
+      expect(subject).to_not be_valid
+      expect(subject.errors[:assertion]).to be_present
+    end
   end
 
   describe "#signed?" do