Commit 6a6a768

mo <mo@mokhan.ca>
2017-11-29 02:50:24
move response builder to a separate file.
1 parent 94e63bb
Changed files (5)
lib/saml/kit/builders/response.rb
@@ -0,0 +1,182 @@
+module Saml
+  module Kit
+    class Response < Document
+      class Builder
+        attr_reader :user, :request
+        attr_accessor :id, :reference_id, :now
+        attr_accessor :version, :status_code
+        attr_accessor :issuer, :sign, :destination, :encrypt
+
+        def initialize(user, request)
+          @user = user
+          @request = request
+          @id = SecureRandom.uuid
+          @reference_id = SecureRandom.uuid
+          @now = Time.now.utc
+          @version = "2.0"
+          @status_code = Namespaces::SUCCESS
+          @issuer = configuration.issuer
+          @destination = destination_for(request)
+          @sign = want_assertions_signed
+          @encrypt = false
+        end
+
+        def want_assertions_signed
+          request.provider.want_assertions_signed
+        rescue => error
+          Saml::Kit.logger.error(error)
+          true
+        end
+
+        def to_xml
+          Signature.sign(sign: sign) do |xml, signature|
+            xml.Response response_options do
+              xml.Issuer(issuer, xmlns: Namespaces::ASSERTION)
+              signature.template(id)
+              xml.Status do
+                xml.StatusCode Value: status_code
+              end
+              assertion(xml, signature)
+            end
+          end
+        end
+
+        def build
+          Response.new(to_xml, request_id: request.id)
+        end
+
+        private
+
+        def assertion(xml, signature)
+          with_encryption(xml) do |xml|
+            xml.Assertion(assertion_options) do
+              xml.Issuer issuer
+              signature.template(reference_id) unless encrypt
+              xml.Subject do
+                xml.NameID user.name_id_for(request.name_id_format), Format: request.name_id_format
+                xml.SubjectConfirmation Method: Namespaces::BEARER do
+                  xml.SubjectConfirmationData "", subject_confirmation_data_options
+                end
+              end
+              xml.Conditions conditions_options do
+                xml.AudienceRestriction do
+                  xml.Audience request.issuer
+                end
+              end
+              xml.AuthnStatement authn_statement_options do
+                xml.AuthnContext do
+                  xml.AuthnContextClassRef Namespaces::PASSWORD
+                end
+              end
+              assertion_attributes = user.assertion_attributes_for(request)
+              if assertion_attributes.any?
+                xml.AttributeStatement do
+                  assertion_attributes.each do |key, value|
+                    xml.Attribute Name: key, NameFormat: Namespaces::URI, FriendlyName: key do
+                      xml.AttributeValue value.to_s
+                    end
+                  end
+                end
+              end
+            end
+          end
+        end
+
+        def with_encryption(xml)
+          if encrypt
+            temp = ::Builder::XmlMarkup.new
+            yield temp
+            raw_xml_to_encrypt = temp.target!
+
+            encryption_certificate = request.provider.encryption_certificates.first
+            public_key = encryption_certificate.public_key
+
+            cipher = OpenSSL::Cipher.new('AES-256-CBC')
+            cipher.encrypt
+            key = cipher.random_key
+            iv = cipher.random_iv
+            encrypted = cipher.update(raw_xml_to_encrypt) + cipher.final
+
+            Saml::Kit.logger.debug ['+iv', iv].inspect
+            Saml::Kit.logger.debug ['+key', key].inspect
+
+            xml.EncryptedAssertion xmlns: Namespaces::ASSERTION do
+              xml.EncryptedData xmlns: Namespaces::XMLENC do
+                xml.EncryptionMethod Algorithm: "http://www.w3.org/2001/04/xmlenc#aes256-cbc"
+                xml.KeyInfo xmlns: Namespaces::XMLDSIG do
+                  xml.EncryptedKey xmlns: Namespaces::XMLENC do
+                    xml.EncryptionMethod Algorithm: "http://www.w3.org/2001/04/xmlenc#rsa-1_5"
+                    xml.CipherData do
+                      xml.CipherValue Base64.encode64(public_key.public_encrypt(key))
+                    end
+                  end
+                end
+                xml.CipherData do
+                  xml.CipherValue Base64.encode64(iv + encrypted)
+                end
+              end
+            end
+          else
+            yield xml
+          end
+        end
+
+        def destination_for(request)
+          if request.signed? && request.trusted?
+            request.acs_url || request.provider.assertion_consumer_service_for(binding: :http_post).try(:location)
+          else
+            request.provider.assertion_consumer_service_for(binding: :http_post).try(:location)
+          end
+        end
+
+        def configuration
+          Saml::Kit.configuration
+        end
+
+        def response_options
+          {
+            ID: id.present? ? "_#{id}" : nil,
+            Version: version,
+            IssueInstant: now.iso8601,
+            Destination: destination,
+            Consent: Namespaces::UNSPECIFIED,
+            InResponseTo: request.id,
+            xmlns: Namespaces::PROTOCOL,
+          }
+        end
+
+        def assertion_options
+          {
+            ID: "_#{reference_id}",
+            IssueInstant: now.iso8601,
+            Version: "2.0",
+            xmlns: Namespaces::ASSERTION,
+          }
+        end
+
+        def subject_confirmation_data_options
+          {
+            InResponseTo: request.id,
+            NotOnOrAfter: 3.hours.since(now).utc.iso8601,
+            Recipient: request.acs_url,
+          }
+        end
+
+        def conditions_options
+          {
+            NotBefore: now.utc.iso8601,
+            NotOnOrAfter: Saml::Kit.configuration.session_timeout.from_now.utc.iso8601,
+          }
+        end
+
+        def authn_statement_options
+          {
+            AuthnInstant: now.iso8601,
+            SessionIndex: assertion_options[:ID],
+            SessionNotOnOrAfter: 3.hours.since(now).utc.iso8601,
+          }
+        end
+      end
+    end
+  end
+end
lib/saml/kit/builders.rb
@@ -2,3 +2,4 @@ require 'saml/kit/builders/authentication_request'
 require 'saml/kit/builders/identity_provider_metadata'
 require 'saml/kit/builders/logout_request'
 require 'saml/kit/builders/logout_response'
+require 'saml/kit/builders/response'
lib/saml/kit/response.rb
@@ -98,183 +98,6 @@ module Saml
         Saml::Kit.logger.error(error)
         Time.at(0).to_datetime
       end
-
-      class Builder
-        attr_reader :user, :request
-        attr_accessor :id, :reference_id, :now
-        attr_accessor :version, :status_code
-        attr_accessor :issuer, :sign, :destination, :encrypt
-
-        def initialize(user, request)
-          @user = user
-          @request = request
-          @id = SecureRandom.uuid
-          @reference_id = SecureRandom.uuid
-          @now = Time.now.utc
-          @version = "2.0"
-          @status_code = Namespaces::SUCCESS
-          @issuer = configuration.issuer
-          @destination = destination_for(request)
-          @sign = want_assertions_signed
-          @encrypt = false
-        end
-
-        def want_assertions_signed
-          request.provider.want_assertions_signed
-        rescue => error
-          Saml::Kit.logger.error(error)
-          true
-        end
-
-        def to_xml
-          Signature.sign(sign: sign) do |xml, signature|
-            xml.Response response_options do
-              xml.Issuer(issuer, xmlns: Namespaces::ASSERTION)
-              signature.template(id)
-              xml.Status do
-                xml.StatusCode Value: status_code
-              end
-              assertion(xml, signature)
-            end
-          end
-        end
-
-        def build
-          Response.new(to_xml, request_id: request.id)
-        end
-
-        private
-
-        def assertion(xml, signature)
-          with_encryption(xml) do |xml|
-            xml.Assertion(assertion_options) do
-              xml.Issuer issuer
-              signature.template(reference_id) unless encrypt
-              xml.Subject do
-                xml.NameID user.name_id_for(request.name_id_format), Format: request.name_id_format
-                xml.SubjectConfirmation Method: Namespaces::BEARER do
-                  xml.SubjectConfirmationData "", subject_confirmation_data_options
-                end
-              end
-              xml.Conditions conditions_options do
-                xml.AudienceRestriction do
-                  xml.Audience request.issuer
-                end
-              end
-              xml.AuthnStatement authn_statement_options do
-                xml.AuthnContext do
-                  xml.AuthnContextClassRef Namespaces::PASSWORD
-                end
-              end
-              assertion_attributes = user.assertion_attributes_for(request)
-              if assertion_attributes.any?
-                xml.AttributeStatement do
-                  assertion_attributes.each do |key, value|
-                    xml.Attribute Name: key, NameFormat: Namespaces::URI, FriendlyName: key do
-                      xml.AttributeValue value.to_s
-                    end
-                  end
-                end
-              end
-            end
-          end
-        end
-
-        def with_encryption(xml)
-          if encrypt
-            temp = ::Builder::XmlMarkup.new
-            yield temp
-            raw_xml_to_encrypt = temp.target!
-
-            encryption_certificate = request.provider.encryption_certificates.first
-            public_key = encryption_certificate.public_key
-
-            cipher = OpenSSL::Cipher.new('AES-256-CBC')
-            cipher.encrypt
-            key = cipher.random_key
-            iv = cipher.random_iv
-            encrypted = cipher.update(raw_xml_to_encrypt) + cipher.final
-
-            Saml::Kit.logger.debug ['+iv', iv].inspect
-            Saml::Kit.logger.debug ['+key', key].inspect
-
-            xml.EncryptedAssertion xmlns: Namespaces::ASSERTION do
-              xml.EncryptedData xmlns: Namespaces::XMLENC do
-                xml.EncryptionMethod Algorithm: "http://www.w3.org/2001/04/xmlenc#aes256-cbc"
-                xml.KeyInfo xmlns: Namespaces::XMLDSIG do
-                  xml.EncryptedKey xmlns: Namespaces::XMLENC do
-                    xml.EncryptionMethod Algorithm: "http://www.w3.org/2001/04/xmlenc#rsa-1_5"
-                    xml.CipherData do
-                      xml.CipherValue Base64.encode64(public_key.public_encrypt(key))
-                    end
-                  end
-                end
-                xml.CipherData do
-                  xml.CipherValue Base64.encode64(iv + encrypted)
-                end
-              end
-            end
-          else
-            yield xml
-          end
-        end
-
-        def destination_for(request)
-          if request.signed? && request.trusted?
-            request.acs_url || request.provider.assertion_consumer_service_for(binding: :http_post).try(:location)
-          else
-            request.provider.assertion_consumer_service_for(binding: :http_post).try(:location)
-          end
-        end
-
-        def configuration
-          Saml::Kit.configuration
-        end
-
-        def response_options
-          {
-            ID: id.present? ? "_#{id}" : nil,
-            Version: version,
-            IssueInstant: now.iso8601,
-            Destination: destination,
-            Consent: Namespaces::UNSPECIFIED,
-            InResponseTo: request.id,
-            xmlns: Namespaces::PROTOCOL,
-          }
-        end
-
-        def assertion_options
-          {
-            ID: "_#{reference_id}",
-            IssueInstant: now.iso8601,
-            Version: "2.0",
-            xmlns: Namespaces::ASSERTION,
-          }
-        end
-
-        def subject_confirmation_data_options
-          {
-            InResponseTo: request.id,
-            NotOnOrAfter: 3.hours.since(now).utc.iso8601,
-            Recipient: request.acs_url,
-          }
-        end
-
-        def conditions_options
-          {
-            NotBefore: now.utc.iso8601,
-            NotOnOrAfter: Saml::Kit.configuration.session_timeout.from_now.utc.iso8601,
-          }
-        end
-
-        def authn_statement_options
-          {
-            AuthnInstant: now.iso8601,
-            SessionIndex: assertion_options[:ID],
-            SessionNotOnOrAfter: 3.hours.since(now).utc.iso8601,
-          }
-        end
-      end
     end
   end
 end
spec/saml/builders/response_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Response::Builder do
+  describe "#to_xml" do
+    subject { described_class.new(user, request) }
+    let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: { email: email, created_at: Time.now.utc.iso8601 }) }
+    let(:request) { double(id: SecureRandom.uuid, acs_url: acs_url, issuer: FFaker::Movie.title, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: nil, trusted?: true, signed?: true) }
+    let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
+    let(:issuer) { FFaker::Movie.title }
+    let(:email) { FFaker::Internet.email }
+
+    it 'returns a proper response for the user' do
+      travel_to 1.second.from_now
+      allow(Saml::Kit.configuration).to receive(:issuer).and_return(issuer)
+      hash = Hash.from_xml(subject.to_xml)
+
+      expect(hash['Response']['ID']).to be_present
+      expect(hash['Response']['Version']).to eql('2.0')
+      expect(hash['Response']['IssueInstant']).to eql(Time.now.utc.iso8601)
+      expect(hash['Response']['Destination']).to eql(acs_url)
+      expect(hash['Response']['InResponseTo']).to eql(request.id)
+      expect(hash['Response']['Issuer']).to eql(issuer)
+      expect(hash['Response']['Status']['StatusCode']['Value']).to eql("urn:oasis:names:tc:SAML:2.0:status:Success")
+
+      expect(hash['Response']['Assertion']['ID']).to be_present
+      expect(hash['Response']['Assertion']['IssueInstant']).to eql(Time.now.utc.iso8601)
+      expect(hash['Response']['Assertion']['Version']).to eql("2.0")
+      expect(hash['Response']['Assertion']['Issuer']).to eql(issuer)
+
+      expect(hash['Response']['Assertion']['Subject']['NameID']).to eql(user.name_id_for)
+      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['Method']).to eql("urn:oasis:names:tc:SAML:2.0:cm:bearer")
+      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['NotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
+      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['Recipient']).to eql(acs_url)
+      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['InResponseTo']).to eql(request.id)
+
+      expect(hash['Response']['Assertion']['Conditions']['NotBefore']).to eql(0.seconds.ago.utc.iso8601)
+      expect(hash['Response']['Assertion']['Conditions']['NotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
+      expect(hash['Response']['Assertion']['Conditions']['AudienceRestriction']['Audience']).to eql(request.issuer)
+
+      expect(hash['Response']['Assertion']['AuthnStatement']['AuthnInstant']).to eql(Time.now.utc.iso8601)
+      expect(hash['Response']['Assertion']['AuthnStatement']['SessionNotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
+      expect(hash['Response']['Assertion']['AuthnStatement']['SessionIndex']).to eql(hash['Response']['Assertion']['ID'])
+      expect(hash['Response']['Assertion']['AuthnStatement']['AuthnContext']['AuthnContextClassRef']).to eql('urn:oasis:names:tc:SAML:2.0:ac:classes:Password')
+
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['Name']).to eql('email')
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['FriendlyName']).to eql('email')
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['NameFormat']).to eql('urn:oasis:names:tc:SAML:2.0:attrname-format:uri')
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['AttributeValue']).to eql(email)
+
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['Name']).to eql('created_at')
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['FriendlyName']).to eql('created_at')
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['NameFormat']).to eql('urn:oasis:names:tc:SAML:2.0:attrname-format:uri')
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['AttributeValue']).to be_present
+    end
+
+    it 'does not add a signature when the SP does not want assertions signed' do
+      builder = Saml::Kit::ServiceProviderMetadata::Builder.new
+      builder.want_assertions_signed = false
+      metadata = builder.build
+      allow(request).to receive(:provider).and_return(metadata)
+
+      hash = Hash.from_xml(subject.to_xml)
+      expect(hash['Response']['Signature']).to be_nil
+    end
+  end
+end
spec/saml/response_spec.rb
@@ -26,69 +26,6 @@ RSpec.describe Saml::Kit::Response do
     end
   end
 
-  describe "#to_xml" do
-    subject { described_class::Builder.new(user, request) }
-    let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: { email: email, created_at: Time.now.utc.iso8601 }) }
-    let(:request) { double(id: SecureRandom.uuid, acs_url: acs_url, issuer: FFaker::Movie.title, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: nil, trusted?: true, signed?: true) }
-    let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
-    let(:issuer) { FFaker::Movie.title }
-    let(:email) { FFaker::Internet.email }
-
-    it 'returns a proper response for the user' do
-      travel_to 1.second.from_now
-      allow(Saml::Kit.configuration).to receive(:issuer).and_return(issuer)
-      hash = Hash.from_xml(subject.to_xml)
-
-      expect(hash['Response']['ID']).to be_present
-      expect(hash['Response']['Version']).to eql('2.0')
-      expect(hash['Response']['IssueInstant']).to eql(Time.now.utc.iso8601)
-      expect(hash['Response']['Destination']).to eql(acs_url)
-      expect(hash['Response']['InResponseTo']).to eql(request.id)
-      expect(hash['Response']['Issuer']).to eql(issuer)
-      expect(hash['Response']['Status']['StatusCode']['Value']).to eql("urn:oasis:names:tc:SAML:2.0:status:Success")
-
-      expect(hash['Response']['Assertion']['ID']).to be_present
-      expect(hash['Response']['Assertion']['IssueInstant']).to eql(Time.now.utc.iso8601)
-      expect(hash['Response']['Assertion']['Version']).to eql("2.0")
-      expect(hash['Response']['Assertion']['Issuer']).to eql(issuer)
-
-      expect(hash['Response']['Assertion']['Subject']['NameID']).to eql(user.name_id_for)
-      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['Method']).to eql("urn:oasis:names:tc:SAML:2.0:cm:bearer")
-      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['NotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
-      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['Recipient']).to eql(acs_url)
-      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['InResponseTo']).to eql(request.id)
-
-      expect(hash['Response']['Assertion']['Conditions']['NotBefore']).to eql(0.seconds.ago.utc.iso8601)
-      expect(hash['Response']['Assertion']['Conditions']['NotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
-      expect(hash['Response']['Assertion']['Conditions']['AudienceRestriction']['Audience']).to eql(request.issuer)
-
-      expect(hash['Response']['Assertion']['AuthnStatement']['AuthnInstant']).to eql(Time.now.utc.iso8601)
-      expect(hash['Response']['Assertion']['AuthnStatement']['SessionNotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
-      expect(hash['Response']['Assertion']['AuthnStatement']['SessionIndex']).to eql(hash['Response']['Assertion']['ID'])
-      expect(hash['Response']['Assertion']['AuthnStatement']['AuthnContext']['AuthnContextClassRef']).to eql('urn:oasis:names:tc:SAML:2.0:ac:classes:Password')
-
-      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['Name']).to eql('email')
-      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['FriendlyName']).to eql('email')
-      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['NameFormat']).to eql('urn:oasis:names:tc:SAML:2.0:attrname-format:uri')
-      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['AttributeValue']).to eql(email)
-
-      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['Name']).to eql('created_at')
-      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['FriendlyName']).to eql('created_at')
-      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['NameFormat']).to eql('urn:oasis:names:tc:SAML:2.0:attrname-format:uri')
-      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['AttributeValue']).to be_present
-    end
-
-    it 'does not add a signature when the SP does not want assertions signed' do
-      builder = Saml::Kit::ServiceProviderMetadata::Builder.new
-      builder.want_assertions_signed = false
-      metadata = builder.build
-      allow(request).to receive(:provider).and_return(metadata)
-
-      hash = Hash.from_xml(subject.to_xml)
-      expect(hash['Response']['Signature']).to be_nil
-    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, name_id_format: Saml::Kit::Namespaces::PERSISTENT, provider: nil, signed?: true, trusted?: true) }
     let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: { id: SecureRandom.uuid }) }