Comparing changes

v1.0.23 v1.0.24
12 commits 10 files changed

Commits

8636a75 fix linter errors. mo 2018-09-18 19:36:08
e39bc9d fix some lint errors. mo 2018-09-18 18:33:10
9d28faa validate a raw response. mo 2018-09-18 18:00:33
7588e0b fix lint errors. mo 2018-09-18 17:54:36
67c41f9 make Assertions buildable. mo 2018-09-17 19:50:12
9d30e02 autocorrect some lint errors. mo 2018-09-17 17:49:01
10697bd refactor assertion builder. mo 2018-09-17 17:46:48
lib/saml/kit/builders/assertion.rb
@@ -7,17 +7,21 @@ module Saml
       # {include:file:lib/saml/kit/builders/templates/assertion.builder}
       class Assertion
         include XmlTemplatable
-        extend Forwardable
-
-        def_delegators :@response_builder,
-          :request, :issuer, :reference_id, :now, :configuration, :user,
-          :version, :destination
 
+        attr_reader :user, :request, :configuration
+        attr_accessor :reference_id
+        attr_accessor :now, :destination
+        attr_accessor :issuer, :version
         attr_accessor :default_name_id_format
 
-        def initialize(response_builder, embed_signature)
-          @response_builder = response_builder
-          self.embed_signature = embed_signature
+        def initialize(user, request, configuration: Saml::Kit.configuration)
+          @user = user
+          @request = request
+          @configuration = configuration
+          @issuer = configuration.entity_id
+          @reference_id = ::Xml::Kit::Id.generate
+          @version = '2.0'
+          @now = Time.now.utc
           self.default_name_id_format = Saml::Kit::Namespaces::UNSPECIFIED_NAMEID
         end
 
@@ -34,8 +38,8 @@ module Saml
           user.assertion_attributes_for(request)
         end
 
-        def signing_key_pair
-          super || @response_builder.signing_key_pair
+        def build
+          Saml::Kit::Assertion.new(to_xml, configuration: configuration)
         end
 
         private
lib/saml/kit/builders/response.rb
@@ -8,7 +8,7 @@ module Saml
       class Response
         include XmlTemplatable
         attr_reader :user, :request
-        attr_accessor :id, :reference_id, :now
+        attr_accessor :id, :now
         attr_accessor :version, :status_code, :status_message
         attr_accessor :issuer, :destination
         attr_reader :configuration
@@ -19,7 +19,6 @@ module Saml
           @user = user
           @request = request
           @id = ::Xml::Kit::Id.generate
-          @reference_id = ::Xml::Kit::Id.generate
           @now = Time.now.utc
           @version = '2.0'
           @status_code = Namespaces::SUCCESS
@@ -46,14 +45,13 @@ module Saml
         def assertion
           @assertion ||=
             begin
-              assertion = Saml::Kit::Builders::Assertion.new(
-                self, embed_signature
-              )
-              if encrypt
-                Saml::Kit::Builders::EncryptedAssertion.new(self, assertion)
-              else
-                assertion
-              end
+              assertion = Assertion.new(user, request, configuration: configuration)
+              assertion.sign_with(@signing_key_pair) if @signing_key_pair
+              assertion.embed_signature = embed_signature unless embed_signature.nil?
+              assertion.now = now
+              assertion.destination = destination
+              assertion.issuer = issuer
+              encrypt ? EncryptedAssertion.new(self, assertion) : assertion
             end
         end
 
lib/saml/kit/concerns/xsd_validatable.rb
@@ -19,7 +19,7 @@ module Saml
 
         Dir.chdir(File.dirname(xsd)) do
           xsd = Nokogiri::XML::Schema(IO.read(xsd))
-          xsd.validate(to_nokogiri).each do |error|
+          xsd.validate(to_nokogiri.document).each do |error|
             errors[:base] << error.message
           end
         end
lib/saml/kit/locales/en.yml
@@ -5,8 +5,12 @@ en:
       Assertion:
         cannot_decrypt: "cannot be decrypted."
         expired: "must not be expired."
-        must_match_issuer: "must match entityId."
+        invalid: "must contain Assertion."
+        invalid_fingerprint: "is not registered."
+        invalid_version: "must be 2.0."
         must_contain_single_assertion: "must contain single Assertion."
+        must_match_issuer: "must match entityId."
+        unregistered: "is unregistered."
       AuthnRequest:
         invalid: "must contain AuthnRequest."
         invalid_fingerprint: "is not registered."
lib/saml/kit/assertion.rb
@@ -5,10 +5,7 @@ module Saml
     # This class validates the Assertion
     # element nested in a Response element
     # of a SAML document.
-    class Assertion
-      include ActiveModel::Validations
-      include Translatable
-      include XmlParseable
+    class Assertion < Document
       extend Forwardable
       XPATH = [
         '/samlp:Response/saml:Assertion',
@@ -21,26 +18,35 @@ module Saml
       validate :must_match_issuer, if: :decryptable?
       validate :must_be_active_session, if: :decryptable?
       validate :must_have_valid_signature, if: :decryptable?
-      attr_reader :name
+      attr_reader :name, :configuration
       attr_accessor :occurred_at
 
       def initialize(
         node, configuration: Saml::Kit.configuration, private_keys: []
       )
         @name = 'Assertion'
-        @to_nokogiri = node
+        @to_nokogiri = node.is_a?(String) ? Nokogiri::XML(node).root : node
         @configuration = configuration
         @occurred_at = Time.current
         @cannot_decrypt = false
         @encrypted = false
         keys = configuration.private_keys(use: :encryption) + private_keys
         decrypt(::Xml::Kit::Decryption.new(private_keys: keys.uniq))
+        super(to_s, name: 'Assertion', configuration: configuration)
+      end
+
+      def id
+        at_xpath('./@ID').try(:value)
       end
 
       def issuer
         at_xpath('./saml:Issuer').try(:text)
       end
 
+      def version
+        at_xpath('./@Version').try(:value)
+      end
+
       def name_id
         at_xpath('./saml:Subject/saml:NameID').try(:text)
       end
@@ -66,9 +72,12 @@ module Saml
         now > drifted_started_at && !expired?(now)
       end
 
-      def attribute_statement
-        @attribute_statement ||=
-          AttributeStatement.new(search('./saml:AttributeStatement'))
+      def expected_type?
+        at_xpath('../saml:Assertion|../saml:EncryptedAssertion').present?
+      end
+
+      def attribute_statement(xpath = './saml:AttributeStatement')
+        @attribute_statement ||= AttributeStatement.new(search(xpath))
       end
 
       def conditions
@@ -90,8 +99,6 @@ module Saml
 
       private
 
-      attr_reader :configuration
-
       def decrypt(decryptor)
         encrypted_assertion = at_xpath('./xmlenc:EncryptedData')
         @encrypted = encrypted_assertion.present?
lib/saml/kit/document.rb
@@ -83,10 +83,11 @@ module Saml
         # @!visibility private
         def builder_class # :nodoc:
           {
-            Response.to_s => Saml::Kit::Builders::Response,
-            LogoutResponse.to_s => Saml::Kit::Builders::LogoutResponse,
+            Assertion.to_s => Saml::Kit::Builders::Assertion,
             AuthenticationRequest.to_s => Saml::Kit::Builders::AuthenticationRequest,
             LogoutRequest.to_s => Saml::Kit::Builders::LogoutRequest,
+            LogoutResponse.to_s => Saml::Kit::Builders::LogoutResponse,
+            Response.to_s => Saml::Kit::Builders::Response,
           }[name] || (raise ArgumentError, "Unknown SAML Document #{name}")
         end
       end
lib/saml/kit/version.rb
@@ -2,6 +2,6 @@
 
 module Saml
   module Kit
-    VERSION = '1.0.23'.freeze
+    VERSION = '1.0.24'.freeze
   end
 end
spec/saml/kit/builders/assertion_builder_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Builders::Assertion do
+  describe '#build' do
+    subject { described_class.new(user, authn_request, configuration: configuration) }
+
+    let(:email) { FFaker::Internet.email }
+    let(:assertion_consumer_service_url) { FFaker::Internet.uri('https') }
+    let(:user) { User.new(attributes: { email: email, created_at: Time.now.utc.iso8601 }) }
+    let(:authn_request) { instance_double(Saml::Kit::AuthenticationRequest, id: Xml::Kit::Id.generate, assertion_consumer_service_url: assertion_consumer_service_url, issuer: issuer, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: provider, trusted?: true, signed?: true) }
+    let(:provider) { instance_double(Saml::Kit::ServiceProviderMetadata, want_assertions_signed: false, encryption_certificates: [configuration.certificates(use: :encryption).last]) }
+    let(:issuer) { FFaker::Internet.uri('https') }
+    let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
+    let(:configuration) do
+      Saml::Kit::Configuration.new do |config|
+        config.entity_id = issuer
+        config.registry = registry
+        config.generate_key_pair_for(use: :signing)
+        config.generate_key_pair_for(use: :encryption)
+      end
+    end
+    let(:metadata) do
+      Saml::Kit::Metadata.build(configuration: configuration, &:build_identity_provider)
+    end
+
+    before { allow(registry).to receive(:metadata_for).and_return(metadata) }
+
+    specify { expect(subject.build).to be_valid }
+    specify { expect(subject.build.issuer).to eql(issuer) }
+    specify { expect(subject.build.name_id).to eql(user.name_id) }
+    specify { expect(subject.build.name_id_format).to eql(Saml::Kit::Namespaces::EMAIL_ADDRESS) }
+    specify { expect(subject.build).to be_signed }
+    specify { expect(subject.build).not_to be_expired }
+    specify { expect(subject.build).to be_active }
+    specify { expect(subject.build).not_to be_encrypted }
+    specify { expect(subject.build.conditions.audiences).to include(issuer) }
+    specify { expect(subject.build.attributes).to eql('email' => user.attributes[:email], 'created_at' => user.attributes[:created_at]) }
+  end
+end
spec/saml/kit/assertion_spec.rb
@@ -2,9 +2,9 @@
 
 RSpec.describe Saml::Kit::Assertion do
   subject do
-    Saml::Kit::Response.build(user, request) do |x|
+    described_class.build(user, request) do |x|
       x.issuer = entity_id
-    end.assertion
+    end
   end
 
   let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: ::Xml::Kit::Id.generate, issuer: FFaker::Internet.uri('https'), assertion_consumer_service_url: FFaker::Internet.uri('https'), name_id_format: Saml::Kit::Namespaces::PERSISTENT, provider: nil, signed?: true, trusted?: true) }
@@ -240,4 +240,27 @@ RSpec.describe Saml::Kit::Assertion do
       expect(response.assertion).to be_valid
     end
   end
+
+  describe '.new' do
+    let(:user) { instance_double(User, name_id_for: SecureRandom.uuid, assertion_attributes_for: {}) }
+    let(:saml_request) { instance_double(Saml::Kit::AuthenticationRequest, id: Xml::Kit::Id.generate, issuer: configuration.entity_id) }
+    let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
+    let(:configuration) do
+      Saml::Kit::Configuration.new do |x|
+        x.entity_id = FFaker::Internet.uri('https')
+        x.registry = registry
+        x.generate_key_pair_for(use: :signing)
+      end
+    end
+    let(:metadata) do
+      Saml::Kit::Metadata.build(configuration: configuration, &:build_identity_provider)
+    end
+
+    before { allow(registry).to receive(:metadata_for).with(configuration.entity_id).and_return(metadata) }
+
+    it 'parses a raw xml assertion' do
+      saml = described_class.build_xml(user, saml_request, configuration: configuration)
+      expect(described_class.new(saml, configuration: configuration)).to be_valid
+    end
+  end
 end
spec/saml/kit/response_spec.rb
@@ -595,4 +595,24 @@ RSpec.describe Saml::Kit::Response do
       expect(result.attributes).to be_empty
     end
   end
+
+  describe '.new' do
+    let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
+    let(:metadata) { instance_double(Saml::Kit::IdentityProviderMetadata) }
+    let(:configuration) do
+      Saml::Kit::Configuration.new do |config|
+        config.entity_id = request.issuer
+        config.registry = registry
+        config.generate_key_pair_for(use: :signing)
+      end
+    end
+
+    it 'parses a raw response' do
+      allow(registry).to receive(:metadata_for).and_return(metadata)
+      allow(metadata).to receive(:matches?).and_return(true)
+
+      saml = described_class.build(user, request, configuration: configuration)
+      expect(described_class.new(saml.to_xml, configuration: configuration)).to be_valid
+    end
+  end
 end