Comparing changes

v1.0.15 v1.0.16
31 commits 60 files changed

Commits

b5f302b bump version. mo 2018-08-09 22:04:17
b8e954c fix lint error. mo 2018-08-09 22:03:46
1662e9a do not default to persistent mo 2018-08-09 22:02:32
165e38c upgrade rubies in travis ci. mo 2018-06-13 18:28:17
b3d2689 fix broken build. mo 2018-06-13 02:41:45
cc4b02a add spec for binding name. mo 2018-03-17 21:11:14
3d8cd33 extact constant and use hash. mo 2018-03-17 20:42:27
ed8ac24 fix some rubocop violations. mo 2018-03-17 20:29:58
164275e fix rubocop error. mo 2018-03-17 20:19:24
cebbf76 save a few lines of code. mo 2018-03-17 20:18:08
147ba00 combine conditional. mo 2018-03-17 20:14:03
8d2b622 map_from -> metadata_from mo 2018-03-17 19:40:06
0618580 extract parser class. mo 2018-03-17 19:32:34
151919b extract organization class. mo 2018-03-17 02:01:47
9e240b6 exclude conditions. mo 2018-03-17 01:37:25
90b104a extract conditions class. mo 2018-03-17 01:35:52
83eb457 shrink assertion. mo 2018-03-17 01:08:35
0ff4615 remove some duplication. mo 2018-03-16 21:07:52
ef957da move .present? to XmlParseable. mo 2018-03-16 20:35:06
2fc8182 remove duplication. mo 2018-03-16 20:32:03
c1ca4bd extract xml parseable. mo 2018-03-16 20:18:02
35ade85 ignore line length of comments. mo 2018-03-16 19:42:26
ca150c6 update the todolist. mo 2018-03-16 19:38:45
dc28041 fix some rubocop errors. mo 2018-03-16 19:37:32
766be49 fix rubocop error. mo 2018-03-16 17:10:15
ca7df42 check for nil. mo 2018-03-16 17:07:43
exe/saml-kit-create-self-signed-certificate
@@ -3,11 +3,15 @@
 
 require 'saml/kit'
 
-Saml::Kit.deprecate("Use the 'saml-kit-cli' gem instead. saml-kit-create-self-signed-certificate")
+message = "Use the 'saml-kit-cli' gem instead."\
+  ' saml-kit-create-self-signed-certificate'
+Saml::Kit.deprecate(message)
 
 puts 'Enter Passphrase:'
 passphrase = STDIN.read.strip
-certificate, private_key = ::Xml::Kit::SelfSignedCertificate.new.create(passphrase: passphrase)
+certificate, private_key = ::Xml::Kit::SelfSignedCertificate.new.create(
+  passphrase: passphrase
+)
 
 puts '** BEGIN File Format **'
 print certificate
exe/saml-kit-decode-http-post
@@ -3,7 +3,8 @@
 
 require 'saml/kit'
 
-Saml::Kit.deprecate("Use the 'saml-kit-cli' gem instead. saml-kit-decode-http-post")
+message = "Use the 'saml-kit-cli' gem instead. saml-kit-decode-http-post"
+Saml::Kit.deprecate(message)
 
 saml = STDIN.read
 binding = Saml::Kit::Bindings::HttpPost.new(location: '')
exe/saml-kit-decode-http-redirect
@@ -3,7 +3,8 @@
 
 require 'saml/kit'
 
-Saml::Kit.deprecate("Use the 'saml-kit-cli' gem instead. saml-kit-decode-http-redirect*")
+message = "Use the 'saml-kit-cli' gem instead. saml-kit-decode-http-redirect*"
+Saml::Kit.deprecate(message)
 
 input = STDIN.read
 binding = Saml::Kit::Bindings::HttpRedirect.new(location: '')
lib/saml/kit/bindings/binding.rb
@@ -56,13 +56,10 @@ module Saml
             SAMLRequest: params[:SAMLRequest] || params['SAMLRequest'],
             SAMLResponse: params[:SAMLResponse] || params['SAMLResponse'],
           }
-          if parameters[:SAMLRequest].present?
-            parameters[:SAMLRequest]
-          elsif parameters[:SAMLResponse].present?
-            parameters[:SAMLResponse]
-          else
-            raise ArgumentError, 'SAMLRequest or SAMLResponse parameter is required.'
-          end
+          return parameters[:SAMLRequest] if parameters[:SAMLRequest].present?
+          return parameters[:SAMLResponse] if parameters[:SAMLResponse].present?
+          message = 'SAMLRequest or SAMLResponse parameter is required.'
+          raise ArgumentError, message
         end
       end
     end
lib/saml/kit/bindings/http_post.rb
@@ -19,8 +19,9 @@ module Saml
         def serialize(builder, relay_state: nil)
           builder.destination = location
           document = builder.build
+          xml = document.to_xml
           saml_params = {
-            document.query_string_parameter => Base64.strict_encode64(document.to_xml),
+            document.query_string_parameter => Base64.strict_encode64(xml),
           }
           saml_params['RelayState'] = relay_state if relay_state.present?
           [location, saml_params]
@@ -28,7 +29,10 @@ module Saml
 
         def deserialize(params, configuration: Saml::Kit.configuration)
           xml = decode(saml_param_from(params))
-          Saml::Kit::Document.to_saml_document(xml, configuration: configuration)
+          Saml::Kit::Document.to_saml_document(
+            xml,
+            configuration: configuration
+          )
         end
       end
     end
lib/saml/kit/bindings/http_redirect.rb
@@ -20,7 +20,8 @@ module Saml
           builder.embed_signature = false
           builder.destination = location
           document = builder.build
-          [UrlBuilder.new(configuration: builder.configuration).build(document, relay_state: relay_state), {}]
+          url_builder = UrlBuilder.new(configuration: builder.configuration)
+          [url_builder.build(document, relay_state: relay_state), {}]
         end
 
         def deserialize(params, configuration: Saml::Kit.configuration)
@@ -34,18 +35,20 @@ module Saml
 
         def deserialize_document_from(params, configuration)
           xml = inflate(decode(unescape(saml_param_from(params))))
-          Saml::Kit::Document.to_saml_document(xml, configuration: configuration)
+          Saml::Kit::Document.to_saml_document(
+            xml,
+            configuration: configuration
+          )
         end
 
         def ensure_valid_signature(params, document)
           signature = params[:Signature]
-          algorithm = params[:SigAlg]
           provider = document.provider
-          return if signature.blank? || algorithm.blank?
+          return if signature.blank? || params[:SigAlg].blank?
           return if provider.nil?
 
           return document.signature_verified! if provider.verify(
-            algorithm_for(algorithm),
+            algorithm_for(params[:SigAlg]),
             decode(signature),
             canonicalize(params)
           )
lib/saml/kit/bindings/url_builder.rb
@@ -17,17 +17,16 @@ module Saml
           @configuration = configuration
         end
 
-        def build(saml_document, relay_state: nil)
-          destination = saml_document.destination
+        def build(document, relay_state: nil)
+          destination = document.destination
           if configuration.sign?
-            payload = canonicalize(saml_document, relay_state)
+            payload = canonicalize(document, relay_state)
             "#{destination}?#{payload}&Signature=#{signature_for(payload)}"
           else
-            payload = to_query_string(
-              saml_document.query_string_parameter => serialize(saml_document.to_xml),
+            "#{destination}?" + to_query_string(
+              document.query_string_parameter => serialize(document.to_xml),
               'RelayState' => relay_state
             )
-            "#{destination}?#{payload}"
           end
         end
 
@@ -39,8 +38,9 @@ module Saml
         end
 
         def canonicalize(saml_document, relay_state)
+          xml = saml_document.to_xml
           to_query_string(
-            saml_document.query_string_parameter => serialize(saml_document.to_xml),
+            saml_document.query_string_parameter => serialize(xml),
             'RelayState' => relay_state,
             'SigAlg' => ::Xml::Kit::Namespaces::SHA256
           )
lib/saml/kit/builders/assertion.rb
@@ -9,7 +9,9 @@ module Saml
         include XmlTemplatable
         extend Forwardable
 
-        def_delegators :@response_builder, :request, :issuer, :reference_id, :now, :configuration, :user, :version, :destination
+        def_delegators :@response_builder,
+          :request, :issuer, :reference_id, :now, :configuration, :user,
+          :version, :destination
 
         def initialize(response_builder, embed_signature)
           @response_builder = response_builder
@@ -17,7 +19,7 @@ module Saml
         end
 
         def name_id_format
-          request.try(:name_id_format) || Saml::Kit::Namespaces::PERSISTENT
+          request.try(:name_id_format)
         end
 
         def name_id
@@ -62,7 +64,8 @@ module Saml
           {
             AuthnInstant: now.iso8601,
             SessionIndex: reference_id,
-            SessionNotOnOrAfter: configuration.session_timeout.since(now).utc.iso8601,
+            SessionNotOnOrAfter:
+            configuration.session_timeout.since(now).utc.iso8601,
           }
         end
       end
lib/saml/kit/builders/authentication_request.rb
@@ -7,7 +7,8 @@ module Saml
       # {include:file:spec/saml/kit/builders/authentication_request_spec.rb}
       class AuthenticationRequest
         include XmlTemplatable
-        attr_accessor :id, :now, :issuer, :assertion_consumer_service_url, :name_id_format, :destination
+        attr_accessor :id, :now, :issuer, :assertion_consumer_service_url
+        attr_accessor :name_id_format, :destination
         attr_accessor :version
         attr_reader :configuration
 
@@ -36,7 +37,8 @@ module Saml
             Destination: destination,
           }
           if assertion_consumer_service_url.present?
-            options[:AssertionConsumerServiceURL] = assertion_consumer_service_url
+            options[:AssertionConsumerServiceURL] =
+              assertion_consumer_service_url
           end
           options
         end
lib/saml/kit/builders/encrypted_assertion.rb
@@ -10,7 +10,9 @@ module Saml
         extend Forwardable
 
         attr_reader :assertion
-        def_delegators :@response_builder, :configuration, :encryption_certificate
+        def_delegators :@response_builder,
+          :configuration,
+          :encryption_certificate
 
         def initialize(response_builder, assertion)
           @response_builder = response_builder
lib/saml/kit/builders/identity_provider_metadata.rb
@@ -13,7 +13,9 @@ module Saml
         attr_reader :logout_urls, :single_sign_on_urls
         attr_reader :configuration
         attr_reader :metadata
-        def_delegators :metadata, :id, :id=, :entity_id, :entity_id=, :organization_name, :organization_name=, :organization_url, :organization_url=, :contact_email, :contact_email=, :to_xml
+        def_delegators :metadata, :id, :id=, :entity_id, :entity_id=,
+          :organization_name, :organization_name=, :organization_url,
+          :organization_url=, :contact_email, :contact_email=, :to_xml
 
         def initialize(configuration: Saml::Kit.configuration)
           @attributes = []
@@ -24,16 +26,24 @@ module Saml
           @name_id_formats = [Namespaces::PERSISTENT]
           @single_sign_on_urls = []
           @want_authn_requests_signed = true
-          @metadata = Saml::Kit::Builders::Metadata.new(configuration: configuration)
+          @metadata = Saml::Kit::Builders::Metadata.new(
+            configuration: configuration
+          )
           @metadata.identity_provider = self
         end
 
         def add_single_sign_on_service(url, binding: :http_post)
-          @single_sign_on_urls.push(location: url, binding: Bindings.binding_for(binding))
+          @single_sign_on_urls.push(
+            location: url,
+            binding: Bindings.binding_for(binding)
+          )
         end
 
         def add_single_logout_service(url, binding: :http_post)
-          @logout_urls.push(location: url, binding: Bindings.binding_for(binding))
+          @logout_urls.push(
+            location: url,
+            binding: Bindings.binding_for(binding)
+          )
         end
 
         def build
lib/saml/kit/builders/metadata.rb
@@ -22,14 +22,18 @@ module Saml
         end
 
         def build_service_provider
-          @service_provider = Saml::Kit::ServiceProviderMetadata.builder(configuration: configuration) do |xx|
-            yield xx if block_given?
+          @service_provider = Saml::Kit::ServiceProviderMetadata.builder(
+            configuration: configuration
+          ) do |x|
+            yield x if block_given?
           end
         end
 
         def build_identity_provider
-          @identity_provider = Saml::Kit::IdentityProviderMetadata.builder(configuration: configuration) do |xx|
-            yield xx if block_given?
+          @identity_provider = Saml::Kit::IdentityProviderMetadata.builder(
+            configuration: configuration
+          ) do |x|
+            yield x if block_given?
           end
         end
 
lib/saml/kit/builders/null.rb
@@ -1,4 +1,3 @@
-
 # frozen_string_literal: true
 
 module Saml
lib/saml/kit/builders/response.rb
@@ -13,7 +13,9 @@ module Saml
         attr_accessor :issuer, :destination
         attr_reader :configuration
 
-        def initialize(user, request = nil, configuration: Saml::Kit.configuration)
+        def initialize(
+          user, request = nil, configuration: Saml::Kit.configuration
+        )
           @user = user
           @request = request
           @id = ::Xml::Kit::Id.generate
@@ -23,23 +25,30 @@ module Saml
           @status_code = Namespaces::SUCCESS
           @status_message = nil
           @issuer = configuration.entity_id
-          @encryption_certificate = request.try(:provider).try(:encryption_certificates).try(:last)
+          @encryption_certificate = request.try(:provider)
+            .try(:encryption_certificates).try(:last)
           @encrypt = encryption_certificate.present?
           @configuration = configuration
         end
 
         def build
-          Saml::Kit::Response.new(to_xml, request_id: request.try(:id), configuration: configuration)
+          Saml::Kit::Response.new(
+            to_xml,
+            request_id: request.try(:id),
+            configuration: configuration
+          )
         end
 
         def assertion=(value)
-          @assertion = value ? value : Null.new
+          @assertion = value || Null.new
         end
 
         def assertion
           @assertion ||=
             begin
-              assertion = Saml::Kit::Builders::Assertion.new(self, embed_signature)
+              assertion = Saml::Kit::Builders::Assertion.new(
+                self, embed_signature
+              )
               if encrypt
                 Saml::Kit::Builders::EncryptedAssertion.new(self, assertion)
               else
lib/saml/kit/builders/service_provider_metadata.rb
@@ -12,7 +12,9 @@ module Saml
         attr_accessor :want_assertions_signed
         attr_reader :configuration
         attr_reader :metadata
-        def_delegators :metadata, :id, :id=, :entity_id, :entity_id=, :organization_name, :organization_name=, :organization_url, :organization_url=, :contact_email, :contact_email=, :to_xml
+        def_delegators :metadata, :id, :id=, :entity_id, :entity_id=,
+          :organization_name, :organization_name=, :organization_url,
+          :organization_url=, :contact_email, :contact_email=, :to_xml
 
         def initialize(configuration: Saml::Kit.configuration)
           @acs_urls = []
@@ -20,7 +22,9 @@ module Saml
           @logout_urls = []
           @name_id_formats = [Namespaces::PERSISTENT]
           @want_assertions_signed = true
-          @metadata = Saml::Kit::Builders::Metadata.new(configuration: configuration)
+          @metadata = Saml::Kit::Builders::Metadata.new(
+            configuration: configuration
+          )
           @metadata.service_provider = self
         end
 
@@ -29,7 +33,10 @@ module Saml
         end
 
         def add_single_logout_service(url, binding: :http_post)
-          @logout_urls.push(location: url, binding: Bindings.binding_for(binding))
+          @logout_urls.push(
+            location: url,
+            binding: Bindings.binding_for(binding)
+          )
         end
 
         def build
lib/saml/kit/buildable.rb → lib/saml/kit/concerns/buildable.rb
File renamed without changes
lib/saml/kit/requestable.rb → lib/saml/kit/concerns/requestable.rb
File renamed without changes
lib/saml/kit/respondable.rb → lib/saml/kit/concerns/respondable.rb
File renamed without changes
lib/saml/kit/serializable.rb → lib/saml/kit/concerns/serializable.rb
File renamed without changes
lib/saml/kit/translatable.rb → lib/saml/kit/concerns/translatable.rb
File renamed without changes
lib/saml/kit/trustable.rb → lib/saml/kit/concerns/trustable.rb
@@ -9,14 +9,15 @@ module Saml
       extend ActiveSupport::Concern
 
       included do
-        validate :must_have_valid_signature, unless: :signature_manually_verified
+        validate :must_have_valid_signature, unless: :signature_verified
         validate :must_be_registered
         validate :must_be_trusted
       end
 
-      # Returns true when the document has an embedded XML Signature or has been verified externally.
+      # Returns true when the document has an embedded XML Signature or has
+      # been verified externally.
       def signed?
-        signature_manually_verified || signature.present?
+        signature_verified || signature.present?
       end
 
       # @!visibility private
@@ -24,9 +25,10 @@ module Saml
         @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.
+      # Returns true when documents is signed and the signing certificate
+      # belongs to a known service entity.
       def trusted?
-        return true if signature_manually_verified
+        return true if signature_verified
         return false unless signed?
         signature.trusted?(provider)
       end
@@ -38,12 +40,12 @@ module Saml
 
       # @!visibility private
       def signature_verified!
-        @signature_manually_verified = true
+        @signature_verified = true
       end
 
       private
 
-      attr_reader :signature_manually_verified
+      attr_reader :signature_verified
 
       def must_have_valid_signature
         return if to_xml.blank?
lib/saml/kit/concerns/xml_parseable.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'saml/kit/namespaces'
+
+module Saml
+  module Kit
+    module XmlParseable
+      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
+
+      # Returns the SAML document returned as a Hash.
+      def to_h
+        @to_h ||= Hash.from_xml(to_s) || {}
+      end
+
+      # Returns the XML document as a String.
+      #
+      # @param pretty [Boolean] true to return a human friendly version
+      # of the XML.
+      def to_xml(pretty: nil)
+        pretty ? to_nokogiri.to_xml(indent: 2) : to_s
+      end
+
+      # Returns the SAML document as an XHTML string.
+      # This is useful for rendering in a web page.
+      def to_xhtml
+        Nokogiri::XML(to_xml, &:noblanks).to_xhtml
+      end
+
+      def present?
+        to_s.present?
+      end
+
+      # @!visibility private
+      def to_nokogiri
+        @to_nokogiri ||= Nokogiri::XML(to_s)
+      end
+
+      # @!visibility private
+      def at_xpath(xpath)
+        return unless present?
+        to_nokogiri.at_xpath(xpath, NAMESPACES)
+      end
+
+      # @!visibility private
+      def search(xpath)
+        to_nokogiri.search(xpath, NAMESPACES)
+      end
+
+      # Returns the XML document as a [String].
+      def to_s
+        content
+      end
+    end
+  end
+end
lib/saml/kit/xml_templatable.rb → lib/saml/kit/concerns/xml_templatable.rb
@@ -5,7 +5,7 @@ module Saml
     # This module is responsible for
     # generating converting templates to xml.
     module XmlTemplatable
-      TEMPLATES_DIR = Pathname.new(File.join(__dir__, 'builders/templates/'))
+      TEMPLATES_DIR = Pathname.new(File.join(__dir__, '../builders/templates/'))
       include ::Xml::Kit::Templatable
 
       def template_path
@@ -16,7 +16,8 @@ module Saml
         "#{self.class.name.split('::').last.underscore}.builder"
       end
 
-      # Returns true if an embedded signature is requested and at least one signing certificate is available via the configuration.
+      # 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?) ||
lib/saml/kit/xsd_validatable.rb → lib/saml/kit/concerns/xsd_validatable.rb
@@ -5,8 +5,18 @@ module Saml
     # This module is responsible for validating
     # xml documents against the SAML XSD's
     module XsdValidatable
+      PROTOCOL_XSD = File.expand_path(
+        '../xsd/saml-schema-protocol-2.0.xsd', File.dirname(__FILE__)
+      ).freeze
+
+      METADATA_XSD = File.expand_path(
+        '../xsd/saml-schema-metadata-2.0.xsd', File.dirname(__FILE__)
+      ).freeze
+
       # @!visibility private
       def matches_xsd?(xsd)
+        return unless to_nokogiri.present?
+
         Dir.chdir(File.dirname(xsd)) do
           xsd = Nokogiri::XML::Schema(IO.read(xsd))
           xsd.validate(to_nokogiri).each do |error|
lib/saml/kit/rspec/have_xpath.rb
@@ -6,11 +6,13 @@ RSpec::Matchers.define :have_xpath do |xpath|
   end
 
   failure_message do |actual|
-    "Expected xpath: #{xpath.inspect} to match in:\n #{xml_pretty_print(actual)}"
+    xml = xml_pretty_print(actual)
+    "Expected xpath: #{xpath.inspect} to match in:\n #{xml}"
   end
 
   failure_message_when_negated do |actual|
-    "Expected xpath: #{xpath.inspect} not to match in:\n #{xml_pretty_print(actual)}"
+    xml = xml_pretty_print(actual)
+    "Expected xpath: #{xpath.inspect} not to match in:\n #{xml}"
   end
 
   def xml_pretty_print(raw_xml)
lib/saml/kit/assertion.rb
@@ -8,10 +8,14 @@ module Saml
     class Assertion
       include ActiveModel::Validations
       include Translatable
+      include XmlParseable
+      extend Forwardable
       XPATH = [
         '/samlp:Response/saml:Assertion',
         '/samlp:Response/saml:EncryptedAssertion'
       ].join('|')
+      def_delegators :conditions, :started_at, :expired_at, :audiences
+      def_delegators :attribute_statement, :attributes
 
       validate :must_be_decryptable
       validate :must_match_issuer, if: :decryptable?
@@ -20,15 +24,17 @@ module Saml
       attr_reader :name
       attr_accessor :occurred_at
 
-      def initialize(node, configuration: Saml::Kit.configuration, private_keys: [])
+      def initialize(
+        node, configuration: Saml::Kit.configuration, private_keys: []
+      )
         @name = 'Assertion'
-        @node = node
+        @to_nokogiri = node
         @configuration = configuration
         @occurred_at = Time.current
         @cannot_decrypt = false
         @encrypted = false
-        private_keys = (configuration.private_keys(use: :encryption) + private_keys).uniq
-        decrypt(::Xml::Kit::Decryption.new(private_keys: private_keys))
+        keys = configuration.private_keys(use: :encryption) + private_keys
+        decrypt(::Xml::Kit::Decryption.new(private_keys: keys.uniq))
       end
 
       def issuer
@@ -39,6 +45,10 @@ module Saml
         at_xpath('./saml:Subject/saml:NameID').try(:text)
       end
 
+      def name_id_format
+        at_xpath('./saml:Subject/saml:NameID').attribute('Format').try(:value)
+      end
+
       def signed?
         signature.present?
       end
@@ -56,23 +66,13 @@ module Saml
         now > drifted_started_at && !expired?(now)
       end
 
-      def attributes
-        @attributes ||= search('./saml:AttributeStatement/saml:Attribute').inject({}) do |memo, item|
-          memo[item.attribute('Name').value] = item.at_xpath('./saml:AttributeValue', Saml::Kit::Document::NAMESPACES).try(:text)
-          memo
-        end.with_indifferent_access
+      def attribute_statement
+        @attribute_statement ||=
+          AttributeStatement.new(search('./saml:AttributeStatement'))
       end
 
-      def started_at
-        parse_date(at_xpath('./saml:Conditions/@NotBefore').try(:value))
-      end
-
-      def expired_at
-        parse_date(at_xpath('./saml:Conditions/@NotOnOrAfter').try(:value))
-      end
-
-      def audiences
-        search('./saml:Conditions/saml:AudienceRestriction/saml:Audience').map(&:text)
+      def conditions
+        @conditions ||= Conditions.new(search('./saml:Conditions'))
       end
 
       def encrypted?
@@ -84,16 +84,8 @@ module Saml
         !@cannot_decrypt
       end
 
-      def present?
-        @node.present?
-      end
-
-      def to_xml(pretty: nil)
-        pretty ? @node.to_xml(indent: 2) : to_s
-      end
-
       def to_s
-        @node.to_s
+        @to_nokogiri.to_s
       end
 
       private
@@ -104,22 +96,14 @@ module Saml
         encrypted_assertion = at_xpath('./xmlenc:EncryptedData')
         @encrypted = encrypted_assertion.present?
         return unless @encrypted
-        @node = decryptor.decrypt_node(encrypted_assertion)
+        @to_nokogiri = 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 StandardError => error
-        Saml::Kit.logger.error(error)
-        Time.at(0).to_datetime
-      end
-
       def must_match_issuer
-        return if audiences.empty?
-        return if audiences.include?(configuration.entity_id)
+        return if audiences.empty? || audiences.include?(configuration.entity_id)
         errors[:audience] << error_message(:must_match_issuer)
       end
 
@@ -130,7 +114,6 @@ module Saml
 
       def must_have_valid_signature
         return if !signed? || signature.valid?
-
         signature.errors.each do |attribute, message|
           errors.add(attribute, message)
         end
@@ -139,15 +122,6 @@ module Saml
       def must_be_decryptable
         errors.add(:base, error_message(:cannot_decrypt)) unless decryptable?
       end
-
-      def at_xpath(xpath)
-        return unless @node
-        @node.at_xpath(xpath, Saml::Kit::Document::NAMESPACES)
-      end
-
-      def search(xpath)
-        @node.search(xpath, Saml::Kit::Document::NAMESPACES)
-      end
     end
   end
 end
lib/saml/kit/attribute_statement.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Saml
+  module Kit
+    class AttributeStatement
+      include XmlParseable
+
+      attr_reader :content
+
+      def initialize(node)
+        @to_nokogiri = node
+        @content = node.to_s
+      end
+
+      def attributes
+        @attributes ||= search('./saml:Attribute').inject({}) do |memo, item|
+          namespace = Saml::Kit::Document::NAMESPACES
+          attribute = item.at_xpath('./saml:AttributeValue', namespace)
+          memo[item.attribute('Name').value] = attribute.try(:text)
+          memo
+        end.with_indifferent_access
+      end
+    end
+  end
+end
lib/saml/kit/authentication_request.rb
@@ -11,9 +11,17 @@ module Saml
     #    end
     #
     #    <?xml version="1.0" encoding="UTF-8"?>
-    #    <samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_ca3a0e72-9530-41f1-9518-c53716de88b2" Version="2.0" IssueInstant="2017-12-19T16:27:44Z" Destination="http://hartmann.info" AssertionConsumerServiceURL="https://carroll.com/acs">
+    #    <samlp:AuthnRequest
+    #      xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
+    #      xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+    #      ID="_ca3a0e72-9530-41f1-9518-c53716de88b2"
+    #      Version="2.0"
+    #      IssueInstant="2017-12-19T16:27:44Z"
+    #      Destination="http://hartmann.info"
+    #      AssertionConsumerServiceURL="https://carroll.com/acs">
     #      <saml:Issuer>Day of the Dangerous Cousins</saml:Issuer>
-    #      <samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"/>
+    #      <samlp:NameIDPolicy
+    #        Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"/>
     #    </samlp:AuthnRequest>
     #
     # Example:
@@ -25,13 +33,16 @@ module Saml
       # Create an instance of an AuthnRequest document.
       #
       # @param xml [String] the raw xml.
-      # @param configuration [Saml::Kit::Configuration] defaults to the global configuration.
+      # @param configuration [Saml::Kit::Configuration] defaults to the global
+      # configuration.
       def initialize(xml, configuration: Saml::Kit.configuration)
         super(xml, name: 'AuthnRequest', configuration: configuration)
       end
 
       # Extract the AssertionConsumerServiceURL from the AuthnRequest
-      #    <samlp:AuthnRequest AssertionConsumerServiceURL="https://carroll.com/acs"></samlp:AuthnRequest>
+      #    <samlp:AuthnRequest
+      #      AssertionConsumerServiceURL="https://carroll.com/acs">
+      #    </samlp:AuthnRequest>
       def assertion_consumer_service_url
         at_xpath('./*/@AssertionConsumerServiceURL').try(:value)
       end
@@ -42,23 +53,31 @@ module Saml
 
       # Extract the NameIDPolicy from the AuthnRequest
       #    <samlp:AuthnRequest>
-      #      <samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"/>
+      #      <samlp:NameIDPolicy
+      #        Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"/>
       #    </samlp:AuthnRequest>
       def name_id_policy
         at_xpath('./*/samlp:NameIDPolicy/@Format').try(:value)
       end
 
       # Generate a Response for a specific user.
-      # @param user [Object] this is a custom user object that can be used for generating a nameid and assertion attributes.
-      # @param binding [Symbol] the SAML binding to use `:http_post` or `:http_redirect`.
-      # @param configuration [Saml::Kit::Configuration] the configuration to use to build the response.
-      def response_for(user, binding:, relay_state: nil, configuration: Saml::Kit.configuration)
-        response_binding = provider.assertion_consumer_service_for(binding: binding)
-        response = Saml::Kit::Response.builder(user, self, configuration: configuration) do |builder|
-          builder.embed_signature = provider.want_assertions_signed
-          yield builder if block_given?
-        end
-        response_binding.serialize(response, relay_state: relay_state)
+      # @param user [Object] this is a custom user object that can be used for
+      # generating a nameid and assertion attributes.
+      # @param binding [Symbol] the SAML binding to use
+      # `:http_post` or `:http_redirect`.
+      # @param configuration [Saml::Kit::Configuration] the configuration to
+      # use to build the response.
+      def response_for(
+        user, binding:, relay_state: nil, configuration: Saml::Kit.configuration
+      )
+        response =
+          Response.builder(user, self, configuration: configuration) do |x|
+            x.embed_signature = provider.want_assertions_signed
+            yield x if block_given?
+          end
+        provider
+          .assertion_consumer_service_for(binding: binding)
+          .serialize(response, relay_state: relay_state)
       end
     end
   end
lib/saml/kit/bindings.rb
@@ -11,9 +11,10 @@ module Saml
     # the different SAML bindings that are
     # supported by this gem.
     module Bindings
-      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
+      BINDINGS_2_0 = 'urn:oasis:names:tc:SAML:2.0:bindings'.freeze
+      HTTP_ARTIFACT = "#{BINDINGS_2_0}:HTTP-Artifact".freeze
+      HTTP_POST = "#{BINDINGS_2_0}:HTTP-POST".freeze
+      HTTP_REDIRECT = "#{BINDINGS_2_0}:HTTP-Redirect".freeze
       ALL = {
         http_post: HTTP_POST,
         http_redirect: HTTP_REDIRECT,
lib/saml/kit/builders.rb
@@ -1,6 +1,5 @@
 # frozen_string_literal: true
 
-require 'saml/kit/xml_templatable'
 require 'saml/kit/builders/assertion'
 require 'saml/kit/builders/authentication_request'
 require 'saml/kit/builders/encrypted_assertion'
lib/saml/kit/composite_metadata.rb
@@ -9,6 +9,7 @@ module Saml
     # and SPSSODescriptor element.
     class CompositeMetadata < Metadata # :nodoc:
       include Enumerable
+
       attr_reader :service_provider, :identity_provider
 
       def initialize(xml)
@@ -19,8 +20,22 @@ module Saml
         ]
       end
 
+      def organization
+        find { |x| x.organization.present? }.try(:organization)
+      end
+
+      def organization_name
+        organization.name
+      end
+
+      def organization_url
+        organization.url
+      end
+
       def services(type)
-        xpath = map { |xxx| "//md:EntityDescriptor/md:#{xxx.name}/md:#{type}" }.join('|')
+        xpath = map do |x|
+          "//md:EntityDescriptor/md:#{x.name}/md:#{type}"
+        end.join('|')
         search(xpath).map do |item|
           binding = item.attribute('Binding').value
           location = item.attribute('Location').value
@@ -37,7 +52,7 @@ module Saml
       end
 
       def method_missing(name, *args)
-        if (target = find { |xxx| xxx.respond_to?(name) })
+        if (target = find { |x| x.respond_to?(name) })
           target.public_send(name, *args)
         else
           super
@@ -45,7 +60,7 @@ module Saml
       end
 
       def respond_to_missing?(method, *)
-        find { |xxx| xxx.respond_to?(method) }
+        find { |x| x.respond_to?(method) }
       end
     end
   end
lib/saml/kit/conditions.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Saml
+  module Kit
+    class Conditions
+      include XmlParseable
+
+      attr_reader :content
+
+      def initialize(node)
+        @to_nokogiri = node
+        @content = node.to_s
+      end
+
+      def started_at
+        parse_iso8601(at_xpath('./@NotBefore').try(:value))
+      end
+
+      def expired_at
+        parse_iso8601(at_xpath('./@NotOnOrAfter').try(:value))
+      end
+
+      def audiences
+        search('./saml:AudienceRestriction/saml:Audience').map(&:text)
+      end
+
+      private
+
+      def parse_iso8601(value)
+        DateTime.parse(value)
+      rescue StandardError => error
+        Saml::Kit.logger.error(error)
+        Time.at(0).to_datetime
+      end
+    end
+  end
+end
lib/saml/kit/configuration.rb
@@ -2,7 +2,8 @@
 
 module Saml
   module Kit
-    # This class represents the main configuration that is use for generating SAML documents.
+    # This class represents the main configuration that is use for generating
+    # SAML documents.
     #
     #   Saml::Kit::Configuration.new do |config|
     #     config.entity_id = "com:saml:kit"
@@ -19,17 +20,25 @@ module Saml
     #   Saml::Kit.configure do |configuration|
     #     configuration.entity_id = "https://www.example.com/saml/metadata"
     #     configuration.generate_key_pair_for(use: :signing)
-    #     configuration.add_key_pair(ENV["X509_CERTIFICATE"], ENV["PRIVATE_KEY"], passphrase: ENV['PRIVATE_KEY_PASSPHRASE'], use: :encryption)
+    #     configuration.add_key_pair(
+    #       ENV["X509_CERTIFICATE"],
+    #         ENV["PRIVATE_KEY"],
+    #         passphrase: ENV['PRIVATE_KEY_PASSPHRASE'],
+    #         use: :encryption
+    #     )
     #   end
     class Configuration
       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})
+      # The signature method to use when generating signatures
+      # (See {Saml::Kit::Builders::XmlSignature::SIGNATURE_METHODS})
       attr_accessor :signature_method
-      # The digest method to use when generating signatures (See {Saml::Kit::Builders::XmlSignature::DIGEST_METHODS})
+      # The digest method to use when generating signatures
+      # (See {Saml::Kit::Builders::XmlSignature::DIGEST_METHODS})
       attr_accessor :digest_method
-      # The metadata registry to use for searching for metadata associated with an issuer.
+      # The metadata registry to use for searching for metadata associated
+      # with an issuer.
       attr_accessor :registry
       # The session timeout to use when generating an Assertion.
       attr_accessor :session_timeout
@@ -57,7 +66,11 @@ module Saml
       # @param use [Symbol] the type of key pair, `:signing` or `:encryption`
       def add_key_pair(certificate, private_key, passphrase: nil, use: :signing)
         ensure_proper_use(use)
-        @key_pairs.push(::Xml::Kit::KeyPair.new(certificate, private_key, passphrase, use.to_sym))
+        @key_pairs.push(
+          ::Xml::Kit::KeyPair.new(
+            certificate, private_key, passphrase, use.to_sym
+          )
+        )
       end
 
       # Generates a unique key pair that can be used for signing or encryption.
@@ -66,27 +79,32 @@ module Saml
       # @param passphrase [String] the private key passphrase to use.
       def generate_key_pair_for(use:, passphrase: SecureRandom.uuid)
         ensure_proper_use(use)
-        certificate, private_key = ::Xml::Kit::SelfSignedCertificate.new.create(passphrase: passphrase)
+        certificate, private_key = ::Xml::Kit::SelfSignedCertificate.new.create(
+          passphrase: passphrase
+        )
         add_key_pair(certificate, private_key, passphrase: passphrase, use: use)
       end
 
       # Return each key pair for a specific use.
       #
-      # @param use [Symbol] the type of key pair to return `nil`, `:signing` or `:encryption`
+      # @param use [Symbol] the type of key pair to return
+      # `nil`, `:signing` or `:encryption`
       def key_pairs(use: nil)
         use.present? ? @key_pairs.find_all { |xxx| xxx.for?(use) } : @key_pairs
       end
 
       # Return each certificate for a specific use.
       #
-      # @param use [Symbol] the type of key pair to return `nil`, `:signing` or `:encryption`
+      # @param use [Symbol] the type of key pair to return
+      # `nil`, `:signing` or `:encryption`
       def certificates(use: nil)
         key_pairs(use: use).flat_map(&:certificate)
       end
 
       # Return each private for a specific use.
       #
-      # @param use [Symbol] the type of key pair to return `nil`, `:signing` or `:encryption`
+      # @param use [Symbol] the type of key pair to return
+      # `nil`, `:signing` or `:encryption`
       def private_keys(use: nil)
         key_pairs(use: use).flat_map(&:private_key)
       end
lib/saml/kit/default_registry.rb
@@ -2,8 +2,10 @@
 
 module Saml
   module Kit
-    # The default metadata registry is used to fetch the metadata associated with an issuer or entity id.
-    # The metadata associated with an issuer is used to verify trust for any SAML documents that are received.
+    # The default metadata registry is used to fetch the metadata associated
+    # with an issuer or entity id.
+    # The metadata associated with an issuer is used to verify trust for any
+    # SAML documents that are received.
     #
     # You can replace the default registry with your own at startup.
     #
@@ -41,12 +43,14 @@ module Saml
       #
       # @param metadata [Saml::Kit::Metadata] the metadata to register.
       def register(metadata)
+        ensure_valid_metadata(metadata)
         Saml::Kit.logger.debug(metadata.to_xml(pretty: true))
         @items[metadata.entity_id] = metadata
       end
 
       # Register metadata via a remote URL.
-      # This will attempt to connect to the remove URL to download the metadata and register it in the registry.
+      # This will attempt to connect to the remove URL to download the
+      # metadata and register it in the registry.
       #
       # @param url [String] the url to download the metadata from.
       # @param verify_ssl [Boolean] enable/disable SSL peer verification.
@@ -57,7 +61,8 @@ module Saml
 
       # Returns the metadata document associated with an issuer or entityID.
       #
-      # @param entity_id [String] the unique entityID/Issuer associated with metadata.
+      # @param entity_id [String] unique entityID/Issuer associated with
+      # metadata.
       def metadata_for(entity_id)
         @items[entity_id]
       end
@@ -69,6 +74,16 @@ module Saml
         end
       end
 
+      private
+
+      def ensure_valid_metadata(metadata)
+        error = ArgumentError.new('Cannot register invalid metadata')
+        raise error if
+          metadata.nil? ||
+          !metadata.respond_to?(:entity_id) ||
+          metadata.invalid?
+      end
+
       # This class is responsible for
       # making HTTP requests to fetch metadata
       # from remote locations.
lib/saml/kit/document.rb
@@ -5,19 +5,12 @@ module Saml
     # This class is a base class for SAML documents.
     class Document
       include ActiveModel::Validations
-      include XsdValidatable
+      include Buildable
       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 XmlParseable
+      include XsdValidatable
+
       attr_accessor :registry
       attr_reader :name
       validates_presence_of :content
@@ -58,44 +51,13 @@ module Saml
         Time.parse(at_xpath('./*/@IssueInstant').try(:value))
       end
 
-      # Returns the SAML document returned as a Hash.
-      def to_h
-        @to_h ||= Hash.from_xml(content) || {}
-      end
-
-      # Returns the SAML document as an XML string.
-      #
-      # @param pretty [Boolean] formats the xml or returns the raw xml.
-      def to_xml(pretty: nil)
-        pretty ? to_nokogiri.to_xml(indent: 2) : to_s
-      end
-
-      # Returns the SAML document as an XHTML string.
-      # This is useful for rendering in a web page.
-      def to_xhtml
-        Nokogiri::XML(to_xml, &:noblanks).to_xhtml
-      end
-
-      # @!visibility private
-      def to_nokogiri
-        @to_nokogiri ||= Nokogiri::XML(to_s)
-      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
-        content
-      end
-
       class << self
+        CONSTRUCTORS = {
+          'AuthnRequest' => -> { Saml::Kit::AuthenticationRequest },
+          'LogoutRequest' => -> { Saml::Kit::LogoutRequest },
+          'LogoutResponse' => -> { Saml::Kit::LogoutResponse },
+          'Response' => -> { Saml::Kit::Response },
+        }.freeze
         XPATH = [
           '/samlp:AuthnRequest',
           '/samlp:LogoutRequest',
@@ -106,14 +68,12 @@ module Saml
         # Returns the raw xml as a Saml::Kit SAML document.
         #
         # @param xml [String] the raw xml string.
-        # @param configuration [Saml::Kit::Configuration] the configuration to use for unpacking the document.
+        # @param configuration [Saml::Kit::Configuration] configuration to use
+        # for unpacking the document.
         def to_saml_document(xml, configuration: Saml::Kit.configuration)
-          constructor = {
-            'AuthnRequest' => Saml::Kit::AuthenticationRequest,
-            'LogoutRequest' => Saml::Kit::LogoutRequest,
-            'LogoutResponse' => Saml::Kit::LogoutResponse,
-            'Response' => Saml::Kit::Response,
-          }[Nokogiri::XML(xml).at_xpath(XPATH, "samlp": ::Saml::Kit::Namespaces::PROTOCOL).name] || InvalidDocument
+          namespaces = { samlp: Namespaces::PROTOCOL }
+          element = Nokogiri::XML(xml).at_xpath(XPATH, namespaces)
+          constructor = CONSTRUCTORS[element.name].try(:call) || InvalidDocument
           constructor.new(xml, configuration: configuration)
         rescue StandardError => error
           Saml::Kit.logger.error(error)
@@ -122,18 +82,12 @@ module Saml
 
         # @!visibility private
         def builder_class # :nodoc:
-          case name
-          when Saml::Kit::Response.to_s
-            Saml::Kit::Builders::Response
-          when Saml::Kit::LogoutResponse.to_s
-            Saml::Kit::Builders::LogoutResponse
-          when Saml::Kit::AuthenticationRequest.to_s
-            Saml::Kit::Builders::AuthenticationRequest
-          when Saml::Kit::LogoutRequest.to_s
-            Saml::Kit::Builders::LogoutRequest
-          else
-            raise ArgumentError, "Unknown SAML Document #{name}"
-          end
+          {
+            Response.to_s => Saml::Kit::Builders::Response,
+            LogoutResponse.to_s => Saml::Kit::Builders::LogoutResponse,
+            AuthenticationRequest.to_s => Saml::Kit::Builders::AuthenticationRequest,
+            LogoutRequest.to_s => Saml::Kit::Builders::LogoutRequest,
+          }[name] || (raise ArgumentError, "Unknown SAML Document #{name}")
         end
       end
 
lib/saml/kit/identity_provider_metadata.rb
@@ -2,16 +2,31 @@
 
 module Saml
   module Kit
-    # This class is used to parse the IDPSSODescriptor from a SAML metadata document.
+    # This class parses the IDPSSODescriptor from a SAML metadata document.
     #
     #  raw_xml = <<-XML
     #  <?xml version="1.0" encoding="UTF-8"?>
-    #  <EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_cfa24e2f-0ec0-4ee3-abb8-b2fcfe394c1c" entityID="">
-    #    <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
-    #      <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://www.example.com/logout"/>
-    #      <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
-    #      <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://www.example.com/login"/>
-    #      <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://www.example.com/login"/>
+    #  <EntityDescriptor
+    #    xmlns="urn:oasis:names:tc:SAML:2.0:metadata"
+    #    xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
+    #    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
+    #    ID="_cfa24e2f-0ec0-4ee3-abb8-b2fcfe394c1c"
+    #    entityID="my-entity-id">
+    #    <IDPSSODescriptor
+    #      WantAuthnRequestsSigned="true"
+    #      protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+    #      <SingleLogoutService
+    #        Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+    #        Location="https://www.example.com/logout" />
+    #      <NameIDFormat>
+    #        urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
+    #      </NameIDFormat>
+    #      <SingleSignOnService
+    #        Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+    #        Location="https://www.example.com/login" />
+    #      <SingleSignOnService
+    #        Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+    #        Location="https://www.example.com/login" />
     #      <saml:Attribute Name="id"/>
     #    </IDPSSODescriptor>
     #  </EntityDescriptor>
@@ -70,14 +85,18 @@ module Saml
       # Creates a AuthnRequest document for the specified binding.
       #
       # @param binding [Symbol] `:http_post` or `:http_redirect`.
-      # @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)
-        builder = Saml::Kit::AuthenticationRequest.builder(configuration: configuration) do |xxx|
-          xxx.embed_signature = want_authn_requests_signed
-          yield xxx if block_given?
-        end
+      # @param relay_state [Object] RelayState to include the returned params.
+      # @param configuration [Saml::Kit::Configuration] the configuration to
+      # use for generating the request.
+      # @return [Array] Url and params encoded using rules for binding.
+      def login_request_for(
+        binding:, relay_state: nil, configuration: Saml::Kit.configuration
+      )
+        builder =
+          AuthenticationRequest.builder(configuration: configuration) do |x|
+            x.embed_signature = want_authn_requests_signed
+            yield x if block_given?
+          end
         request_binding = single_sign_on_service_for(binding: binding)
         request_binding.serialize(builder, relay_state: relay_state)
       end
lib/saml/kit/invalid_document.rb
@@ -10,7 +10,7 @@ module Saml
         model.errors[:base] << model.error_message(:invalid)
       end
 
-      def initialize(xml, configuration: nil)
+      def initialize(xml, *)
         super(xml, name: 'InvalidDocument')
       end
 
lib/saml/kit/logout_request.rb
@@ -16,7 +16,8 @@ module Saml
     #
     # See {Saml::Kit::Builders::LogoutRequest} for a list of available settings.
     #
-    # This class can also be used to generate the correspondong LogoutResponse for a LogoutRequest.
+    # This class can also be used to generate the correspondong LogoutResponse
+    # for a LogoutRequest.
     #
     #   document = Saml::Kit::LogoutRequest.new(raw_xml)
     #   url, saml_params = document.response_for(binding: :http_post)
@@ -31,7 +32,7 @@ module Saml
       # A new instance of LogoutRequest
       #
       # @param xml [String] The raw xml string.
-      # @param configuration [Saml::Kit::Configuration] the configuration to use.
+      # @param configuration [Saml::Kit::Configuration] configuration to use.
       def initialize(xml, configuration: Saml::Kit.configuration)
         super(xml, name: 'LogoutRequest', configuration: configuration)
       end
@@ -45,11 +46,15 @@ module Saml
         at_xpath('./*/saml:NameID/@Format').try(:value)
       end
 
-      # Generates a Serialized LogoutResponse using the encoding rules for the specified binding.
+      # Generates a Serialized LogoutResponse using the encoding rules for
+      # the specified binding.
       #
-      # @param binding [Symbol] The binding to use `:http_redirect` or `:http_post`.
-      # @param relay_state [Object] The RelayState to include in the RelayState param.
-      # @return [Array] Returns an array with a url and Hash of parameters to return to the requestor.
+      # @param binding [Symbol] The binding to use `:http_redirect` or
+      # `:http_post`.
+      # @param relay_state [Object] The RelayState to include in the
+      # RelayState param.
+      # @return [Array] Returns an array with a url and Hash of parameters to
+      # return to the requestor.
       def response_for(binding:, relay_state: nil)
         builder = Saml::Kit::LogoutResponse.builder(self) do |xxx|
           yield xxx if block_given?
lib/saml/kit/logout_response.rb
@@ -10,7 +10,9 @@ module Saml
     class LogoutResponse < Document
       include Respondable
 
-      def initialize(xml, request_id: nil, configuration: Saml::Kit.configuration)
+      def initialize(
+        xml, request_id: nil, configuration: Saml::Kit.configuration
+      )
         @request_id = request_id
         super(xml, name: 'LogoutResponse', configuration: configuration)
       end
lib/saml/kit/metadata.rb
@@ -8,46 +8,43 @@ module Saml
     #
     # It can also be used to generate a new metadata string.
     #
-    #   metadata = Saml::Kit::Metadata.build do |builder|
-    #     builder.entity_id = "my-issuer"
-    #     builder.build_service_provider do |x|
-    #       x.add_assertion_consumer_service(assertions_url, binding: :http_post)
-    #       x.add_single_logout_service(logout_url, binding: :http_post)
-    #     end
-    #     builder.build_identity_provider do |x|
-    #       x.add_single_sign_on_service(login_url, binding: :http_redirect)
-    #       x.add_single_logout_service(logout_url, binding: :http_post)
-    #     end
-    #   end
-    #   puts metadata.to_xml(pretty: true)
+    #  metadata = Saml::Kit::Metadata.build do |builder|
+    #    builder.entity_id = "my-issuer"
+    #    builder.build_service_provider do |x|
+    #      x.add_assertion_consumer_service(assertions_url, binding: :http_post)
+    #      x.add_single_logout_service(logout_url, binding: :http_post)
+    #    end
+    #    builder.build_identity_provider do |x|
+    #      x.add_single_sign_on_service(login_url, binding: :http_redirect)
+    #      x.add_single_logout_service(logout_url, binding: :http_post)
+    #    end
+    #  end
+    #  puts metadata.to_xml(pretty: true)
     #
-    # See {Saml::Kit::Builders::ServiceProviderMetadata} and {Saml::Kit::Builders::IdentityProviderMetadata}
+    # See {Saml::Kit::Builders::ServiceProviderMetadata} and
+    # {Saml::Kit::Builders::IdentityProviderMetadata}
     # for a list of options that can be specified.
     # {include:file:spec/examples/metadata_spec.rb}
     class Metadata
       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,
-        md: Namespaces::METADATA,
-        saml: Namespaces::ASSERTION,
-        samlp: Namespaces::PROTOCOL,
-      }.freeze
+      include Translatable
+      include XmlParseable
+      include XsdValidatable
+      extend Forwardable
+
+      def_delegator :organization, :organization_name, :organization_url
 
       validates_presence_of :metadata
       validate :must_contain_descriptor
       validate :must_match_xsd
       validate :must_have_valid_signature
 
-      attr_reader :name
+      attr_reader :name, :content
 
-      def initialize(name, xml)
+      def initialize(name, content)
         @name = name
-        @xml = xml
+        @content = content
       end
 
       # Returns the /EntityDescriptor/@entityID
@@ -60,14 +57,8 @@ module Saml
         search("/md:EntityDescriptor/md:#{name}/md:NameIDFormat").map(&:text)
       end
 
-      # Returns the Organization Name
-      def organization_name
-        at_xpath('/md:EntityDescriptor/md:Organization/md:OrganizationName').try(:text)
-      end
-
-      # Returns the Organization URL
-      def organization_url
-        at_xpath('/md:EntityDescriptor/md:Organization/md:OrganizationURL').try(:text)
+      def organization(xpath = '/md:EntityDescriptor/md:Organization')
+        @organization ||= Organization.new(at_xpath(xpath))
       end
 
       # Returns the Company
@@ -76,10 +67,15 @@ module Saml
       end
 
       # Returns each of the X509 certificates.
-      def certificates
-        @certificates ||= search("/md:EntityDescriptor/md:#{name}/md:KeyDescriptor").map do |item|
-          cert = item.at_xpath('./ds:KeyInfo/ds:X509Data/ds:X509Certificate', 'ds' => ::Xml::Kit::Namespaces::XMLDSIG).try(:text)
-          ::Xml::Kit::Certificate.new(cert, use: item.attribute('use').try(:value))
+      def certificates(
+        xpath = "/md:EntityDescriptor/md:#{name}/md:KeyDescriptor"
+      )
+        @certificates ||= search(xpath).map do |item|
+          xpath = './ds:KeyInfo/ds:X509Data/ds:X509Certificate'
+          namespaces = { 'ds' => ::Xml::Kit::Namespaces::XMLDSIG }
+          cert = item.at_xpath(xpath, namespaces).try(:text)
+          use_attribute = item.attribute('use')
+          ::Xml::Kit::Certificate.new(cert, use: use_attribute.try(:value))
         end
       end
 
@@ -95,7 +91,8 @@ module Saml
 
       # Returns each of the service endpoints supported by this metadata.
       #
-      # @param type [String] the type of service. .E.g. `AssertionConsumerServiceURL`
+      # @param type [String] the type of service.
+      # .E.g. `AssertionConsumerServiceURL`
       def services(type)
         search("/md:EntityDescriptor/md:#{name}/md:#{type}").map do |item|
           binding = item.attribute('Binding').value
@@ -107,10 +104,12 @@ module Saml
       # Returns a specifing service binding.
       #
       # @param binding [Symbol] can be `:http_post` or `:http_redirect`.
-      # @param type [Symbol] can be on the service element like `AssertionConsumerServiceURL`, `SingleSignOnService` or `SingleLogoutService`.
+      # @param type [Symbol] can be on the service element like
+      # `AssertionConsumerServiceURL`, `SingleSignOnService` or
+      # `SingleLogoutService`.
       def service_for(binding:, type:)
         binding = Saml::Kit::Bindings.binding_for(binding)
-        services(type).find { |xxx| xxx.binding?(binding) }
+        services(type).find { |x| x.binding?(binding) }
       end
 
       # Returns each of the SingleLogoutService bindings
@@ -127,99 +126,56 @@ module Saml
 
       # Creates a serialized LogoutRequest.
       #
-      # @param user [Object] a user object that responds to `name_id_for` and `assertion_attributes_for`.
+      # @param user [Object] a user object that responds to `name_id_for` and
+      # `assertion_attributes_for`.
       # @param binding [Symbol] can be `:http_post` or `:http_redirect`.
       # @param relay_state [String] the relay state to have echo'd back.
-      # @return [Array] Returns an array with a url and Hash of parameters to send to the other party.
+      # @return [Array] Returns an array with a url and Hash of parameters to
+      # send to the other party.
       def logout_request_for(user, binding: :http_post, relay_state: nil)
-        builder = Saml::Kit::LogoutRequest.builder(user) { |xxx| yield xxx if block_given? }
+        builder = LogoutRequest.builder(user) { |x| yield x if block_given? }
         request_binding = single_logout_service_for(binding: binding)
         request_binding.serialize(builder, relay_state: relay_state)
       end
 
       # Returns the certificate that matches the fingerprint
       #
-      # @param fingerprint [Saml::Kit::Fingerprint] the fingerprint to search for.
-      # @param use [Symbol] the type of certificates to look at. Can be `:signing` or `:encryption`.
-      # @return [Xml::Kit::Certificate] returns the matching `{Xml::Kit::Certificate}`
+      # @param fingerprint [Saml::Kit::Fingerprint] the fingerprint to search.
+      # @param use [Symbol] the type of certificates to look at.
+      # Can be `:signing` or `:encryption`.
+      # @return [Xml::Kit::Certificate] returns the matching
+      # `{Xml::Kit::Certificate}`
       def matches?(fingerprint, use: :signing)
-        certificates.find { |xxx| xxx.for?(use) && xxx.fingerprint == fingerprint }
-      end
-
-      # Returns the XML document converted to a Hash.
-      def to_h
-        @to_h ||= Hash.from_xml(to_xml)
-      end
-
-      # Returns the XML document as a String.
-      #
-      # @param pretty [Boolean] true to return a human friendly version of the XML.
-      def to_xml(pretty: nil)
-        pretty ? to_nokogiri.to_xml(indent: 2) : to_s
-      end
-
-      # Returns the XML document as a [String].
-      def to_s
-        @xml
+        certificates.find { |x| x.for?(use) && x.fingerprint == fingerprint }
       end
 
       # Verifies the signature and data using the signing certificates.
       #
-      # @param algorithm [OpenSSL::Digest] the digest algorithm to use. E.g. `OpenSSL::Digest::SHA256`
+      # @param algorithm [OpenSSL::Digest] the digest algorithm to use.
+      # E.g. `OpenSSL::Digest::SHA256`
       # @param signature [String] the signature to verify
       # @param data [String] the data that is used to produce the signature.
-      # @return [Xml::Kit::Certificate] the certificate that was used to produce the signature.
+      # @return [Xml::Kit::Certificate] the certificate that was used to
+      # produce the signature.
       def verify(algorithm, signature, data)
         signing_certificates.find do |certificate|
           certificate.public_key.verify(algorithm, signature, data)
         end
       end
 
-      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)
-          document = Nokogiri::XML(content)
-          return unless document.at_xpath('/md:EntityDescriptor', NAMESPACES)
-          sp = document.at_xpath('/md:EntityDescriptor/md:SPSSODescriptor', NAMESPACES)
-          idp = document.at_xpath('/md:EntityDescriptor/md:IDPSSODescriptor', NAMESPACES)
-          if sp && idp
-            Saml::Kit::CompositeMetadata.new(content)
-          elsif sp
-            Saml::Kit::ServiceProviderMetadata.new(content)
-          elsif idp
-            Saml::Kit::IdentityProviderMetadata.new(content)
-          end
-        end
-
-        # @!visibility private
-        def builder_class
-          Saml::Kit::Builders::Metadata
-        end
+      def signature(xpath = '/md:EntityDescriptor/ds:Signature')
+        @signature ||= Signature.new(at_xpath(xpath))
       end
 
-      private
-
-      attr_reader :xml
-
-      # @!visibility private
-      def to_nokogiri
-        @to_nokogiri ||= Nokogiri::XML(xml)
+      def self.from(content)
+        Saml::Kit::Parser.new.metadata_from(content)
       end
 
-      def at_xpath(xpath)
-        to_nokogiri.at_xpath(xpath, NAMESPACES)
+      def self.builder_class
+        Saml::Kit::Builders::Metadata
       end
 
-      def search(xpath)
-        to_nokogiri.search(xpath, NAMESPACES)
-      end
+      private
 
       def metadata
         at_xpath("/md:EntityDescriptor/md:#{name}").present?
@@ -234,9 +190,7 @@ module Saml
       end
 
       def must_have_valid_signature
-        return unless signature.present?
-
-        signature.valid?
+        return if !signature.present? || signature.valid?
         signature.errors.each do |attribute, error|
           errors[attribute] << error
         end
lib/saml/kit/namespaces.rb
@@ -18,7 +18,8 @@ module Saml
       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
+      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
lib/saml/kit/organization.rb
@@ -0,0 +1,36 @@
+module Saml
+  module Kit
+    class Organization
+      include XmlParseable
+
+      attr_reader :content
+
+      def initialize(node)
+        @to_nokogiri = node
+        @content = node.to_s
+      end
+
+      # Returns the Organization Name
+      def name
+        at_xpath('./md:OrganizationName').try(:text)
+      end
+
+      # Returns the Organization URL
+      def url
+        at_xpath('./md:OrganizationURL').try(:text)
+      end
+
+      # @deprecated
+      def organization_name
+        Saml::Kit.deprecate('`organization_name` is deprecated. Use `organization.name`')
+        name
+      end
+
+      # @deprecated
+      def organization_url
+        Saml::Kit.deprecate('`organization_url` is deprecated. Use `organization.url`')
+        url
+      end
+    end
+  end
+end
lib/saml/kit/parser.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Saml
+  module Kit
+    class Parser
+      # 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 metadata_from(content)
+        document = Nokogiri::XML(content)
+        return unless document.at_xpath('/md:EntityDescriptor', XmlParseable::NAMESPACES)
+
+        xpath = '/md:EntityDescriptor/md:SPSSODescriptor'
+        sp = document.at_xpath(xpath, XmlParseable::NAMESPACES)
+        xpath = '/md:EntityDescriptor/md:IDPSSODescriptor'
+        idp = document.at_xpath(xpath, XmlParseable::NAMESPACES)
+        if sp && idp
+          Saml::Kit::CompositeMetadata.new(content)
+        elsif sp
+          Saml::Kit::ServiceProviderMetadata.new(content)
+        elsif idp
+          Saml::Kit::IdentityProviderMetadata.new(content)
+        end
+      end
+    end
+  end
+end
lib/saml/kit/response.rb
@@ -14,7 +14,11 @@ module Saml
       validate :must_be_valid_assertion
       validate :must_contain_single_assertion
 
-      def initialize(xml, request_id: nil, configuration: Saml::Kit.configuration)
+      def initialize(
+        xml,
+        request_id: nil,
+        configuration: Saml::Kit.configuration
+      )
         @request_id = request_id
         super(xml, name: 'Response', configuration: configuration)
       end
@@ -26,7 +30,11 @@ module Saml
             if node.nil?
               Saml::Kit::NullAssertion.new
             else
-              Saml::Kit::Assertion.new(node, configuration: @configuration, private_keys: private_keys)
+              Saml::Kit::Assertion.new(
+                node,
+                configuration: @configuration,
+                private_keys: private_keys
+              )
             end
           end
       end
lib/saml/kit/service_provider_metadata.rb
@@ -25,7 +25,8 @@ module Saml
 
       # Returns true when the metadata demands that Assertions must be signed.
       def want_assertions_signed
-        attribute = at_xpath("/md:EntityDescriptor/md:#{name}").attribute('WantAssertionsSigned')
+        element = at_xpath("/md:EntityDescriptor/md:#{name}")
+        attribute = element.attribute('WantAssertionsSigned')
         return true if attribute.nil?
         attribute.text.casecmp('true').zero?
       end
lib/saml/kit/signature.rb
@@ -21,12 +21,14 @@ module Saml
 
       # Returns the embedded X509 Certificate
       def certificate
-        value = at_xpath('./ds:KeyInfo/ds:X509Data/ds:X509Certificate').try(:text)
+        xpath = './ds:KeyInfo/ds:X509Data/ds:X509Certificate'
+        value = at_xpath(xpath).try(:text)
         return if value.nil?
         ::Xml::Kit::Certificate.new(value, use: :signing)
       end
 
-      # Returns true when the fingerprint of the certificate matches one of the certificates registered in the metadata.
+      # Returns true when the fingerprint of the certificate matches one of
+      # the certificates registered in the metadata.
       def trusted?(metadata)
         return false if metadata.nil?
         metadata.matches?(certificate.fingerprint, use: :signing).present?
@@ -44,7 +46,8 @@ module Saml
       end
 
       def digest_method
-        at_xpath('./ds:SignedInfo/ds:Reference/ds:DigestMethod/@Algorithm').try(:value)
+        xpath = './ds:SignedInfo/ds:Reference/ds:DigestMethod/@Algorithm'
+        at_xpath(xpath).try(:value)
       end
 
       def signature_value
@@ -56,11 +59,20 @@ module Saml
       end
 
       def canonicalization_method
-        at_xpath('./ds:SignedInfo/ds:CanonicalizationMethod/@Algorithm').try(:value)
+        xpath = './ds:SignedInfo/ds:CanonicalizationMethod/@Algorithm'
+        at_xpath(xpath).try(:value)
       end
 
       def transforms
-        node.search('./ds:SignedInfo/ds:Reference/ds:Transforms/ds:Transform/@Algorithm', Saml::Kit::Document::NAMESPACES).try(:map, &:value)
+        xpath = xpath_for([
+          '.',
+          'ds:SignedInfo',
+          'ds:Reference',
+          'ds:Transforms',
+          'ds:Transform',
+          '@Algorithm',
+        ])
+        node.search(xpath, Saml::Kit::Document::NAMESPACES).try(:map, &:value)
       end
 
       # Returns the XML Hash.
@@ -115,6 +127,10 @@ module Saml
       def dsignature
         @dsignature ||= Xmldsig::Signature.new(node, 'ID=$uri or @Id')
       end
+
+      def xpath_for(segments)
+        segments.join('/')
+      end
     end
   end
 end
lib/saml/kit/version.rb
@@ -2,6 +2,6 @@
 
 module Saml
   module Kit
-    VERSION = '1.0.15'.freeze
+    VERSION = '1.0.16'.freeze
   end
 end
lib/saml/kit.rb
@@ -17,26 +17,33 @@ require 'securerandom'
 require 'uri'
 require 'xml/kit'
 
-require 'saml/kit/buildable'
+require 'saml/kit/concerns/buildable'
+require 'saml/kit/concerns/requestable'
+require 'saml/kit/concerns/respondable'
+require 'saml/kit/concerns/serializable'
+require 'saml/kit/concerns/translatable'
+require 'saml/kit/concerns/trustable'
+require 'saml/kit/concerns/xml_parseable'
+require 'saml/kit/concerns/xml_templatable'
+require 'saml/kit/concerns/xsd_validatable'
+
 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/attribute_statement'
 require 'saml/kit/authentication_request'
 require 'saml/kit/bindings'
+require 'saml/kit/conditions'
 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/organization'
+require 'saml/kit/parser'
 require 'saml/kit/composite_metadata'
 require 'saml/kit/response'
 require 'saml/kit/identity_provider_metadata'
spec/saml/kit/bindings/http_post_spec.rb
@@ -5,6 +5,8 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
 
   let(:location) { FFaker::Internet.uri('https') }
 
+  specify { expect(subject.binding).to eql(Saml::Kit::Bindings::HTTP_POST) }
+
   describe 'equality' do
     it 'is referentially equal' do
       expect(subject).to eql(subject)
spec/saml/kit/bindings/http_redirect_spec.rb
@@ -5,6 +5,8 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
 
   let(:location) { FFaker::Internet.http_url }
 
+  specify { expect(subject.binding).to eql(Saml::Kit::Bindings::HTTP_REDIRECT) }
+
   describe '#serialize' do
     let(:relay_state) { 'ECHO' }
     let(:configuration) do
spec/saml/kit/assertion_spec.rb
@@ -13,6 +13,7 @@ RSpec.describe Saml::Kit::Assertion do
 
   specify { expect(subject.issuer).to eql(entity_id) }
   specify { expect(subject.name_id).to eql(user.name_id) }
+  specify { expect(subject.name_id_format).to eql(Saml::Kit::Namespaces::PERSISTENT) }
   specify { expect(subject.started_at.to_i).to eql(Time.now.utc.to_i) }
   specify { expect(subject.expired_at.to_i).to eql(Saml::Kit.configuration.session_timeout.since(Time.now).utc.to_i) }
   specify { expect(subject.attributes).to eql('id' => user.attributes[:id]) }
@@ -52,7 +53,7 @@ RSpec.describe Saml::Kit::Assertion do
          </AuthnStatement>
         </Assertion>
       </Response>
-XML
+      XML
       document = Nokogiri::XML(xml)
       node = document.at_xpath('//saml:Assertion', 'saml' => Saml::Kit::Namespaces::ASSERTION)
       subject = described_class.new(node, configuration: configuration)
@@ -89,7 +90,7 @@ XML
          </AuthnStatement>
         </Assertion>
       </Response>
-XML
+      XML
       document = Nokogiri::XML(xml)
       node = document.at_xpath('//saml:Assertion', 'saml' => Saml::Kit::Namespaces::ASSERTION)
       subject = described_class.new(node, configuration: configuration)
@@ -129,7 +130,7 @@ XML
          </AuthnStatement>
         </Assertion>
         </Response>
-XML
+      XML
       subject = described_class.new(Nokogiri::XML(xml))
       expect(subject).to be_present
     end
spec/saml/kit/default_registry_spec.rb
@@ -7,11 +7,13 @@ RSpec.describe Saml::Kit::DefaultRegistry do
   let(:service_provider_metadata) do
     Saml::Kit::ServiceProviderMetadata.build do |builder|
       builder.entity_id = entity_id
+      builder.add_assertion_consumer_service(FFaker::Internet.uri('https'), binding: :http_post)
     end
   end
   let(:identity_provider_metadata) do
     Saml::Kit::IdentityProviderMetadata.build do |builder|
       builder.entity_id = entity_id
+      builder.add_single_sign_on_service(FFaker::Internet.uri('https'), binding: :http_post)
     end
   end
 
@@ -78,13 +80,47 @@ RSpec.describe Saml::Kit::DefaultRegistry do
     end
   end
 
+  describe '#register' do
+    it 'registers the metadata' do
+      metadata = Saml::Kit::IdentityProviderMetadata.build do |xxx|
+        xxx.entity_id = FFaker::Internet.uri('https')
+        xxx.add_single_sign_on_service(FFaker::Internet.uri('https'), binding: :http_post)
+      end
+      subject.register(metadata)
+      expect(subject.metadata_for(metadata.entity_id)).to eql(metadata)
+    end
+
+    it 'raises an error when the metadata is invalid' do
+      expect do
+        subject.register(Saml::Kit::IdentityProviderMetadata.build)
+      end.to raise_error(/Cannot register invalid metadata/)
+    end
+
+    it 'raises an error when the document is not a metadata' do
+      authn_request = Saml::Kit::AuthenticationRequest.build
+      allow(authn_request).to receive(:valid?).and_return(true)
+
+      expect do
+        subject.register(authn_request)
+      end.to raise_error(/Cannot register invalid metadata/)
+    end
+
+    it 'raises an error when the document is nil' do
+      expect do
+        subject.register(nil)
+      end.to raise_error(/Cannot register invalid metadata/)
+    end
+  end
+
   describe '#each' do
     it 'yields each registered metadata' do
-      idp = Saml::Kit::IdentityProviderMetadata.build do |config|
-        config.entity_id = 'idp'
+      idp = Saml::Kit::IdentityProviderMetadata.build do |xxx|
+        xxx.entity_id = 'idp'
+        xxx.add_single_sign_on_service(FFaker::Internet.uri('https'), binding: :http_post)
       end
-      sp = Saml::Kit::ServiceProviderMetadata.build do |config|
-        config.entity_id = 'sp'
+      sp = Saml::Kit::ServiceProviderMetadata.build do |xxx|
+        xxx.entity_id = 'sp'
+        xxx.add_assertion_consumer_service(FFaker::Internet.uri('https'), binding: :http_post)
       end
 
       subject.register(idp)
spec/saml/kit/metadata_spec.rb
@@ -49,10 +49,9 @@ RSpec.describe Saml::Kit::Metadata do
       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.single_sign_on_services.count).to eq(2)
+      expect(result.assertion_consumer_services.count).to eq(1)
+      expect(result.single_logout_services.count).to eq(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')
spec/saml/kit/response_spec.rb
@@ -471,7 +471,7 @@ RSpec.describe Saml::Kit::Response do
            </Attribute>
          </AttributeStatement>
         </Assertion>
-XML
+      XML
     end
 
     it 'parses the encrypted assertion' do
@@ -510,7 +510,7 @@ XML
             </xenc:EncryptedData>
           </saml:EncryptedAssertion>
         </samlp:Response>
-XML
+      XML
 
       subject = described_class.new(xml)
       expect(subject.attributes).to match_array([
.reek
@@ -1,125 +0,0 @@
----
-Attribute:
-  exclude:
-  - Saml::Kit::Assertion#occurred_at
-  - Saml::Kit::Builders::AuthenticationRequest#assertion_consumer_service_url
-  - Saml::Kit::Builders::AuthenticationRequest#destination
-  - Saml::Kit::Builders::AuthenticationRequest#id
-  - Saml::Kit::Builders::AuthenticationRequest#issuer
-  - Saml::Kit::Builders::AuthenticationRequest#name_id_format
-  - Saml::Kit::Builders::AuthenticationRequest#now
-  - Saml::Kit::Builders::AuthenticationRequest#version
-  - Saml::Kit::Builders::IdentityProviderMetadata#attributes
-  - Saml::Kit::Builders::IdentityProviderMetadata#name_id_formats
-  - Saml::Kit::Builders::IdentityProviderMetadata#want_authn_requests_signed
-  - Saml::Kit::Builders::LogoutRequest#destination
-  - Saml::Kit::Builders::LogoutRequest#id
-  - Saml::Kit::Builders::LogoutRequest#issuer
-  - Saml::Kit::Builders::LogoutRequest#name_id_format
-  - Saml::Kit::Builders::LogoutRequest#now
-  - Saml::Kit::Builders::LogoutRequest#version
-  - Saml::Kit::Builders::LogoutResponse#destination
-  - Saml::Kit::Builders::LogoutResponse#id
-  - Saml::Kit::Builders::LogoutResponse#issuer
-  - Saml::Kit::Builders::LogoutResponse#now
-  - Saml::Kit::Builders::LogoutResponse#status_code
-  - Saml::Kit::Builders::LogoutResponse#version
-  - Saml::Kit::Builders::Metadata#contact_email
-  - Saml::Kit::Builders::Metadata#entity_id
-  - Saml::Kit::Builders::Metadata#id
-  - Saml::Kit::Builders::Metadata#identity_provider
-  - Saml::Kit::Builders::Metadata#organization_name
-  - Saml::Kit::Builders::Metadata#organization_url
-  - Saml::Kit::Builders::Metadata#service_provider
-  - Saml::Kit::Builders::Response#destination
-  - Saml::Kit::Builders::Response#id
-  - Saml::Kit::Builders::Response#issuer
-  - Saml::Kit::Builders::Response#now
-  - Saml::Kit::Builders::Response#reference_id
-  - Saml::Kit::Builders::Response#status_code
-  - Saml::Kit::Builders::Response#status_message
-  - Saml::Kit::Builders::Response#version
-  - Saml::Kit::Builders::ServiceProviderMetadata#acs_urls
-  - Saml::Kit::Builders::ServiceProviderMetadata#logout_urls
-  - Saml::Kit::Builders::ServiceProviderMetadata#name_id_formats
-  - Saml::Kit::Builders::ServiceProviderMetadata#want_assertions_signed
-  - Saml::Kit::Configuration#clock_drift
-  - Saml::Kit::Configuration#digest_method
-  - Saml::Kit::Configuration#entity_id
-  - Saml::Kit::Configuration#logger
-  - Saml::Kit::Configuration#registry
-  - Saml::Kit::Configuration#session_timeout
-  - Saml::Kit::Configuration#signature_method
-  - Saml::Kit::Document#registry
-BooleanParameter:
-  exclude:
-  - Saml::Kit::DefaultRegistry#register_url
-  - Saml::Kit::DefaultRegistry::HttpApi#initialize
-ControlParameter:
-  exclude:
-  - Saml::Kit::Assertion#to_xml
-  - Saml::Kit::Bindings::HttpRedirect#algorithm_for
-  - Saml::Kit::Document#to_xml
-  - Saml::Kit::Metadata#to_xml
-  - Saml::Kit::Signature#to_xml
-TooManyInstanceVariables:
-  max_instance_variables: 15
-TooManyMethods:
-  max_methods: 30
-TooManyStatements:
-  max_statements: 10
-UtilityFunction:
-  exclude:
-  - Saml::Kit::Assertion#parse_date
-  - Saml::Kit::Bindings::HttpRedirect#algorithm_for
-  - Saml::Kit::Bindings::HttpRedirect#canonicalize
-  - Saml::Kit::Bindings::HttpRedirect#normalize
-  - Saml::Kit::Bindings::HttpRedirect#params_to_hash
-  - Saml::Kit::NullAssertion#expired_at
-  - Saml::Kit::NullAssertion#started_at
-  - uri_for
-  - xml_document
-  - Saml::Kit::Serializable#decode
-  - Saml::Kit::Serializable#deflate
-  - Saml::Kit::Serializable#encode
-  - Saml::Kit::Serializable#escape
-  - Saml::Kit::Serializable#inflate
-  - Saml::Kit::Serializable#unescape
-LongParameterList:
-  max_params: 4
-DuplicateMethodCall:
-  exclude:
-  - Saml::Kit::Bindings::Binding#saml_param_from
-  - Saml::Kit::Builders::Response#initialize
-  - Saml::Kit::XmlTemplatable#sign?
-FeatureEnvy:
-  exclude:
-  - Saml::Kit::Bindings::Binding#saml_param_from
-  - Saml::Kit::Bindings::HttpRedirect#serialize
-  - Saml::Kit::DefaultRegistry#register
-  - Saml::Kit::Metadata#matches?
-  - Saml::Kit::Signature#expected_digest_value
-  - Saml::Kit::Signature#trusted?
-NilCheck:
-  exclude:
-  - Saml::Kit::Bindings::HttpRedirect#ensure_valid_signature
-  - Saml::Kit::IdentityProviderMetadata#want_authn_requests_signed
-  - Saml::Kit::LogoutRequest#single_logout_service
-  - Saml::Kit::Respondable#must_match_request_id
-  - Saml::Kit::Response#assertion
-  - Saml::Kit::ServiceProviderMetadata#want_assertions_signed
-  - Saml::Kit::Signature#certificate
-  - Saml::Kit::Signature#trusted?
-  - Saml::Kit::Signature#validate_signature
-  - Saml::Kit::XmlTemplatable#sign?
-ManualDispatch:
-  exclude:
-  - Saml::Kit::Builders::Assertion#assertion_attributes
-  - Saml::Kit::CompositeMetadata#method_missing
-  - Saml::Kit::CompositeMetadata#respond_to_missing?
-UnusedParameters:
-  exclude:
-  - Saml::Kit::InvalidDocument#initialize
-TooManyConstants:
-  exclude:
-  - Saml::Kit::Namespaces
.rubocop.yml
@@ -14,6 +14,11 @@ AllCops:
     - 'vendor/**/*'
   TargetRubyVersion: 2.2
 
+Layout/AlignParameters:
+  Enabled: true
+  EnforcedStyle: with_fixed_indentation
+  IndentationWidth: 2
+
 Layout/ClassStructure:
   Enabled: true
   Categories:
@@ -39,6 +44,10 @@ Layout/IndentArray:
 Layout/IndentHeredoc:
   EnforcedStyle: active_support
 
+Layout/MultilineMethodCallIndentation:
+  Enabled: true
+  EnforcedStyle: indented
+
 Lint/AmbiguousBlockAssociation:
   Exclude:
     - 'spec/**/*.rb'
@@ -61,7 +70,10 @@ Metrics/ModuleLength:
 
 Metrics/LineLength:
   Exclude:
+    - 'lib/saml/kit/builders/templates/*.builder'
     - 'spec/**/*.rb'
+  IgnoredPatterns:
+    - '^#*'
 
 Naming/FileName:
   Exclude:
.rubocop_todo.yml
@@ -1,28 +1,16 @@
 # This configuration was generated by
 # `rubocop --auto-gen-config`
-# on 2018-02-16 22:08:54 -0700 using RuboCop version 0.52.1.
+# on 2018-03-16 13:38:09 -0600 using RuboCop version 0.53.0.
 # 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: 20
-
-# Offense count: 3
-# Configuration parameters: CountComments.
-Metrics/ClassLength:
-  Max: 136
+  Max: 19
 
-# Offense count: 6
+# Offense count: 11
 # Configuration parameters: CountComments.
 Metrics/MethodLength:
   Max: 13
@@ -30,16 +18,4 @@ Metrics/MethodLength:
 # 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
+    - 'lib/saml/kit/conditions.rb'
.travis.yml
@@ -2,10 +2,10 @@ sudo: false
 language: ruby
 cache: bundler
 rvm:
-  - 2.2.9
-  - 2.3.6
-  - 2.4.3
-  - 2.5.0
+  - 2.2.10
+  - 2.3.7
+  - 2.4.4
+  - 2.5.1
 script:
   - bin/cibuild
   - bin/lint
Rakefile
@@ -2,16 +2,12 @@
 
 require 'bundler/audit/task'
 require 'bundler/gem_tasks'
-require 'reek/rake/task'
 require 'rspec/core/rake_task'
 require 'rubocop/rake_task'
 
 RSpec::Core::RakeTask.new(:spec)
 RuboCop::RakeTask.new(:rubocop)
 Bundler::Audit::Task.new
-Reek::Rake::Task.new(:reek) do |task|
-  task.config_file = '.reek'
-end
 
-task lint: [:rubocop, :reek, 'bundle:audit']
+task lint: [:rubocop, 'bundle:audit']
 task default: :spec
saml-kit.gemspec
@@ -34,7 +34,6 @@ Gem::Specification.new do |spec|
   spec.add_development_dependency 'bundler-audit', '~> 0.6'
   spec.add_development_dependency 'ffaker', '~> 2.7'
   spec.add_development_dependency 'rake', '~> 10.0'
-  spec.add_development_dependency 'reek', '~> 4.8'
   spec.add_development_dependency 'rspec', '~> 3.0'
   spec.add_development_dependency 'rspec-benchmark', '~> 0.3'
   spec.add_development_dependency 'rubocop', '~> 0.52'