Comparing changes

v1.0.6 v1.0.7
49 commits 92 files changed

Commits

4872ac4 run lint job separately. mo 2018-02-17 05:20:18
af14f7e run lint in ci. mo 2018-02-17 05:19:32
f36a4e7 generate rubocop todolist. mo 2018-02-17 05:10:21
6cf01ce default 80 chars per line. mo 2018-02-17 05:08:51
ba22310 remove rubocop warnings. mo 2018-02-17 05:07:02
86d59a8 make rubocop happier. mo 2018-02-17 04:44:21
d3d53c9 fix cop violations. mo 2018-02-17 00:57:37
0e1afd6 use active_support heredoc. mo 2018-02-17 00:11:59
e562597 spec/saml -> spec/saml/kit mo 2018-02-17 00:00:40
12a2aef rubocop:auto_correct mo 2018-02-16 23:59:17
20f6271 define rubocop configuration. mo 2018-02-16 23:56:48
94c8eed add rubocop-rspec. mo 2018-02-16 23:42:42
075804c add rubocop mo 2018-02-16 23:28:19
aa19580 remove duplicate bundle install. mo 2018-02-16 23:21:24
dfd3afc scripts to rule them all. mo 2018-02-16 23:15:52
5d10b20 add metadata#signature. mo 2018-02-16 22:11:29
9febf08 lazy creation of xml_hash. mo 2018-02-16 18:04:53
a2c76e5 use xpath to find cert. mo 2018-02-16 18:03:00
1ef60db extract values from Signature mo 2018-02-16 17:47:17
be0e929 fetch last assertion. mo 2018-02-15 23:43:27
f261b2d remove duplicate private keys. mo 2018-02-15 23:35:58
7001a45 bump version. mo 2018-02-15 23:33:41
1e4867c extract null assertion. mo 2018-02-15 23:30:33
981a3b4 attempt to decrypt immediately. mo 2018-02-15 23:05:04
e841e24 fix broken specs. mo 2018-02-15 22:57:00
88d5956 decrypt the nokogiri node. mo 2018-02-15 22:47:44
1e833ba fix broken specs. mo 2018-02-15 19:28:34
97f541e add spec for signature#to_h mo 2018-02-15 18:28:35
b1b2631 include bugfixes from xml-kit. mo 2018-02-15 17:40:53
e120441 mark new methods as private api. mo 2018-02-15 17:37:26
aaff27b validate individual signatures. mo 2018-02-15 17:34:41
fcd5607 check if a signature is present. mo 2018-02-14 23:04:52
98f414e fix broken spec. mo 2018-02-14 22:47:08
dda948e do not duplicate the errors. mo 2018-02-14 22:43:42
Changed files (92)
bin
exe
lib
spec
bin/cibuild
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+# script/cibuild: Setup environment for CI to run tests. This is primarily
+#                 designed to run on the continuous integration server.
+
+set -e
+
+cd "$(dirname "$0")/.."
+
+echo "Started at…"
+date "+%H:%M:%S"
+
+# GC customizations
+export RUBY_GC_MALLOC_LIMIT=79000000
+export RUBY_GC_HEAP_INIT_SLOTS=800000
+export RUBY_HEAP_FREE_MIN=100000
+export RUBY_HEAP_SLOTS_INCREMENT=400000
+export RUBY_HEAP_SLOTS_GROWTH_FACTOR=1
+
+gem install bundler --no-ri --no-rdoc --conservative
+# run tests
+ruby -v
+bin/test
bin/console
@@ -1,7 +1,7 @@
 #!/usr/bin/env ruby
 
-require "bundler/setup"
-require "saml/kit"
+require 'bundler/setup'
+require 'saml/kit'
 
 # You can add fixtures and/or initialization code here to make experimenting
 # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +10,5 @@ require "saml/kit"
 # require "pry"
 # Pry.start
 
-require "irb"
+require 'irb'
 IRB.start(__FILE__)
bin/lint
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+set -e
+
+[ -z "$DEBUG" ] || set -x
+
+echo "==> Running setup…"
+date "+%H:%M:%S"
+bin/setup
+
+echo "==> Running linters…"
+date "+%H:%M:%S"
+bundle exec rake rubocop
bin/setup
@@ -3,6 +3,6 @@ set -euo pipefail
 IFS=$'\n\t'
 set -vx
 
-bundle install
+bundle check || bundle install --jobs $(nproc)
 
 # Do any other automated setup that you need to do here
bin/test
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+# script/test: Run test suite for application. Optionally pass in a path to an
+#              individual test file to run a single test.
+
+
+set -e
+
+cd "$(dirname "$0")/.."
+
+[ -z "$DEBUG" ] || set -x
+
+echo "==> Running setup…"
+date "+%H:%M:%S"
+bin/setup
+
+echo "==> Running tests…"
+date "+%H:%M:%S"
+bundle exec rake spec
exe/saml-kit-create-self-signed-certificate
@@ -3,22 +3,22 @@ require 'saml/kit'
 
 Saml::Kit.deprecate("Use the 'saml-kit-cli' gem instead. saml-kit-create-self-signed-certificate")
 
-puts "Enter Passphrase:"
+puts 'Enter Passphrase:'
 passphrase = STDIN.read.strip
 certificate, private_key = ::Xml::Kit::SelfSignedCertificate.new.create(passphrase: passphrase)
 
-puts "** BEGIN File Format **"
+puts '** BEGIN File Format **'
 print certificate
 puts private_key
-puts "***********************"
+puts '***********************'
 
 puts
 
-puts "*** BEGIN ENV Format **"
+puts '*** BEGIN ENV Format **'
 puts certificate.inspect
 puts private_key.inspect
-puts "***********************"
+puts '***********************'
 
 puts
-puts "Private Key Passphrase:"
+puts 'Private Key Passphrase:'
 puts passphrase.inspect
exe/saml-kit-decode-http-redirect
@@ -6,9 +6,13 @@ Saml::Kit.deprecate("Use the 'saml-kit-cli' gem instead. saml-kit-decode-http-re
 input = STDIN.read
 binding = Saml::Kit::Bindings::HttpRedirect.new(location: '')
 
-uri = URI.parse(input) rescue nil
+uri = begin
+        URI.parse(input)
+      rescue StandardError
+        nil
+      end
 if uri
-  query_params =  Hash[uri.query.split('&').map { |x| x.split('=', 2) }]
+  query_params = Hash[uri.query.split('&').map { |x| x.split('=', 2) }]
   puts binding.deserialize(query_params).to_xml(pretty: true)
 else
   puts binding.deserialize('SAMLRequest' => input).to_xml(pretty: true)
lib/saml/kit/bindings/binding.rb
@@ -14,12 +14,12 @@ module Saml
           binding == other
         end
 
-        def serialize(builder, relay_state: nil)
+        def serialize(*)
           []
         end
 
-        def deserialize(params)
-          raise ArgumentError.new("Unsupported binding")
+        def deserialize(_params)
+          raise ArgumentError, 'Unsupported binding'
         end
 
         def to_h
@@ -27,7 +27,7 @@ module Saml
         end
 
         def ==(other)
-          self.to_s == other.to_s
+          to_s == other.to_s
         end
 
         def eql?(other)
@@ -58,7 +58,7 @@ module Saml
           elsif parameters[:SAMLResponse].present?
             parameters[:SAMLResponse]
           else
-            raise ArgumentError.new("SAMLRequest or SAMLResponse parameter is required.")
+            raise ArgumentError, 'SAMLRequest or SAMLResponse parameter is required.'
           end
         end
       end
lib/saml/kit/bindings/http_redirect.rb
@@ -17,7 +17,7 @@ module Saml
         end
 
         def deserialize(params, configuration: Saml::Kit.configuration)
-          parameters = normalize(params)
+          parameters = normalize(params_to_hash(params))
           document = deserialize_document_from!(parameters, configuration)
           ensure_valid_signature!(parameters, document)
           document
@@ -35,25 +35,25 @@ module Saml
           return if document.provider.nil?
 
           if document.provider.verify(
-              algorithm_for(params[:SigAlg]),
-              decode(params[:Signature]),
-              canonicalize(params)
+            algorithm_for(params[:SigAlg]),
+            decode(params[:Signature]),
+            canonicalize(params)
           )
             document.signature_verified!
           else
-            raise ArgumentError.new("Invalid Signature")
+            raise ArgumentError, 'Invalid Signature'
           end
         end
 
         def canonicalize(params)
-          [:SAMLRequest, :SAMLResponse, :RelayState, :SigAlg].map do |key|
+          %i[SAMLRequest SAMLResponse RelayState SigAlg].map do |key|
             value = params[key]
             value.present? ? "#{key}=#{value}" : nil
           end.compact.join('&')
         end
 
         def algorithm_for(algorithm)
-          case algorithm =~ /(rsa-)?sha(.*?)$/i && $2.to_i
+          case algorithm =~ /(rsa-)?sha(.*?)$/i && Regexp.last_match(2).to_i
           when 256
             OpenSSL::Digest::SHA256.new
           when 384
@@ -74,6 +74,11 @@ module Saml
             SigAlg: params['SigAlg'] || params[:SigAlg],
           }
         end
+
+        def params_to_hash(value)
+          return value unless value.is_a?(String)
+          Hash[URI.parse(value).query.split('&').map { |x| x.split('=', 2) }]
+        end
       end
     end
   end
lib/saml/kit/bindings/url_builder.rb
@@ -17,7 +17,7 @@ module Saml
           else
             payload = to_query_string(
               saml_document.query_string_parameter => serialize(saml_document.to_xml),
-              'RelayState' => relay_state,
+              'RelayState' => relay_state
             )
             "#{saml_document.destination}?#{payload}"
           end
@@ -34,7 +34,7 @@ module Saml
           to_query_string(
             saml_document.query_string_parameter => serialize(saml_document.to_xml),
             'RelayState' => relay_state,
-            'SigAlg' => ::Xml::Kit::Namespaces::SHA256,
+            'SigAlg' => ::Xml::Kit::Namespaces::SHA256
           )
         end
 
lib/saml/kit/builders/templates/assertion.builder
@@ -4,7 +4,7 @@ xml.Assertion(assertion_options) do
   xml.Subject do
     xml.NameID name_id, Format: name_id_format
     xml.SubjectConfirmation Method: Saml::Kit::Namespaces::BEARER do
-      xml.SubjectConfirmationData "", subject_confirmation_data_options
+      xml.SubjectConfirmationData '', subject_confirmation_data_options
     end
   end
   xml.Conditions conditions_options do
lib/saml/kit/builders/templates/metadata.builder
@@ -4,11 +4,11 @@ xml.EntityDescriptor entity_descriptor_options do
   render identity_provider, xml: xml
   render service_provider, xml: xml
   xml.Organization do
-    xml.OrganizationName organization_name, 'xml:lang': "en"
-    xml.OrganizationDisplayName organization_name, 'xml:lang': "en"
-    xml.OrganizationURL organization_url, 'xml:lang': "en"
+    xml.OrganizationName organization_name, 'xml:lang': 'en'
+    xml.OrganizationDisplayName organization_name, 'xml:lang': 'en'
+    xml.OrganizationURL organization_url, 'xml:lang': 'en'
   end
-  xml.ContactPerson contactType: "technical" do
+  xml.ContactPerson contactType: 'technical' do
     xml.Company "mailto:#{contact_email}"
   end
 end
lib/saml/kit/builders/templates/service_provider_metadata.builder
@@ -12,6 +12,6 @@ xml.SPSSODescriptor descriptor_options do
     xml.NameIDFormat format
   end
   acs_urls.each_with_index do |item, index|
-    xml.AssertionConsumerService Binding: item[:binding], Location: item[:location], index: index, isDefault: index == 0 ? true : false
+    xml.AssertionConsumerService Binding: item[:binding], Location: item[:location], index: index, isDefault: index.zero?
   end
 end
lib/saml/kit/builders/assertion.rb
@@ -25,6 +25,10 @@ module Saml
           user.assertion_attributes_for(request)
         end
 
+        def signing_key_pair
+          super || @response_builder.signing_key_pair
+        end
+
         private
 
         def assertion_options
lib/saml/kit/builders/authentication_request.rb
@@ -15,7 +15,7 @@ module Saml
           @issuer = configuration.entity_id
           @name_id_format = Namespaces::PERSISTENT
           @now = Time.now.utc
-          @version = "2.0"
+          @version = '2.0'
         end
 
         def build
@@ -26,8 +26,8 @@ module Saml
 
         def request_options
           options = {
-            "xmlns:samlp" => Namespaces::PROTOCOL,
-            "xmlns:saml" => Namespaces::ASSERTION,
+            'xmlns:samlp' => Namespaces::PROTOCOL,
+            'xmlns:saml' => Namespaces::ASSERTION,
             ID: id,
             Version: version,
             IssueInstant: now.utc.iso8601,
lib/saml/kit/builders/logout_request.rb
@@ -16,7 +16,7 @@ module Saml
           @issuer = configuration.entity_id
           @name_id_format = Saml::Kit::Namespaces::PERSISTENT
           @now = Time.now.utc
-          @version = "2.0"
+          @version = '2.0'
         end
 
         def build
lib/saml/kit/builders/logout_response.rb
@@ -16,7 +16,7 @@ module Saml
           @now = Time.now.utc
           @request = request
           @status_code = Namespaces::SUCCESS
-          @version = "2.0"
+          @version = '2.0'
         end
 
         def build
lib/saml/kit/builders/response.rb
@@ -17,9 +17,10 @@ module Saml
           @id = ::Xml::Kit::Id.generate
           @reference_id = ::Xml::Kit::Id.generate
           @now = Time.now.utc
-          @version = "2.0"
+          @version = '2.0'
           @status_code = Namespaces::SUCCESS
           @issuer = configuration.entity_id
+          @encryption_certificate = request.try(:provider).try(:encryption_certificates).try(:last)
           @encrypt = encryption_certificate.present?
           @configuration = configuration
         end
@@ -28,13 +29,6 @@ module Saml
           Saml::Kit::Response.new(to_xml, request_id: request.id, configuration: configuration)
         end
 
-        def encryption_certificate
-          request.provider.encryption_certificates.first
-        rescue => error
-          Saml::Kit.logger.error(error)
-          nil
-        end
-
         def assertion
           @assertion ||=
             begin
lib/saml/kit/locales/en.yml
@@ -3,12 +3,13 @@ en:
   saml/kit:
     errors:
       Assertion:
+        cannot_decrypt: "cannot be decrypted."
         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."
+        invalid_fingerprint: "is not registered."
         unregistered: "is unregistered."
       IDPSSODescriptor:
         invalid: "must contain IDPSSODescriptor."
@@ -16,16 +17,24 @@ en:
       InvalidDocument:
         invalid: "must contain valid SAMLRequest"
       LogoutRequest:
-        invalid_fingerprint: "does not match."
+        invalid_fingerprint: "is not registered."
         unregistered: "is unregistered."
       LogoutResponse:
         unregistered: "is unregistered."
+      NullAssertion:
+        invalid: "is missing."
       Response:
         invalid: "must contain Response."
-        invalid_fingerprint: "does not match."
+        invalid_fingerprint: "is not registered."
         invalid_response_to: "must match request id."
         invalid_version: "must be 2.0."
         unregistered: "must originate from registered identity provider."
+        must_contain_single_assertion: "must contain a single Assertion."
+      Signature:
+        certificate: "Not valid before %{not_before}. Not valid after %{not_after}."
+        digest_value: "is invalid."
+        empty: "is missing."
+        signature: "is invalid."
       SPSSODescriptor:
         invalid: "must contain SPSSODescriptor."
         invalid_signature: "invalid signature."
lib/saml/kit/rspec/have_query_param.rb
@@ -6,7 +6,7 @@ RSpec::Matchers.define :have_query_param do |key|
   end
 
   def query_params_from(url)
-    Hash[query_for(url).split("&").map { |x| x.split('=', 2) }]
+    Hash[query_for(url).split('&').map { |x| x.split('=', 2) }]
   end
 
   def uri_for(url)
lib/saml/kit/assertion.rb
@@ -3,17 +3,29 @@ module Saml
     class Assertion
       include ActiveModel::Validations
       include Translatable
-
-      validate :must_match_issuer
-      validate :must_be_active_session
+      XPATH = [
+        '/samlp:Response/saml:Assertion',
+        '/samlp:Response/saml:EncryptedAssertion'
+      ].join('|')
+
+      validate :must_be_decryptable
+      validate :must_match_issuer, if: :decryptable?
+      validate :must_be_active_session, if: :decryptable?
+      validate :must_have_valid_signature, if: :decryptable?
       attr_reader :name
       attr_accessor :occurred_at
 
-      def initialize(xml_hash, configuration: Saml::Kit.configuration)
-        @name = "Assertion"
-        @xml_hash = xml_hash
+      def initialize(node, configuration: Saml::Kit.configuration, private_keys: [])
+        @name = 'Assertion'
+        @node = node
+        @xml_hash = hash_from(node)['Response'] || {}
         @configuration = configuration
         @occurred_at = Time.current
+        decrypt!(::Xml::Kit::Decryption.new(
+                   private_keys: (
+                     configuration.private_keys(use: :encryption) + private_keys
+                   ).uniq
+        ))
       end
 
       def issuer
@@ -29,8 +41,7 @@ module Saml
       end
 
       def signature
-        xml_hash = assertion.fetch('Signature', nil)
-        xml_hash ? Signature.new(xml_hash) : nil
+        @signature ||= Signature.new(at_xpath('./ds:Signature'))
       end
 
       def expired?(now = occurred_at)
@@ -47,7 +58,7 @@ module Saml
           begin
             attrs = assertion.fetch('AttributeStatement', {}).fetch('Attribute', [])
             items = if attrs.is_a? Hash
-                      [[attrs["Name"], attrs["AttributeValue"]]]
+                      [[attrs['Name'], attrs['AttributeValue']]]
                     else
                       attrs.map { |item| [item['Name'], item['AttributeValue']] }
                     end
@@ -65,57 +76,88 @@ module Saml
 
       def audiences
         Array(assertion['Conditions']['AudienceRestriction']['Audience'])
-      rescue => error
+      rescue StandardError => error
         Saml::Kit.logger.error(error)
         []
       end
 
       def encrypted?
-        @xml_hash.fetch('Response', {}).fetch('EncryptedAssertion', nil).present?
+        @xml_hash.fetch('EncryptedAssertion', nil).present?
+      end
+
+      def decryptable?
+        return true unless encrypted?
+        !@cannot_decrypt
       end
 
       def present?
         assertion.present?
       end
 
+      def to_xml(pretty: false)
+        pretty ? @node.to_xml(indent: 2) : @node.to_s
+      end
+
       private
 
       attr_reader :configuration
 
       def assertion
         @assertion ||=
-          if encrypted?
-            private_keys = configuration.private_keys(use: :encryption)
-            decryptor = ::Xml::Kit::Decryption.new(private_keys: private_keys)
-            decrypted = decryptor.decrypt_hash(@xml_hash['Response']['EncryptedAssertion'])
-            Saml::Kit.logger.debug(decrypted)
-            Hash.from_xml(decrypted)['Assertion']
-          else
-            result = @xml_hash.fetch('Response', {}).fetch('Assertion', {})
+          begin
+            result = (hash_from(@node)['Response'] || {})['Assertion']
             return result if result.is_a?(Hash)
-
-            errors[:assertion] << error_message(:must_contain_single_assertion)
             {}
           end
       end
 
+      def decrypt!(decryptor)
+        return unless encrypted?
+
+        encrypted_assertion = @node.at_xpath('./xmlenc:EncryptedData', Saml::Kit::Document::NAMESPACES)
+        @node = decryptor.decrypt_node(encrypted_assertion)
+      rescue Xml::Kit::DecryptionError => error
+        @cannot_decrypt = true
+        Saml::Kit.logger.error(error)
+      end
+
       def parse_date(value)
         DateTime.parse(value)
-      rescue => error
+      rescue StandardError => error
         Saml::Kit.logger.error(error)
         Time.at(0).to_datetime
       end
 
       def must_match_issuer
-        unless audiences.include?(configuration.entity_id)
-          errors[:audience] << error_message(:must_match_issuer)
-        end
+        return if audiences.include?(configuration.entity_id)
+        errors[:audience] << error_message(:must_match_issuer)
       end
 
       def must_be_active_session
         return if active?
         errors[:base] << error_message(:expired)
       end
+
+      def must_have_valid_signature
+        return if !signed? || signature.valid?
+
+        signature.errors.each do |attribute, message|
+          errors.add(attribute, message)
+        end
+      end
+
+      def must_be_decryptable
+        errors.add(:base, error_message(:cannot_decrypt)) unless decryptable?
+      end
+
+      def at_xpath(xpath)
+        @node.at_xpath(xpath, Saml::Kit::Document::NAMESPACES)
+      end
+
+      def hash_from(node)
+        return {} if node.nil?
+        Hash.from_xml(node.document.root.to_s) || {}
+      end
     end
   end
 end
lib/saml/kit/authentication_request.rb
@@ -25,7 +25,7 @@ module Saml
       # @param xml [String] the raw xml.
       # @param configuration [Saml::Kit::Configuration] defaults to the global configuration.
       def initialize(xml, configuration: Saml::Kit.configuration)
-        super(xml, name: "AuthnRequest", configuration: configuration)
+        super(xml, name: 'AuthnRequest', configuration: configuration)
       end
 
       # Extract the AssertionConsumerServiceURL from the AuthnRequest
lib/saml/kit/bindings.rb
@@ -1,19 +1,19 @@
-require "saml/kit/bindings/binding"
-require "saml/kit/bindings/http_post"
-require "saml/kit/bindings/http_redirect"
-require "saml/kit/bindings/url_builder"
+require 'saml/kit/bindings/binding'
+require 'saml/kit/bindings/http_post'
+require 'saml/kit/bindings/http_redirect'
+require 'saml/kit/bindings/url_builder'
 
 module Saml
   module Kit
     module Bindings
-      HTTP_ARTIFACT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact'
-      HTTP_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
-      HTTP_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+      HTTP_ARTIFACT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact'.freeze
+      HTTP_POST = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'.freeze
+      HTTP_REDIRECT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'.freeze
       ALL = {
         http_post: HTTP_POST,
         http_redirect: HTTP_REDIRECT,
         http_artifact: HTTP_ARTIFACT,
-      }
+      }.freeze
 
       def self.binding_for(binding)
         ALL[binding]
lib/saml/kit/buildable.rb
@@ -4,19 +4,19 @@ module Saml
       extend ActiveSupport::Concern
 
       class_methods do
-        def build(*args) # :yields builder
+        def build(*args)
           builder(*args) do |builder|
             yield builder if block_given?
           end.build
         end
 
-        def build_xml(*args) # :yields builder
+        def build_xml(*args)
           builder(*args) do |builder|
             yield builder if block_given?
           end.to_xml
         end
 
-        def builder(*args) # :yields builder
+        def builder(*args)
           builder_class.new(*args).tap do |builder|
             yield builder if block_given?
           end
lib/saml/kit/composite_metadata.rb
@@ -5,7 +5,7 @@ module Saml
       attr_reader :service_provider, :identity_provider
 
       def initialize(xml)
-        super("IDPSSODescriptor", xml)
+        super('IDPSSODescriptor', xml)
         @metadatum = [
           Saml::Kit::ServiceProviderMetadata.new(xml),
           Saml::Kit::IdentityProviderMetadata.new(xml),
@@ -13,10 +13,10 @@ module Saml
       end
 
       def services(type)
-        xpath = map { |x| "//md:EntityDescriptor/md:#{x.name}/md:#{type}" }.join("|")
+        xpath = map { |x| "//md:EntityDescriptor/md:#{x.name}/md:#{type}" }.join('|')
         document.find_all(xpath).map do |item|
-          binding = item.attribute("Binding").value
-          location = item.attribute("Location").value
+          binding = item.attribute('Binding').value
+          location = item.attribute('Location').value
           Saml::Kit::Bindings.create_for(binding, location)
         end
       end
@@ -30,12 +30,16 @@ module Saml
       end
 
       def method_missing(name, *args)
-        if target = find { |x| x.respond_to?(name) }
+        if (target = find { |x| x.respond_to?(name) })
           target.public_send(name, *args)
         else
           super
         end
       end
+
+      def respond_to_missing?(method, *)
+        find { |x| x.respond_to?(method) }
+      end
     end
   end
 end
lib/saml/kit/configuration.rb
@@ -20,7 +20,7 @@ module Saml
     #     configuration.add_key_pair(ENV["X509_CERTIFICATE"], ENV["PRIVATE_KEY"], passphrase: ENV['PRIVATE_KEY_PASSPHRASE'], use: :encryption)
     #   end
     class Configuration
-      USES = [:signing, :encryption]
+      USES = %i[signing encryption].freeze
       # The issuer to use in requests or responses from this entity to use.
       attr_accessor :entity_id
       # The signature method to use when generating signatures (See {Saml::Kit::Builders::XmlSignature::SIGNATURE_METHODS})
@@ -36,7 +36,7 @@ module Saml
       # The total allowable clock drift for session timeout validation.
       attr_accessor :clock_drift
 
-      def initialize # :yields configuration
+      def initialize
         @clock_drift = 30.seconds
         @digest_method = :SHA256
         @key_pairs = []
@@ -85,7 +85,7 @@ module Saml
       # Return each private for a specific use.
       #
       # @param use [Symbol] the type of key pair to return `nil`, `:signing` or `:encryption`
-      def private_keys(use: :signing)
+      def private_keys(use: nil)
         key_pairs(use: use).flat_map(&:private_key)
       end
 
@@ -97,10 +97,10 @@ module Saml
       private
 
       def ensure_proper_use!(use)
-        unless USES.include?(use)
-          error_message = "Use must be either :signing or :encryption"
-          raise ArgumentError.new(error_message)
-        end
+        return if USES.include?(use)
+
+        error_message = 'Use must be either :signing or :encryption'
+        raise ArgumentError, error_message
       end
     end
   end
lib/saml/kit/default_registry.rb
@@ -62,7 +62,7 @@ module Saml
 
       # Yields each registered [Saml::Kit::Metadata] to the block.
       def each
-        @items.each do |key, value|
+        @items.each_value do |value|
           yield value
         end
       end
lib/saml/kit/document.rb
@@ -1,19 +1,20 @@
 module Saml
   module Kit
     class Document
-      PROTOCOL_XSD = File.expand_path("./xsd/saml-schema-protocol-2.0.xsd", File.dirname(__FILE__)).freeze
+      include ActiveModel::Validations
+      include XsdValidatable
+      include Translatable
+      include Trustable
+      include Buildable
+      PROTOCOL_XSD = File.expand_path('./xsd/saml-schema-protocol-2.0.xsd', File.dirname(__FILE__)).freeze
       NAMESPACES = {
         "NameFormat": ::Saml::Kit::Namespaces::ATTR_SPLAT,
         "ds": ::Xml::Kit::Namespaces::XMLDSIG,
         "md": ::Saml::Kit::Namespaces::METADATA,
         "saml": ::Saml::Kit::Namespaces::ASSERTION,
         "samlp": ::Saml::Kit::Namespaces::PROTOCOL,
+        'xmlenc' => ::Xml::Kit::Namespaces::XMLENC,
       }.freeze
-      include ActiveModel::Validations
-      include XsdValidatable
-      include Translatable
-      include Trustable
-      include Buildable
       validates_presence_of :content
       validates_presence_of :id
       validate :must_match_xsd
@@ -60,13 +61,28 @@ module Saml
       #
       # @param pretty [Boolean] formats the xml or returns the raw xml.
       def to_xml(pretty: false)
-        pretty ? Nokogiri::XML(content).to_xml(indent: 2) : content
+        pretty ? to_nokogiri.to_xml(indent: 2) : content
       end
 
-      # Returns the SAML document as an XHTML string. 
+      # Returns the SAML document as an XHTML string.
       # This is useful for rendering in a web page.
       def to_xhtml
-        Nokogiri::XML(content, &:noblanks).to_xhtml
+        Nokogiri::XML(to_xml, &:noblanks).to_xhtml
+      end
+
+      # @!visibility private
+      def to_nokogiri
+        @nokogiri ||= Nokogiri::XML(content)
+      end
+
+      # @!visibility private
+      def at_xpath(xpath)
+        to_nokogiri.at_xpath(xpath, NAMESPACES)
+      end
+
+      # @!visibility private
+      def search(xpath)
+        to_nokogiri.search(xpath, NAMESPACES)
       end
 
       def to_s
@@ -75,11 +91,11 @@ module Saml
 
       class << self
         XPATH = [
-          "/samlp:AuthnRequest",
-          "/samlp:LogoutRequest",
-          "/samlp:LogoutResponse",
-          "/samlp:Response",
-        ].join("|")
+          '/samlp:AuthnRequest',
+          '/samlp:LogoutRequest',
+          '/samlp:LogoutResponse',
+          '/samlp:Response',
+        ].join('|')
 
         # Returns the raw xml as a Saml::Kit SAML document.
         #
@@ -87,16 +103,16 @@ module Saml
         # @param configuration [Saml::Kit::Configuration] the configuration to use for unpacking the document.
         def to_saml_document(xml, configuration: Saml::Kit.configuration)
           xml_document = ::Xml::Kit::Document.new(xml, namespaces: {
-            "samlp": ::Saml::Kit::Namespaces::PROTOCOL
-          })
+                                                    "samlp": ::Saml::Kit::Namespaces::PROTOCOL
+                                                  })
           constructor = {
-            "AuthnRequest" => Saml::Kit::AuthenticationRequest,
-            "LogoutRequest" => Saml::Kit::LogoutRequest,
-            "LogoutResponse" => Saml::Kit::LogoutResponse,
-            "Response" => Saml::Kit::Response,
+            'AuthnRequest' => Saml::Kit::AuthenticationRequest,
+            'LogoutRequest' => Saml::Kit::LogoutRequest,
+            'LogoutResponse' => Saml::Kit::LogoutResponse,
+            'Response' => Saml::Kit::Response,
           }[xml_document.find_by(XPATH).name] || InvalidDocument
           constructor.new(xml, configuration: configuration)
-        rescue => error
+        rescue StandardError => error
           Saml::Kit.logger.error(error)
           InvalidDocument.new(xml, configuration: configuration)
         end
@@ -113,7 +129,7 @@ module Saml
           when Saml::Kit::LogoutRequest.to_s
             Saml::Kit::Builders::LogoutRequest
           else
-            raise ArgumentError.new("Unknown SAML Document #{name}")
+            raise ArgumentError, "Unknown SAML Document #{name}"
           end
         end
       end
@@ -140,7 +156,7 @@ module Saml
 
       def must_be_valid_version
         return unless expected_type?
-        return if "2.0" == version
+        return if version == '2.0'
         errors[:version] << error_message(:invalid_version)
       end
     end
lib/saml/kit/identity_provider_metadata.rb
@@ -32,15 +32,15 @@ module Saml
     # {include:file:spec/examples/identity_provider_metadata_spec.rb}
     class IdentityProviderMetadata < Metadata
       def initialize(xml)
-        super("IDPSSODescriptor", xml)
+        super('IDPSSODescriptor', xml)
       end
 
       # Returns the IDPSSODescriptor/@WantAuthnRequestsSigned attribute.
       def want_authn_requests_signed
         xpath = "/md:EntityDescriptor/md:#{name}"
-        attribute = document.find_by(xpath).attribute("WantAuthnRequestsSigned")
+        attribute = document.find_by(xpath).attribute('WantAuthnRequestsSigned')
         return true if attribute.nil?
-        attribute.text.downcase == "true"
+        attribute.text.casecmp('true').zero?
       end
 
       # Returns each of the SingleSignOnService elements.
@@ -59,8 +59,8 @@ module Saml
       def attributes
         document.find_all("/md:EntityDescriptor/md:#{name}/saml:Attribute").map do |item|
           {
-            format: item.attribute("NameFormat").try(:value),
-            name: item.attribute("Name").value,
+            format: item.attribute('NameFormat').try(:value),
+            name: item.attribute('Name').value,
           }
         end
       end
@@ -71,7 +71,7 @@ module Saml
       # @param relay_state [Object] The RelayState to include the returned SAML params.
       # @param configuration [Saml::Kit::Configuration] the configuration to use for generating the request.
       # @return [Array] The url and saml params encoded using the rules for the specified binding.
-      def login_request_for(binding:, relay_state: nil, configuration: Saml::Kit.configuration) # :yields builder
+      def login_request_for(binding:, relay_state: nil, configuration: Saml::Kit.configuration)
         builder = Saml::Kit::AuthenticationRequest.builder(configuration: configuration) do |x|
           x.embed_signature = want_authn_requests_signed
           yield x if block_given?
lib/saml/kit/invalid_document.rb
@@ -7,12 +7,12 @@ module Saml
       end
 
       def initialize(xml, configuration: nil)
-        super(xml, name: "InvalidDocument")
+        super(xml, name: 'InvalidDocument')
       end
 
       def to_h
         super
-      rescue
+      rescue StandardError
         {}
       end
     end
lib/saml/kit/logout_request.rb
@@ -31,7 +31,7 @@ module Saml
       # @param xml [String] The raw xml string.
       # @param configuration [Saml::Kit::Configuration] the configuration to use.
       def initialize(xml, configuration: Saml::Kit.configuration)
-        super(xml, name: "LogoutRequest", configuration: configuration)
+        super(xml, name: 'LogoutRequest', configuration: configuration)
       end
 
       # Returns the NameID value.
lib/saml/kit/logout_response.rb
@@ -10,7 +10,7 @@ module Saml
 
       def initialize(xml, request_id: nil, configuration: Saml::Kit.configuration)
         @request_id = request_id
-        super(xml, name: "LogoutResponse", configuration: configuration)
+        super(xml, name: 'LogoutResponse', configuration: configuration)
       end
     end
   end
lib/saml/kit/metadata.rb
@@ -23,7 +23,11 @@ module Saml
     # for a list of options that can be specified.
     # {include:file:spec/examples/metadata_spec.rb}
     class Metadata
-      METADATA_XSD = File.expand_path("./xsd/saml-schema-metadata-2.0.xsd", File.dirname(__FILE__)).freeze
+      include ActiveModel::Validations
+      include XsdValidatable
+      include Translatable
+      include Buildable
+      METADATA_XSD = File.expand_path('./xsd/saml-schema-metadata-2.0.xsd', File.dirname(__FILE__)).freeze
       NAMESPACES = {
         "NameFormat": Namespaces::ATTR_SPLAT,
         "ds": ::Xml::Kit::Namespaces::XMLDSIG,
@@ -31,10 +35,6 @@ module Saml
         "saml": Namespaces::ASSERTION,
         "samlp": Namespaces::PROTOCOL,
       }.freeze
-      include ActiveModel::Validations
-      include XsdValidatable
-      include Translatable
-      include Buildable
 
       validates_presence_of :metadata
       validate :must_contain_descriptor
@@ -50,7 +50,7 @@ module Saml
 
       # Returns the /EntityDescriptor/@entityID
       def entity_id
-        document.find_by("/md:EntityDescriptor/@entityID").value
+        document.find_by('/md:EntityDescriptor/@entityID').value
       end
 
       # Returns the supported NameIDFormats.
@@ -60,23 +60,23 @@ module Saml
 
       # Returns the Organization Name
       def organization_name
-        document.find_by("/md:EntityDescriptor/md:Organization/md:OrganizationName").try(:text)
+        document.find_by('/md:EntityDescriptor/md:Organization/md:OrganizationName').try(:text)
       end
 
       # Returns the Organization URL
       def organization_url
-        document.find_by("/md:EntityDescriptor/md:Organization/md:OrganizationURL").try(:text)
+        document.find_by('/md:EntityDescriptor/md:Organization/md:OrganizationURL').try(:text)
       end
 
       # Returns the Company
       def contact_person_company
-        document.find_by("/md:EntityDescriptor/md:ContactPerson/md:Company").try(:text)
+        document.find_by('/md:EntityDescriptor/md:ContactPerson/md:Company').try(:text)
       end
 
       # Returns each of the X509 certificates.
       def certificates
         @certificates ||= document.find_all("/md:EntityDescriptor/md:#{name}/md:KeyDescriptor").map do |item|
-          cert = item.at_xpath("./ds:KeyInfo/ds:X509Data/ds:X509Certificate", NAMESPACES).text
+          cert = item.at_xpath('./ds:KeyInfo/ds:X509Data/ds:X509Certificate', NAMESPACES).text
           attribute = item.attribute('use')
           use = attribute.nil? ? nil : item.attribute('use').value
           ::Xml::Kit::Certificate.new(cert, use: use)
@@ -98,8 +98,8 @@ module Saml
       # @param type [String] the type of service. .E.g. `AssertionConsumerServiceURL`
       def services(type)
         document.find_all("/md:EntityDescriptor/md:#{name}/md:#{type}").map do |item|
-          binding = item.attribute("Binding").value
-          location = item.attribute("Location").value
+          binding = item.attribute('Binding').value
+          location = item.attribute('Location').value
           Saml::Kit::Bindings.create_for(binding, location)
         end
       end
@@ -179,25 +179,31 @@ module Saml
         end
       end
 
-      # Creates a `{Saml::Kit::Metadata}` object from a raw XML [String].
-      #
-      # @param content [String] the raw metadata XML.
-      # @return [Saml::Kit::Metadata] the metadata document or subclass.
-      def self.from(content)
-        hash = Hash.from_xml(content)
-        entity_descriptor = hash["EntityDescriptor"]
-        if entity_descriptor.key?("SPSSODescriptor") && entity_descriptor.key?("IDPSSODescriptor")
-          Saml::Kit::CompositeMetadata.new(content)
-        elsif entity_descriptor.keys.include?("SPSSODescriptor")
-          Saml::Kit::ServiceProviderMetadata.new(content)
-        elsif entity_descriptor.keys.include?("IDPSSODescriptor")
-          Saml::Kit::IdentityProviderMetadata.new(content)
+      def signature
+        @signature ||= Signature.new(at_xpath('/md:EntityDescriptor/ds:Signature'))
+      end
+
+      class << self
+        # Creates a `{Saml::Kit::Metadata}` object from a raw XML [String].
+        #
+        # @param content [String] the raw metadata XML.
+        # @return [Saml::Kit::Metadata] the metadata document or subclass.
+        def from(content)
+          hash = Hash.from_xml(content)
+          entity_descriptor = hash['EntityDescriptor']
+          if entity_descriptor.key?('SPSSODescriptor') && entity_descriptor.key?('IDPSSODescriptor')
+            Saml::Kit::CompositeMetadata.new(content)
+          elsif entity_descriptor.keys.include?('SPSSODescriptor')
+            Saml::Kit::ServiceProviderMetadata.new(content)
+          elsif entity_descriptor.keys.include?('IDPSSODescriptor')
+            Saml::Kit::IdentityProviderMetadata.new(content)
+          end
         end
-      end
 
-      # @!visibility private
-      def self.builder_class
-        Saml::Kit::Builders::Metadata
+        # @!visibility private
+        def builder_class
+          Saml::Kit::Builders::Metadata
+        end
       end
 
       private
@@ -208,6 +214,10 @@ module Saml
         @document ||= ::Xml::Kit::Document.new(xml, namespaces: NAMESPACES)
       end
 
+      def at_xpath(xpath)
+        document.find_by(xpath)
+      end
+
       def metadata
         document.find_by("/md:EntityDescriptor/md:#{name}").present?
       end
@@ -221,20 +231,12 @@ module Saml
       end
 
       def must_have_valid_signature
-        return if to_xml.blank?
-
-        unless valid_signature?
-          errors[:base] << error_message(:invalid_signature)
-        end
-      end
+        return unless signature.present?
 
-      def valid_signature?
-        xml = document
-        result = xml.valid?
-        xml.errors.each do |error|
-          errors[:base] << error
+        signature.valid?
+        signature.errors.each do |attribute, error|
+          errors[attribute] << error
         end
-        result
       end
     end
   end
lib/saml/kit/namespaces.rb
@@ -1,32 +1,32 @@
 module Saml
   module Kit
     module Namespaces
-      SAML_2_0 = "urn:oasis:names:tc:SAML:2.0"
-      SAML_1_1 = "urn:oasis:names:tc:SAML:1.1"
-      ATTR_NAME_FORMAT = "#{SAML_2_0}:attrname-format"
-      NAME_ID_FORMAT_1_1 = "#{SAML_1_1}:nameid-format"
-      NAME_ID_FORMAT_2_0 = "#{SAML_2_0}:nameid-format"
-      STATUS = "#{SAML_2_0}:status"
+      SAML_2_0 = 'urn:oasis:names:tc:SAML:2.0'.freeze
+      SAML_1_1 = 'urn:oasis:names:tc:SAML:1.1'.freeze
+      ATTR_NAME_FORMAT = "#{SAML_2_0}:attrname-format".freeze
+      NAME_ID_FORMAT_1_1 = "#{SAML_1_1}:nameid-format".freeze
+      NAME_ID_FORMAT_2_0 = "#{SAML_2_0}:nameid-format".freeze
+      STATUS = "#{SAML_2_0}:status".freeze
 
-      ASSERTION = "#{SAML_2_0}:assertion"
-      ATTR_SPLAT = "#{ATTR_NAME_FORMAT}:*"
-      BASIC = "#{ATTR_NAME_FORMAT}:basic"
-      BEARER = "#{SAML_2_0}:cm:bearer"
-      EMAIL_ADDRESS = "#{NAME_ID_FORMAT_1_1}:emailAddress"
-      INVALID_NAME_ID_POLICY = "#{STATUS}:InvalidNameIDPolicy"
-      METADATA = "#{SAML_2_0}:metadata"
-      PASSWORD = "#{SAML_2_0}:ac:classes:Password"
-      PASSWORD_PROTECTED = "#{SAML_2_0}:ac:classes:PasswordProtectedTransport"
-      PERSISTENT = "#{NAME_ID_FORMAT_2_0}:persistent"
-      PROTOCOL = "#{SAML_2_0}:protocol"
-      REQUESTER_ERROR = "#{STATUS}:Requester"
-      RESPONDER_ERROR = "#{STATUS}:Responder"
-      SUCCESS = "#{STATUS}:Success"
-      TRANSIENT = "#{NAME_ID_FORMAT_2_0}:transient"
-      UNSPECIFIED = "#{SAML_2_0}:consent:unspecified"
-      UNSPECIFIED_NAMEID = "#{NAME_ID_FORMAT_1_1}:unspecified"
-      URI = "#{ATTR_NAME_FORMAT}:uri"
-      VERSION_MISMATCH_ERROR = "#{STATUS}:VersionMismatch"
+      ASSERTION = "#{SAML_2_0}:assertion".freeze
+      ATTR_SPLAT = "#{ATTR_NAME_FORMAT}:*".freeze
+      BASIC = "#{ATTR_NAME_FORMAT}:basic".freeze
+      BEARER = "#{SAML_2_0}:cm:bearer".freeze
+      EMAIL_ADDRESS = "#{NAME_ID_FORMAT_1_1}:emailAddress".freeze
+      INVALID_NAME_ID_POLICY = "#{STATUS}:InvalidNameIDPolicy".freeze
+      METADATA = "#{SAML_2_0}:metadata".freeze
+      PASSWORD = "#{SAML_2_0}:ac:classes:Password".freeze
+      PASSWORD_PROTECTED = "#{SAML_2_0}:ac:classes:PasswordProtectedTransport".freeze
+      PERSISTENT = "#{NAME_ID_FORMAT_2_0}:persistent".freeze
+      PROTOCOL = "#{SAML_2_0}:protocol".freeze
+      REQUESTER_ERROR = "#{STATUS}:Requester".freeze
+      RESPONDER_ERROR = "#{STATUS}:Responder".freeze
+      SUCCESS = "#{STATUS}:Success".freeze
+      TRANSIENT = "#{NAME_ID_FORMAT_2_0}:transient".freeze
+      UNSPECIFIED = "#{SAML_2_0}:consent:unspecified".freeze
+      UNSPECIFIED_NAMEID = "#{NAME_ID_FORMAT_1_1}:unspecified".freeze
+      URI = "#{ATTR_NAME_FORMAT}:uri".freeze
+      VERSION_MISMATCH_ERROR = "#{STATUS}:VersionMismatch".freeze
     end
   end
 end
lib/saml/kit/null_assertion.rb
@@ -0,0 +1,17 @@
+module Saml
+  module Kit
+    class NullAssertion
+      include ActiveModel::Validations
+      include Translatable
+      validate :invalid
+
+      def invalid
+        errors[:assertion].push(error_message(:invalid))
+      end
+
+      def name
+        'NullAssertion'
+      end
+    end
+  end
+end
lib/saml/kit/respondable.rb
@@ -33,10 +33,9 @@ module Saml
 
       def must_match_request_id
         return if request_id.nil?
+        return if in_response_to == request_id
 
-        if in_response_to != request_id
-          errors[:in_response_to] << error_message(:invalid_response_to)
-        end
+        errors[:in_response_to] << error_message(:invalid_response_to)
       end
     end
   end
lib/saml/kit/response.rb
@@ -8,14 +8,23 @@ module Saml
       def_delegators :assertion, :name_id, :[], :attributes
 
       validate :must_be_valid_assertion
+      validate :must_contain_single_assertion
 
       def initialize(xml, request_id: nil, configuration: Saml::Kit.configuration)
         @request_id = request_id
-        super(xml, name: "Response", configuration: configuration)
+        super(xml, name: 'Response', configuration: configuration)
       end
 
-      def assertion
-        @assertion ||= Saml::Kit::Assertion.new(to_h, configuration: @configuration)
+      def assertion(private_keys = configuration.private_keys(use: :encryption))
+        @assertion ||=
+          begin
+            node = assertion_nodes.last
+            if node.nil?
+              Saml::Kit::NullAssertion.new
+            else
+              Saml::Kit::Assertion.new(node, configuration: @configuration, private_keys: private_keys)
+            end
+          end
       end
 
       private
@@ -23,9 +32,19 @@ module Saml
       def must_be_valid_assertion
         assertion.valid?
         assertion.errors.each do |attribute, error|
-          self.errors[attribute] << error
+          attribute = :assertion if attribute == :base
+          errors[attribute] << error
         end
       end
+
+      def must_contain_single_assertion
+        return if assertion_nodes.count <= 1
+        errors[:base] << error_message(:must_contain_single_assertion)
+      end
+
+      def assertion_nodes
+        search(Saml::Kit::Assertion::XPATH)
+      end
     end
   end
 end
lib/saml/kit/service_provider_metadata.rb
@@ -3,7 +3,7 @@ module Saml
     # {include:file:spec/examples/service_provider_metadata_spec.rb}
     class ServiceProviderMetadata < Metadata
       def initialize(xml)
-        super("SPSSODescriptor", xml)
+        super('SPSSODescriptor', xml)
       end
 
       # Returns each of the AssertionConsumerService bindings.
@@ -20,9 +20,9 @@ module Saml
 
       # Returns true when the metadata demands that Assertions must be signed.
       def want_assertions_signed
-        attribute = document.find_by("/md:EntityDescriptor/md:#{name}").attribute("WantAssertionsSigned")
+        attribute = document.find_by("/md:EntityDescriptor/md:#{name}").attribute('WantAssertionsSigned')
         return true if attribute.nil?
-        attribute.text.downcase == "true"
+        attribute.text.casecmp('true').zero?
       end
 
       # @!visibility private
lib/saml/kit/signature.rb
@@ -2,14 +2,21 @@ module Saml
   module Kit
     class Signature
       include ActiveModel::Validations
+      include Translatable
 
-      def initialize(xml_hash)
-        @xml_hash = xml_hash
+      validate :validate_signature
+      validate :validate_certificate
+
+      attr_reader :name
+
+      def initialize(node)
+        @name = 'Signature'
+        @node = node
       end
 
       # Returns the embedded X509 Certificate
       def certificate
-        value = to_h.fetch('KeyInfo', {}).fetch('X509Data', {}).fetch('X509Certificate', nil)
+        value = at_xpath('./ds:KeyInfo/ds:X509Data/ds:X509Certificate').try(:text)
         return if value.nil?
         ::Xml::Kit::Certificate.new(value, use: :signing)
       end
@@ -20,9 +27,72 @@ module Saml
         metadata.matches?(certificate.fingerprint, use: :signing)
       end
 
+      def digest_value
+        at_xpath('./ds:SignedInfo/ds:Reference/ds:DigestValue').try(:text)
+      end
+
+      def digest_method
+        at_xpath('./ds:SignedInfo/ds:Reference/ds:DigestMethod/@Algorithm').try(:value)
+      end
+
+      def signature_value
+        at_xpath('./ds:SignatureValue').try(:text)
+      end
+
+      def signature_method
+        at_xpath('./ds:SignedInfo/ds:SignatureMethod/@Algorithm').try(:value)
+      end
+
+      def canonicalization_method
+        at_xpath('./ds:SignedInfo/ds:CanonicalizationMethod/@Algorithm').try(:value)
+      end
+
+      def transforms
+        node.search('./ds:SignedInfo/ds:Reference/ds:Transforms/ds:Transform/@Algorithm', Saml::Kit::Document::NAMESPACES).try(:map, &:value)
+      end
+
       # Returns the XML Hash.
       def to_h
-        @xml_hash
+        @xml_hash ||= present? ? Hash.from_xml(to_xml)['Signature'] : {}
+      end
+
+      def present?
+        node
+      end
+
+      def to_xml
+        node.to_s
+      end
+
+      private
+
+      attr_reader :node
+
+      def validate_signature
+        return errors[:base].push(error_message(:empty)) if certificate.nil?
+
+        signature = Xmldsig::Signature.new(@node, 'ID=$uri or @Id')
+        return if signature.valid?(certificate.x509)
+        signature.errors.each do |attribute|
+          errors.add(attribute, error_message(attribute))
+        end
+      end
+
+      def validate_certificate(now = Time.now.utc)
+        return unless certificate.present?
+        return if certificate.active?(now)
+
+        message = error_message(
+          :certificate,
+          not_before: certificate.not_before,
+          not_after: certificate.not_after
+        )
+        errors.add(:certificate, message)
+      end
+
+      def at_xpath(xpath)
+        return nil unless node
+        node.at_xpath(xpath, Saml::Kit::Document::NAMESPACES)
       end
     end
   end
lib/saml/kit/translatable.rb
@@ -2,8 +2,9 @@ module Saml
   module Kit
     module Translatable
       # @!visibility private
-      def error_message(attribute, type: :invalid)
-        I18n.translate(attribute, scope: "saml/kit.errors.#{name}")
+      def error_message(attribute, options = {})
+        default_options = { scope: "saml/kit.errors.#{name}" }
+        I18n.translate(attribute, default_options.merge(options))
       end
     end
   end
lib/saml/kit/trustable.rb
@@ -16,8 +16,7 @@ module Saml
 
       # @!visibility private
       def signature
-        xml_hash = to_h.fetch(name, {}).fetch('Signature', nil)
-        xml_hash ? Signature.new(xml_hash) : nil
+        @signature ||= Signature.new(at_xpath("/samlp:#{name}/ds:Signature"))
       end
 
       # Returns true when documents is signed and the signing certificate belongs to a known service entity.
@@ -43,16 +42,10 @@ module Saml
 
       def must_have_valid_signature
         return if to_xml.blank?
+        return unless signature.present?
 
-        xml = ::Xml::Kit::Document.new(to_xml, namespaces: {
-          "NameFormat": Namespaces::ATTR_SPLAT,
-          "ds": ::Xml::Kit::Namespaces::XMLDSIG,
-          "md": Namespaces::METADATA,
-          "saml": Namespaces::ASSERTION,
-          "samlp": Namespaces::PROTOCOL,
-        })
-        xml.valid?
-        xml.errors.each do |attribute, error|
+        signature.valid?
+        signature.errors.each do |attribute, error|
           errors[attribute] << error
         end
       end
lib/saml/kit/version.rb
@@ -1,5 +1,5 @@
 module Saml
   module Kit
-    VERSION = "1.0.6"
+    VERSION = '1.0.7'.freeze
   end
 end
lib/saml/kit/xml_templatable.rb
@@ -4,16 +4,21 @@ module Saml
       include ::Xml::Kit::Templatable
 
       def template_path
-        root_path = File.expand_path(File.dirname(__FILE__))
-        template_name = "#{self.class.name.split("::").last.underscore}.builder"
-        File.join(root_path, "builders/templates/", template_name)
+        root_path = __dir__
+        template_name = "#{self.class.name.split('::').last.underscore}.builder"
+        File.join(root_path, 'builders/templates/', template_name)
       end
 
       # Returns true if an embedded signature is requested and at least one signing certificate is available via the configuration.
       def sign?
         return configuration.sign? if embed_signature.nil?
         (embed_signature && configuration.sign?) ||
-          (embed_signature && @signing_key_pair.present?)
+          (embed_signature && signing_key_pair.present?)
+      end
+
+      def encrypt_with(key_pair)
+        self.encrypt = true
+        self.encryption_certificate = key_pair.certificate
       end
 
       def digest_method
@@ -25,7 +30,7 @@ module Saml
       end
 
       def signing_key_pair
-        configuration.key_pairs(use: :signing).last
+        @signing_key_pair || configuration.key_pairs(use: :signing).last
       end
     end
   end
lib/saml/kit.rb
@@ -1,46 +1,49 @@
-require "saml/kit/version"
+require 'saml/kit/version'
 
-require "active_model"
-require "active_support/core_ext/date/calculations"
-require "active_support/core_ext/hash/conversions"
-require "active_support/core_ext/hash/indifferent_access"
-require "active_support/core_ext/numeric/time"
-require "active_support/deprecation"
-require "active_support/duration"
-require "forwardable"
-require "logger"
-require "net/http"
-require "nokogiri"
-require "securerandom"
-require "xml/kit"
+require 'active_model'
+require 'active_support/core_ext/date/calculations'
+require 'active_support/core_ext/hash/conversions'
+require 'active_support/core_ext/hash/indifferent_access'
+require 'active_support/core_ext/numeric/time'
+require 'active_support/deprecation'
+require 'active_support/duration'
+require 'forwardable'
+require 'logger'
+require 'net/http'
+require 'nokogiri'
+require 'securerandom'
+require 'uri'
+require 'xml/kit'
 
-require "saml/kit/buildable"
-require "saml/kit/builders"
-require "saml/kit/namespaces"
-require "saml/kit/serializable"
-require "saml/kit/xsd_validatable"
-require "saml/kit/respondable"
-require "saml/kit/requestable"
-require "saml/kit/trustable"
-require "saml/kit/translatable"
-require "saml/kit/document"
+require 'saml/kit/buildable'
+require 'saml/kit/builders'
+require 'saml/kit/namespaces'
+require 'saml/kit/serializable'
+require 'saml/kit/xsd_validatable'
+require 'saml/kit/respondable'
+require 'saml/kit/requestable'
+require 'saml/kit/trustable'
+require 'saml/kit/translatable'
+require 'saml/kit/document'
 
-require "saml/kit/assertion"
-require "saml/kit/authentication_request"
-require "saml/kit/bindings"
-require "saml/kit/configuration"
-require "saml/kit/default_registry"
-require "saml/kit/logout_response"
-require "saml/kit/logout_request"
-require "saml/kit/metadata"
-require "saml/kit/composite_metadata"
-require "saml/kit/response"
-require "saml/kit/identity_provider_metadata"
-require "saml/kit/invalid_document"
-require "saml/kit/service_provider_metadata"
-require "saml/kit/signature"
+require 'saml/kit/assertion'
+require 'saml/kit/authentication_request'
+require 'saml/kit/bindings'
+require 'saml/kit/configuration'
+require 'saml/kit/default_registry'
+require 'saml/kit/logout_response'
+require 'saml/kit/logout_request'
+require 'saml/kit/metadata'
+require 'saml/kit/null_assertion'
+require 'saml/kit/composite_metadata'
+require 'saml/kit/response'
+require 'saml/kit/identity_provider_metadata'
+require 'saml/kit/invalid_document'
+require 'saml/kit/service_provider_metadata'
+require 'saml/kit/signature'
 
-I18n.load_path += Dir[File.expand_path("kit/locales/*.yml", File.dirname(__FILE__))]
+I18n.load_path +=
+  Dir[File.expand_path('kit/locales/*.yml', File.dirname(__FILE__))]
 
 module Saml
   module Kit
spec/examples/logout_request_spec.rb
@@ -1,7 +1,7 @@
-require_relative './user'
+require_relative './principal'
 
 RSpec.describe "Logout Request" do
-  let(:user) { User.new(id: SecureRandom.uuid, email: "hello@example.com") }
+  let(:user) { Principal.new(id: SecureRandom.uuid, email: "hello@example.com") }
 
   it 'produces a SAMLRequest' do
     xml = Saml::Kit::Metadata.build_xml do |builder|
spec/examples/logout_response_spec.rb
@@ -1,7 +1,7 @@
-require_relative './user'
+require_relative './principal'
 
 RSpec.describe "Logout Response" do
-  let(:user) { User.new(id: SecureRandom.uuid, email: "hello@example.com") }
+  let(:user) { Principal.new(id: SecureRandom.uuid, email: "hello@example.com") }
 
   it 'generates a logout response' do
     xml = Saml::Kit::Metadata.build_xml do |builder|
spec/examples/user.rb → spec/examples/principal.rb
@@ -1,4 +1,4 @@
-class User
+class Principal
   attr_reader :id, :email
 
   def initialize(id:, email:)
spec/examples/response_spec.rb
@@ -1,7 +1,7 @@
-require_relative './user'
+require_relative './principal'
 
 RSpec.describe "Response" do
-  let(:user) { User.new(id: SecureRandom.uuid, email: "hello@example.com") }
+  let(:user) { Principal.new(id: SecureRandom.uuid, email: "hello@example.com") }
   let(:request) { Saml::Kit::AuthenticationRequest.build }
 
   it 'consumes a Response' do
spec/saml/bindings/binding_spec.rb → spec/saml/kit/bindings/binding_spec.rb
@@ -1,17 +1,18 @@
 RSpec.describe Saml::Kit::Bindings::Binding do
-  let(:location) { FFaker::Internet.http_url }
   subject { described_class.new(binding: Saml::Kit::Bindings::HTTP_ARTIFACT, location: location) }
 
-  describe "#serialize" do
+  let(:location) { FFaker::Internet.http_url }
+
+  describe '#serialize' do
     it 'ignores other bindings' do
       expect(subject.serialize(Saml::Kit::AuthenticationRequest)).to be_empty
     end
   end
 
-  describe "#deserialize" do
+  describe '#deserialize' do
     it 'raises an error' do
       expect do
-        subject.deserialize('SAMLRequest' => "CORRUPT")
+        subject.deserialize('SAMLRequest' => 'CORRUPT')
       end.to raise_error(/Unsupported binding/)
     end
   end
spec/saml/bindings/http_post_spec.rb → spec/saml/kit/bindings/http_post_spec.rb
@@ -1,27 +1,28 @@
 RSpec.describe Saml::Kit::Bindings::HttpPost do
-  let(:location) { FFaker::Internet.uri("https") }
   subject { described_class.new(location: location) }
 
-  describe "equality" do
+  let(:location) { FFaker::Internet.uri('https') }
+
+  describe 'equality' do
     it 'is referentially equal' do
       expect(subject).to eql(subject)
     end
 
     it 'is equal by value' do
       expect(subject).to eql(
-        Saml::Kit::Bindings::HttpPost.new(location: location)
+        described_class.new(location: location)
       )
     end
 
     it 'is not equal' do
-      expect(subject).to_not eql(
-        described_class.new(location: FFaker::Internet.uri("https"))
+      expect(subject).not_to eql(
+        described_class.new(location: FFaker::Internet.uri('https'))
       )
     end
   end
 
-  describe "#serialize" do
-    let(:relay_state) { "ECHO" }
+  describe '#serialize' do
+    let(:relay_state) { 'ECHO' }
     let(:configuration) do
       Saml::Kit::Configuration.new do |config|
         config.generate_key_pair_for(use: :signing)
@@ -42,7 +43,7 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
     end
 
     it 'returns a SAMLRequest for a LogoutRequest' do
-      user = double(:user, name_id_for: SecureRandom.uuid)
+      user = User.new
       builder = Saml::Kit::LogoutRequest.builder_class.new(user, configuration: configuration)
       url, saml_params = subject.serialize(builder, relay_state: relay_state)
 
@@ -74,11 +75,11 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
       url, saml_params = subject.serialize(builder)
 
       expect(url).to eql(location)
-      expect(saml_params.keys).to_not include('RelayState')
+      expect(saml_params.keys).not_to include('RelayState')
     end
   end
 
-  describe "#deserialize" do
+  describe '#deserialize' do
     it 'deserializes to an AuthnRequest' do
       builder = Saml::Kit::AuthenticationRequest.builder_class.new
       _, params = subject.serialize(builder)
@@ -87,7 +88,7 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
     end
 
     it 'deserializes to a LogoutRequest' do
-      user = double(:user, name_id_for: SecureRandom.uuid)
+      user = User.new
       builder = Saml::Kit::LogoutRequest.builder_class.new(user)
       _, params = subject.serialize(builder)
       result = subject.deserialize(params)
@@ -95,8 +96,8 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
     end
 
     it 'deserializes to a Response' do
-      user = double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: [])
-      request = double(:request, id: SecureRandom.uuid, provider: nil, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: FFaker::Internet.http_url, signed?: true, trusted?: true)
+      user = User.new
+      request = instance_double(Saml::Kit::AuthenticationRequest, id: SecureRandom.uuid, provider: nil, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: FFaker::Internet.http_url, signed?: true, trusted?: true)
       builder = Saml::Kit::Response.builder_class.new(user, request)
       _, params = subject.serialize(builder)
       result = subject.deserialize(params)
spec/saml/bindings/http_redirect_spec.rb → spec/saml/kit/bindings/http_redirect_spec.rb
@@ -1,9 +1,10 @@
 RSpec.describe Saml::Kit::Bindings::HttpRedirect do
-  let(:location) { FFaker::Internet.http_url }
   subject { described_class.new(location: location) }
 
-  describe "#serialize" do
-    let(:relay_state) { "ECHO" }
+  let(:location) { FFaker::Internet.http_url }
+
+  describe '#serialize' do
+    let(:relay_state) { 'ECHO' }
     let(:configuration) do
       Saml::Kit::Configuration.new do |config|
         config.generate_key_pair_for(use: :signing)
@@ -12,7 +13,7 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
 
     it 'encodes the request using the HTTP-Redirect encoding' do
       builder = Saml::Kit::AuthenticationRequest.builder(configuration: configuration)
-      url, _ = subject.serialize(builder, relay_state: relay_state)
+      url, = subject.serialize(builder, relay_state: relay_state)
       expect(url).to start_with(location)
       expect(url).to have_query_param('SAMLRequest')
       expect(url).to have_query_param('SigAlg')
@@ -20,28 +21,34 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
     end
   end
 
-  describe "#deserialize" do
+  describe '#deserialize' do
     let(:entity_id) { FFaker::Internet.http_url }
     let(:provider) { Saml::Kit::IdentityProviderMetadata.build }
 
-    before :each do
+    before do
       allow(Saml::Kit.configuration.registry).to receive(:metadata_for).with(entity_id).and_return(provider)
       allow(Saml::Kit.configuration).to receive(:entity_id).and_return(entity_id)
     end
 
     it 'deserializes the SAMLRequest to an AuthnRequest' do
-      url, _ = subject.serialize(Saml::Kit::AuthenticationRequest.builder)
+      url, = subject.serialize(Saml::Kit::AuthenticationRequest.builder)
       result = subject.deserialize(query_params_from(url))
       expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
     end
 
+    it 'deserializes the raw query_string to an AuthnRequest' do
+      url, = subject.serialize(Saml::Kit::AuthenticationRequest.builder, relay_state: 'HELLO')
+      result = subject.deserialize(url)
+      expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
+    end
+
     it 'deserializes the SAMLRequest to an AuthnRequest with symbols for keys' do
       configuration = Saml::Kit::Configuration.new do |config|
         config.entity_id = entity_id
         config.generate_key_pair_for(use: :signing)
       end
       provider = Saml::Kit::IdentityProviderMetadata.build(configuration: configuration)
-      url, _ = subject.serialize(Saml::Kit::AuthenticationRequest.builder(configuration: configuration))
+      url, = subject.serialize(Saml::Kit::AuthenticationRequest.builder(configuration: configuration))
       allow(configuration.registry).to receive(:metadata_for).with(entity_id).and_return(provider)
 
       result = subject.deserialize(query_params_from(url).symbolize_keys, configuration: configuration)
@@ -50,64 +57,66 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
       expect(result).to be_trusted
     end
 
-    it 'deserializes the SAMLRequest to an AuthnRequest with symbols for keys' do
-      url, _ = subject.serialize(Saml::Kit::AuthenticationRequest.builder)
+    it 'deserializes the SAMLRequest to an AuthnRequest with symbolized keys' do
+      url, = subject.serialize(Saml::Kit::AuthenticationRequest.builder)
       result = subject.deserialize(query_params_from(url).symbolize_keys)
       expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
     end
 
     it 'deserializes the SAMLRequest to an AuthnRequest when given a custom params object' do
       class Parameters
+        attr_reader :params
+
         def initialize(params)
           @params = params
         end
 
         def [](key)
-          @params[key]
+          params[key]
         end
       end
-      url, _ = subject.serialize(Saml::Kit::AuthenticationRequest.builder)
+      url, = subject.serialize(Saml::Kit::AuthenticationRequest.builder)
       result = subject.deserialize(Parameters.new(query_params_from(url)))
       expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
     end
 
     it 'deserializes the SAMLRequest to a LogoutRequest' do
-      user = double(:user, name_id_for: SecureRandom.uuid)
-      url, _ = subject.serialize(Saml::Kit::LogoutRequest.builder(user))
+      user = User.new
+      url, = subject.serialize(Saml::Kit::LogoutRequest.builder(user))
       result = subject.deserialize(query_params_from(url))
       expect(result).to be_instance_of(Saml::Kit::LogoutRequest)
     end
 
     it 'returns an invalid request when the SAMLRequest is invalid' do
       expect do
-        subject.deserialize({ 'SAMLRequest' => "nonsense" })
+        subject.deserialize('SAMLRequest' => 'nonsense')
       end.to raise_error(Zlib::DataError)
     end
 
     it 'deserializes the SAMLResponse to a Response' do
-      user = double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: [])
-      request = double(:request, id: SecureRandom.uuid, provider: nil, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: entity_id, signed?: true, trusted?: true)
-      url, _ = subject.serialize(Saml::Kit::Response.builder(user, request))
+      user = User.new
+      request = instance_double(Saml::Kit::AuthenticationRequest, id: SecureRandom.uuid, provider: nil, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: entity_id, signed?: true, trusted?: true)
+      url, = subject.serialize(Saml::Kit::Response.builder(user, request))
       result = subject.deserialize(query_params_from(url))
       expect(result).to be_instance_of(Saml::Kit::Response)
     end
 
     it 'deserializes the SAMLResponse to a LogoutResponse' do
-      request = double(:request, id: SecureRandom.uuid, provider: provider, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: FFaker::Internet.http_url)
-      url, _ = subject.serialize(Saml::Kit::LogoutResponse.builder(request))
+      request = instance_double(Saml::Kit::LogoutRequest, id: SecureRandom.uuid, provider: provider, issuer: FFaker::Internet.http_url)
+      url, = subject.serialize(Saml::Kit::LogoutResponse.builder(request))
       result = subject.deserialize(query_params_from(url))
       expect(result).to be_instance_of(Saml::Kit::LogoutResponse)
     end
 
     it 'raises an error when the content is invalid' do
       expect do
-        subject.deserialize('SAMLResponse' => "nonsense")
+        subject.deserialize('SAMLResponse' => 'nonsense')
       end.to raise_error(Zlib::DataError)
     end
 
     it 'raises an error when a saml parameter is not specified' do
       expect do
-        subject.deserialize({ })
+        subject.deserialize({})
       end.to raise_error(ArgumentError)
     end
 
@@ -116,7 +125,7 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
         config.entity_id = entity_id
         config.generate_key_pair_for(use: :signing)
       end
-      url, _ = subject.serialize(
+      url, = subject.serialize(
         Saml::Kit::AuthenticationRequest.builder(configuration: configuration) do |x|
           x.embed_signature = true
         end
@@ -134,7 +143,7 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
       end
       allow(Saml::Kit.configuration.registry).to receive(:metadata_for).with(entity_id).and_return(provider)
 
-      url, _ = subject.serialize(Saml::Kit::AuthenticationRequest.builder)
+      url, = subject.serialize(Saml::Kit::AuthenticationRequest.builder)
       result = subject.deserialize(query_params_from(url))
       expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
       expect(result).to be_valid
@@ -144,14 +153,14 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
       configuration = Saml::Kit::Configuration.new do |config|
         config.generate_key_pair_for(use: :signing)
       end
-      url, _ = subject.serialize(Saml::Kit::AuthenticationRequest.builder(configuration: configuration))
+      url, = subject.serialize(Saml::Kit::AuthenticationRequest.builder(configuration: configuration))
 
       other_configuration = Saml::Kit::Configuration.new
       allow(other_configuration.registry).to receive(:metadata_for).and_return(nil)
 
       result = subject.deserialize(query_params_from(url), configuration: other_configuration)
-      expect(result).to_not be_signed
-      expect(result).to_not be_trusted
+      expect(result).not_to be_signed
+      expect(result).not_to be_trusted
     end
   end
 end
spec/saml/bindings/url_builder_spec.rb → spec/saml/kit/bindings/url_builder_spec.rb
@@ -1,6 +1,6 @@
 RSpec.describe Saml::Kit::Bindings::UrlBuilder do
-  describe "#build" do
-    let(:xml) { "<xml></xml>" }
+  describe '#build' do
+    let(:xml) { '<xml></xml>' }
     let(:destination) { FFaker::Internet.http_url }
     let(:relay_state) { FFaker::Movie.title }
 
@@ -12,6 +12,7 @@ RSpec.describe Saml::Kit::Bindings::UrlBuilder do
     ].each do |(response_type, query_string_parameter)|
       describe response_type.to_s do
         subject { described_class.new(configuration: configuration) }
+
         let(:configuration) do
           Saml::Kit::Configuration.new do |config|
             config.generate_key_pair_for(use: :signing)
@@ -21,12 +22,12 @@ RSpec.describe Saml::Kit::Bindings::UrlBuilder do
         let(:response) { instance_double(response_type, destination: destination, to_xml: xml, query_string_parameter: query_string_parameter) }
 
         def to_query_params(url)
-          Hash[URI.parse(url).query.split("&").map { |x| x.split('=', 2) }]
+          Hash[URI.parse(url).query.split('&').map { |x| x.split('=', 2) }]
         end
 
         it 'returns a url containing the target location' do
           result_uri = URI.parse(subject.build(response))
-          expect(result_uri.scheme).to eql("http")
+          expect(result_uri.scheme).to eql('http')
           expect(result_uri.host).to eql(URI.parse(destination).host)
         end
 
@@ -34,7 +35,7 @@ RSpec.describe Saml::Kit::Bindings::UrlBuilder do
           result = subject.build(response, relay_state: relay_state)
           query_params = to_query_params(result)
           level = Zlib::BEST_COMPRESSION
-          expected = CGI.escape(Base64.encode64(Zlib::Deflate.deflate(xml, level)[2..-5]).gsub(/\n/, ''))
+          expected = CGI.escape(Base64.encode64(Zlib::Deflate.deflate(xml, level)[2..-5]).delete("\n"))
           expect(result).to include("#{query_string_parameter}=#{expected}")
           expect(query_params[query_string_parameter]).to eql(expected)
         end
spec/saml/builders/authentication_request_spec.rb → spec/saml/kit/builders/authentication_request_spec.rb
@@ -1,14 +1,15 @@
 RSpec.describe Saml::Kit::Builders::AuthenticationRequest do
   subject { described_class.new(configuration: configuration) }
+
   let(:configuration) do
     config = Saml::Kit::Configuration.new
     config.entity_id = issuer
     config
   end
 
-  describe "#to_xml" do
+  describe '#to_xml' do
     let(:issuer) { FFaker::Movie.title }
-    let(:assertion_consumer_service_url) { "https://airport.dev/session/acs" }
+    let(:assertion_consumer_service_url) { 'https://airport.dev/session/acs' }
 
     it 'returns a valid authentication request' do
       travel_to 1.second.from_now
spec/saml/builders/identity_provider_metadata_spec.rb → spec/saml/kit/builders/identity_provider_metadata_spec.rb
@@ -1,5 +1,6 @@
 RSpec.describe Saml::Kit::Builders::IdentityProviderMetadata do
   subject { described_class.new(configuration: configuration) }
+
   let(:configuration) do
     Saml::Kit::Configuration.new do |config|
       config.generate_key_pair_for(use: :signing)
@@ -8,7 +9,7 @@ RSpec.describe Saml::Kit::Builders::IdentityProviderMetadata do
   end
   let(:email) { FFaker::Internet.email }
   let(:org_name) { FFaker::Movie.title }
-  let(:url) { FFaker::Internet.uri("https") }
+  let(:url) { FFaker::Internet.uri('https') }
   let(:entity_id) { FFaker::Movie.title }
 
   it 'builds a proper metadata' do
@@ -21,9 +22,9 @@ RSpec.describe Saml::Kit::Builders::IdentityProviderMetadata do
       Saml::Kit::Namespaces::TRANSIENT,
       Saml::Kit::Namespaces::EMAIL_ADDRESS,
     ]
-    subject.add_single_sign_on_service("https://www.example.com/login", binding: :http_redirect)
-    subject.add_single_logout_service("https://www.example.com/logout", binding: :http_post)
-    subject.attributes << "id"
+    subject.add_single_sign_on_service('https://www.example.com/login', binding: :http_redirect)
+    subject.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
+    subject.attributes << 'id'
 
     result = Hash.from_xml(subject.build.to_xml)
 
@@ -37,17 +38,17 @@ RSpec.describe Saml::Kit::Builders::IdentityProviderMetadata do
       Saml::Kit::Namespaces::EMAIL_ADDRESS,
     ])
     expect(result['EntityDescriptor']['IDPSSODescriptor']['SingleSignOnService']['Binding']).to eql(Saml::Kit::Bindings::HTTP_REDIRECT)
-    expect(result['EntityDescriptor']['IDPSSODescriptor']['SingleSignOnService']['Location']).to eql("https://www.example.com/login")
+    expect(result['EntityDescriptor']['IDPSSODescriptor']['SingleSignOnService']['Location']).to eql('https://www.example.com/login')
     expect(result['EntityDescriptor']['IDPSSODescriptor']['SingleLogoutService']['Binding']).to eql(Saml::Kit::Bindings::HTTP_POST)
-    expect(result['EntityDescriptor']['IDPSSODescriptor']['SingleLogoutService']['Location']).to eql("https://www.example.com/logout")
-    expect(result['EntityDescriptor']['IDPSSODescriptor']['Attribute']['Name']).to eql("id")
+    expect(result['EntityDescriptor']['IDPSSODescriptor']['SingleLogoutService']['Location']).to eql('https://www.example.com/logout')
+    expect(result['EntityDescriptor']['IDPSSODescriptor']['Attribute']['Name']).to eql('id')
     certificates = result['EntityDescriptor']['IDPSSODescriptor']['KeyDescriptor'].map { |x| x['KeyInfo']['X509Data']['X509Certificate'] }
     expected_certificates = configuration.certificates.map(&:stripped)
     expect(certificates).to match_array(expected_certificates)
     expect(result['EntityDescriptor']['Organization']['OrganizationName']).to eql(org_name)
     expect(result['EntityDescriptor']['Organization']['OrganizationDisplayName']).to eql(org_name)
     expect(result['EntityDescriptor']['Organization']['OrganizationURL']).to eql(url)
-    expect(result['EntityDescriptor']['ContactPerson']['contactType']).to eql("technical")
+    expect(result['EntityDescriptor']['ContactPerson']['contactType']).to eql('technical')
     expect(result['EntityDescriptor']['ContactPerson']['Company']).to eql("mailto:#{email}")
   end
 end
spec/saml/builders/logout_request_spec.rb → spec/saml/kit/builders/logout_request_spec.rb
@@ -1,6 +1,7 @@
 RSpec.describe Saml::Kit::Builders::LogoutRequest do
   subject { described_class.new(user, configuration: configuration) }
-  let(:user) { double(:user, name_id_for: name_id) }
+
+  let(:user) { User.new(name_id: name_id) }
   let(:name_id) { SecureRandom.uuid }
   let(:configuration) do
     Saml::Kit::Configuration.new do |config|
@@ -19,7 +20,7 @@ RSpec.describe Saml::Kit::Builders::LogoutRequest do
     xml_hash = Hash.from_xml(result)
 
     expect(xml_hash['LogoutRequest']['ID']).to eql(subject.id)
-    expect(xml_hash['LogoutRequest']['Version']).to eql("2.0")
+    expect(xml_hash['LogoutRequest']['Version']).to eql('2.0')
     expect(xml_hash['LogoutRequest']['IssueInstant']).to eql(Time.now.utc.iso8601)
     expect(xml_hash['LogoutRequest']['Destination']).to eql(subject.destination)
 
spec/saml/builders/logout_response_spec.rb → spec/saml/kit/builders/logout_response_spec.rb
@@ -1,11 +1,12 @@
 RSpec.describe Saml::Kit::Builders::LogoutResponse do
   subject { described_class.new(request) }
-  let(:user) { double(:user, name_id_for: SecureRandom.uuid) }
+
+  let(:user) { User.new }
   let(:request) { Saml::Kit::Builders::LogoutRequest.new(user).build }
   let(:issuer) { FFaker::Internet.http_url }
   let(:destination) { FFaker::Internet.http_url }
 
-  describe "#build" do
+  describe '#build' do
     it 'builds a logout response' do
       travel_to 1.second.from_now
 
@@ -14,7 +15,7 @@ RSpec.describe Saml::Kit::Builders::LogoutResponse do
       result = subject.build
       expect(result.id).to be_present
       expect(result.issue_instant).to eql(Time.now.utc)
-      expect(result.version).to eql("2.0")
+      expect(result.version).to eql('2.0')
       expect(result.issuer).to eql(issuer)
       expect(result.status_code).to eql(Saml::Kit::Namespaces::SUCCESS)
       expect(result.in_response_to).to eql(request.id)
spec/saml/builders/metadata_spec.rb → spec/saml/kit/builders/metadata_spec.rb
@@ -1,7 +1,8 @@
 RSpec.describe Saml::Kit::Builders::Metadata do
-  describe ".build" do
+  describe '.build' do
     subject { Saml::Kit::Metadata }
-    let(:url) { FFaker::Internet.uri("https") }
+
+    let(:url) { FFaker::Internet.uri('https') }
 
     it 'builds metadata for a service provider' do
       result = subject.build do |builder|
@@ -57,7 +58,7 @@ RSpec.describe Saml::Kit::Builders::Metadata do
         3.times { config.generate_key_pair_for(use: :signing) }
       end
       metadata = Saml::Kit::Metadata.build(configuration: configuration) do |builder|
-        builder.entity_id = FFaker::Internet.uri("https")
+        builder.entity_id = FFaker::Internet.uri('https')
         builder.build_identity_provider do |x|
           x.embed_signature = true
           x.add_single_sign_on_service(url, binding: :http_post)
spec/saml/builders/response_spec.rb → spec/saml/kit/builders/response_spec.rb
@@ -1,5 +1,6 @@
 RSpec.describe Saml::Kit::Builders::Response do
   subject { described_class.new(user, request, configuration: configuration) }
+
   let(:configuration) do
     Saml::Kit::Configuration.new do |config|
       config.entity_id = issuer
@@ -8,13 +9,13 @@ RSpec.describe Saml::Kit::Builders::Response do
     end
   end
   let(:email) { FFaker::Internet.email }
-  let(:assertion_consumer_service_url) { FFaker::Internet.uri("https") }
-  let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: { email: email, created_at: Time.now.utc.iso8601 }) }
-  let(:request) { double(:request, 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) { double(:provider, want_assertions_signed: false, encryption_certificates: [configuration.certificates(use: :encryption).last] ) }
-  let(:issuer) { FFaker::Internet.uri("https") }
+  let(:assertion_consumer_service_url) { FFaker::Internet.uri('https') }
+  let(:user) { User.new(attributes: { email: email, created_at: Time.now.utc.iso8601 }) }
+  let(: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') }
 
-  describe "#build" do
+  describe '#build' do
     it 'builds a response with the request_id' do
       expect(subject.build.request_id).to eql(request.id)
     end
@@ -37,7 +38,7 @@ RSpec.describe Saml::Kit::Builders::Response do
     end
   end
 
-  describe "#to_xml" do
+  describe '#to_xml' do
     it 'returns a proper response for the user' do
       travel_to 1.second.from_now
       allow(Saml::Kit.configuration).to receive(:entity_id).and_return(issuer)
@@ -51,15 +52,15 @@ RSpec.describe Saml::Kit::Builders::Response do
       expect(hash['Response']['Destination']).to eql(assertion_consumer_service_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']['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']['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']['NameID']).to eql(user.name_id)
+      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(assertion_consumer_service_url)
       expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['InResponseTo']).to eql(request.id)
@@ -145,13 +146,13 @@ RSpec.describe Saml::Kit::Builders::Response do
       subject.embed_signature = false
 
       result = Saml::Kit::Response.new(subject.to_xml, configuration: configuration)
-      expect(result).to_not be_signed
-      expect(result.assertion).to_not be_signed
+      expect(result).not_to be_signed
+      expect(result.assertion).not_to be_signed
       expect(result.assertion).to be_encrypted
     end
   end
 
-  describe ".build" do
+  describe '.build' do
     let(:configuration) do
       Saml::Kit::Configuration.new do |config|
         config.entity_id = issuer
spec/saml/builders/service_provider_metadata_spec.rb → spec/saml/kit/builders/service_provider_metadata_spec.rb
@@ -1,5 +1,6 @@
 RSpec.describe Saml::Kit::Builders::ServiceProviderMetadata do
   subject { described_class.new(configuration: configuration) }
+
   let(:configuration) do
     Saml::Kit::Configuration.new do |config|
       config.generate_key_pair_for(use: :signing)
@@ -9,8 +10,8 @@ RSpec.describe Saml::Kit::Builders::ServiceProviderMetadata do
   let(:assertion_consumer_service_url) { FFaker::Internet.http_url }
   let(:email) { FFaker::Internet.email }
   let(:org_name) { FFaker::Movie.title }
-  let(:url) { FFaker::Internet.uri("https") }
-  let(:entity_id) { FFaker::Internet.uri("https") }
+  let(:url) { FFaker::Internet.uri('https') }
+  let(:entity_id) { FFaker::Internet.uri('https') }
 
   it 'builds the service provider metadata' do
     subject.contact_email = email
@@ -25,7 +26,7 @@ RSpec.describe Saml::Kit::Builders::ServiceProviderMetadata do
     ]
     result = Hash.from_xml(subject.build.to_xml)
 
-    expect(result['EntityDescriptor']['xmlns']).to eql("urn:oasis:names:tc:SAML:2.0:metadata")
+    expect(result['EntityDescriptor']['xmlns']).to eql('urn:oasis:names:tc:SAML:2.0:metadata')
     expect(result['EntityDescriptor']['ID']).to be_present
     expect(result['EntityDescriptor']['entityID']).to eql(entity_id)
     expect(result['EntityDescriptor']['SPSSODescriptor']['AuthnRequestsSigned']).to eql('true')
@@ -36,18 +37,18 @@ RSpec.describe Saml::Kit::Builders::ServiceProviderMetadata do
       Saml::Kit::Namespaces::TRANSIENT,
       Saml::Kit::Namespaces::EMAIL_ADDRESS,
     ])
-    expect(result['EntityDescriptor']['SPSSODescriptor']['AssertionConsumerService']['Binding']).to eql("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST")
+    expect(result['EntityDescriptor']['SPSSODescriptor']['AssertionConsumerService']['Binding']).to eql('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST')
     expect(result['EntityDescriptor']['SPSSODescriptor']['AssertionConsumerService']['Location']).to eql(assertion_consumer_service_url)
     expect(result['EntityDescriptor']['SPSSODescriptor']['AssertionConsumerService']['isDefault']).to eql('true')
     expect(result['EntityDescriptor']['SPSSODescriptor']['AssertionConsumerService']['index']).to eql('0')
     expect(result['EntityDescriptor']['Signature']).to be_present
-    expect(result['EntityDescriptor']['SPSSODescriptor']['KeyDescriptor'].map { |x| x['use'] }).to match_array(['signing', 'encryption'])
+    expect(result['EntityDescriptor']['SPSSODescriptor']['KeyDescriptor'].map { |x| x['use'] }).to match_array(%w[signing encryption])
     expected_certificates = configuration.certificates.map(&:stripped)
     expect(result['EntityDescriptor']['SPSSODescriptor']['KeyDescriptor'].map { |x| x['KeyInfo']['X509Data']['X509Certificate'] }).to match_array(expected_certificates)
     expect(result['EntityDescriptor']['Organization']['OrganizationName']).to eql(org_name)
     expect(result['EntityDescriptor']['Organization']['OrganizationDisplayName']).to eql(org_name)
     expect(result['EntityDescriptor']['Organization']['OrganizationURL']).to eql(url)
-    expect(result['EntityDescriptor']['ContactPerson']['contactType']).to eql("technical")
+    expect(result['EntityDescriptor']['ContactPerson']['contactType']).to eql('technical')
     expect(result['EntityDescriptor']['ContactPerson']['Company']).to eql("mailto:#{email}")
   end
 end
spec/saml/kit/assertion_spec.rb
@@ -0,0 +1,204 @@
+RSpec.describe Saml::Kit::Assertion do
+  describe '#active?' do
+    let(:configuration) do
+      Saml::Kit::Configuration.new do |config|
+        config.session_timeout = 30.minutes
+        config.clock_drift = 30.seconds
+      end
+    end
+
+    it 'is valid after a valid session window + drift' do
+      now = Time.current
+      travel_to now
+      not_on_or_after = configuration.session_timeout.since(now).iso8601
+      xml = <<-XML.strip_heredoc
+        <Response>
+        <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{Xml::Kit::Id.generate}" IssueInstant="#{now.iso8601}" Version="2.0">
+         <Issuer>#{FFaker::Internet.uri('https')}</Issuer>
+         <Subject>
+           <NameID Format="#{Saml::Kit::Namespaces::PERSISTENT}">#{SecureRandom.uuid}</NameID>
+           <SubjectConfirmation Method="#{Saml::Kit::Namespaces::BEARER}">
+             <SubjectConfirmationData InResponseTo="#{SecureRandom.uuid}" NotOnOrAfter="#{not_on_or_after}" Recipient="#{FFaker::Internet.uri('https')}"/>
+           </SubjectConfirmation>
+         </Subject>
+         <Conditions NotBefore="#{now.utc.iso8601}" NotOnOrAfter="#{not_on_or_after}">
+           <AudienceRestriction>
+             <Audience>#{FFaker::Internet.uri('https')}</Audience>
+           </AudienceRestriction>
+         </Conditions>
+         <AuthnStatement AuthnInstant="#{now.utc.iso8601}" SessionIndex="#{Xml::Kit::Id.generate}" SessionNotOnOrAfter="#{not_on_or_after}">
+           <AuthnContext>
+             <AuthnContextClassRef>#{Saml::Kit::Namespaces::PASSWORD}</AuthnContextClassRef>
+           </AuthnContext>
+         </AuthnStatement>
+        </Assertion>
+        </Response>
+XML
+      subject = described_class.new(Nokogiri::XML(xml), configuration: configuration)
+      travel_to((configuration.clock_drift - 1.second).before(now))
+      expect(subject).to be_active
+      expect(subject).not_to be_expired
+    end
+
+    it 'interprets integers correctly' do
+      configuration.clock_drift = 30
+      now = Time.current
+      travel_to now
+      not_before = now.utc.iso8601
+      not_after = configuration.session_timeout.since(now).iso8601
+      xml = <<-XML.strip_heredoc
+        <Response>
+        <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{Xml::Kit::Id.generate}" IssueInstant="#{now.iso8601}" Version="2.0">
+         <Issuer>#{FFaker::Internet.uri('https')}</Issuer>
+         <Subject>
+           <NameID Format="#{Saml::Kit::Namespaces::PERSISTENT}">#{SecureRandom.uuid}</NameID>
+           <SubjectConfirmation Method="#{Saml::Kit::Namespaces::BEARER}">
+             <SubjectConfirmationData InResponseTo="#{SecureRandom.uuid}" NotOnOrAfter="#{not_after}" Recipient="#{FFaker::Internet.uri('https')}"/>
+           </SubjectConfirmation>
+         </Subject>
+         <Conditions NotBefore="#{not_before}" NotOnOrAfter="#{not_after}">
+           <AudienceRestriction>
+             <Audience>#{FFaker::Internet.uri('https')}</Audience>
+           </AudienceRestriction>
+         </Conditions>
+         <AuthnStatement AuthnInstant="#{now.utc.iso8601}" SessionIndex="#{Xml::Kit::Id.generate}" SessionNotOnOrAfter="#{not_after}">
+           <AuthnContext>
+             <AuthnContextClassRef>#{Saml::Kit::Namespaces::PASSWORD}</AuthnContextClassRef>
+           </AuthnContext>
+         </AuthnStatement>
+        </Assertion>
+        </Response>
+XML
+      subject = described_class.new(Nokogiri::XML(xml), configuration: configuration)
+      expect(subject).to be_active
+      expect(subject).not_to be_expired
+    end
+  end
+
+  describe '#present?' do
+    it 'returns false when the assertion is empty' do
+      subject = described_class.new(nil)
+      expect(subject).not_to be_present
+    end
+
+    it 'returns true when the assertion is present' do
+      not_before = Time.now.utc.iso8601
+      not_after = 10.minutes.from_now.iso8601
+      xml = <<-XML.strip_heredoc
+        <Response>
+        <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{Xml::Kit::Id.generate}" IssueInstant="#{Time.now.iso8601}" Version="2.0">
+         <Issuer>#{FFaker::Internet.uri('https')}</Issuer>
+         <Subject>
+           <NameID Format="#{Saml::Kit::Namespaces::PERSISTENT}">#{SecureRandom.uuid}</NameID>
+           <SubjectConfirmation Method="#{Saml::Kit::Namespaces::BEARER}">
+             <SubjectConfirmationData InResponseTo="#{SecureRandom.uuid}" NotOnOrAfter="#{not_after}" Recipient="#{FFaker::Internet.uri('https')}"/>
+           </SubjectConfirmation>
+         </Subject>
+         <Conditions NotBefore="#{not_before}" NotOnOrAfter="#{not_after}">
+           <AudienceRestriction>
+             <Audience>#{FFaker::Internet.uri('https')}</Audience>
+           </AudienceRestriction>
+         </Conditions>
+         <AuthnStatement AuthnInstant="#{Time.now.utc.iso8601}" SessionIndex="#{Xml::Kit::Id.generate}" SessionNotOnOrAfter="#{not_after}">
+           <AuthnContext>
+             <AuthnContextClassRef>#{Saml::Kit::Namespaces::PASSWORD}</AuthnContextClassRef>
+           </AuthnContext>
+         </AuthnStatement>
+        </Assertion>
+        </Response>
+XML
+      subject = described_class.new(Nokogiri::XML(xml))
+      expect(subject).to be_present
+    end
+  end
+
+  describe '#signed?' do
+    let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: ::Xml::Kit::Id.generate, issuer: FFaker::Internet.http_url, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, provider: nil, signed?: true, trusted?: true) }
+    let(:user) { User.new(attributes: { id: SecureRandom.uuid }) }
+
+    it 'detects a signature in an encrypted assertion' do
+      encryption_key_pair = Xml::Kit::KeyPair.generate(use: :encryption)
+      response = Saml::Kit::Response.build(user, request) do |x|
+        x.sign_with(Xml::Kit::KeyPair.generate(use: :signing))
+        x.encrypt_with(encryption_key_pair)
+      end
+      assertion = response.assertion([encryption_key_pair.private_key])
+      expect(response).to be_signed
+      expect(assertion).to be_signed
+    end
+  end
+
+  describe '#to_xml' do
+    let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: ::Xml::Kit::Id.generate, issuer: FFaker::Internet.http_url, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, provider: nil, signed?: true, trusted?: true) }
+    let(:user) { User.new(attributes: { id: SecureRandom.uuid }) }
+
+    it 'returns the decrypted xml' do
+      encryption_key_pair = Xml::Kit::KeyPair.generate(use: :encryption)
+      response = Saml::Kit::Response.build(user, request) do |x|
+        x.sign_with(Xml::Kit::KeyPair.generate(use: :signing))
+        x.encrypt_with(encryption_key_pair)
+      end
+      assertion = response.assertion([encryption_key_pair.private_key])
+      expect(assertion.to_xml).not_to include('EncryptedAssertion')
+      expect(assertion.to_xml).to include('Assertion')
+    end
+  end
+
+  describe '#valid?' do
+    let(:entity_id) { FFaker::Internet.uri('https') }
+    let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: ::Xml::Kit::Id.generate, issuer: entity_id, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, provider: nil, signed?: true, trusted?: true) }
+    let(:name_id) { SecureRandom.uuid }
+    let(:user) { User.new(name_id: name_id, attributes: { id: SecureRandom.uuid }) }
+    let(:registry) { instance_double(Saml::Kit::DefaultRegistry, metadata_for: idp) }
+    let(:idp) { Saml::Kit::IdentityProviderMetadata.build(configuration: configuration) }
+    let(:configuration) do
+      Saml::Kit::Configuration.new do |x|
+        x.entity_id = entity_id
+        x.generate_key_pair_for(use: :signing)
+      end
+    end
+
+    before do
+      allow(configuration.registry).to receive(:metadata_for).with(entity_id).and_return(idp)
+    end
+
+    it 'is invalid when the encrypted signature is invalid' do
+      xml = Saml::Kit::Response.build_xml(user, request, configuration: configuration)
+      altered = xml.gsub(name_id, 'altered')
+      document = Nokogiri::XML(altered)
+      assertion = document.at_xpath('/samlp:Response/saml:Assertion', Saml::Kit::Document::NAMESPACES)
+      key_pair = Xml::Kit::KeyPair.generate(use: :encryption)
+      encrypted = Xml::Kit::Encryption.new(assertion.to_xml, key_pair.public_key).to_xml
+      response = Saml::Kit::Response.new(encrypted, configuration: configuration)
+      expect(response.assertion([key_pair.private_key])).to be_invalid
+    end
+
+    it 'is valid when the encrypted signature is valid' do
+      key_pair = Xml::Kit::KeyPair.generate(use: :encryption)
+      response = Saml::Kit::Response.build(user, request, configuration: configuration) do |x|
+        x.encrypt_with(key_pair)
+      end
+      expect(response.assertion([key_pair.private_key])).to be_valid
+    end
+
+    it 'is invalid when the assertion signature is invalid' do
+      xml = Saml::Kit::Response.build_xml(user, request, configuration: configuration)
+      altered = xml.gsub(name_id, 'altered')
+      response = Saml::Kit::Response.new(altered, configuration: configuration)
+      expect(response.assertion).to be_invalid
+      expect(response.assertion.errors[:digest_value]).to match_array(['is invalid.'])
+    end
+
+    it 'is invalid when the response signature is invalid' do
+      xml = Saml::Kit::Response.build_xml(user, request, configuration: configuration)
+      altered = xml.gsub('StatusCode', 'ALTERED')
+      response = Saml::Kit::Response.new(altered, configuration: configuration)
+      expect(response).to be_invalid
+    end
+
+    it 'is valid' do
+      response = Saml::Kit::Response.build(user, request, configuration: configuration)
+      expect(response.assertion).to be_valid
+    end
+  end
+end
spec/saml/authentication_request_spec.rb → spec/saml/kit/authentication_request_spec.rb
@@ -1,5 +1,6 @@
 RSpec.describe Saml::Kit::AuthenticationRequest do
   subject { described_class.new(raw_xml, configuration: configuration) }
+
   let(:id) { Xml::Kit::Id.generate }
   let(:assertion_consumer_service_url) { "https://#{FFaker::Internet.domain_name}/acs" }
   let(:issuer) { FFaker::Movie.title }
@@ -27,11 +28,11 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
   it { expect(subject.name_id_format).to eql(name_id_format) }
   it { expect(subject.destination).to eql(destination) }
 
-  describe "#valid?" do
+  describe '#valid?' do
     let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
     let(:metadata) { Saml::Kit::ServiceProviderMetadata.build(configuration: configuration) }
 
-    before :each do
+    before do
       allow(configuration).to receive(:registry).and_return(registry)
       allow(registry).to receive(:metadata_for).and_return(metadata)
     end
@@ -82,10 +83,10 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
       id = Xml::Kit::Id.generate
       key_pair = ::Xml::Kit::KeyPair.generate(use: :signing)
       signed_xml = ::Xml::Kit::Signatures.sign(key_pair: key_pair) do |xml, signature|
-        xml.tag!('samlp:AuthnRequest', "xmlns:samlp" => Saml::Kit::Namespaces::PROTOCOL, AssertionConsumerServiceURL: assertion_consumer_service_url, ID: id) do
+        xml.tag!('samlp:AuthnRequest', 'xmlns:samlp' => Saml::Kit::Namespaces::PROTOCOL, AssertionConsumerServiceURL: assertion_consumer_service_url, ID: id) do
           signature.template(id)
           xml.Fake do
-            xml.NotAllowed "Huh?"
+            xml.NotAllowed 'Huh?'
           end
         end
       end
@@ -94,11 +95,11 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
 
     it 'validates a request without a signature' do
       now = Time.now.utc
-      raw_xml = <<-XML
-<samlp:AuthnRequest AssertionConsumerServiceURL='#{assertion_consumer_service_url}' ID='#{Xml::Kit::Id.generate}' IssueInstant='#{now.iso8601}' Version='2.0' xmlns:saml='#{Saml::Kit::Namespaces::ASSERTION}' xmlns:samlp='#{Saml::Kit::Namespaces::PROTOCOL}'>
-  <saml:Issuer>#{issuer}</saml:Issuer>
-  <samlp:NameIDPolicy AllowCreate='true' Format='#{Saml::Kit::Namespaces::EMAIL_ADDRESS}'/>
-</samlp:AuthnRequest>
+      raw_xml = <<-XML.strip_heredoc
+        <samlp:AuthnRequest AssertionConsumerServiceURL='#{assertion_consumer_service_url}' ID='#{Xml::Kit::Id.generate}' IssueInstant='#{now.iso8601}' Version='2.0' xmlns:saml='#{Saml::Kit::Namespaces::ASSERTION}' xmlns:samlp='#{Saml::Kit::Namespaces::PROTOCOL}'>
+          <saml:Issuer>#{issuer}</saml:Issuer>
+          <samlp:NameIDPolicy AllowCreate='true' Format='#{Saml::Kit::Namespaces::EMAIL_ADDRESS}'/>
+        </samlp:AuthnRequest>
       XML
 
       subject = described_class.new(raw_xml, configuration: configuration)
@@ -108,11 +109,11 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
 
     it 'is valid when there is no signature, and the issuer is registered' do
       now = Time.now.utc
-      raw_xml = <<-XML
-<samlp:AuthnRequest AssertionConsumerServiceURL='#{assertion_consumer_service_url}' ID='#{Xml::Kit::Id.generate}' IssueInstant='#{now.iso8601}' Version='2.0' xmlns:saml='#{Saml::Kit::Namespaces::ASSERTION}' xmlns:samlp='#{Saml::Kit::Namespaces::PROTOCOL}'>
-  <saml:Issuer>#{issuer}</saml:Issuer>
-  <samlp:NameIDPolicy AllowCreate='true' Format='#{Saml::Kit::Namespaces::PERSISTENT}'/>
-</samlp:AuthnRequest>
+      raw_xml = <<-XML.strip_heredoc
+        <samlp:AuthnRequest AssertionConsumerServiceURL='#{assertion_consumer_service_url}' ID='#{Xml::Kit::Id.generate}' IssueInstant='#{now.iso8601}' Version='2.0' xmlns:saml='#{Saml::Kit::Namespaces::ASSERTION}' xmlns:samlp='#{Saml::Kit::Namespaces::PROTOCOL}'>
+          <saml:Issuer>#{issuer}</saml:Issuer>
+          <samlp:NameIDPolicy AllowCreate='true' Format='#{Saml::Kit::Namespaces::PERSISTENT}'/>
+        </samlp:AuthnRequest>
       XML
 
       allow(registry).to receive(:metadata_for).with(issuer).and_return(metadata)
@@ -122,11 +123,11 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
 
     it 'is invalid when there is no signature, and the issuer is not registered' do
       now = Time.now.utc
-      raw_xml = <<-XML
-<samlp:AuthnRequest AssertionConsumerServiceURL='#{assertion_consumer_service_url}' ID='#{Xml::Kit::Id.generate}' IssueInstant='#{now.iso8601}' Version='2.0' xmlns:saml='#{Saml::Kit::Namespaces::ASSERTION}' xmlns:samlp='#{Saml::Kit::Namespaces::PROTOCOL}'>
-  <saml:Issuer>#{issuer}</saml:Issuer>
-  <samlp:NameIDPolicy AllowCreate='true' Format='#{Saml::Kit::Namespaces::PERSISTENT}'/>
-</samlp:AuthnRequest>
+      raw_xml = <<-XML.strip_heredoc
+        <samlp:AuthnRequest AssertionConsumerServiceURL='#{assertion_consumer_service_url}' ID='#{Xml::Kit::Id.generate}' IssueInstant='#{now.iso8601}' Version='2.0' xmlns:saml='#{Saml::Kit::Namespaces::ASSERTION}' xmlns:samlp='#{Saml::Kit::Namespaces::PROTOCOL}'>
+          <saml:Issuer>#{issuer}</saml:Issuer>
+          <samlp:NameIDPolicy AllowCreate='true' Format='#{Saml::Kit::Namespaces::PERSISTENT}'/>
+        </samlp:AuthnRequest>
       XML
 
       allow(registry).to receive(:metadata_for).with(issuer).and_return(nil)
@@ -134,7 +135,7 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
       expect(subject).to be_invalid
     end
 
-    context "when the certificate is expired" do
+    context 'when the certificate is expired' do
       let(:expired_certificate) do
         certificate = OpenSSL::X509::Certificate.new
         certificate.public_key = private_key.public_key
@@ -145,7 +146,7 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
       let(:private_key) { OpenSSL::PKey::RSA.new(2048) }
       let(:digest_algorithm) { OpenSSL::Digest::SHA256.new }
 
-      before :each do
+      before do
         expired_certificate.sign(private_key, digest_algorithm)
       end
 
@@ -162,7 +163,7 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
     end
   end
 
-  describe "#assertion_consumer_service_url" do
+  describe '#assertion_consumer_service_url' do
     let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
     let(:metadata) { instance_double(Saml::Kit::ServiceProviderMetadata) }
 
@@ -182,9 +183,9 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
     end
   end
 
-  describe ".build" do
-    let(:url) { FFaker::Internet.uri("https") }
-    let(:entity_id) { FFaker::Internet.uri("https") }
+  describe '.build' do
+    let(:url) { FFaker::Internet.uri('https') }
+    let(:entity_id) { FFaker::Internet.uri('https') }
 
     it 'provides a nice API for building metadata' do
       result = described_class.build do |builder|
@@ -198,17 +199,17 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
     end
   end
 
-  describe "#response_for" do
-    let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: []) }
+  describe '#response_for' do
+    let(:user) { User.new }
     let(:provider) do
       Saml::Kit::ServiceProviderMetadata.build do |x|
-        x.add_assertion_consumer_service(FFaker::Internet.uri("https"), binding: :http_post)
+        x.add_assertion_consumer_service(FFaker::Internet.uri('https'), binding: :http_post)
       end
     end
 
     it 'serializes a response' do
       allow(subject).to receive(:provider).and_return(provider)
-      url, saml_params = subject.response_for(user, binding: :http_post, relay_state: FFaker::Movie.title)
+      _url, saml_params = subject.response_for(user, binding: :http_post, relay_state: FFaker::Movie.title)
 
       response = provider.assertion_consumer_service_for(binding: :http_post).deserialize(saml_params)
       expect(response).to be_instance_of(Saml::Kit::Response)
@@ -220,7 +221,7 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
         config.generate_key_pair_for(use: :signing)
       end
       key_pair = configuration.key_pairs(use: :signing).first
-      url, saml_params = subject.response_for(user, binding: :http_post, configuration: configuration) do |builder|
+      _url, saml_params = subject.response_for(user, binding: :http_post, configuration: configuration) do |builder|
         builder.sign_with(key_pair)
       end
 
@@ -228,4 +229,16 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
       expect(response).to be_instance_of(Saml::Kit::Response)
     end
   end
+
+  describe '#signature.valid?' do
+    it 'returns true when the signature is valid' do
+      expect(subject.signature).to be_valid
+    end
+
+    it 'returns false when the signature does not match' do
+      raw_xml.gsub!(issuer, 'corrupt')
+      subject = described_class.new(raw_xml, configuration: configuration)
+      expect(subject.signature).not_to be_valid
+    end
+  end
 end
spec/saml/bindings_spec.rb → spec/saml/kit/bindings_spec.rb
@@ -1,15 +1,16 @@
 RSpec.describe Saml::Kit::Bindings do
-  describe ".to_symbol" do
+  describe '.to_symbol' do
     subject { described_class }
 
-    it { expect(subject.to_symbol(Saml::Kit::Bindings::HTTP_POST)).to eql(:http_post) }
-    it { expect(subject.to_symbol(Saml::Kit::Bindings::HTTP_REDIRECT)).to eql(:http_redirect) }
+    it { expect(subject.to_symbol(Saml::Kit::Bindings::HTTP_POST)).to be(:http_post) }
+    it { expect(subject.to_symbol(Saml::Kit::Bindings::HTTP_REDIRECT)).to be(:http_redirect) }
     it { expect(subject.to_symbol('unknown')).to eql('unknown') }
   end
 
-  describe ".create_for" do
+  describe '.create_for' do
     subject { described_class }
-    let(:location) { FFaker::Internet.uri("https") }
+
+    let(:location) { FFaker::Internet.uri('https') }
 
     it 'returns an HTTP redirect binding' do
       expect(
spec/saml/composite_metadata_spec.rb → spec/saml/kit/composite_metadata_spec.rb
@@ -1,72 +1,73 @@
 RSpec.describe Saml::Kit::CompositeMetadata do
   subject { described_class.new(xml) }
-  let(:post_binding) { Saml::Kit::Bindings::HTTP_POST  }
+
+  let(:post_binding) { Saml::Kit::Bindings::HTTP_POST }
   let(:redirect_binding) { Saml::Kit::Bindings::HTTP_REDIRECT }
-  let(:sign_on_service) { FFaker::Internet.uri("https") }
-  let(:assertion_consumer_service) { FFaker::Internet.uri("https") }
-  let(:sp_logout_service) { FFaker::Internet.uri("https") }
-  let(:idp_logout_service) { FFaker::Internet.uri("https") }
-  let(:entity_id) { FFaker::Internet.uri("https") }
+  let(:sign_on_service) { FFaker::Internet.uri('https') }
+  let(:assertion_consumer_service) { FFaker::Internet.uri('https') }
+  let(:sp_logout_service) { FFaker::Internet.uri('https') }
+  let(:idp_logout_service) { FFaker::Internet.uri('https') }
+  let(:entity_id) { FFaker::Internet.uri('https') }
   let(:sp_signing_certificate) { ::Xml::Kit::KeyPair.generate(use: :signing).certificate }
   let(:sp_encryption_certificate) { ::Xml::Kit::KeyPair.generate(use: :encryption).certificate }
   let(:idp_signing_certificate) { ::Xml::Kit::KeyPair.generate(use: :signing).certificate }
   let(:idp_encryption_certificate) { ::Xml::Kit::KeyPair.generate(use: :encryption).certificate }
   let(:xml) do
-    <<-XML
-<EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="#{::Xml::Kit::Id.generate}" entityID="#{entity_id}">
-  <SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
-    <KeyDescriptor use="signing">
-      <KeyInfo xmlns="#{::Xml::Kit::Namespaces::XMLDSIG}">
-        <X509Data>
-          <X509Certificate>#{sp_signing_certificate.stripped}</X509Certificate>
-        </X509Data>
-      </KeyInfo>
-    </KeyDescriptor>
-    <KeyDescriptor use="encryption">
-      <KeyInfo xmlns="#{::Xml::Kit::Namespaces::XMLDSIG}">
-        <X509Data>
-          <X509Certificate>#{sp_encryption_certificate.stripped}</X509Certificate>
-        </X509Data>
-      </KeyInfo>
-    </KeyDescriptor>
-    <SingleLogoutService Binding="#{post_binding}" Location="#{sp_logout_service}"/>
-    <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
-    <AssertionConsumerService Binding="#{post_binding}" Location="#{assertion_consumer_service}" index="0" isDefault="true"/>
-  </SPSSODescriptor>
-  <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
-    <KeyDescriptor use="signing">
-      <KeyInfo xmlns="#{::Xml::Kit::Namespaces::XMLDSIG}">
-        <X509Data>
-          <X509Certificate>#{idp_signing_certificate.stripped}</X509Certificate>
-        </X509Data>
-      </KeyInfo>
-    </KeyDescriptor>
-    <KeyDescriptor use="encryption">
-      <KeyInfo xmlns="#{::Xml::Kit::Namespaces::XMLDSIG}">
-        <X509Data>
-          <X509Certificate>#{idp_encryption_certificate.stripped}</X509Certificate>
-        </X509Data>
-      </KeyInfo>
-    </KeyDescriptor>
-    <SingleLogoutService Binding="#{post_binding}" Location="#{idp_logout_service}"/>
-    <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
-    <SingleSignOnService Binding="#{post_binding}" Location="#{sign_on_service}"/>
-    <SingleSignOnService Binding="#{redirect_binding}" Location="#{sign_on_service}"/>
-    <Attribute xmlns="#{Saml::Kit::Namespaces::ASSERTION}" Name="id" ></Attribute>
-  </IDPSSODescriptor>
-  <Organization>
-    <OrganizationName xml:lang="en">Acme, Inc</OrganizationName>
-    <OrganizationDisplayName xml:lang="en">Acme, Inc</OrganizationDisplayName>
-    <OrganizationURL xml:lang="en">http://localhost:5000/</OrganizationURL>
-  </Organization>
-  <ContactPerson contactType="technical">
-    <Company>mailto:hi@example.com</Company>
-  </ContactPerson>
-</EntityDescriptor>
+    <<-XML.strip_heredoc
+      <EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="#{::Xml::Kit::Id.generate}" entityID="#{entity_id}">
+        <SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
+          <KeyDescriptor use="signing">
+            <KeyInfo xmlns="#{::Xml::Kit::Namespaces::XMLDSIG}">
+              <X509Data>
+                <X509Certificate>#{sp_signing_certificate.stripped}</X509Certificate>
+              </X509Data>
+            </KeyInfo>
+          </KeyDescriptor>
+          <KeyDescriptor use="encryption">
+            <KeyInfo xmlns="#{::Xml::Kit::Namespaces::XMLDSIG}">
+              <X509Data>
+                <X509Certificate>#{sp_encryption_certificate.stripped}</X509Certificate>
+              </X509Data>
+            </KeyInfo>
+          </KeyDescriptor>
+          <SingleLogoutService Binding="#{post_binding}" Location="#{sp_logout_service}"/>
+          <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
+          <AssertionConsumerService Binding="#{post_binding}" Location="#{assertion_consumer_service}" index="0" isDefault="true"/>
+        </SPSSODescriptor>
+        <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
+          <KeyDescriptor use="signing">
+            <KeyInfo xmlns="#{::Xml::Kit::Namespaces::XMLDSIG}">
+              <X509Data>
+                <X509Certificate>#{idp_signing_certificate.stripped}</X509Certificate>
+              </X509Data>
+            </KeyInfo>
+          </KeyDescriptor>
+          <KeyDescriptor use="encryption">
+            <KeyInfo xmlns="#{::Xml::Kit::Namespaces::XMLDSIG}">
+              <X509Data>
+                <X509Certificate>#{idp_encryption_certificate.stripped}</X509Certificate>
+              </X509Data>
+            </KeyInfo>
+          </KeyDescriptor>
+          <SingleLogoutService Binding="#{post_binding}" Location="#{idp_logout_service}"/>
+          <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
+          <SingleSignOnService Binding="#{post_binding}" Location="#{sign_on_service}"/>
+          <SingleSignOnService Binding="#{redirect_binding}" Location="#{sign_on_service}"/>
+          <Attribute xmlns="#{Saml::Kit::Namespaces::ASSERTION}" Name="id" ></Attribute>
+        </IDPSSODescriptor>
+        <Organization>
+          <OrganizationName xml:lang="en">Acme, Inc</OrganizationName>
+          <OrganizationDisplayName xml:lang="en">Acme, Inc</OrganizationDisplayName>
+          <OrganizationURL xml:lang="en">http://localhost:5000/</OrganizationURL>
+        </Organization>
+        <ContactPerson contactType="technical">
+          <Company>mailto:hi@example.com</Company>
+        </ContactPerson>
+      </EntityDescriptor>
     XML
   end
 
-  describe "#single_sign_on_services" do
+  describe '#single_sign_on_services' do
     it 'returns the single sign on services from the idp' do
       expect(subject.single_sign_on_services).to match_array([
         Saml::Kit::Bindings::HttpPost.new(location: sign_on_service),
@@ -75,7 +76,7 @@ RSpec.describe Saml::Kit::CompositeMetadata do
     end
   end
 
-  describe "#single_sign_on_service_for" do
+  describe '#single_sign_on_service_for' do
     it 'returns the post binding' do
       expect(subject.single_sign_on_service_for(binding: :http_post)).to eql(
         Saml::Kit::Bindings::HttpPost.new(location: sign_on_service)
@@ -138,7 +139,7 @@ RSpec.describe Saml::Kit::CompositeMetadata do
   end
   it do
     expect(subject.service_for(type: 'AssertionConsumerService', binding: :http_post)).to eql(
-      Saml::Kit::Bindings::HttpPost.new(location: assertion_consumer_service),
+      Saml::Kit::Bindings::HttpPost.new(location: assertion_consumer_service)
     )
   end
   it do
@@ -149,7 +150,7 @@ RSpec.describe Saml::Kit::CompositeMetadata do
   end
   it do
     expect(subject.service_for(type: 'SingleSignOnService', binding: :http_post)).to eql(
-      Saml::Kit::Bindings::HttpPost.new(location: sign_on_service),
+      Saml::Kit::Bindings::HttpPost.new(location: sign_on_service)
     )
   end
 
@@ -166,8 +167,5 @@ RSpec.describe Saml::Kit::CompositeMetadata do
     )
   end
 
-  it do
-    user = double(:user, name_id_for: SecureRandom.uuid)
-    expect(subject.logout_request_for(user)).to be_present
-  end
+  specify { expect(subject.logout_request_for(User.new)).to be_present }
 end
spec/saml/configuration_spec.rb → spec/saml/kit/configuration_spec.rb
@@ -1,5 +1,5 @@
 RSpec.describe Saml::Kit::Configuration do
-  describe "#generate_key_pair_for" do
+  describe '#generate_key_pair_for' do
     subject { described_class.new }
 
     it 'raises an error when the use is not known' do
@@ -10,17 +10,18 @@ RSpec.describe Saml::Kit::Configuration do
 
     it 'generates a signing key pair' do
       subject.generate_key_pair_for(use: :signing)
-      expect(subject.key_pairs(use: :signing).count).to eql(1)
+      expect(subject.key_pairs(use: :signing).count).to be(1)
     end
 
     it 'generates an encryption key pair' do
       subject.generate_key_pair_for(use: :encryption)
-      expect(subject.key_pairs(use: :encryption).count).to eql(1)
+      expect(subject.key_pairs(use: :encryption).count).to be(1)
     end
   end
 
-  describe "#add_key_pair" do
+  describe '#add_key_pair' do
     subject { described_class.new }
+
     let(:certificate) do
       certificate = OpenSSL::X509::Certificate.new
       certificate.public_key = private_key.public_key
@@ -36,12 +37,12 @@ RSpec.describe Saml::Kit::Configuration do
 
     it 'adds a signing key pair' do
       subject.add_key_pair(certificate.to_pem, private_key.export, use: :signing)
-      expect(subject.key_pairs(use: :signing).count).to eql(1)
+      expect(subject.key_pairs(use: :signing).count).to be(1)
     end
 
     it 'adds an encryption key pair' do
       subject.add_key_pair(certificate.to_pem, private_key.export, use: :encryption)
-      expect(subject.key_pairs(use: :encryption).count).to eql(1)
+      expect(subject.key_pairs(use: :encryption).count).to be(1)
     end
   end
 end
spec/saml/kit/default_registry_spec.rb
@@ -0,0 +1,94 @@
+RSpec.describe Saml::Kit::DefaultRegistry do
+  subject { described_class.new }
+
+  let(:entity_id) { FFaker::Internet.http_url }
+  let(:service_provider_metadata) do
+    Saml::Kit::ServiceProviderMetadata.build do |builder|
+      builder.entity_id = entity_id
+    end
+  end
+  let(:identity_provider_metadata) do
+    Saml::Kit::IdentityProviderMetadata.build do |builder|
+      builder.entity_id = entity_id
+    end
+  end
+
+  describe '#metadata_for' do
+    it 'returns the metadata for the entity_id' do
+      subject.register(service_provider_metadata)
+      expect(subject.metadata_for(entity_id)).to eql(service_provider_metadata)
+    end
+  end
+
+  describe '#register_url' do
+    let(:url) { FFaker::Internet.http_url }
+
+    it 'fetches the SP metadata from a remote url and registers it' do
+      stub_request(:get, url)
+        .to_return(status: 200, body: service_provider_metadata.to_xml)
+      subject.register_url(url)
+
+      result = subject.metadata_for(entity_id)
+      expect(result).to be_present
+      expect(result).to be_instance_of(Saml::Kit::ServiceProviderMetadata)
+    end
+
+    it 'fetches the IDP metadata from a remote url' do
+      stub_request(:get, url)
+        .to_return(status: 200, body: identity_provider_metadata.to_xml)
+      subject.register_url(url)
+
+      result = subject.metadata_for(entity_id)
+      expect(result).to be_present
+      expect(result).to be_instance_of(Saml::Kit::IdentityProviderMetadata)
+    end
+
+    it 'registers metadata that serves as both an IDP and SP' do
+      xml = <<-XML.strip_heredoc
+        <EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="#{::Xml::Kit::Id.generate}" entityID="#{entity_id}">
+          <SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
+            <SingleLogoutService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri('https')}"/>
+            <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
+            <AssertionConsumerService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri('https')}" index="0" isDefault="true"/>
+          </SPSSODescriptor>
+          <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
+            <SingleLogoutService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri('https')}"/>
+            <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
+            <SingleSignOnService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri('https')}"/>
+            <SingleSignOnService Binding="#{Saml::Kit::Bindings::HTTP_REDIRECT}" Location="#{FFaker::Internet.uri('https')}"/>
+          </IDPSSODescriptor>
+          <Organization>
+            <OrganizationName xml:lang="en">Acme, Inc</OrganizationName>
+            <OrganizationDisplayName xml:lang="en">Acme, Inc</OrganizationDisplayName>
+            <OrganizationURL xml:lang="en">http://localhost:5000/</OrganizationURL>
+          </Organization>
+          <ContactPerson contactType="technical">
+            <Company>mailto:hi@example.com</Company>
+          </ContactPerson>
+        </EntityDescriptor>
+      XML
+      stub_request(:get, url).to_return(status: 200, body: xml)
+      subject.register_url(url)
+
+      result = subject.metadata_for(entity_id)
+      expect(result).to be_present
+      expect(result).to be_instance_of(Saml::Kit::CompositeMetadata)
+    end
+  end
+
+  describe '#each' do
+    it 'yields each registered metadata' do
+      idp = Saml::Kit::IdentityProviderMetadata.build do |config|
+        config.entity_id = 'idp'
+      end
+      sp = Saml::Kit::ServiceProviderMetadata.build do |config|
+        config.entity_id = 'sp'
+      end
+
+      subject.register(idp)
+      subject.register(sp)
+
+      expect(subject.map(&:to_xml)).to match_array([idp.to_xml, sp.to_xml])
+    end
+  end
+end
spec/saml/document_spec.rb → spec/saml/kit/document_spec.rb
@@ -1,7 +1,8 @@
 RSpec.describe Saml::Kit::Document do
-  describe ".to_saml_document" do
+  describe '.to_saml_document' do
     subject { described_class }
-    let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: { id: SecureRandom.uuid }) }
+
+    let(:user) { User.new(attributes: { id: SecureRandom.uuid }) }
     let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: Xml::Kit::Id.generate, issuer: FFaker::Internet.http_url, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, provider: nil, signed?: true, trusted?: true) }
 
     it 'returns a Response' do
@@ -40,7 +41,7 @@ RSpec.describe Saml::Kit::Document do
     end
 
     it 'returns an invalid document when the xml is not XML' do
-      result = subject.to_saml_document("NOT XML")
+      result = subject.to_saml_document('NOT XML')
       expect(result).to be_instance_of(Saml::Kit::InvalidDocument)
     end
 
spec/saml/identity_provider_metadata_spec.rb → spec/saml/kit/identity_provider_metadata_spec.rb
@@ -1,16 +1,16 @@
 RSpec.describe Saml::Kit::IdentityProviderMetadata do
   subject { described_class.new(raw_metadata) }
 
-  context "okta metadata" do
-    let(:raw_metadata) { IO.read("spec/fixtures/metadata/okta.xml") }
+  describe 'okta metadata' do
+    let(:raw_metadata) { IO.read('spec/fixtures/metadata/okta.xml') }
     let(:certificate) do
       Hash.from_xml(raw_metadata)['EntityDescriptor']['IDPSSODescriptor']['KeyDescriptor']['KeyInfo']['X509Data']['X509Certificate']
     end
 
-    it { expect(subject.entity_id).to eql("http://www.okta.com/1") }
-    it { expect(subject.name_id_formats).to match_array([ Saml::Kit::Namespaces::EMAIL_ADDRESS, Saml::Kit::Namespaces::UNSPECIFIED_NAMEID ]) }
+    it { expect(subject.entity_id).to eql('http://www.okta.com/1') }
+    it { expect(subject.name_id_formats).to match_array([Saml::Kit::Namespaces::EMAIL_ADDRESS, Saml::Kit::Namespaces::UNSPECIFIED_NAMEID]) }
     it do
-      location = "https://dev.oktapreview.com/app/example/1/sso/saml"
+      location = 'https://dev.oktapreview.com/app/example/1/sso/saml'
       expect(subject.single_sign_on_services.map(&:to_h)).to match_array([
         { binding: Saml::Kit::Bindings::HTTP_POST, location: location },
         { binding: Saml::Kit::Bindings::HTTP_REDIRECT, location: location },
@@ -18,15 +18,15 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
     end
     it { expect(subject.single_logout_services).to be_empty }
     it do
-      fingerprint = "9F:74:13:3B:BC:5A:7B:8B:2D:4F:8B:EF:1E:88:EB:D1:AE:BC:19:BF:CA:19:C6:2F:0F:4B:31:1D:68:98:B0:1B"
+      fingerprint = '9F:74:13:3B:BC:5A:7B:8B:2D:4F:8B:EF:1E:88:EB:D1:AE:BC:19:BF:CA:19:C6:2F:0F:4B:31:1D:68:98:B0:1B'
       expect(subject.certificates).to match_array([::Xml::Kit::Certificate.new(certificate, use: :signing)])
       expect(subject.certificates.first.fingerprint.to_s).to eql(fingerprint)
     end
     it { expect(subject.attributes).to be_empty }
   end
 
-  context "active directory" do
-    let(:raw_metadata) { IO.read("spec/fixtures/metadata/ad_2012.xml") }
+  describe 'active directory' do
+    let(:raw_metadata) { IO.read('spec/fixtures/metadata/ad_2012.xml') }
     let(:xml_hash) { Hash.from_xml(raw_metadata) }
     let(:signing_certificate) do
       xml_hash['EntityDescriptor']['IDPSSODescriptor']['KeyDescriptor'].find { |x| x['use'] == 'signing' }['KeyInfo']['X509Data']['X509Certificate']
@@ -35,7 +35,7 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
       xml_hash['EntityDescriptor']['IDPSSODescriptor']['KeyDescriptor'].find { |x| x['use'] == 'encryption' }['KeyInfo']['X509Data']['X509Certificate']
     end
 
-    it { expect(subject.entity_id).to eql("http://www.example.com/adfs/services/trust") }
+    it { expect(subject.entity_id).to eql('http://www.example.com/adfs/services/trust') }
     it do
       expect(subject.name_id_formats).to match_array([
         Saml::Kit::Namespaces::EMAIL_ADDRESS,
@@ -44,14 +44,14 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
       ])
     end
     it do
-      location = "https://www.example.com/adfs/ls/"
+      location = 'https://www.example.com/adfs/ls/'
       expect(subject.single_sign_on_services.map(&:to_h)).to match_array([
         { location: location, binding: Saml::Kit::Bindings::HTTP_REDIRECT },
         { location: location, binding: Saml::Kit::Bindings::HTTP_POST },
       ])
     end
     it do
-      location = "https://www.example.com/adfs/ls/"
+      location = 'https://www.example.com/adfs/ls/'
       expect(subject.single_logout_services.map(&:to_h)).to match_array([
         { location: location, binding: Saml::Kit::Bindings::HTTP_REDIRECT },
         { location: location, binding: Saml::Kit::Bindings::HTTP_POST },
@@ -66,7 +66,7 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
     it { expect(subject.attributes).to be_present }
   end
 
-  describe "#validate" do
+  describe '#validate' do
     it 'valid when given valid identity provider metadata' do
       subject = described_class.build do |builder|
         builder.attributes = [:email]
@@ -81,13 +81,13 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
     it 'is invalid, when given service provider metadata' do
       service_provider_metadata = Saml::Kit::ServiceProviderMetadata.build.to_xml
       subject = described_class.new(service_provider_metadata)
-      expect(subject).to_not be_valid
-      expect(subject.errors[:base]).to include(I18n.translate("saml/kit.errors.IDPSSODescriptor.invalid"))
+      expect(subject).not_to be_valid
+      expect(subject.errors[:base]).to include(I18n.translate('saml/kit.errors.IDPSSODescriptor.invalid'))
     end
 
     it 'is invalid, when the metadata is nil' do
       subject = described_class.new(nil)
-      expect(subject).to_not be_valid
+      expect(subject).not_to be_valid
       expect(subject.errors[:metadata]).to include("can't be blank")
     end
 
@@ -100,25 +100,24 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
         end
       end
       subject = described_class.new(xml.target!)
-      expect(subject).to_not be_valid
+      expect(subject).not_to be_valid
       expect(subject.errors[:base][0]).to include("1:0: ERROR: Element '{urn:oasis:names:tc:SAML:2.0:metadata}EntityDescriptor'")
     end
 
     it 'is invalid, when the signature is invalid' do
       old_url = 'https://www.example.com/adfs/ls/'
       new_url = 'https://myserver.com/hacked'
-      metadata_xml = IO.read("spec/fixtures/metadata/ad_2012.xml").gsub(old_url, new_url)
+      metadata_xml = IO.read('spec/fixtures/metadata/ad_2012.xml').gsub(old_url, new_url)
 
       subject = described_class.new(metadata_xml)
       expect(subject).to be_invalid
-      expect(subject.errors[:base]).to include("invalid signature.")
+      expect(subject.errors[:base]).to be_empty
+      expect(subject.errors[:digest_value]).to match_array(['is invalid.'])
+      expect(subject.errors[:signature]).to match_array(['is invalid.'])
     end
   end
 
-  describe "#single_sign_on_service_for" do
-    let(:post_url) { FFaker::Internet.http_url }
-    let(:redirect_url) { FFaker::Internet.http_url }
-
+  describe '#single_sign_on_service_for' do
     subject do
       described_class.build do |builder|
         builder.add_single_sign_on_service(redirect_url, binding: :http_redirect)
@@ -126,6 +125,9 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
       end
     end
 
+    let(:post_url) { FFaker::Internet.http_url }
+    let(:redirect_url) { FFaker::Internet.http_url }
+
     it 'returns the POST binding' do
       result = subject.single_sign_on_service_for(binding: :http_post)
       expect(result.location).to eql(post_url)
@@ -143,7 +145,7 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
     end
   end
 
-  describe "#want_authn_requests_signed" do
+  describe '#want_authn_requests_signed' do
     it 'returns true when enabled' do
       subject = described_class.build do |builder|
         builder.want_authn_requests_signed = true
@@ -161,15 +163,15 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
     it 'returns true when the attribute is missing' do
       xml = described_class.build do |builder|
         builder.want_authn_requests_signed = false
-      end.to_xml.gsub("WantAuthnRequestsSigned=\"false\"", "")
+      end.to_xml.gsub('WantAuthnRequestsSigned="false"', '')
       subject = described_class.new(xml)
       expect(subject.want_authn_requests_signed).to be(true)
     end
   end
 
-  describe "#single_logout_service_for" do
-    let(:redirect_url) { FFaker::Internet.uri("https") }
-    let(:post_url) { FFaker::Internet.uri("https") }
+  describe '#single_logout_service_for' do
+    let(:redirect_url) { FFaker::Internet.uri('https') }
+    let(:post_url) { FFaker::Internet.uri('https') }
     let(:subject) do
       described_class.build do |builder|
         builder.add_single_logout_service(redirect_url, binding: :http_redirect)
@@ -187,9 +189,9 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
     end
   end
 
-  describe ".build" do
-    let(:url) { FFaker::Internet.uri("https") }
-    let(:entity_id) { FFaker::Internet.uri("https") }
+  describe '.build' do
+    let(:url) { FFaker::Internet.uri('https') }
+    let(:entity_id) { FFaker::Internet.uri('https') }
 
     it 'provides a nice API for building metadata' do
       result = described_class.build do |builder|
@@ -203,12 +205,12 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
     end
   end
 
-  describe "#login_request_for" do
+  describe '#login_request_for' do
     it 'returns a serialized login request' do
       subject = described_class.build do |x|
-        x.add_single_sign_on_service(FFaker::Internet.uri("https"), binding: :http_post)
+        x.add_single_sign_on_service(FFaker::Internet.uri('https'), binding: :http_post)
       end
-      url, saml_params = subject.login_request_for(binding: :http_post, relay_state: FFaker::Movie.title)
+      _url, saml_params = subject.login_request_for(binding: :http_post, relay_state: FFaker::Movie.title)
       result = subject.single_sign_on_service_for(binding: :http_post).deserialize(saml_params)
       expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
     end
spec/saml/invalid_document_spec.rb → spec/saml/kit/invalid_document_spec.rb
@@ -1,14 +1,13 @@
 RSpec.describe Saml::Kit::InvalidDocument do
   it 'is invalid' do
-    subject = described_class.new("<xml></xml>")
+    subject = described_class.new('<xml></xml>')
     expect(subject).to be_invalid
     expect(subject.errors[:base]).to be_present
   end
 
   it 'is invalid with something that not xml' do
-    subject = described_class.new("NOT XML")
+    subject = described_class.new('NOT XML')
     expect(subject).to be_invalid
     expect(subject.errors[:base]).to be_present
   end
 end
-
spec/saml/kit_spec.rb → spec/saml/kit/kit_spec.rb
@@ -1,5 +1,5 @@
 RSpec.describe Saml::Kit do
-  it "has a version number" do
+  it 'has a version number' do
     expect(Saml::Kit::VERSION).not_to be nil
   end
 end
spec/saml/logout_request_spec.rb → spec/saml/kit/logout_request_spec.rb
@@ -1,8 +1,9 @@
 RSpec.describe Saml::Kit::LogoutRequest do
   subject { described_class.build(user, configuration: configuration) }
-  let(:user) { double(:user, name_id_for: name_id) }
+
+  let(:user) { User.new(name_id: name_id) }
   let(:name_id) { SecureRandom.uuid }
-  let(:entity_id) { FFaker::Internet.uri("https") }
+  let(:entity_id) { FFaker::Internet.uri('https') }
   let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
   let(:configuration) do
     Saml::Kit::Configuration.new do |config|
@@ -25,11 +26,11 @@ RSpec.describe Saml::Kit::LogoutRequest do
   end
 
   it 'parses the version' do
-    expect(subject.version).to eql("2.0")
+    expect(subject.version).to eql('2.0')
   end
 
   it 'parses the destination' do
-    destination = FFaker::Internet.uri("https")
+    destination = FFaker::Internet.uri('https')
     subject = described_class.build(user, configuration: configuration) do |builder|
       builder.destination = destination
     end
@@ -40,15 +41,15 @@ RSpec.describe Saml::Kit::LogoutRequest do
     expect(subject.name_id).to eql(name_id)
   end
 
-  describe "#valid?" do
+  describe '#valid?' do
     let(:metadata) do
       Saml::Kit::ServiceProviderMetadata.build(configuration: configuration) do |builder|
         builder.entity_id = entity_id
-        builder.add_single_logout_service(FFaker::Internet.uri("https"), binding: :http_post)
+        builder.add_single_logout_service(FFaker::Internet.uri('https'), binding: :http_post)
       end
     end
 
-    before :each do
+    before do
       allow(registry).to receive(:metadata_for).and_return(metadata)
     end
 
@@ -57,7 +58,7 @@ RSpec.describe Saml::Kit::LogoutRequest do
     end
 
     it 'is invalid if the document has been tampered with' do
-      issuer = FFaker::Internet.uri("https")
+      issuer = FFaker::Internet.uri('https')
       raw_xml = described_class.build(user, configuration: configuration) do |builder|
         builder.issuer = issuer
       end.to_xml.gsub(issuer, 'corrupt')
@@ -98,11 +99,11 @@ RSpec.describe Saml::Kit::LogoutRequest do
     end
 
     it 'is valid when a single logout service url is available via the registry' do
-      issuer = FFaker::Internet.uri("https")
+      issuer = FFaker::Internet.uri('https')
       allow(registry).to receive(:metadata_for).with(issuer).and_return(metadata)
       allow(metadata).to receive(:matches?).and_return(true)
       allow(metadata).to receive(:single_logout_services).and_return([
-        Saml::Kit::Bindings::HttpPost.new(location: FFaker::Internet.uri("https"))
+        Saml::Kit::Bindings::HttpPost.new(location: FFaker::Internet.uri('https'))
       ])
 
       subject = described_class.build(user, configuration: configuration) do |builder|
@@ -118,7 +119,7 @@ RSpec.describe Saml::Kit::LogoutRequest do
         xml.LogoutRequest ID: id do
           signature.template(id)
           xml.Fake do
-            xml.NotAllowed "Huh?"
+            xml.NotAllowed 'Huh?'
           end
         end
       end
@@ -126,10 +127,10 @@ RSpec.describe Saml::Kit::LogoutRequest do
     end
   end
 
-  describe "#response_for" do
+  describe '#response_for' do
     let(:provider) do
       Saml::Kit::IdentityProviderMetadata.build do |builder|
-        builder.add_single_logout_service(FFaker::Internet.uri("https"), binding: :http_post)
+        builder.add_single_logout_service(FFaker::Internet.uri('https'), binding: :http_post)
       end
     end
 
spec/saml/kit/logout_response_spec.rb
@@ -0,0 +1,5 @@
+RSpec.describe Saml::Kit::LogoutResponse do
+  it 'exists' do
+    expect(described_class).to be(described_class)
+  end
+end
spec/saml/kit/metadata_spec.rb
@@ -0,0 +1,89 @@
+RSpec.describe Saml::Kit::Metadata do
+  describe '.from' do
+    subject { described_class }
+
+    it 'returns an identity provider metadata' do
+      xml = described_class.build_xml do |x|
+        x.build_identity_provider
+      end
+      expect(subject.from(xml)).to be_instance_of(Saml::Kit::IdentityProviderMetadata)
+    end
+
+    it 'returns a service provider metadata' do
+      xml = described_class.build_xml do |x|
+        x.build_service_provider
+      end
+      expect(subject.from(xml)).to be_instance_of(Saml::Kit::ServiceProviderMetadata)
+    end
+
+    it 'generates a full metadata in a reasonable amount of time' do
+      expect do
+        described_class.build_xml do |x|
+          x.build_identity_provider
+          x.build_service_provider
+        end
+      end.to perform_under(10).ms
+    end
+
+    it 'returns a composite' do
+      xml = <<-XML.strip_heredoc
+        <EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="#{Xml::Kit::Id.generate}" entityID="#{FFaker::Internet.uri('https')}">
+          <SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
+            <SingleLogoutService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri('https')}"/>
+            <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
+            <AssertionConsumerService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri('https')}" index="0" isDefault="true"/>
+          </SPSSODescriptor>
+          <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
+            <SingleLogoutService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri('https')}"/>
+            <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
+            <SingleSignOnService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri('https')}"/>
+            <SingleSignOnService Binding="#{Saml::Kit::Bindings::HTTP_REDIRECT}" Location="#{FFaker::Internet.uri('https')}"/>
+          </IDPSSODescriptor>
+          <Organization>
+            <OrganizationName xml:lang="en">Acme, Inc</OrganizationName>
+            <OrganizationDisplayName xml:lang="en">Acme, Inc</OrganizationDisplayName>
+            <OrganizationURL xml:lang="en">http://localhost:5000/</OrganizationURL>
+          </Organization>
+          <ContactPerson contactType="technical">
+            <Company>mailto:hi@example.com</Company>
+          </ContactPerson>
+        </EntityDescriptor>
+      XML
+      result = subject.from(xml)
+      expect(result).to be_present
+
+      expect(result.single_sign_on_services.count).to be(2)
+      expect(result.assertion_consumer_services.count).to be(1)
+      expect(result.single_logout_services.count).to be(2)
+      expect(result.organization_name).to eql('Acme, Inc')
+      expect(result.organization_url).to eql('http://localhost:5000/')
+      expect(result.contact_person_company).to eql('mailto:hi@example.com')
+    end
+  end
+
+  describe '#certificates' do
+    it 'returns each certificate when missing a "use"' do
+      configuration = Saml::Kit::Configuration.new do |config|
+        config.generate_key_pair_for(use: :signing)
+      end
+      xml = described_class.build_xml(configuration: configuration) do |x|
+        x.embed_signature = false
+        x.build_identity_provider
+      end
+      modified_xml = xml.gsub(/use/, 'misuse')
+      subject = described_class.from(modified_xml)
+      expect(subject.certificates.count).to be(1)
+    end
+  end
+
+  describe '#signature' do
+    it 'returns the signature' do
+      subject = described_class.build do |x|
+        x.sign_with(::Xml::Kit::KeyPair.generate(use: :signing))
+        x.build_identity_provider
+      end
+
+      expect(subject.signature).to be_present
+    end
+  end
+end
spec/saml/kit/response_spec.rb
@@ -0,0 +1,536 @@
+RSpec.describe Saml::Kit::Response do
+  describe '#valid?' do
+    subject { described_class.build(user, request, configuration: configuration) }
+
+    let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: ::Xml::Kit::Id.generate, issuer: FFaker::Internet.http_url, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, provider: nil, signed?: true, trusted?: true) }
+    let(:user) { User.new(attributes: { id: SecureRandom.uuid }) }
+    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 'is valid' do
+      allow(registry).to receive(:metadata_for).and_return(metadata)
+      allow(metadata).to receive(:matches?).and_return(true)
+      expect(subject).to be_valid
+    end
+
+    it 'is invalid when blank' do
+      allow(registry).to receive(:metadata_for).and_return(nil)
+      subject = described_class.new('')
+      expect(subject).to be_invalid
+      expect(subject.errors[:content]).to be_present
+      expect(subject.errors[:assertion]).to match_array(['is missing.'])
+    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)
+      status_code = FFaker::Movie.title
+      xml = described_class.build(user, request) do |builder|
+        builder.status_code = status_code
+      end.to_xml.gsub(status_code, 'TAMPERED')
+      subject = described_class.new(xml)
+      expect(subject).to be_invalid
+    end
+
+    it 'is invalid when not a Response' do
+      allow(registry).to receive(:metadata_for).and_return(nil)
+      subject = described_class.new(Saml::Kit::IdentityProviderMetadata.build.to_xml)
+      expect(subject).to be_invalid
+      expect(subject.errors[:base]).to include(subject.error_message(: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(subject).to be_invalid
+      expect(subject.errors[:fingerprint]).to be_present
+    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)
+      id = Xml::Kit::Id.generate
+      key_pair = ::Xml::Kit::KeyPair.generate(use: :signing)
+      signed_xml = ::Xml::Kit::Signatures.sign(key_pair: key_pair) do |xml, signature|
+        xml.tag! 'samlp:Response', 'xmlns:samlp' => Saml::Kit::Namespaces::PROTOCOL, ID: id do
+          signature.template(id)
+          xml.Fake do
+            xml.NotAllowed 'Huh?'
+          end
+        end
+      end
+      subject = described_class.new(signed_xml)
+      expect(subject).to be_invalid
+      expect(subject.errors[:base]).to be_present
+    end
+
+    it 'validates the version' do
+      allow(registry).to receive(:metadata_for).and_return(metadata)
+      allow(metadata).to receive(:matches?).and_return(true)
+      subject = described_class.build(user, request) do |builder|
+        builder.version = '1.1'
+      end
+      expect(subject).to be_invalid
+      expect(subject.errors[:version]).to be_present
+    end
+
+    it 'validates the id' do
+      allow(registry).to receive(:metadata_for).and_return(metadata)
+      allow(metadata).to receive(:matches?).and_return(true)
+      subject = described_class.build(user, request) do |builder|
+        builder.id = nil
+      end
+      expect(subject).to be_invalid
+      expect(subject.errors[:id]).to be_present
+    end
+
+    it 'validates the status code' do
+      allow(registry).to receive(:metadata_for).and_return(metadata)
+      allow(metadata).to receive(:matches?).and_return(true)
+      subject = described_class.build(user, request) do |builder|
+        builder.status_code = Saml::Kit::Namespaces::REQUESTER_ERROR
+      end
+      expect(subject).to be_invalid
+      expect(subject.errors[:status_code]).to be_present
+    end
+
+    it 'validates the InResponseTo' do
+      allow(registry).to receive(:metadata_for).and_return(metadata)
+      allow(metadata).to receive(:matches?).and_return(true)
+      xml = described_class.build(user, request).to_xml
+      subject = described_class.new(xml, request_id: SecureRandom.uuid)
+
+      expect(subject).to be_invalid
+      expect(subject.errors[:in_response_to]).to be_present
+    end
+
+    it 'is invalid after a valid session window' do
+      allow(registry).to receive(:metadata_for).and_return(metadata)
+      allow(metadata).to receive(:matches?).and_return(true)
+
+      subject = described_class.build(user, request)
+      travel_to Saml::Kit.configuration.session_timeout.from_now + 5.seconds
+      expect(subject).not_to be_valid
+      expect(subject.errors[:assertion]).to match_array(['must not be expired.'])
+    end
+
+    it 'is invalid before the valid session window' do
+      allow(registry).to receive(:metadata_for).and_return(metadata)
+      allow(metadata).to receive(:matches?).and_return(true)
+
+      subject = described_class.build(user, request)
+      travel_to((Saml::Kit.configuration.clock_drift + 1.second).before(Time.now))
+      expect(subject).to be_invalid
+      expect(subject.errors[:assertion]).to match_array(['must not be expired.'])
+    end
+
+    it 'is invalid when the audience does not match the expected issuer' do
+      allow(registry).to receive(:metadata_for).and_return(metadata)
+      allow(metadata).to receive(:matches?).and_return(true)
+
+      allow(configuration).to receive(:issuer).and_return(FFaker::Internet.uri('https'))
+      allow(request).to receive(:issuer).and_return(FFaker::Internet.uri('https'))
+
+      expect(subject).to be_invalid
+      expect(subject.errors[:audience]).to be_present
+    end
+
+    it 'is invalid' do
+      now = Time.now.utc
+      destination = FFaker::Internet.uri('https')
+      raw_xml = <<-XML.strip_heredoc
+        <?xml version="1.0"?>
+        <samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{Xml::Kit::Id.generate}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{destination}" Consent="#{Saml::Kit::Namespaces::UNSPECIFIED}" InResponseTo="#{request.id}">
+          <Issuer xmlns="#{Saml::Kit::Namespaces::ASSERTION}">#{request.issuer}</Issuer>
+          <samlp:Status>
+            <samlp:StatusCode Value="#{Saml::Kit::Namespaces::RESPONDER_ERROR}"/>
+          </samlp:Status>
+        </samlp:Response>
+      XML
+
+      allow(registry).to receive(:metadata_for).with(request.issuer).and_return(metadata)
+      subject = described_class.new(raw_xml)
+      expect(subject).to be_invalid
+    end
+
+    it 'is invalid when there are 2 assertions' do
+      id = Xml::Kit::Id.generate
+      issuer = FFaker::Internet.uri('https')
+      key_pair = ::Xml::Kit::KeyPair.generate(use: :signing)
+      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: Xml::Kit::Id.generate,
+        IssueInstant: Time.now.iso8601,
+        Version: '2.0',
+        xmlns: Saml::Kit::Namespaces::ASSERTION,
+      }
+      raw_xml = ::Xml::Kit::Signatures.sign(key_pair: key_pair) 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: Xml::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(raw_xml)
+      expect(subject).not_to be_valid
+      expect(subject.errors.full_messages).to include('must contain a single Assertion.')
+    end
+
+    it 'is invalid when the assertion has a signature and has been tampered with' do
+      user = User.new(attributes: { token: SecureRandom.uuid })
+      request = Saml::Kit::AuthenticationRequest.build
+      document = described_class.build(user, request, configuration: configuration) do |x|
+        x.embed_signature = false
+        x.assertion.embed_signature = true
+      end
+
+      altered_xml = document.to_xml.gsub(/token/, 'heck')
+      subject = described_class.new(altered_xml)
+      expect(subject).not_to be_valid
+      expect(subject.errors[:digest_value]).to be_present
+    end
+
+    it 'is invalid when we do not have a private key to decrypt the assertion' do
+      xml = described_class.build_xml(user, request) do |x|
+        x.encrypt_with(::Xml::Kit::KeyPair.generate(use: :encryption))
+      end
+
+      subject = described_class.new(xml)
+      expect(subject).to be_invalid
+      expect(subject.errors[:assertion]).to match_array(['cannot be decrypted.'])
+    end
+  end
+
+  describe '#signed?' do
+    let(:now) { Time.now.utc }
+    let(:id) { Xml::Kit::Id.generate }
+    let(:url) { FFaker::Internet.uri('https') }
+
+    it 'returns true when the Assertion is signed' do
+      xml = <<-XML.strip_heredoc
+        <?xml version="1.0"?>
+        <samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
+          <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0">
+            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+              <ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+                <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+                <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+                <ds:Reference URI="##{id}">
+                  <ds:Transforms>
+                    <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
+                    <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+                  </ds:Transforms>
+                  <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+                  <ds:DigestValue></ds:DigestValue>
+                </ds:Reference>
+              </ds:SignedInfo>
+              <ds:SignatureValue></ds:SignatureValue>
+              <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+                <ds:X509Data>
+                  <ds:X509Certificate></ds:X509Certificate>
+                </ds:X509Data>
+              </KeyInfo>
+            </ds:Signature>
+          </Assertion>
+        </samlp:Response>
+      XML
+      subject = described_class.new(xml)
+      expect(subject).not_to be_signed
+      expect(subject.assertion).to be_signed
+    end
+
+    it 'returns true when the Response is signed' do
+      xml = <<-XML.strip_heredoc
+        <?xml version="1.0"?>
+        <samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
+          <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+            <ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+              <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+              <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+              <ds:Reference URI="##{id}">
+                <ds:Transforms>
+                  <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
+                  <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+                </ds:Transforms>
+                <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+                <ds:DigestValue></ds:DigestValue>
+              </ds:Reference>
+            </ds:SignedInfo>
+            <ds:SignatureValue></ds:SignatureValue>
+            <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+              <ds:X509Data>
+                <ds:X509Certificate></ds:X509Certificate>
+              </ds:X509Data>
+            </KeyInfo>
+          </ds:Signature>
+          <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
+        </samlp:Response>
+      XML
+      subject = described_class.new(xml)
+      expect(subject).to be_signed
+    end
+
+    it 'returns false when there is no signature' do
+      xml = <<-XML.strip_heredoc
+        <?xml version="1.0"?>
+        <samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
+          <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
+        </samlp:Response>
+      XML
+      subject = described_class.new(xml)
+      expect(subject).not_to be_signed
+    end
+  end
+
+  describe '#certificate' do
+    let(:now) { Time.now.utc }
+    let(:id) { Xml::Kit::Id.generate }
+    let(:url) { FFaker::Internet.uri('https') }
+    let(:certificate) do
+      ::Xml::Kit::Certificate.new(
+        ::Xml::Kit::SelfSignedCertificate.new.create(passphrase: 'password')[0],
+        use: :signing
+      )
+    end
+
+    it 'returns the certificate when the Assertion is signed' do
+      xml = <<-XML.strip_heredoc
+        <?xml version="1.0"?>
+        <samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
+          <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0">
+            <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+              <ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+                <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+                <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+                <ds:Reference URI="##{id}">
+                  <ds:Transforms>
+                    <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
+                    <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+                  </ds:Transforms>
+                  <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+                  <ds:DigestValue></ds:DigestValue>
+                </ds:Reference>
+              </ds:SignedInfo>
+              <ds:SignatureValue></ds:SignatureValue>
+              <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+                <ds:X509Data>
+                  <ds:X509Certificate>#{certificate.stripped}</ds:X509Certificate>
+                </ds:X509Data>
+              </KeyInfo>
+            </ds:Signature>
+          </Assertion>
+        </samlp:Response>
+      XML
+      subject = described_class.new(xml)
+      expect(subject.signature).not_to be_present
+      expect(subject.assertion.signature).to be_present
+      expect(subject.assertion.signature.certificate.stripped).to eql(certificate.stripped)
+    end
+
+    it 'returns the certificate when the Response is signed' do
+      xml = <<-XML.strip_heredoc
+        <?xml version="1.0"?>
+        <samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
+          <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+            <ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+              <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+              <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+              <ds:Reference URI="##{id}">
+                <ds:Transforms>
+                  <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
+                  <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+                </ds:Transforms>
+                <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+                <ds:DigestValue></ds:DigestValue>
+              </ds:Reference>
+            </ds:SignedInfo>
+            <ds:SignatureValue></ds:SignatureValue>
+            <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+              <ds:X509Data>
+                <ds:X509Certificate>#{certificate}</ds:X509Certificate>
+              </ds:X509Data>
+            </KeyInfo>
+          </ds:Signature>
+          <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
+        </samlp:Response>
+      XML
+      subject = described_class.new(xml)
+      expect(subject.signature.certificate).to eql(certificate)
+    end
+
+    it 'returns nil when there is no signature' do
+      xml = <<-XML.strip_heredoc
+        <?xml version="1.0"?>
+        <samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
+          <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
+        </samlp:Response>
+      XML
+      subject = described_class.new(xml)
+      expect(subject.signature).not_to be_present
+    end
+  end
+
+  describe 'encrypted assertion' do
+    let(:id) { Xml::Kit::Id.generate }
+    let(:now) { Time.now.utc }
+    let(:assertion_consumer_service_url) { FFaker::Internet.uri('https') }
+    let(:password) { FFaker::Movie.title }
+    let(:email) { FFaker::Internet.email }
+    let(:created_at) { Time.now }
+    let(:assertion) do
+      <<-XML.strip_heredoc
+        <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="2017-11-23T04:33:58Z" Version="2.0">
+         <Issuer>#{FFaker::Internet.uri('https')}</Issuer>
+         <Subject>
+           <NameID Format="#{Saml::Kit::Namespaces::PERSISTENT}">#{SecureRandom.uuid}</NameID>
+           <SubjectConfirmation Method="#{Saml::Kit::Namespaces::BEARER}">
+             <SubjectConfirmationData InResponseTo="#{SecureRandom.uuid}" NotOnOrAfter="2017-11-23T07:33:58Z" Recipient="https://westyundt.ca/acs"/>
+           </SubjectConfirmation>
+         </Subject>
+         <Conditions NotBefore="2017-11-23T04:33:58Z" NotOnOrAfter="2017-11-23T07:33:58Z">
+           <AudienceRestriction>
+             <Audience>American Wolves</Audience>
+           </AudienceRestriction>
+         </Conditions>
+         <AuthnStatement AuthnInstant="2017-11-23T04:33:58Z" SessionIndex="_11d39a7f-1b86-43ed-90d7-68090a857ca8" SessionNotOnOrAfter="2017-11-23T07:33:58Z">
+           <AuthnContext>
+             <AuthnContextClassRef>#{Saml::Kit::Namespaces::PASSWORD}</AuthnContextClassRef>
+           </AuthnContext>
+         </AuthnStatement>
+         <AttributeStatement>
+           <Attribute Name="email" NameFormat="#{Saml::Kit::Namespaces::URI}">
+             <AttributeValue>#{email}</AttributeValue>
+           </Attribute>
+           <Attribute Name="created_at" NameFormat="#{Saml::Kit::Namespaces::URI}">
+             <AttributeValue>#{created_at.iso8601}</AttributeValue>
+           </Attribute>
+         </AttributeStatement>
+        </Assertion>
+XML
+    end
+
+    it 'parses the encrypted assertion' do
+      certificate_pem, private_key_pem = ::Xml::Kit::SelfSignedCertificate.new.create(passphrase: password)
+      public_key = OpenSSL::X509::Certificate.new(certificate_pem).public_key
+      private_key = OpenSSL::PKey::RSA.new(private_key_pem, password)
+
+      allow(Saml::Kit.configuration).to receive(:private_keys).with(use: :encryption).and_return([private_key])
+
+      cipher = OpenSSL::Cipher.new('AES-128-CBC')
+      cipher.encrypt
+      key = cipher.random_key
+      iv = cipher.random_iv
+      encrypted = cipher.update(assertion) + cipher.final
+
+      xml = <<-XML.strip_heredoc
+        <samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" xmlns:saml="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{assertion_consumer_service_url}" InResponseTo="#{Xml::Kit::Id.generate}">
+          <saml:Issuer>#{FFaker::Internet.uri('https')}</saml:Issuer>
+          <samlp:Status>
+            <samlp:StatusCode Value="#{Saml::Kit::Namespaces::SUCCESS}"/>
+          </samlp:Status>
+          <saml:EncryptedAssertion>
+            <xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
+            <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
+            <dsig:KeyInfo xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
+              <xenc:EncryptedKey>
+                <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
+                <xenc:CipherData>
+                  <xenc:CipherValue>#{Base64.encode64(public_key.public_encrypt(key))}</xenc:CipherValue>
+                </xenc:CipherData>
+              </xenc:EncryptedKey>
+            </dsig:KeyInfo>
+            <xenc:CipherData>
+              <xenc:CipherValue>#{Base64.encode64(iv + encrypted)}</xenc:CipherValue>
+            </xenc:CipherData>
+            </xenc:EncryptedData>
+          </saml:EncryptedAssertion>
+        </samlp:Response>
+XML
+
+      subject = described_class.new(xml)
+      expect(subject.attributes).to match_array([
+        ['created_at', created_at.iso8601],
+        ['email', email]
+      ])
+    end
+  end
+
+  describe 'parsing' do
+    let(:user) { User.new(attributes: attributes) }
+    let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: Xml::Kit::Id.generate, signed?: true, trusted?: true, provider: nil, assertion_consumer_service_url: FFaker::Internet.uri('https'), name_id_format: '', issuer: FFaker::Internet.uri('https')) }
+    let(:attributes) { { name: 'mo' } }
+
+    it 'returns the name id' do
+      subject = described_class.build(user, request)
+      expect(subject.name_id).to eql(user.name_id)
+    end
+
+    it 'returns the single attributes' do
+      subject = described_class.build(user, request)
+      expect(subject.attributes).to eql('name' => 'mo')
+    end
+
+    it 'returns the multiple attributes' do
+      attributes[:age] = 33
+      subject = described_class.build(user, request)
+      expect(subject.attributes).to eql('name' => 'mo', 'age' => '33')
+    end
+  end
+end
spec/saml/service_provider_metadata_spec.rb → spec/saml/kit/service_provider_metadata_spec.rb
@@ -1,9 +1,9 @@
 RSpec.describe Saml::Kit::ServiceProviderMetadata do
-  let(:entity_id) { FFaker::Internet.uri("https") }
-  let(:acs_post_url) { FFaker::Internet.uri("https") }
-  let(:acs_redirect_url) { FFaker::Internet.uri("https") }
-  let(:logout_post_url) { FFaker::Internet.uri("https") }
-  let(:logout_redirect_url) { FFaker::Internet.uri("https") }
+  let(:entity_id) { FFaker::Internet.uri('https') }
+  let(:acs_post_url) { FFaker::Internet.uri('https') }
+  let(:acs_redirect_url) { FFaker::Internet.uri('https') }
+  let(:logout_post_url) { FFaker::Internet.uri('https') }
+  let(:logout_redirect_url) { FFaker::Internet.uri('https') }
 
   describe described_class do
     subject do
@@ -48,7 +48,7 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
     end
   end
 
-  describe "#validate" do
+  describe '#validate' do
     let(:service_provider_metadata) do
       described_class.build(configuration: configuration) do |builder|
         builder.entity_id = entity_id
@@ -69,9 +69,9 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
     end
 
     it 'is invalid, when given identity provider metadata' do
-      subject = described_class.new(IO.read("spec/fixtures/metadata/okta.xml"))
+      subject = described_class.new(IO.read('spec/fixtures/metadata/okta.xml'))
       expect(subject).to be_invalid
-      expect(subject.errors[:base]).to include(I18n.translate("saml/kit.errors.SPSSODescriptor.invalid"))
+      expect(subject.errors[:base]).to include(I18n.translate('saml/kit.errors.SPSSODescriptor.invalid'))
     end
 
     it 'is invalid, when the metadata is nil' do
@@ -89,7 +89,7 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
         end
       end
       subject = described_class.new(xml.target!)
-      expect(subject).to_not be_valid
+      expect(subject).not_to be_valid
       expect(subject.errors[:base][0]).to include("1:0: ERROR: Element '{urn:oasis:names:tc:SAML:2.0:metadata}EntityDescriptor'")
     end
 
@@ -98,46 +98,47 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
       metadata_xml = service_provider_metadata.gsub(acs_post_url, new_url)
       subject = described_class.new(metadata_xml)
       expect(subject).to be_invalid
-      expect(subject.errors[:base]).to include("invalid signature.")
+      expect(subject.errors[:digest_value]).to include('is invalid.')
     end
 
     it 'is invalid when 0 ACS endpoints are specified' do
-      xml = <<-XML
-<?xml version="1.0" encoding="UTF-8"?>
-<EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="#{::Xml::Kit::Id.generate}" entityID="#{entity_id}">
-  <SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
-    <SingleLogoutService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}"/>
-    <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
-  </SPSSODescriptor>
-</EntityDescriptor>
+      xml = <<-XML.strip_heredoc
+        <?xml version="1.0" encoding="UTF-8"?>
+        <EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="#{::Xml::Kit::Id.generate}" entityID="#{entity_id}">
+          <SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
+            <SingleLogoutService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri('https')}"/>
+            <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
+          </SPSSODescriptor>
+        </EntityDescriptor>
       XML
       expect(described_class.new(xml)).to be_invalid
     end
   end
 
-  describe "#matches?" do
+  describe '#matches?' do
+    subject { described_class.build(configuration: configuration) }
+
     let(:configuration) do
       config = Saml::Kit::Configuration.new
       config.generate_key_pair_for(use: :signing)
       config
     end
-    subject { Saml::Kit::ServiceProviderMetadata.build(configuration: configuration) }
 
     it 'returns true when the fingerprint matches one of the signing certificates' do
       certificate = Hash.from_xml(subject.to_xml)['EntityDescriptor']['Signature']['KeyInfo']['X509Data']['X509Certificate']
       fingerprint = ::Xml::Kit::Fingerprint.new(certificate)
-      expect(subject.matches?(fingerprint)).to be_truthy
+      expect(subject).to be_matches(fingerprint)
     end
 
     it 'returns false when the fingerprint does not match one of the signing certificates' do
-      certificate, _ = ::Xml::Kit::SelfSignedCertificate.new.create(passphrase: 'password')
+      certificate, = ::Xml::Kit::SelfSignedCertificate.new.create(passphrase: 'password')
       fingerprint = ::Xml::Kit::Fingerprint.new(certificate)
-      expect(subject.matches?(fingerprint)).to be_falsey
+      expect(subject).not_to be_matches(fingerprint)
     end
   end
 
-  describe ".build" do
-    let(:assertion_consumer_service_url) { FFaker::Internet.uri("https") }
+  describe '.build' do
+    let(:assertion_consumer_service_url) { FFaker::Internet.uri('https') }
 
     it 'provides a nice API for building metadata' do
       result = described_class.build do |builder|
spec/saml/kit/signature_spec.rb
@@ -0,0 +1,98 @@
+RSpec.describe Saml::Kit::Signature do
+  subject { described_class.new(signed_document.at_xpath('//ds:Signature')) }
+
+  let(:key_pair) { ::Xml::Kit::KeyPair.generate(use: :signing) }
+  let(:signed_document) do
+    Saml::Kit::AuthenticationRequest.build do |x|
+      x.sign_with(key_pair)
+    end
+  end
+  let(:xml_hash) { Hash.from_xml(subject.to_xml) }
+
+  specify { expect(subject.digest_value).to eql(xml_hash['Signature']['SignedInfo']['Reference']['DigestValue']) }
+  specify { expect(subject.digest_method).to eql(xml_hash['Signature']['SignedInfo']['Reference']['DigestMethod']['Algorithm']) }
+  specify { expect(subject.signature_value).to eql(xml_hash['Signature']['SignatureValue']) }
+  specify { expect(subject.signature_method).to eql(xml_hash['Signature']['SignedInfo']['SignatureMethod']['Algorithm']) }
+  specify { expect(subject.canonicalization_method).to eql(xml_hash['Signature']['SignedInfo']['CanonicalizationMethod']['Algorithm']) }
+  specify { expect(subject.transforms).to eql(xml_hash['Signature']['SignedInfo']['Reference']['Transforms']['Transform'].map { |x| x['Algorithm'] }) }
+  specify do
+    expected = ::Xml::Kit::Certificate.new(xml_hash['Signature']['KeyInfo']['X509Data']['X509Certificate'], use: :signing)
+    expect(subject.certificate).to eql(expected)
+  end
+
+  describe '#valid?' do
+    it 'returns true when the signature is valid' do
+      expect(subject).to be_valid
+    end
+
+    it 'is invalid when the xml has been tampered' do
+      signed_document.at_xpath('//saml:Issuer').content = 'INVALID'
+      expect(subject).not_to be_valid
+      expect(subject.errors[:digest_value]).to be_present
+    end
+
+    it 'is invalid when the signature is missing' do
+      subject = described_class.new(nil)
+      expect(subject).not_to be_valid
+      expect(subject.errors[:base]).to match_array(['is missing.'])
+    end
+
+    describe 'certificate validation' do
+      let(:key_pair) { ::Xml::Kit::KeyPair.new(expired_certificate, private_key, nil, :signing) }
+      let(:private_key) { OpenSSL::PKey::RSA.new(2048) }
+      let(:expired_certificate) do
+        certificate = OpenSSL::X509::Certificate.new
+        certificate.not_before = not_before
+        certificate.not_after = not_after
+        certificate.public_key = private_key.public_key
+        certificate.sign(private_key, OpenSSL::Digest::SHA256.new)
+        certificate
+      end
+
+      context 'when the certificate is expired' do
+        let(:not_before) { 10.minutes.ago }
+        let(:not_after) { 1.minute.ago }
+
+        it 'is invalid' do
+          expect(subject).to be_invalid
+          expect(subject.errors[:certificate]).to match_array([
+            "Not valid before #{expired_certificate.not_before}. Not valid after #{expired_certificate.not_after}."
+          ])
+        end
+      end
+
+      context 'when the certificate is not active yet' do
+        let(:not_before) { 10.minutes.from_now }
+        let(:not_after) { 20.minute.from_now }
+
+        it 'invalid' do
+          expect(subject).to be_invalid
+          expect(subject.errors[:certificate]).to match_array([
+            "Not valid before #{expired_certificate.not_before}. Not valid after #{expired_certificate.not_after}."
+          ])
+        end
+      end
+    end
+  end
+
+  describe '#to_h' do
+    it 'returns a hash representation of the signature' do
+      expected = Hash.from_xml(signed_document.to_s)['AuthnRequest']['Signature']
+      expect(subject.to_h).to eql(expected)
+    end
+  end
+
+  describe '#present?' do
+    context 'when a signature is not present' do
+      it 'return false' do
+        expect(described_class.new(nil)).not_to be_present
+      end
+    end
+
+    context 'when a signature is present' do
+      it 'returns true' do
+        expect(subject).to be_present
+      end
+    end
+  end
+end
spec/saml/assertion_spec.rb
@@ -1,63 +0,0 @@
-RSpec.describe Saml::Kit::Assertion do
-  describe "#active?" do
-    let(:configuration) do
-      Saml::Kit::Configuration.new do |config|
-        config.session_timeout = 30.minutes
-        config.clock_drift = 30.seconds
-      end
-    end
-
-    it 'is valid after a valid session window + drift' do
-      now = Time.current
-      travel_to now
-      xml_hash = {
-        'Response' => {
-          'Assertion' => {
-            'Conditions' => {
-              'NotBefore' => now.utc.iso8601,
-              'NotOnOrAfter' => configuration.session_timeout.since(now).iso8601,
-            }
-          }
-        }
-      }
-      subject = described_class.new(xml_hash, configuration: configuration)
-      travel_to (configuration.clock_drift - 1.second).before(now)
-      expect(subject).to be_active
-      expect(subject).to_not be_expired
-    end
-
-    it 'interprets integers correctly' do
-      configuration.clock_drift = 30
-      now = Time.current
-      travel_to now
-      xml_hash = {
-        'Response' => {
-          'Assertion' => {
-            'Conditions' => {
-              'NotBefore' => now.utc.iso8601,
-              'NotOnOrAfter' => configuration.session_timeout.since(now).iso8601,
-            }
-          }
-        }
-      }
-
-      subject = described_class.new(xml_hash, configuration: configuration)
-      expect(subject).to be_active
-      expect(subject).to_not be_expired
-    end
-  end
-
-  describe "#present?" do
-    it 'returns false when the assertion is empty' do
-      xml_hash = { 'Response' => { } }
-      subject = described_class.new(xml_hash)
-      expect(subject).to_not be_present
-    end
-
-    it 'returns true when the assertion is present' do
-      xml_hash = { 'Response' => { 'Assertion' => { 'Conditions' => { } } } }
-      subject = described_class.new(xml_hash)
-      expect(subject).to be_present
-    end
-  end
-end
spec/saml/default_registry_spec.rb
@@ -1,93 +0,0 @@
-RSpec.describe Saml::Kit::DefaultRegistry do
-  subject { described_class.new }
-  let(:entity_id) { FFaker::Internet.http_url }
-  let(:service_provider_metadata) do
-    Saml::Kit::ServiceProviderMetadata.build do |builder|
-      builder.entity_id = entity_id
-    end
-  end
-  let(:identity_provider_metadata) do
-    Saml::Kit::IdentityProviderMetadata.build do |builder|
-      builder.entity_id = entity_id
-    end
-  end
-
-  describe "#metadata_for" do
-    it 'returns the metadata for the entity_id' do
-      subject.register(service_provider_metadata)
-      expect(subject.metadata_for(entity_id)).to eql(service_provider_metadata)
-    end
-  end
-
-  describe "#register_url" do
-    let(:url) { FFaker::Internet.http_url }
-
-    it 'fetches the SP metadata from a remote url and registers it' do
-      stub_request(:get, url).
-        to_return(status: 200, body: service_provider_metadata.to_xml)
-      subject.register_url(url)
-
-      result = subject.metadata_for(entity_id)
-      expect(result).to be_present
-      expect(result).to be_instance_of(Saml::Kit::ServiceProviderMetadata)
-    end
-
-    it 'fetches the IDP metadata from a remote url' do
-      stub_request(:get, url).
-        to_return(status: 200, body: identity_provider_metadata.to_xml)
-      subject.register_url(url)
-
-      result = subject.metadata_for(entity_id)
-      expect(result).to be_present
-      expect(result).to be_instance_of(Saml::Kit::IdentityProviderMetadata)
-    end
-
-    it 'registers metadata that serves as both an IDP and SP' do
-      xml = <<-XML
-<EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="#{::Xml::Kit::Id.generate}" entityID="#{entity_id}">
-  <SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
-    <SingleLogoutService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}"/>
-    <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
-    <AssertionConsumerService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}" index="0" isDefault="true"/>
-  </SPSSODescriptor>
-  <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
-    <SingleLogoutService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}"/>
-    <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
-    <SingleSignOnService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}"/>
-    <SingleSignOnService Binding="#{Saml::Kit::Bindings::HTTP_REDIRECT}" Location="#{FFaker::Internet.uri("https")}"/>
-  </IDPSSODescriptor>
-  <Organization>
-    <OrganizationName xml:lang="en">Acme, Inc</OrganizationName>
-    <OrganizationDisplayName xml:lang="en">Acme, Inc</OrganizationDisplayName>
-    <OrganizationURL xml:lang="en">http://localhost:5000/</OrganizationURL>
-  </Organization>
-  <ContactPerson contactType="technical">
-    <Company>mailto:hi@example.com</Company>
-  </ContactPerson>
-</EntityDescriptor>
-      XML
-      stub_request(:get, url).to_return(status: 200, body: xml)
-      subject.register_url(url)
-
-      result = subject.metadata_for(entity_id)
-      expect(result).to be_present
-      expect(result).to be_instance_of(Saml::Kit::CompositeMetadata)
-    end
-  end
-
-  describe "#each" do
-    it 'yields each registered metadata' do
-      idp = Saml::Kit::IdentityProviderMetadata.build do |config|
-        config.entity_id = "idp"
-      end
-      sp = Saml::Kit::ServiceProviderMetadata.build do |config|
-        config.entity_id = "sp"
-      end
-
-      subject.register(idp)
-      subject.register(sp)
-
-      expect(subject.map(&:to_xml)).to match_array([idp.to_xml, sp.to_xml])
-    end
-  end
-end
spec/saml/logout_response_spec.rb
@@ -1,2 +0,0 @@
-RSpec.describe Saml::Kit::LogoutResponse do
-end
spec/saml/metadata_spec.rb
@@ -1,65 +0,0 @@
-RSpec.describe Saml::Kit::Metadata do
-  describe ".from" do
-    subject { described_class }
-
-    it 'returns an identity provider metadata' do
-      xml = Saml::Kit::IdentityProviderMetadata.build.to_xml
-      expect(subject.from(xml)).to be_instance_of(Saml::Kit::IdentityProviderMetadata)
-    end
-
-    it 'returns a service provider metadata' do
-      xml = Saml::Kit::ServiceProviderMetadata.build.to_xml
-      expect(subject.from(xml)).to be_instance_of(Saml::Kit::ServiceProviderMetadata)
-    end
-
-    it 'returns a composite' do
-      xml = <<-XML
-<EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="#{Xml::Kit::Id.generate}" entityID="#{FFaker::Internet.uri("https")}">
-  <SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
-    <SingleLogoutService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}"/>
-    <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
-    <AssertionConsumerService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}" index="0" isDefault="true"/>
-  </SPSSODescriptor>
-  <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
-    <SingleLogoutService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}"/>
-    <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
-    <SingleSignOnService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}"/>
-    <SingleSignOnService Binding="#{Saml::Kit::Bindings::HTTP_REDIRECT}" Location="#{FFaker::Internet.uri("https")}"/>
-  </IDPSSODescriptor>
-  <Organization>
-    <OrganizationName xml:lang="en">Acme, Inc</OrganizationName>
-    <OrganizationDisplayName xml:lang="en">Acme, Inc</OrganizationDisplayName>
-    <OrganizationURL xml:lang="en">http://localhost:5000/</OrganizationURL>
-  </Organization>
-  <ContactPerson contactType="technical">
-    <Company>mailto:hi@example.com</Company>
-  </ContactPerson>
-</EntityDescriptor>
-      XML
-      result = subject.from(xml)
-      expect(result).to be_present
-
-      expect(result.single_sign_on_services.count).to eql(2)
-      expect(result.assertion_consumer_services.count).to eql(1)
-      expect(result.single_logout_services.count).to eql(2)
-      expect(result.organization_name).to eql("Acme, Inc")
-      expect(result.organization_url).to eql("http://localhost:5000/")
-      expect(result.contact_person_company).to eql("mailto:hi@example.com")
-    end
-  end
-
-  describe "#certificates" do
-    it 'returns each certificate when missing a "use"' do
-      configuration = Saml::Kit::Configuration.new do |config|
-        config.generate_key_pair_for(use: :signing)
-      end
-      xml = Saml::Kit::Metadata.build_xml(configuration: configuration) do |x|
-        x.embed_signature = false
-        x.build_identity_provider
-      end
-      modified_xml = xml.gsub(/use/, 'misuse')
-      subject = described_class.from(modified_xml)
-      expect(subject.certificates.count).to eql(1)
-    end
-  end
-end
spec/saml/response_spec.rb
@@ -1,523 +0,0 @@
-RSpec.describe Saml::Kit::Response do
-  describe "#valid?" do
-    let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: ::Xml::Kit::Id.generate, issuer: FFaker::Internet.http_url, assertion_consumer_service_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 }) }
-    let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
-    let(:metadata) { instance_double(Saml::Kit::IdentityProviderMetadata) }
-    subject { described_class.build(user, request, configuration: configuration) }
-    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 'is valid' do
-      allow(registry).to receive(:metadata_for).and_return(metadata)
-      allow(metadata).to receive(:matches?).and_return(true)
-      expect(subject).to be_valid
-    end
-
-    it 'is invalid when blank' do
-      allow(registry).to receive(:metadata_for).and_return(nil)
-      subject = described_class.new("")
-      expect(subject).to be_invalid
-      expect(subject.errors[:content]).to be_present
-    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)
-      status_code = FFaker::Movie.title
-      xml = described_class.build(user, request) do |builder|
-        builder.status_code = status_code
-      end.to_xml.gsub(status_code, "TAMPERED")
-      subject = described_class.new(xml)
-      expect(subject).to be_invalid
-    end
-
-    it 'is invalid when not a Response' do
-      allow(registry).to receive(:metadata_for).and_return(nil)
-      subject = described_class.new(Saml::Kit::IdentityProviderMetadata.build.to_xml)
-      expect(subject).to be_invalid
-      expect(subject.errors[:base]).to include(subject.error_message(: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(subject).to be_invalid
-      expect(subject.errors[:fingerprint]).to be_present
-    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)
-      id = Xml::Kit::Id.generate
-      key_pair = ::Xml::Kit::KeyPair.generate(use: :signing)
-      signed_xml = ::Xml::Kit::Signatures.sign(key_pair: key_pair) do |xml, signature|
-        xml.tag! "samlp:Response", "xmlns:samlp" => Saml::Kit::Namespaces::PROTOCOL, ID: id do
-          signature.template(id)
-          xml.Fake do
-            xml.NotAllowed "Huh?"
-          end
-        end
-      end
-      subject = described_class.new(signed_xml)
-      expect(subject).to be_invalid
-      expect(subject.errors[:base]).to be_present
-    end
-
-    it 'validates the version' do
-      allow(registry).to receive(:metadata_for).and_return(metadata)
-      allow(metadata).to receive(:matches?).and_return(true)
-      subject = described_class.build(user, request) do |builder|
-        builder.version = "1.1"
-      end
-      expect(subject).to be_invalid
-      expect(subject.errors[:version]).to be_present
-    end
-
-    it 'validates the id' do
-      allow(registry).to receive(:metadata_for).and_return(metadata)
-      allow(metadata).to receive(:matches?).and_return(true)
-      subject = described_class.build(user, request) do |builder|
-        builder.id = nil
-      end
-      expect(subject).to be_invalid
-      expect(subject.errors[:id]).to be_present
-    end
-
-    it 'validates the status code' do
-      allow(registry).to receive(:metadata_for).and_return(metadata)
-      allow(metadata).to receive(:matches?).and_return(true)
-      subject = described_class.build(user, request) do |builder|
-        builder.status_code = Saml::Kit::Namespaces::REQUESTER_ERROR
-      end
-      expect(subject).to be_invalid
-      expect(subject.errors[:status_code]).to be_present
-    end
-
-    it 'validates the InResponseTo' do
-      allow(registry).to receive(:metadata_for).and_return(metadata)
-      allow(metadata).to receive(:matches?).and_return(true)
-      xml = described_class.build(user, request).to_xml
-      subject = described_class.new(xml, request_id: SecureRandom.uuid)
-
-      expect(subject).to be_invalid
-      expect(subject.errors[:in_response_to]).to be_present
-    end
-
-    it 'is invalid after a valid session window' do
-      allow(registry).to receive(:metadata_for).and_return(metadata)
-      allow(metadata).to receive(:matches?).and_return(true)
-
-      subject = described_class.build(user, request)
-      travel_to Saml::Kit.configuration.session_timeout.from_now + 5.seconds
-      expect(subject).to_not be_valid
-      expect(subject.errors[:base]).to be_present
-    end
-
-    it 'is invalid before the valid session window' do
-      allow(registry).to receive(:metadata_for).and_return(metadata)
-      allow(metadata).to receive(:matches?).and_return(true)
-
-      subject = described_class.build(user, request)
-      travel_to (Saml::Kit.configuration.clock_drift + 1.second).before(Time.now)
-      expect(subject).to be_invalid
-      expect(subject.errors[:base]).to be_present
-    end
-
-    it 'is invalid when the audience does not match the expected issuer' do
-      allow(registry).to receive(:metadata_for).and_return(metadata)
-      allow(metadata).to receive(:matches?).and_return(true)
-
-      allow(configuration).to receive(:issuer).and_return(FFaker::Internet.uri("https"))
-      allow(request).to receive(:issuer).and_return(FFaker::Internet.uri("https"))
-
-      expect(subject).to be_invalid
-      expect(subject.errors[:audience]).to be_present
-    end
-
-    it 'is invalid' do
-      now = Time.now.utc
-      destination = FFaker::Internet.uri("https")
-      raw_xml = <<-XML
-<?xml version="1.0"?>
-<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{Xml::Kit::Id.generate}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{destination}" Consent="#{Saml::Kit::Namespaces::UNSPECIFIED}" InResponseTo="#{request.id}">
-  <Issuer xmlns="#{Saml::Kit::Namespaces::ASSERTION}">#{request.issuer}</Issuer>
-  <samlp:Status>
-    <samlp:StatusCode Value="#{Saml::Kit::Namespaces::RESPONDER_ERROR}"/>
-  </samlp:Status>
-</samlp:Response>
-      XML
-
-      allow(registry).to receive(:metadata_for).with(request.issuer).and_return(metadata)
-      subject = described_class.new(raw_xml)
-      expect(subject).to be_invalid
-    end
-
-    it 'is invalid when there are 2 assertions' do
-      id = Xml::Kit::Id.generate
-      issuer = FFaker::Internet.uri("https")
-      key_pair = ::Xml::Kit::KeyPair.generate(use: :signing)
-      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: Xml::Kit::Id.generate,
-        IssueInstant: Time.now.iso8601,
-        Version: "2.0",
-        xmlns: Saml::Kit::Namespaces::ASSERTION,
-      }
-      xml = ::Xml::Kit::Signatures.sign(key_pair: key_pair) 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: Xml::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
-
-    it 'is invalid when the assertion has a signature and has been tampered with' do
-      user = double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: { token: SecureRandom.uuid })
-      request = Saml::Kit::AuthenticationRequest.build
-      document = described_class.build(user, request, configuration: configuration) do |x|
-        x.embed_signature = false
-        x.assertion.embed_signature = true
-      end
-
-      altered_xml = document.to_xml.gsub(/token/, 'heck')
-      subject = described_class.new(altered_xml)
-      expect(subject).to_not be_valid
-      expect(subject.errors[:digest_value]).to be_present
-    end
-  end
-
-  describe "#signed?" do
-    let(:now) { Time.now.utc }
-    let(:id) { Xml::Kit::Id.generate }
-    let(:url) { FFaker::Internet.uri("https") }
-
-    it 'returns true when the Assertion is signed' do
-      xml = <<-XML
-<?xml version="1.0"?>
-<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
-  <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0">
-    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
-      <ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
-        <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
-        <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
-        <ds:Reference URI="##{id}">
-          <ds:Transforms>
-            <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
-            <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
-          </ds:Transforms>
-          <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
-          <ds:DigestValue></ds:DigestValue>
-        </ds:Reference>
-      </ds:SignedInfo>
-      <ds:SignatureValue></ds:SignatureValue>
-      <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
-        <ds:X509Data>
-          <ds:X509Certificate></ds:X509Certificate>
-        </ds:X509Data>
-      </KeyInfo>
-    </ds:Signature>
-  </Assertion>
-</samlp:Response>
-      XML
-      subject = described_class.new(xml)
-      expect(subject).to_not be_signed
-      expect(subject.assertion).to be_signed
-    end
-
-    it 'returns true when the Response is signed' do
-      xml = <<-XML
-<?xml version="1.0"?>
-<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
-  <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
-    <ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
-      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
-      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
-      <ds:Reference URI="##{id}">
-        <ds:Transforms>
-          <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
-          <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
-        </ds:Transforms>
-        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
-        <ds:DigestValue></ds:DigestValue>
-      </ds:Reference>
-    </ds:SignedInfo>
-    <ds:SignatureValue></ds:SignatureValue>
-    <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
-      <ds:X509Data>
-        <ds:X509Certificate></ds:X509Certificate>
-      </ds:X509Data>
-    </KeyInfo>
-  </ds:Signature>
-  <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
-</samlp:Response>
-      XML
-      subject = described_class.new(xml)
-      expect(subject).to be_signed
-    end
-
-    it 'returns false when there is no signature' do
-      xml = <<-XML
-<?xml version="1.0"?>
-<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
-  <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
-</samlp:Response>
-      XML
-      subject = described_class.new(xml)
-      expect(subject).to_not be_signed
-    end
-  end
-
-  describe "#certificate" do
-    let(:now) { Time.now.utc }
-    let(:id) { Xml::Kit::Id.generate }
-    let(:url) { FFaker::Internet.uri("https") }
-    let(:certificate) do
-      ::Xml::Kit::Certificate.new(
-        ::Xml::Kit::SelfSignedCertificate.new.create(passphrase: "password")[0],
-        use: :signing
-      )
-    end
-
-    it 'returns the certificate when the Assertion is signed' do
-      xml = <<-XML
-<?xml version="1.0"?>
-<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
-  <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0">
-    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
-      <ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
-        <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
-        <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
-        <ds:Reference URI="##{id}">
-          <ds:Transforms>
-            <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
-            <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
-          </ds:Transforms>
-          <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
-          <ds:DigestValue></ds:DigestValue>
-        </ds:Reference>
-      </ds:SignedInfo>
-      <ds:SignatureValue></ds:SignatureValue>
-      <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
-        <ds:X509Data>
-          <ds:X509Certificate>#{certificate.stripped}</ds:X509Certificate>
-        </ds:X509Data>
-      </KeyInfo>
-    </ds:Signature>
-  </Assertion>
-</samlp:Response>
-      XML
-      subject = described_class.new(xml)
-      expect(subject.signature).to be_nil
-      expect(subject.assertion.signature).to be_present
-      expect(subject.assertion.signature.certificate.stripped).to eql(certificate.stripped)
-    end
-
-    it 'returns the certificate when the Response is signed' do
-      xml = <<-XML
-<?xml version="1.0"?>
-<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
-  <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
-    <ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
-      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
-      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
-      <ds:Reference URI="##{id}">
-        <ds:Transforms>
-          <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
-          <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
-        </ds:Transforms>
-        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
-        <ds:DigestValue></ds:DigestValue>
-      </ds:Reference>
-    </ds:SignedInfo>
-    <ds:SignatureValue></ds:SignatureValue>
-    <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
-      <ds:X509Data>
-        <ds:X509Certificate>#{certificate}</ds:X509Certificate>
-      </ds:X509Data>
-    </KeyInfo>
-  </ds:Signature>
-  <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
-</samlp:Response>
-      XML
-      subject = described_class.new(xml)
-      expect(subject.signature.certificate).to eql(certificate)
-    end
-
-    it 'returns nil when there is no signature' do
-      xml = <<-XML
-<?xml version="1.0"?>
-<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
-  <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
-</samlp:Response>
-      XML
-      subject = described_class.new(xml)
-      expect(subject.signature).to be_nil
-    end
-  end
-
-  describe "encrypted assertion" do
-    let(:id) { Xml::Kit::Id.generate }
-    let(:now) { Time.now.utc }
-    let(:assertion_consumer_service_url) { FFaker::Internet.uri("https") }
-    let(:password) { FFaker::Movie.title }
-    let(:email) { FFaker::Internet.email }
-    let(:created_at) { DateTime.now }
-    let(:assertion) do
-      <<-XML
-<Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="2017-11-23T04:33:58Z" Version="2.0">
- <Issuer>#{FFaker::Internet.uri("https")}</Issuer>
- <Subject>
-   <NameID Format="#{Saml::Kit::Namespaces::PERSISTENT}">#{SecureRandom.uuid}</NameID>
-   <SubjectConfirmation Method="#{Saml::Kit::Namespaces::BEARER}">
-     <SubjectConfirmationData InResponseTo="#{SecureRandom.uuid}" NotOnOrAfter="2017-11-23T07:33:58Z" Recipient="https://westyundt.ca/acs"/>
-   </SubjectConfirmation>
- </Subject>
- <Conditions NotBefore="2017-11-23T04:33:58Z" NotOnOrAfter="2017-11-23T07:33:58Z">
-   <AudienceRestriction>
-     <Audience>American Wolves</Audience>
-   </AudienceRestriction>
- </Conditions>
- <AuthnStatement AuthnInstant="2017-11-23T04:33:58Z" SessionIndex="_11d39a7f-1b86-43ed-90d7-68090a857ca8" SessionNotOnOrAfter="2017-11-23T07:33:58Z">
-   <AuthnContext>
-     <AuthnContextClassRef>#{Saml::Kit::Namespaces::PASSWORD}</AuthnContextClassRef>
-   </AuthnContext>
- </AuthnStatement>
- <AttributeStatement>
-   <Attribute Name="email" NameFormat="#{Saml::Kit::Namespaces::URI}">
-     <AttributeValue>#{email}</AttributeValue>
-   </Attribute>
-   <Attribute Name="created_at" NameFormat="#{Saml::Kit::Namespaces::URI}">
-     <AttributeValue>#{created_at.iso8601}</AttributeValue>
-   </Attribute>
- </AttributeStatement>
-</Assertion>
-XML
-    end
-
-    it 'parses the encrypted assertion' do
-      certificate_pem, private_key_pem = ::Xml::Kit::SelfSignedCertificate.new.create(passphrase: password)
-      public_key = OpenSSL::X509::Certificate.new(certificate_pem).public_key
-      private_key = OpenSSL::PKey::RSA.new(private_key_pem, password)
-
-      allow(Saml::Kit.configuration).to receive(:private_keys).with(use: :encryption).and_return([private_key])
-
-      cipher = OpenSSL::Cipher.new('AES-128-CBC')
-      cipher.encrypt
-      key = cipher.random_key
-      iv = cipher.random_iv
-      encrypted = cipher.update(assertion) + cipher.final
-
-      xml = <<-XML
-<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" xmlns:saml="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{assertion_consumer_service_url}" InResponseTo="#{Xml::Kit::Id.generate}">
-  <saml:Issuer>#{FFaker::Internet.uri("https")}</saml:Issuer>
-  <samlp:Status>
-    <samlp:StatusCode Value="#{Saml::Kit::Namespaces::SUCCESS}"/>
-  </samlp:Status>
-  <saml:EncryptedAssertion>
-    <xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
-    <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
-    <dsig:KeyInfo xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
-      <xenc:EncryptedKey>
-        <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
-        <xenc:CipherData>
-          <xenc:CipherValue>#{Base64.encode64(public_key.public_encrypt(key))}</xenc:CipherValue>
-        </xenc:CipherData>
-      </xenc:EncryptedKey>
-    </dsig:KeyInfo>
-    <xenc:CipherData>
-      <xenc:CipherValue>#{Base64.encode64(iv + encrypted)}</xenc:CipherValue>
-    </xenc:CipherData>
-    </xenc:EncryptedData>
-  </saml:EncryptedAssertion>
-</samlp:Response>
-XML
-
-      subject = described_class.new(xml)
-      expect(subject.attributes).to match_array([
-        ["created_at", created_at.iso8601],
-        ["email", email]
-      ])
-    end
-  end
-
-  describe "parsing" do
-    let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: attributes) }
-    let(:request) { double(:request, id: Xml::Kit::Id.generate, signed?: true, trusted?: true, provider: nil, assertion_consumer_service_url: FFaker::Internet.uri("https"), name_id_format: '', issuer: FFaker::Internet.uri("https")) }
-    let(:attributes) { { name: 'mo' } }
-
-    it 'returns the name id' do
-      subject = described_class.build(user, request)
-      expect(subject.name_id).to eql(user.name_id_for)
-    end
-
-    it 'returns the single attributes' do
-      subject = described_class.build(user, request)
-      expect(subject.attributes).to eql('name' => 'mo')
-    end
-
-    it 'returns the multiple attributes' do
-      attributes[:age] = 33
-      subject = described_class.build(user, request)
-      expect(subject.attributes).to eql('name' => 'mo', 'age' => '33')
-    end
-  end
-end
spec/saml/signature_spec.rb
@@ -1,22 +0,0 @@
-RSpec.describe Saml::Kit::Signature do
-  describe "#valid?" do
-    let(:key_pair) { ::Xml::Kit::KeyPair.generate(use: :signing) }
-
-    it 'returns true when the signature is valid' do
-      signed_document = Saml::Kit::AuthenticationRequest.build do |x|
-        x.sign_with(key_pair)
-      end
-      subject = described_class.new(Hash.from_xml(signed_document.to_xml))
-      expect(subject).to be_valid
-    end
-
-    xit 'is invalid when the xml has been tampered' do
-      signed_document = Saml::Kit::AuthenticationRequest.build do |x|
-        x.sign_with(key_pair)
-      end
-      tampered_xml = signed_document.to_xml.gsub("Issuer", "Hacked")
-      subject = described_class.new(Hash.from_xml(tampered_xml))
-      expect(subject).to_not be_valid
-    end
-  end
-end
spec/support/rspec_benchmark.rb
@@ -0,0 +1,5 @@
+require 'rspec-benchmark'
+
+RSpec.configure do |config|
+  config.include RSpec::Benchmark::Matchers
+end
spec/support/test_helpers.rb
@@ -1,6 +1,6 @@
 module TestHelpers
   def query_params_from(url)
-    Hash[query_for(url).split("&").map { |x| x.split('=', 2) }]
+    Hash[query_for(url).split('&').map { |x| x.split('=', 2) }]
   end
 
   def uri_for(url)
spec/support/user.rb
@@ -0,0 +1,16 @@
+class User
+  attr_reader :name_id, :attributes
+
+  def initialize(name_id: SecureRandom.uuid, attributes: {})
+    @name_id = name_id
+    @attributes = attributes
+  end
+
+  def name_id_for(_format)
+    name_id
+  end
+
+  def assertion_attributes_for(_request)
+    attributes
+  end
+end
spec/spec_helper.rb
@@ -2,12 +2,12 @@ require 'simplecov'
 SimpleCov.start do
   add_filter '/spec/'
 end
-require "bundler/setup"
-require "saml/kit"
-require "saml/kit/rspec"
-require "active_support/testing/time_helpers"
-require "ffaker"
-require "webmock/rspec"
+require 'bundler/setup'
+require 'saml/kit'
+require 'saml/kit/rspec'
+require 'active_support/testing/time_helpers'
+require 'ffaker'
+require 'webmock/rspec'
 
 Saml::Kit.configuration.logger.level = Xml::Kit.logger.level = Logger::FATAL
 
@@ -15,7 +15,7 @@ Dir[File.join(Dir.pwd, 'spec/support/**/*.rb')].each { |f| require f }
 RSpec.configure do |config|
   config.include ActiveSupport::Testing::TimeHelpers
   # Enable flags like --only-failures and --next-failure
-  config.example_status_persistence_file_path = ".rspec_status"
+  config.example_status_persistence_file_path = '.rspec_status'
 
   # Disable RSpec exposing methods globally on `Module` and `main`
   config.disable_monkey_patching!
@@ -23,7 +23,7 @@ RSpec.configure do |config|
   config.expect_with :rspec do |c|
     c.syntax = :expect
   end
-  config.after :each do
+  config.after do
     travel_back
   end
 end
.gitlab-ci.yml
@@ -5,11 +5,11 @@ before_script:
   - echo "en_US.UTF-8 UTF-8" > /etc/locale.gen
   - locale-gen
   - export LC_ALL=en_US.UTF-8
-  - ruby -v
-  - which ruby
-  - gem install bundler --no-ri --no-rdoc
-  - bundle install --jobs $(nproc) "${FLAGS[@]}"
 
 rspec:
   script:
-    - bundle exec rspec
+    - bin/cibuild
+
+lint:
+  script:
+    - bin/lint
.rubocop.yml
@@ -0,0 +1,92 @@
+inherit_from: .rubocop_todo.yml
+
+require:
+  - rubocop/cop/internal_affairs
+  - rubocop-rspec
+
+AllCops:
+  Exclude:
+    - 'coverage/**/*'
+    - 'pkg/**/*'
+    - 'spec/fixtures/**/*'
+    - 'spec/examples/**/*'
+    - 'tmp/**/*'
+    - 'vendor/**/*'
+  TargetRubyVersion: 2.2
+
+Layout/ClassStructure:
+  Enabled: true
+  Categories:
+    module_inclusion:
+      - include
+      - prepend
+      - extend
+  ExpectedOrder:
+      - module_inclusion
+      - constants
+      - public_class_methods
+      - initializer
+      - instance_methods
+      - protected_methods
+      - private_methods
+
+Layout/EndOfLine:
+  EnforcedStyle: lf
+
+Layout/IndentArray:
+  EnforcedStyle: consistent
+
+Layout/IndentHeredoc:
+  EnforcedStyle: active_support
+
+Lint/AmbiguousBlockAssociation:
+  Exclude:
+    - 'spec/**/*.rb'
+
+Lint/InterpolationCheck:
+  Exclude:
+    - 'spec/**/*.rb'
+
+Metrics/BlockLength:
+  Exclude:
+    - '**/**/*.builder'
+    - '**/*.rake'
+    - '*.gemspec'
+    - 'Rakefile'
+    - 'spec/**/*.rb'
+
+Metrics/ModuleLength:
+  Exclude:
+    - 'spec/**/*.rb'
+
+Metrics/LineLength:
+  Exclude:
+    - 'spec/**/*.rb'
+
+Naming/FileName:
+  Exclude:
+    - 'lib/saml-kit.rb'
+
+Style/Documentation:
+  Enabled: false
+
+Style/StringLiterals:
+  EnforcedStyle: 'single_quotes'
+
+Style/TrailingCommaInLiteral:
+  Enabled: false
+
+RSpec/ExampleLength:
+  Max: 80
+
+RSpec/MultipleExpectations:
+  Enabled: false
+
+RSpec/NamedSubject:
+  Enabled: false
+
+RSpec/NestedGroups:
+  Max: 7
+
+RSpec/SubjectStub:
+  Enabled: false
.rubocop_todo.yml
@@ -0,0 +1,45 @@
+# This configuration was generated by
+# `rubocop --auto-gen-config`
+# on 2018-02-16 22:08:54 -0700 using RuboCop version 0.52.1.
+# The point is for the user to remove these configuration records
+# one by one as the offenses are removed from the code base.
+# Note that changes in the inspected code, or installation of new
+# versions of RuboCop, may require this file to be generated again.
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods.
+Lint/UnusedMethodArgument:
+  Exclude:
+    - 'lib/saml/kit/invalid_document.rb'
+
+# Offense count: 2
+Metrics/AbcSize:
+  Max: 16
+
+# Offense count: 3
+# Configuration parameters: CountComments.
+Metrics/ClassLength:
+  Max: 136
+
+# Offense count: 6
+# Configuration parameters: CountComments.
+Metrics/MethodLength:
+  Max: 13
+
+# Offense count: 1
+Style/DateTime:
+  Exclude:
+    - 'lib/saml/kit/assertion.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/IfUnlessModifier:
+  Exclude:
+    - 'lib/saml/kit/builders/authentication_request.rb'
+
+# Offense count: 128
+# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
+# URISchemes: http, https
+Metrics/LineLength:
+  Max: 313
.travis.yml
@@ -1,6 +1,10 @@
 sudo: false
 language: ruby
 rvm:
-  - 2.4.2
-before_install: gem install bundler -v 1.15.4
-script: "bundle exec rspec"
+  - 2.2.9
+  - 2.3.6
+  - 2.4.3
+  - 2.5.0
+script:
+  - bin/cibuild
+  - bin/lint
Gemfile
@@ -1,6 +1,6 @@
-source "https://rubygems.org"
+source 'https://rubygems.org'
 
-git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
+git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
 
 # Specify your gem's dependencies in saml-kit.gemspec
 gemspec
Rakefile
@@ -1,6 +1,8 @@
-require "bundler/gem_tasks"
-require "rspec/core/rake_task"
+require 'bundler/gem_tasks'
+require 'rspec/core/rake_task'
 
 RSpec::Core::RakeTask.new(:spec)
+task default: :spec
 
-task :default => :spec
+require 'rubocop/rake_task'
+RuboCop::RakeTask.new(:rubocop)
saml-kit.gemspec
@@ -1,34 +1,37 @@
-# coding: utf-8
-lib = File.expand_path("../lib", __FILE__)
+
+lib = File.expand_path('../lib', __FILE__)
 $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
-require "saml/kit/version"
+require 'saml/kit/version'
 
 Gem::Specification.new do |spec|
-  spec.name          = "saml-kit"
+  spec.name          = 'saml-kit'
   spec.version       = Saml::Kit::VERSION
-  spec.authors       = ["mo khan"]
-  spec.email         = ["mo@mokhan.ca"]
+  spec.authors       = ['mo khan']
+  spec.email         = ['mo@mokhan.ca']
 
-  spec.summary       = %q{A simple toolkit for working with SAML.}
-  spec.description   = %q{A simple toolkit for working with SAML.}
-  spec.homepage      = "https://github.com/saml-kit/saml-kit"
-  spec.license       = "MIT"
+  spec.summary       = 'A simple toolkit for working with SAML.'
+  spec.description   = 'A simple toolkit for working with SAML.'
+  spec.homepage      = 'https://github.com/saml-kit/saml-kit'
+  spec.license       = 'MIT'
   spec.required_ruby_version = '>= 2.2.0'
 
-  spec.files         = `git ls-files -z`.split("\x0").reject do |f|
+  spec.files = `git ls-files -z`.split("\x0").reject do |f|
     f.match(%r{^(test|spec|features)/})
   end
-  spec.metadata["yard.run"] = "yri"
-  spec.bindir        = "exe"
+  spec.metadata['yard.run'] = 'yri'
+  spec.bindir        = 'exe'
   spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
-  spec.require_paths = ["lib"]
+  spec.require_paths = ['lib']
 
-  spec.add_dependency "activemodel", ">= 4.2.0"
-  spec.add_dependency "xml-kit", ">= 0.1.5", "<= 1.0.0"
-  spec.add_development_dependency "bundler", "~> 1.15"
-  spec.add_development_dependency "ffaker", "~> 2.7"
-  spec.add_development_dependency "rake", "~> 10.0"
-  spec.add_development_dependency "rspec", "~> 3.0"
-  spec.add_development_dependency "simplecov", "~> 0.15.1"
-  spec.add_development_dependency "webmock", "~> 3.1"
+  spec.add_dependency 'activemodel', '>= 4.2.0'
+  spec.add_dependency 'xml-kit', '>= 0.1.10', '<= 1.0.0'
+  spec.add_development_dependency 'bundler', '~> 1.15'
+  spec.add_development_dependency 'ffaker', '~> 2.7'
+  spec.add_development_dependency 'rake', '~> 10.0'
+  spec.add_development_dependency 'rspec', '~> 3.0'
+  spec.add_development_dependency 'rubocop', '~> 0.52'
+  spec.add_development_dependency 'rubocop-rspec', '~> 1.22'
+  spec.add_development_dependency 'simplecov', '~> 0.15'
+  spec.add_development_dependency 'webmock', '~> 3.1'
+  spec.add_development_dependency 'rspec-benchmark', '~> 0.3'
 end