Comparing changes

v0.2.1 v0.2.2
54 commits 48 files changed

Commits

babd11b remove Cipher warning. mo 2017-12-02 03:24:29
23ea2e0 remove some duplication.g mo 2017-12-02 03:16:23
5d8a9e6 parse or many attributes mo 2017-12-02 03:13:22
aaf2156 add missing spec. mo 2017-12-02 02:47:12
21b4501 add missing specs. mo 2017-12-02 02:43:43
b26e13b add missing spec. mo 2017-12-02 02:37:50
8342132 add missing spec. mo 2017-12-02 02:35:41
7f080fc correct id generation. mo 2017-12-02 02:04:58
63f5705 add shorcut to registry. mo 2017-12-02 01:29:22
470b442 push up logout_request_for. mo 2017-12-01 23:05:13
cfcc423 expire cert after 30 days. mo 2017-11-30 23:47:36
96611d1 group file and env formats. mo 2017-11-30 23:38:59
5f50201 bump version. mo 2017-11-30 23:29:27
34c8745 deprecate old builder classes. mo 2017-11-30 21:03:10
1c52448 decouple from builders. mo 2017-11-30 18:04:57
b73fc75 decouple from builders. mo 2017-11-30 18:02:18
1155912 update spec to use .build api. mo 2017-11-30 17:49:31
5d26012 use new .build api. mo 2017-11-30 17:47:33
6520721 use the .build api. mo 2017-11-30 17:33:47
7d815da remove duplicate test. mo 2017-11-30 17:13:41
c89280b use builder api. mo 2017-11-30 17:02:27
43c59d0 start to use the new build api. mo 2017-11-30 16:58:25
b667cc2 provide .build api for clients. mo 2017-11-30 16:52:03
ca1538c make idp buildable. mo 2017-11-30 16:35:11
7b4ce78 move spec to proper file. mo 2017-11-30 16:22:19
df87f1d move response to new namespaces. mo 2017-11-30 16:21:11
dfd11e1 move logout response. mo 2017-11-30 16:16:31
b227074 add build api. mo 2017-11-29 21:12:19
fea786d merge describes. mo 2017-11-29 02:59:43
exe/saml-kit-create-self-signed-certificate
@@ -0,0 +1,21 @@
+#!/usr/bin/env ruby
+require 'saml/kit'
+
+password = STDIN.read.strip
+certificate, private_key = Saml::Kit::SelfSignedCertificate.new(password).create
+
+puts "** BEGIN File Format **"
+print certificate
+puts private_key
+puts "***********************"
+
+puts
+
+puts "*** BEGIN ENV Format **"
+puts certificate.inspect
+puts private_key.inspect
+puts "***********************"
+
+puts
+puts "Private Key Password:"
+puts password.inspect
lib/saml/kit/bindings/http_post.rb
@@ -9,7 +9,6 @@ module Saml
         end
 
         def serialize(builder, relay_state: nil)
-          builder.sign = true
           builder.destination = location
           document = builder.build
           saml_params = {
lib/saml/kit/builders/authentication_request.rb
@@ -0,0 +1,48 @@
+module Saml
+  module Kit
+    module Builders
+      class AuthenticationRequest
+        attr_accessor :id, :now, :issuer, :assertion_consumer_service_url, :name_id_format, :sign, :destination
+        attr_accessor :version
+
+        def initialize(configuration: Saml::Kit.configuration, sign: true)
+          @id = Id.generate
+          @issuer = configuration.issuer
+          @name_id_format = Namespaces::PERSISTENT
+          @now = Time.now.utc
+          @version = "2.0"
+          @sign = sign
+        end
+
+        def to_xml
+          Signature.sign(sign: sign) do |xml, signature|
+            xml.tag!('samlp:AuthnRequest', request_options) do
+              xml.tag!('saml:Issuer', issuer)
+              signature.template(id)
+              xml.tag!('samlp:NameIDPolicy', Format: name_id_format)
+            end
+          end
+        end
+
+        def build
+          Saml::Kit::AuthenticationRequest.new(to_xml)
+        end
+
+        private
+
+        def request_options
+          options = {
+            "xmlns:samlp" => Namespaces::PROTOCOL,
+            "xmlns:saml" => Namespaces::ASSERTION,
+            ID: id,
+            Version: version,
+            IssueInstant: now.utc.iso8601,
+            Destination: destination,
+          }
+          options[:AssertionConsumerServiceURL] = assertion_consumer_service_url if assertion_consumer_service_url.present?
+          options
+        end
+      end
+    end
+  end
+end
lib/saml/kit/builders/identity_provider_metadata.rb
@@ -0,0 +1,103 @@
+module Saml
+  module Kit
+    module Builders
+      class IdentityProviderMetadata
+        attr_accessor :id, :organization_name, :organization_url, :contact_email, :entity_id, :attributes, :name_id_formats
+        attr_accessor :want_authn_requests_signed, :sign
+        attr_reader :logout_urls, :single_sign_on_urls
+
+        def initialize(configuration = Saml::Kit.configuration)
+          @id = Id.generate
+          @entity_id = configuration.issuer
+          @attributes = []
+          @name_id_formats = [Namespaces::PERSISTENT]
+          @single_sign_on_urls = []
+          @logout_urls = []
+          @configuration = configuration
+          @sign = true
+          @want_authn_requests_signed = true
+        end
+
+        def add_single_sign_on_service(url, binding: :http_post)
+          @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))
+        end
+
+        def to_xml
+          Signature.sign(sign: sign) do |xml, signature|
+            xml.instruct!
+            xml.EntityDescriptor entity_descriptor_options do
+              signature.template(id)
+              xml.IDPSSODescriptor idp_sso_descriptor_options do
+                if @configuration.signing_certificate_pem.present?
+                  xml.KeyDescriptor use: "signing" do
+                    xml.KeyInfo "xmlns": Namespaces::XMLDSIG do
+                      xml.X509Data do
+                        xml.X509Certificate @configuration.stripped_signing_certificate
+                      end
+                    end
+                  end
+                end
+                if @configuration.encryption_certificate_pem.present?
+                  xml.KeyDescriptor use: "encryption" do
+                    xml.KeyInfo "xmlns": Namespaces::XMLDSIG do
+                      xml.X509Data do
+                        xml.X509Certificate @configuration.stripped_encryption_certificate
+                      end
+                    end
+                  end
+                end
+                logout_urls.each do |item|
+                  xml.SingleLogoutService Binding: item[:binding], Location: item[:location]
+                end
+                name_id_formats.each do |format|
+                  xml.NameIDFormat format
+                end
+                single_sign_on_urls.each do |item|
+                  xml.SingleSignOnService Binding: item[:binding], Location: item[:location]
+                end
+                attributes.each do |attribute|
+                  xml.tag! 'saml:Attribute', Name: attribute
+                end
+              end
+              xml.Organization do
+                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.Company "mailto:#{contact_email}"
+              end
+            end
+          end
+        end
+
+        def build
+          Saml::Kit::IdentityProviderMetadata.new(to_xml)
+        end
+
+        private
+
+        def entity_descriptor_options
+          {
+            'xmlns': Namespaces::METADATA,
+            'xmlns:ds': Namespaces::XMLDSIG,
+            'xmlns:saml': Namespaces::ASSERTION,
+            ID: id,
+            entityID: entity_id,
+          }
+        end
+
+        def idp_sso_descriptor_options
+          {
+            WantAuthnRequestsSigned: want_authn_requests_signed,
+            protocolSupportEnumeration: Namespaces::PROTOCOL,
+          }
+        end
+      end
+    end
+  end
+end
lib/saml/kit/builders/logout_request.rb
@@ -0,0 +1,55 @@
+module Saml
+  module Kit
+    module Builders
+      class LogoutRequest
+        attr_accessor :id, :destination, :issuer, :name_id_format, :now
+        attr_accessor :sign, :version
+        attr_reader :user
+
+        def initialize(user, configuration: Saml::Kit.configuration, sign: true)
+          @user = user
+          @id = "_#{SecureRandom.uuid}"
+          @issuer = configuration.issuer
+          @name_id_format = Saml::Kit::Namespaces::PERSISTENT
+          @now = Time.now.utc
+          @version = "2.0"
+          @sign = sign
+        end
+
+        def to_xml
+          Signature.sign(sign: sign) do |xml, signature|
+            xml.instruct!
+            xml.LogoutRequest logout_request_options do
+              xml.Issuer({ xmlns: Namespaces::ASSERTION }, issuer)
+              signature.template(id)
+              xml.NameID name_id_options, user.name_id_for(name_id_format)
+            end
+          end
+        end
+
+        def build
+          Saml::Kit::LogoutRequest.new(to_xml)
+        end
+
+        private
+
+        def logout_request_options
+          {
+            ID: id,
+            Version: version,
+            IssueInstant: now.utc.iso8601,
+            Destination: destination,
+            xmlns: Namespaces::PROTOCOL,
+          }
+        end
+
+        def name_id_options
+          {
+            Format: name_id_format,
+            xmlns: Namespaces::ASSERTION,
+          }
+        end
+      end
+    end
+  end
+end
lib/saml/kit/builders/logout_response.rb
@@ -0,0 +1,50 @@
+module Saml
+  module Kit
+    module Builders
+      class LogoutResponse
+        attr_accessor :id, :issuer, :version, :status_code, :sign, :now, :destination
+        attr_reader :request
+
+        def initialize(user, request, configuration: Saml::Kit.configuration, sign: true)
+          @user = user
+          @now = Time.now.utc
+          @request = request
+          @id = Id.generate
+          @version = "2.0"
+          @status_code = Namespaces::SUCCESS
+          @sign = sign
+          @issuer = configuration.issuer
+        end
+
+        def to_xml
+          Signature.sign(sign: sign) do |xml, signature|
+            xml.LogoutResponse logout_response_options do
+              xml.Issuer(issuer, xmlns: Namespaces::ASSERTION)
+              signature.template(id)
+              xml.Status do
+                xml.StatusCode Value: status_code
+              end
+            end
+          end
+        end
+
+        def build
+          Saml::Kit::LogoutResponse.new(to_xml, request_id: request.id)
+        end
+
+        private
+
+        def logout_response_options
+          {
+            xmlns: Namespaces::PROTOCOL,
+            ID: id,
+            Version: version,
+            IssueInstant: now.utc.iso8601,
+            Destination: destination,
+            InResponseTo: request.id,
+          }
+        end
+      end
+    end
+  end
+end
lib/saml/kit/builders/response.rb
@@ -0,0 +1,182 @@
+module Saml
+  module Kit
+    module Builders
+      class Response
+        attr_reader :user, :request
+        attr_accessor :id, :reference_id, :now
+        attr_accessor :version, :status_code
+        attr_accessor :issuer, :sign, :destination, :encrypt
+
+        def initialize(user, request)
+          @user = user
+          @request = request
+          @id = Id.generate
+          @reference_id = Id.generate
+          @now = Time.now.utc
+          @version = "2.0"
+          @status_code = Namespaces::SUCCESS
+          @issuer = configuration.issuer
+          @destination = destination_for(request)
+          @sign = want_assertions_signed
+          @encrypt = false
+        end
+
+        def want_assertions_signed
+          request.provider.want_assertions_signed
+        rescue => error
+          Saml::Kit.logger.error(error)
+          true
+        end
+
+        def to_xml
+          Signature.sign(sign: sign) do |xml, signature|
+            xml.Response response_options do
+              xml.Issuer(issuer, xmlns: Namespaces::ASSERTION)
+              signature.template(id)
+              xml.Status do
+                xml.StatusCode Value: status_code
+              end
+              assertion(xml, signature)
+            end
+          end
+        end
+
+        def build
+          Saml::Kit::Response.new(to_xml, request_id: request.id)
+        end
+
+        private
+
+        def assertion(xml, signature)
+          with_encryption(xml) do |xml|
+            xml.Assertion(assertion_options) do
+              xml.Issuer issuer
+              signature.template(reference_id) unless encrypt
+              xml.Subject do
+                xml.NameID user.name_id_for(request.name_id_format), Format: request.name_id_format
+                xml.SubjectConfirmation Method: Namespaces::BEARER do
+                  xml.SubjectConfirmationData "", subject_confirmation_data_options
+                end
+              end
+              xml.Conditions conditions_options do
+                xml.AudienceRestriction do
+                  xml.Audience request.issuer
+                end
+              end
+              xml.AuthnStatement authn_statement_options do
+                xml.AuthnContext do
+                  xml.AuthnContextClassRef Namespaces::PASSWORD
+                end
+              end
+              assertion_attributes = user.assertion_attributes_for(request)
+              if assertion_attributes.any?
+                xml.AttributeStatement do
+                  assertion_attributes.each do |key, value|
+                    xml.Attribute Name: key, NameFormat: Namespaces::URI, FriendlyName: key do
+                      xml.AttributeValue value.to_s
+                    end
+                  end
+                end
+              end
+            end
+          end
+        end
+
+        def with_encryption(xml)
+          if encrypt
+            temp = ::Builder::XmlMarkup.new
+            yield temp
+            raw_xml_to_encrypt = temp.target!
+
+            encryption_certificate = request.provider.encryption_certificates.first
+            public_key = encryption_certificate.public_key
+
+            cipher = OpenSSL::Cipher.new('AES-256-CBC')
+            cipher.encrypt
+            key = cipher.random_key
+            iv = cipher.random_iv
+            encrypted = cipher.update(raw_xml_to_encrypt) + cipher.final
+
+            Saml::Kit.logger.debug ['+iv', iv].inspect
+            Saml::Kit.logger.debug ['+key', key].inspect
+
+            xml.EncryptedAssertion xmlns: Namespaces::ASSERTION do
+              xml.EncryptedData xmlns: Namespaces::XMLENC do
+                xml.EncryptionMethod Algorithm: "http://www.w3.org/2001/04/xmlenc#aes256-cbc"
+                xml.KeyInfo xmlns: Namespaces::XMLDSIG do
+                  xml.EncryptedKey xmlns: Namespaces::XMLENC do
+                    xml.EncryptionMethod Algorithm: "http://www.w3.org/2001/04/xmlenc#rsa-1_5"
+                    xml.CipherData do
+                      xml.CipherValue Base64.encode64(public_key.public_encrypt(key))
+                    end
+                  end
+                end
+                xml.CipherData do
+                  xml.CipherValue Base64.encode64(iv + encrypted)
+                end
+              end
+            end
+          else
+            yield xml
+          end
+        end
+
+        def destination_for(request)
+          if request.signed? && request.trusted?
+            request.assertion_consumer_service_url || request.provider.assertion_consumer_service_for(binding: :http_post).try(:location)
+          else
+            request.provider.assertion_consumer_service_for(binding: :http_post).try(:location)
+          end
+        end
+
+        def configuration
+          Saml::Kit.configuration
+        end
+
+        def response_options
+          {
+            ID: id,
+            Version: version,
+            IssueInstant: now.iso8601,
+            Destination: destination,
+            Consent: Namespaces::UNSPECIFIED,
+            InResponseTo: request.id,
+            xmlns: Namespaces::PROTOCOL,
+          }
+        end
+
+        def assertion_options
+          {
+            ID: reference_id,
+            IssueInstant: now.iso8601,
+            Version: "2.0",
+            xmlns: Namespaces::ASSERTION,
+          }
+        end
+
+        def subject_confirmation_data_options
+          {
+            InResponseTo: request.id,
+            NotOnOrAfter: 3.hours.since(now).utc.iso8601,
+            Recipient: request.assertion_consumer_service_url,
+          }
+        end
+
+        def conditions_options
+          {
+            NotBefore: now.utc.iso8601,
+            NotOnOrAfter: Saml::Kit.configuration.session_timeout.from_now.utc.iso8601,
+          }
+        end
+
+        def authn_statement_options
+          {
+            AuthnInstant: now.iso8601,
+            SessionIndex: assertion_options[:ID],
+            SessionNotOnOrAfter: 3.hours.since(now).utc.iso8601,
+          }
+        end
+      end
+    end
+  end
+end
lib/saml/kit/builders/service_provider_metadata.rb
@@ -0,0 +1,89 @@
+module Saml
+  module Kit
+    module Builders
+      class ServiceProviderMetadata
+        attr_accessor :id, :entity_id, :acs_urls, :logout_urls, :name_id_formats, :sign
+        attr_accessor :want_assertions_signed
+
+        def initialize(configuration = Saml::Kit.configuration)
+          @id = Id.generate
+          @configuration = configuration
+          @entity_id = configuration.issuer
+          @acs_urls = []
+          @logout_urls = []
+          @name_id_formats = [Namespaces::PERSISTENT]
+          @sign = true
+          @want_assertions_signed = true
+        end
+
+        def add_assertion_consumer_service(url, binding: :http_post)
+          @acs_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))
+        end
+
+        def to_xml
+          Signature.sign(sign: sign) do |xml, signature|
+            xml.instruct!
+            xml.EntityDescriptor entity_descriptor_options do
+              signature.template(id)
+              xml.SPSSODescriptor descriptor_options do
+                if @configuration.signing_certificate_pem.present?
+                  xml.KeyDescriptor use: "signing" do
+                    xml.KeyInfo "xmlns": Namespaces::XMLDSIG do
+                      xml.X509Data do
+                        xml.X509Certificate @configuration.stripped_signing_certificate
+                      end
+                    end
+                  end
+                end
+                if @configuration.encryption_certificate_pem.present?
+                  xml.KeyDescriptor use: "encryption" do
+                    xml.KeyInfo "xmlns": Namespaces::XMLDSIG do
+                      xml.X509Data do
+                        xml.X509Certificate @configuration.stripped_encryption_certificate
+                      end
+                    end
+                  end
+                end
+                logout_urls.each do |item|
+                  xml.SingleLogoutService Binding: item[:binding], Location: item[:location]
+                end
+                name_id_formats.each do |format|
+                  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
+                end
+              end
+            end
+          end
+        end
+
+        def build
+          Saml::Kit::ServiceProviderMetadata.new(to_xml)
+        end
+
+        private
+
+        def entity_descriptor_options
+          {
+            'xmlns': Namespaces::METADATA,
+            ID: id,
+            entityID: entity_id,
+          }
+        end
+
+        def descriptor_options
+          {
+            AuthnRequestsSigned: sign,
+            WantAssertionsSigned: want_assertions_signed,
+            protocolSupportEnumeration: Namespaces::PROTOCOL,
+          }
+        end
+      end
+    end
+  end
+end
lib/saml/kit/authentication_request.rb
@@ -7,7 +7,7 @@ module Saml
         super(xml, name: "AuthnRequest")
       end
 
-      def acs_url
+      def assertion_consumer_service_url
         to_h[name]['AssertionConsumerServiceURL']
       end
 
@@ -15,54 +15,16 @@ module Saml
         to_h[name]['NameIDPolicy']['Format']
       end
 
-      def response_for(user)
-        Response::Builder.new(user, self)
-      end
-
-      private
-
-      class Builder
-        attr_accessor :id, :now, :issuer, :acs_url, :name_id_format, :sign, :destination
-        attr_accessor :version
-
-        def initialize(configuration: Saml::Kit.configuration, sign: true)
-          @id = SecureRandom.uuid
-          @issuer = configuration.issuer
-          @name_id_format = Namespaces::PERSISTENT
-          @now = Time.now.utc
-          @version = "2.0"
-          @sign = sign
-        end
-
-        def to_xml
-          Signature.sign(sign: sign) do |xml, signature|
-            xml.tag!('samlp:AuthnRequest', request_options) do
-              xml.tag!('saml:Issuer', issuer)
-              signature.template(id)
-              xml.tag!('samlp:NameIDPolicy', Format: name_id_format)
-            end
-          end
-        end
-
-        def build
-          AuthenticationRequest.new(to_xml)
-        end
-
-        private
-
-        def request_options
-          options = {
-            "xmlns:samlp" => Namespaces::PROTOCOL,
-            "xmlns:saml" => Namespaces::ASSERTION,
-            ID: "_#{id}",
-            Version: version,
-            IssueInstant: now.utc.iso8601,
-            Destination: destination,
-          }
-          options[:AssertionConsumerServiceURL] = acs_url if acs_url.present?
-          options
+      def response_for(user, binding:, relay_state: nil)
+        response_binding = provider.assertion_consumer_service_for(binding: binding)
+        builder = Saml::Kit::Response.builder(user, self) do |x|
+          x.sign = provider.want_assertions_signed
+          yield x if block_given?
         end
+        response_binding.serialize(builder, relay_state: relay_state)
       end
+
+      Builder = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Saml::Kit::AuthenticationRequest::Builder', 'Saml::Kit::Builders::AuthenticationRequest')
     end
   end
 end
lib/saml/kit/buildable.rb
@@ -0,0 +1,21 @@
+module Saml
+  module Kit
+    module Buildable
+      extend ActiveSupport::Concern
+
+      class_methods do
+        def build(*args)
+          builder(*args).tap do |x|
+            yield x if block_given?
+          end.build
+        end
+
+        def builder(*args)
+          builder_class.new(*args).tap do |builder|
+            yield builder if block_given?
+          end
+        end
+      end
+    end
+  end
+end
lib/saml/kit/builders.rb
@@ -0,0 +1,13 @@
+require 'saml/kit/builders/authentication_request'
+require 'saml/kit/builders/identity_provider_metadata'
+require 'saml/kit/builders/logout_request'
+require 'saml/kit/builders/logout_response'
+require 'saml/kit/builders/response'
+require 'saml/kit/builders/service_provider_metadata'
+
+module Saml
+  module Kit
+    module Builders
+    end
+  end
+end
lib/saml/kit/document.rb
@@ -5,6 +5,7 @@ module Saml
       include XsdValidatable
       include ActiveModel::Validations
       include Trustable
+      include Buildable
       validates_presence_of :content
       validates_presence_of :id
       validate :must_match_xsd
@@ -76,6 +77,21 @@ module Saml
           Saml::Kit.logger.error(error)
           InvalidDocument.new(xml)
         end
+
+        def builder_class
+          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.new("Unknown SAML Document #{name}")
+          end
+        end
       end
 
       private
lib/saml/kit/id.rb
@@ -0,0 +1,9 @@
+module Saml
+  module Kit
+    class Id
+      def self.generate
+        "_#{SecureRandom.uuid}"
+      end
+    end
+  end
+end
lib/saml/kit/identity_provider_metadata.rb
@@ -29,105 +29,20 @@ module Saml
         end
       end
 
-      private
-
-      class Builder
-        attr_accessor :id, :organization_name, :organization_url, :contact_email, :entity_id, :attributes, :name_id_formats
-        attr_accessor :want_authn_requests_signed, :sign
-        attr_reader :logout_urls, :single_sign_on_urls
-
-        def initialize(configuration = Saml::Kit.configuration)
-          @id = SecureRandom.uuid
-          @entity_id = configuration.issuer
-          @attributes = []
-          @name_id_formats = [Namespaces::PERSISTENT]
-          @single_sign_on_urls = []
-          @logout_urls = []
-          @configuration = configuration
-          @sign = true
-          @want_authn_requests_signed = true
-        end
-
-        def add_single_sign_on_service(url, binding: :http_post)
-          @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))
-        end
-
-        def to_xml
-          Signature.sign(sign: sign) do |xml, signature|
-            xml.instruct!
-            xml.EntityDescriptor entity_descriptor_options do
-              signature.template(id)
-              xml.IDPSSODescriptor idp_sso_descriptor_options do
-                if @configuration.signing_certificate_pem.present?
-                  xml.KeyDescriptor use: "signing" do
-                    xml.KeyInfo "xmlns": Namespaces::XMLDSIG do
-                      xml.X509Data do
-                        xml.X509Certificate @configuration.stripped_signing_certificate
-                      end
-                    end
-                  end
-                end
-                if @configuration.encryption_certificate_pem.present?
-                  xml.KeyDescriptor use: "encryption" do
-                    xml.KeyInfo "xmlns": Namespaces::XMLDSIG do
-                      xml.X509Data do
-                        xml.X509Certificate @configuration.stripped_encryption_certificate
-                      end
-                    end
-                  end
-                end
-                logout_urls.each do |item|
-                  xml.SingleLogoutService Binding: item[:binding], Location: item[:location]
-                end
-                name_id_formats.each do |format|
-                  xml.NameIDFormat format
-                end
-                single_sign_on_urls.each do |item|
-                  xml.SingleSignOnService Binding: item[:binding], Location: item[:location]
-                end
-                attributes.each do |attribute|
-                  xml.tag! 'saml:Attribute', Name: attribute
-                end
-              end
-              xml.Organization do
-                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.Company "mailto:#{contact_email}"
-              end
-            end
-          end
-        end
-
-        def build
-          IdentityProviderMetadata.new(to_xml)
-        end
-
-        private
-
-        def entity_descriptor_options
-          {
-            'xmlns': Namespaces::METADATA,
-            'xmlns:ds': Namespaces::XMLDSIG,
-            'xmlns:saml': Namespaces::ASSERTION,
-            ID: "_#{id}",
-            entityID: entity_id,
-          }
+      def login_request_for(binding:, relay_state: nil)
+        builder = Saml::Kit::AuthenticationRequest.builder do |x|
+          x.sign = 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
 
-        def idp_sso_descriptor_options
-          {
-            WantAuthnRequestsSigned: want_authn_requests_signed,
-            protocolSupportEnumeration: Namespaces::PROTOCOL,
-          }
-        end
+      def self.builder_class
+        Saml::Kit::Builders::IdentityProviderMetadata
       end
+
+      Builder = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Saml::Kit::IdentityProviderMetadata::Builder', 'Saml::Kit::Builders::IdentityProviderMetadata')
     end
   end
 end
lib/saml/kit/logout_request.rb
@@ -18,61 +18,15 @@ module Saml
         urls.first
       end
 
-      def response_for(user)
-        LogoutResponse::Builder.new(user, self)
-      end
-
-      private
-
-      class Builder
-        attr_accessor :id, :destination, :issuer, :name_id_format, :now
-        attr_accessor :sign, :version
-        attr_reader :user
-
-        def initialize(user, configuration: Saml::Kit.configuration, sign: true)
-          @user = user
-          @id = SecureRandom.uuid
-          @issuer = configuration.issuer
-          @name_id_format = Saml::Kit::Namespaces::PERSISTENT
-          @now = Time.now.utc
-          @version = "2.0"
-          @sign = sign
-        end
-
-        def to_xml
-          Signature.sign(sign: sign) do |xml, signature|
-            xml.instruct!
-            xml.LogoutRequest logout_request_options do
-              xml.Issuer({ xmlns: Namespaces::ASSERTION }, issuer)
-              signature.template(id)
-              xml.NameID name_id_options, user.name_id_for(name_id_format)
-            end
-          end
-        end
-
-        def build
-          Saml::Kit::LogoutRequest.new(to_xml)
-        end
-
-        private
-
-        def logout_request_options
-          {
-            ID: "_#{id}",
-            Version: version,
-            IssueInstant: now.utc.iso8601,
-            Destination: destination,
-            xmlns: Namespaces::PROTOCOL,
-          }
-        end
-
-        def name_id_options
-          {
-            Format: name_id_format,
-            xmlns: Namespaces::ASSERTION,
-          }
+      def response_for(user, binding:, relay_state: nil)
+        builder = Saml::Kit::LogoutResponse.builder(user, self) do |x|
+          yield x if block_given?
         end
+        response_binding = provider.single_logout_service_for(binding: binding)
+        response_binding.serialize(builder, relay_state: relay_state)
       end
+
+      Builder = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Saml::Kit::LogoutRequest::Builder', 'Saml::Kit::Builders::LogoutRequest')
     end
   end
 end
lib/saml/kit/logout_response.rb
@@ -8,56 +8,7 @@ module Saml
         super(xml, name: "LogoutResponse")
       end
 
-      private
-
-      class Builder
-        attr_accessor :id, :issuer, :version, :status_code, :sign, :now, :destination
-        attr_reader :request
-
-        def initialize(user, request, configuration: Saml::Kit.configuration, sign: true)
-          @user = user
-          @now = Time.now.utc
-          @request = request
-          @id = SecureRandom.uuid
-          @version = "2.0"
-          @status_code = Namespaces::SUCCESS
-          @sign = sign
-          @issuer = configuration.issuer
-          provider = configuration.registry.metadata_for(@issuer)
-          if provider
-            @destination = provider.single_logout_service_for(binding: :http_post).try(:location)
-          end
-        end
-
-        def to_xml
-          Signature.sign(sign: sign) do |xml, signature|
-            xml.LogoutResponse logout_response_options do
-              xml.Issuer(issuer, xmlns: Namespaces::ASSERTION)
-              signature.template(id)
-              xml.Status do
-                xml.StatusCode Value: status_code
-              end
-            end
-          end
-        end
-
-        def build
-          LogoutResponse.new(to_xml, request_id: request.id)
-        end
-
-        private
-
-        def logout_response_options
-          {
-            xmlns: Namespaces::PROTOCOL,
-            ID: "_#{id}",
-            Version: version,
-            IssueInstant: now.utc.iso8601,
-            Destination: destination,
-            InResponseTo: request.id,
-          }
-        end
-      end
+      Builder = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Saml::Kit::LogoutResponse::Builder', 'Saml::Kit::Builders::LogoutResponse')
     end
   end
 end
lib/saml/kit/metadata.rb
@@ -3,6 +3,7 @@ module Saml
     class Metadata
       include ActiveModel::Validations
       include XsdValidatable
+      include Buildable
       METADATA_XSD = File.expand_path("./xsd/saml-schema-metadata-2.0.xsd", File.dirname(__FILE__)).freeze
 
       validates_presence_of :metadata
@@ -63,6 +64,14 @@ module Saml
         service_for(binding: binding, type: 'SingleLogoutService')
       end
 
+      def logout_request_for(user, binding: :http_post, relay_state: nil)
+        builder = Saml::Kit::LogoutRequest.builder(user) do |x|
+          yield x if block_given?
+        end
+        request_binding = single_logout_service_for(binding: binding)
+        request_binding.serialize(builder, relay_state: relay_state)
+      end
+
       def matches?(fingerprint, use: :signing)
         certificates.find do |certificate|
           certificate.for?(use) && certificate.fingerprint == fingerprint
lib/saml/kit/response.rb
@@ -20,11 +20,16 @@ module Saml
       end
 
       def attributes
-        @attributes ||= Hash[
-          assertion.fetch('AttributeStatement', {}).fetch('Attribute', []).map do |item|
-            [item['Name'].to_sym, item['AttributeValue']]
+        @attributes ||=
+          begin
+            attrs = assertion.fetch('AttributeStatement', {}).fetch('Attribute', [])
+            items = if attrs.is_a? Hash
+              [[attrs["Name"], attrs["AttributeValue"]]]
+            else
+              attrs.map { |item| [item['Name'], item['AttributeValue']] }
+            end
+            Hash[items].with_indifferent_access
           end
-        ].with_indifferent_access
       end
 
       def started_at
@@ -51,7 +56,7 @@ module Saml
         @assertion =
           begin
             if encrypted?
-              decrypted = Cryptography.new.decrypt(to_h.fetch(name, {}).fetch('EncryptedAssertion', {}))
+              decrypted = XmlDecryption.new.decrypt(to_h.fetch(name, {}).fetch('EncryptedAssertion', {}))
               Saml::Kit.logger.debug(decrypted)
               Hash.from_xml(decrypted)['Assertion']
             else
@@ -99,182 +104,7 @@ module Saml
         Time.at(0).to_datetime
       end
 
-      class Builder
-        attr_reader :user, :request
-        attr_accessor :id, :reference_id, :now
-        attr_accessor :version, :status_code
-        attr_accessor :issuer, :sign, :destination, :encrypt
-
-        def initialize(user, request)
-          @user = user
-          @request = request
-          @id = SecureRandom.uuid
-          @reference_id = SecureRandom.uuid
-          @now = Time.now.utc
-          @version = "2.0"
-          @status_code = Namespaces::SUCCESS
-          @issuer = configuration.issuer
-          @destination = destination_for(request)
-          @sign = want_assertions_signed
-          @encrypt = false
-        end
-
-        def want_assertions_signed
-          request.provider.want_assertions_signed
-        rescue => error
-          Saml::Kit.logger.error(error)
-          true
-        end
-
-        def to_xml
-          Signature.sign(sign: sign) do |xml, signature|
-            xml.Response response_options do
-              xml.Issuer(issuer, xmlns: Namespaces::ASSERTION)
-              signature.template(id)
-              xml.Status do
-                xml.StatusCode Value: status_code
-              end
-              assertion(xml, signature)
-            end
-          end
-        end
-
-        def build
-          Response.new(to_xml, request_id: request.id)
-        end
-
-        private
-
-        def assertion(xml, signature)
-          with_encryption(xml) do |xml|
-            xml.Assertion(assertion_options) do
-              xml.Issuer issuer
-              signature.template(reference_id) unless encrypt
-              xml.Subject do
-                xml.NameID user.name_id_for(request.name_id_format), Format: request.name_id_format
-                xml.SubjectConfirmation Method: Namespaces::BEARER do
-                  xml.SubjectConfirmationData "", subject_confirmation_data_options
-                end
-              end
-              xml.Conditions conditions_options do
-                xml.AudienceRestriction do
-                  xml.Audience request.issuer
-                end
-              end
-              xml.AuthnStatement authn_statement_options do
-                xml.AuthnContext do
-                  xml.AuthnContextClassRef Namespaces::PASSWORD
-                end
-              end
-              assertion_attributes = user.assertion_attributes_for(request)
-              if assertion_attributes.any?
-                xml.AttributeStatement do
-                  assertion_attributes.each do |key, value|
-                    xml.Attribute Name: key, NameFormat: Namespaces::URI, FriendlyName: key do
-                      xml.AttributeValue value.to_s
-                    end
-                  end
-                end
-              end
-            end
-          end
-        end
-
-        def with_encryption(xml)
-          if encrypt
-            temp = ::Builder::XmlMarkup.new
-            yield temp
-            raw_xml_to_encrypt = temp.target!
-
-            encryption_certificate = request.provider.encryption_certificates.first
-            public_key = encryption_certificate.public_key
-
-            cipher = OpenSSL::Cipher.new('AES-256-CBC')
-            cipher.encrypt
-            key = cipher.random_key
-            iv = cipher.random_iv
-            encrypted = cipher.update(raw_xml_to_encrypt) + cipher.final
-
-            Saml::Kit.logger.debug ['+iv', iv].inspect
-            Saml::Kit.logger.debug ['+key', key].inspect
-
-            xml.EncryptedAssertion xmlns: Namespaces::ASSERTION do
-              xml.EncryptedData xmlns: Namespaces::XMLENC do
-                xml.EncryptionMethod Algorithm: "http://www.w3.org/2001/04/xmlenc#aes256-cbc"
-                xml.KeyInfo xmlns: Namespaces::XMLDSIG do
-                  xml.EncryptedKey xmlns: Namespaces::XMLENC do
-                    xml.EncryptionMethod Algorithm: "http://www.w3.org/2001/04/xmlenc#rsa-1_5"
-                    xml.CipherData do
-                      xml.CipherValue Base64.encode64(public_key.public_encrypt(key))
-                    end
-                  end
-                end
-                xml.CipherData do
-                  xml.CipherValue Base64.encode64(iv + encrypted)
-                end
-              end
-            end
-          else
-            yield xml
-          end
-        end
-
-        def destination_for(request)
-          if request.signed? && request.trusted?
-            request.acs_url || request.provider.assertion_consumer_service_for(binding: :http_post).try(:location)
-          else
-            request.provider.assertion_consumer_service_for(binding: :http_post).try(:location)
-          end
-        end
-
-        def configuration
-          Saml::Kit.configuration
-        end
-
-        def response_options
-          {
-            ID: id.present? ? "_#{id}" : nil,
-            Version: version,
-            IssueInstant: now.iso8601,
-            Destination: destination,
-            Consent: Namespaces::UNSPECIFIED,
-            InResponseTo: request.id,
-            xmlns: Namespaces::PROTOCOL,
-          }
-        end
-
-        def assertion_options
-          {
-            ID: "_#{reference_id}",
-            IssueInstant: now.iso8601,
-            Version: "2.0",
-            xmlns: Namespaces::ASSERTION,
-          }
-        end
-
-        def subject_confirmation_data_options
-          {
-            InResponseTo: request.id,
-            NotOnOrAfter: 3.hours.since(now).utc.iso8601,
-            Recipient: request.acs_url,
-          }
-        end
-
-        def conditions_options
-          {
-            NotBefore: now.utc.iso8601,
-            NotOnOrAfter: Saml::Kit.configuration.session_timeout.from_now.utc.iso8601,
-          }
-        end
-
-        def authn_statement_options
-          {
-            AuthnInstant: now.iso8601,
-            SessionIndex: assertion_options[:ID],
-            SessionNotOnOrAfter: 3.hours.since(now).utc.iso8601,
-          }
-        end
-      end
+      Builder = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Saml::Kit::Response::Builder', 'Saml::Kit::Builders::Response')
     end
   end
 end
lib/saml/kit/self_signed_certificate.rb
@@ -1,6 +1,8 @@
 module Saml
   module Kit
     class SelfSignedCertificate
+      SUBJECT="/C=CA/ST=Alberta/L=Calgary/O=SamlKit/OU=SamlKit/CN=SamlKit"
+
       def initialize(password)
         @password = password
       end
@@ -9,20 +11,25 @@ module Saml
         rsa_key = OpenSSL::PKey::RSA.new(2048)
         public_key = rsa_key.public_key
         certificate = OpenSSL::X509::Certificate.new
-        certificate.subject = certificate.issuer = OpenSSL::X509::Name.parse("/C=CA/ST=Alberta/L=Calgary/O=Xsig/OU=Xsig/CN=Xsig")
+        certificate.subject = certificate.issuer = OpenSSL::X509::Name.parse(SUBJECT)
         certificate.not_before = DateTime.now.beginning_of_day
-        certificate.not_after = 1.year.from_now.end_of_day
+        certificate.not_after = 30.days.from_now
         certificate.public_key = public_key
         certificate.serial = 0x0
         certificate.version = 2
         factory = OpenSSL::X509::ExtensionFactory.new
         factory.subject_certificate = factory.issuer_certificate = certificate
-        certificate.extensions = [ factory.create_extension("basicConstraints","CA:TRUE", true), factory.create_extension("subjectKeyIdentifier", "hash"), ]
-        certificate.add_extension(factory.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always"))
+        certificate.extensions = [
+          factory.create_extension("basicConstraints","CA:TRUE", true),
+          factory.create_extension("subjectKeyIdentifier", "hash"),
+        ]
+        certificate.add_extension(
+          factory.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always")
+        )
         certificate.sign(rsa_key, OpenSSL::Digest::SHA256.new)
         [
           certificate.to_pem,
-          rsa_key.to_pem(OpenSSL::Cipher::Cipher.new('des3'), @password)
+          rsa_key.to_pem(OpenSSL::Cipher.new('AES-256-CBC'), @password)
         ]
       end
     end
lib/saml/kit/service_provider_metadata.rb
@@ -15,92 +15,15 @@ module Saml
 
       def want_assertions_signed
         attribute = document.find_by("/md:EntityDescriptor/md:#{name}").attribute("WantAssertionsSigned")
+        return true if attribute.nil?
         attribute.text.downcase == "true"
       end
 
-      class Builder
-        attr_accessor :id, :entity_id, :acs_urls, :logout_urls, :name_id_formats, :sign
-        attr_accessor :want_assertions_signed
-
-        def initialize(configuration = Saml::Kit.configuration)
-          @id = SecureRandom.uuid
-          @configuration = configuration
-          @entity_id = configuration.issuer
-          @acs_urls = []
-          @logout_urls = []
-          @name_id_formats = [Namespaces::PERSISTENT]
-          @sign = true
-          @want_assertions_signed = true
-        end
-
-        def add_assertion_consumer_service(url, binding: :http_post)
-          @acs_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))
-        end
-
-        def to_xml
-          Signature.sign(sign: sign) do |xml, signature|
-            xml.instruct!
-            xml.EntityDescriptor entity_descriptor_options do
-              signature.template(id)
-              xml.SPSSODescriptor descriptor_options do
-                if @configuration.signing_certificate_pem.present?
-                  xml.KeyDescriptor use: "signing" do
-                    xml.KeyInfo "xmlns": Namespaces::XMLDSIG do
-                      xml.X509Data do
-                        xml.X509Certificate @configuration.stripped_signing_certificate
-                      end
-                    end
-                  end
-                end
-                if @configuration.encryption_certificate_pem.present?
-                  xml.KeyDescriptor use: "encryption" do
-                    xml.KeyInfo "xmlns": Namespaces::XMLDSIG do
-                      xml.X509Data do
-                        xml.X509Certificate @configuration.stripped_encryption_certificate
-                      end
-                    end
-                  end
-                end
-                logout_urls.each do |item|
-                  xml.SingleLogoutService Binding: item[:binding], Location: item[:location]
-                end
-                name_id_formats.each do |format|
-                  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
-                end
-              end
-            end
-          end
-        end
-
-        def build
-          ServiceProviderMetadata.new(to_xml)
-        end
-
-        private
-
-        def entity_descriptor_options
-          {
-            'xmlns': Namespaces::METADATA,
-            ID: "_#{id}",
-            entityID: entity_id,
-          }
-        end
-
-        def descriptor_options
-          {
-            AuthnRequestsSigned: sign,
-            WantAssertionsSigned: want_assertions_signed,
-            protocolSupportEnumeration: Namespaces::PROTOCOL,
-          }
-        end
+      def self.builder_class
+        Saml::Kit::Builders::ServiceProviderMetadata
       end
+
+      Builder = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Saml::Kit::ServiceProviderMetadata::Builder', 'Saml::Kit::Builders::ServiceProviderMetadata')
     end
   end
 end
lib/saml/kit/signature.rb
@@ -34,7 +34,7 @@ module Saml
           xml.SignedInfo do
             xml.CanonicalizationMethod Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#"
             xml.SignatureMethod Algorithm: SIGNATURE_METHODS[configuration.signature_method]
-            xml.Reference URI: "#_#{reference_id}" do
+            xml.Reference URI: "##{reference_id}" do
               xml.Transforms do
                 xml.Transform Algorithm: "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
                 xml.Transform Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#"
lib/saml/kit/trustable.rb
@@ -30,11 +30,7 @@ module Saml
       end
 
       def provider
-        registry.metadata_for(issuer)
-      end
-
-      def registry
-        Saml::Kit.configuration.registry
+        Saml::Kit.registry.metadata_for(issuer)
       end
 
       def signature_verified!
lib/saml/kit/version.rb
@@ -1,5 +1,5 @@
 module Saml
   module Kit
-    VERSION = "0.2.1"
+    VERSION = "0.2.2"
   end
 end
lib/saml/kit/cryptography.rb → lib/saml/kit/xml_decryption.rb
@@ -1,6 +1,6 @@
 module Saml
   module Kit
-    class Cryptography
+    class XmlDecryption
       attr_reader :private_key
 
       def initialize(private_key = Saml::Kit.configuration.encryption_private_key)
lib/saml/kit.rb
@@ -5,6 +5,7 @@ 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 "builder"
 require "logger"
@@ -13,6 +14,8 @@ require "nokogiri"
 require "securerandom"
 require "xmldsig"
 
+require "saml/kit/buildable"
+require "saml/kit/builders"
 require "saml/kit/namespaces"
 require "saml/kit/serializable"
 require "saml/kit/xsd_validatable"
@@ -26,19 +29,20 @@ require "saml/kit/bindings"
 require "saml/kit/certificate"
 require "saml/kit/configuration"
 require "saml/kit/crypto"
-require "saml/kit/cryptography"
 require "saml/kit/default_registry"
 require "saml/kit/fingerprint"
 require "saml/kit/logout_response"
 require "saml/kit/logout_request"
 require "saml/kit/metadata"
 require "saml/kit/response"
+require "saml/kit/id"
 require "saml/kit/identity_provider_metadata"
 require "saml/kit/invalid_document"
 require "saml/kit/self_signed_certificate"
 require "saml/kit/service_provider_metadata"
 require "saml/kit/signature"
 require "saml/kit/xml"
+require "saml/kit/xml_decryption"
 
 I18n.load_path += Dir[File.expand_path("kit/locales/*.yml", File.dirname(__FILE__))]
 
@@ -56,6 +60,10 @@ module Saml
       def logger
         configuration.logger
       end
+
+      def registry
+        configuration.registry
+      end
     end
   end
 end
spec/saml/bindings/http_post_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
     let(:relay_state) { "ECHO" }
 
     it 'encodes the request using the HTTP-POST encoding for a AuthenticationRequest' do
-      builder = Saml::Kit::AuthenticationRequest::Builder.new
+      builder = Saml::Kit::AuthenticationRequest.builder_class.new
       url, saml_params = subject.serialize(builder, relay_state: relay_state)
 
       expect(url).to eql(location)
@@ -22,7 +22,7 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
 
     it 'returns a SAMLRequest for a LogoutRequest' do
       user = double(:user, name_id_for: SecureRandom.uuid)
-      builder = Saml::Kit::LogoutRequest::Builder.new(user)
+      builder = Saml::Kit::LogoutRequest.builder_class.new(user)
       url, saml_params = subject.serialize(builder, relay_state: relay_state)
 
       expect(url).to eql(location)
@@ -37,7 +37,7 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
     it 'returns a SAMLResponse for a LogoutResponse' do
       user = double(:user, name_id_for: SecureRandom.uuid)
       request = instance_double(Saml::Kit::AuthenticationRequest, id: SecureRandom.uuid)
-      builder = Saml::Kit::LogoutResponse::Builder.new(user, request)
+      builder = Saml::Kit::LogoutResponse.builder_class.new(user, request)
       url, saml_params = subject.serialize(builder, relay_state: relay_state)
 
       expect(url).to eql(location)
@@ -50,7 +50,7 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
     end
 
     it 'excludes the RelayState when blank' do
-      builder = Saml::Kit::AuthenticationRequest::Builder.new
+      builder = Saml::Kit::AuthenticationRequest.builder_class.new
       url, saml_params = subject.serialize(builder)
 
       expect(url).to eql(location)
@@ -60,7 +60,7 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
 
   describe "#deserialize" do
     it 'deserializes to an AuthnRequest' do
-      builder = Saml::Kit::AuthenticationRequest::Builder.new
+      builder = Saml::Kit::AuthenticationRequest.builder_class.new
       _, params = subject.serialize(builder)
       result = subject.deserialize(params)
       expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
@@ -68,7 +68,7 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
 
     it 'deserializes to a LogoutRequest' do
       user = double(:user, name_id_for: SecureRandom.uuid)
-      builder = Saml::Kit::LogoutRequest::Builder.new(user)
+      builder = Saml::Kit::LogoutRequest.builder_class.new(user)
       _, params = subject.serialize(builder)
       result = subject.deserialize(params)
       expect(result).to be_instance_of(Saml::Kit::LogoutRequest)
@@ -76,8 +76,8 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
 
     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, acs_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.new(user, request)
+      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)
+      builder = Saml::Kit::Response.builder_class.new(user, request)
       _, params = subject.serialize(builder)
       result = subject.deserialize(params)
       expect(result).to be_instance_of(Saml::Kit::Response)
spec/saml/bindings/http_redirect_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
     let(:relay_state) { "ECHO" }
 
     it 'encodes the request using the HTTP-Redirect encoding' do
-      builder = Saml::Kit::AuthenticationRequest::Builder.new
+      builder = Saml::Kit::AuthenticationRequest.builder_class.new
       url, _ = subject.serialize(builder, relay_state: relay_state)
       expect(url).to start_with(location)
       expect(url).to have_query_param('SAMLRequest')
@@ -19,7 +19,7 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
 
   describe "#deserialize" do
     let(:issuer) { FFaker::Internet.http_url }
-    let(:provider) { Saml::Kit::IdentityProviderMetadata::Builder.new.build }
+    let(:provider) { Saml::Kit::IdentityProviderMetadata.build }
 
     before :each do
       allow(Saml::Kit.configuration.registry).to receive(:metadata_for).with(issuer).and_return(provider)
@@ -27,14 +27,14 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
     end
 
     it 'deserializes the SAMLRequest to an AuthnRequest' do
-      url, _ = subject.serialize(Saml::Kit::AuthenticationRequest::Builder.new)
+      url, _ = subject.serialize(Saml::Kit::AuthenticationRequest.builder_class.new)
       result = subject.deserialize(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.new(user))
+      url, _ = subject.serialize(Saml::Kit::LogoutRequest.builder_class.new(user))
       result = subject.deserialize(query_params_from(url))
       expect(result).to be_instance_of(Saml::Kit::LogoutRequest)
     end
@@ -47,16 +47,16 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
 
     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, acs_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: issuer, signed?: true, trusted?: true)
-      url, _ = subject.serialize(Saml::Kit::Response::Builder.new(user, request))
+      request = double(:request, id: SecureRandom.uuid, provider: nil, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: issuer, signed?: true, trusted?: true)
+      url, _ = subject.serialize(Saml::Kit::Response.builder_class.new(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
       user = double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: [])
-      request = double(:request, id: SecureRandom.uuid, provider: provider, acs_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: FFaker::Internet.http_url)
-      url, _ = subject.serialize(Saml::Kit::LogoutResponse::Builder.new(user, request))
+      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_class.new(user, request))
       result = subject.deserialize(query_params_from(url))
       expect(result).to be_instance_of(Saml::Kit::LogoutResponse)
     end
@@ -74,7 +74,7 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
     end
 
     it 'raises an error when the signature does not match' do
-      url, _ = subject.serialize(Saml::Kit::AuthenticationRequest::Builder.new)
+      url, _ = subject.serialize(Saml::Kit::AuthenticationRequest.builder_class.new)
       query_params = query_params_from(url)
       query_params['Signature'] = 'invalid'
       expect do
@@ -83,12 +83,12 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
     end
 
     it 'returns a signed document, when a signature is missing' do
-      builder = Saml::Kit::ServiceProviderMetadata::Builder.new
-      builder.add_assertion_consumer_service(FFaker::Internet.http_url, binding: :http_post)
-      provider = builder.build
+      provider = Saml::Kit::ServiceProviderMetadata.build do |builder|
+        builder.add_assertion_consumer_service(FFaker::Internet.http_url, binding: :http_post)
+      end
       allow(Saml::Kit.configuration.registry).to receive(:metadata_for).with(issuer).and_return(provider)
 
-      url, _ = subject.serialize(Saml::Kit::AuthenticationRequest::Builder.new)
+      url, _ = subject.serialize(Saml::Kit::AuthenticationRequest.builder_class.new)
       result = subject.deserialize(query_params_from(url))
       expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
       expect(result).to be_valid
spec/saml/builders/authentication_request_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Builders::AuthenticationRequest do
+  subject { described_class.new(configuration: configuration) }
+  let(:configuration) do
+    config = Saml::Kit::Configuration.new
+    config.issuer = issuer
+    config
+  end
+
+  describe "#to_xml" do
+    let(:issuer) { FFaker::Movie.title }
+    let(:assertion_consumer_service_url) { "https://airport.dev/session/acs" }
+
+    it 'returns a valid authentication request' do
+      travel_to 1.second.from_now
+      subject.assertion_consumer_service_url = assertion_consumer_service_url
+      result = Hash.from_xml(subject.to_xml)
+
+      expect(result['AuthnRequest']['ID']).to be_present
+      expect(result['AuthnRequest']['Version']).to eql('2.0')
+      expect(result['AuthnRequest']['IssueInstant']).to eql(Time.now.utc.iso8601)
+      expect(result['AuthnRequest']['AssertionConsumerServiceURL']).to eql(assertion_consumer_service_url)
+      expect(result['AuthnRequest']['Issuer']).to eql(issuer)
+      expect(result['AuthnRequest']['NameIDPolicy']['Format']).to eql(Saml::Kit::Namespaces::PERSISTENT)
+    end
+  end
+end
spec/saml/builders/identity_provider_metadata_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Builders::IdentityProviderMetadata do
+  subject { described_class.new }
+  let(:email) { FFaker::Internet.email }
+  let(:org_name) { FFaker::Movie.title }
+  let(:url) { FFaker::Internet.uri("https") }
+  let(:entity_id) { FFaker::Movie.title }
+
+  it 'builds a proper metadata' do
+    subject.contact_email = email
+    subject.entity_id = entity_id
+    subject.organization_name = org_name
+    subject.organization_url = url
+    subject.name_id_formats = [
+      Saml::Kit::Namespaces::PERSISTENT,
+      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"
+
+    result = Hash.from_xml(subject.build.to_xml)
+
+    expect(result['EntityDescriptor']['ID']).to be_present
+    expect(result['EntityDescriptor']['entityID']).to eql(entity_id)
+    expect(result['EntityDescriptor']['IDPSSODescriptor']['protocolSupportEnumeration']).to eql(Saml::Kit::Namespaces::PROTOCOL)
+    expect(result['EntityDescriptor']['IDPSSODescriptor']['WantAuthnRequestsSigned']).to eql('true')
+    expect(result['EntityDescriptor']['IDPSSODescriptor']['NameIDFormat']).to match_array([
+      Saml::Kit::Namespaces::PERSISTENT,
+      Saml::Kit::Namespaces::TRANSIENT,
+      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']['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")
+    certificates = result['EntityDescriptor']['IDPSSODescriptor']['KeyDescriptor'].map { |x| x['KeyInfo']['X509Data']['X509Certificate'] }
+    expect(certificates).to match_array([
+      Saml::Kit.configuration.stripped_signing_certificate,
+      Saml::Kit.configuration.stripped_encryption_certificate,
+    ])
+    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']['Company']).to eql("mailto:#{email}")
+  end
+end
spec/saml/builders/logout_request_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Builders::LogoutRequest do
+  subject { described_class.new(user) }
+  let(:user) { double(:user, name_id_for: name_id) }
+  let(:name_id) { SecureRandom.uuid }
+
+  it 'produces the expected xml' do
+    travel_to 1.second.from_now
+    subject.id = Saml::Kit::Id.generate
+    subject.destination = FFaker::Internet.http_url
+    subject.issuer = FFaker::Internet.http_url
+    subject.name_id_format = Saml::Kit::Namespaces::TRANSIENT
+
+    result = subject.to_xml
+    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']['IssueInstant']).to eql(Time.now.utc.iso8601)
+    expect(xml_hash['LogoutRequest']['Destination']).to eql(subject.destination)
+
+    expect(xml_hash['LogoutRequest']['Issuer']).to eql(subject.issuer)
+    expect(xml_hash['LogoutRequest']['NameID']).to eql(name_id)
+    expect(result).to have_xpath("//samlp:LogoutRequest//saml:NameID[@Format=\"#{subject.name_id_format}\"]")
+  end
+
+  it 'includes a signature by default' do
+    xml_hash = Hash.from_xml(subject.to_xml)
+    expect(xml_hash['LogoutRequest']['Signature']).to be_present
+  end
+
+  it 'excludes a signature' do
+    subject.sign = false
+    xml_hash = Hash.from_xml(subject.to_xml)
+    expect(xml_hash['LogoutRequest']['Signature']).to be_nil
+  end
+
+  it 'builds a LogoutRequest' do
+    travel_to 1.second.from_now
+    result = subject.build
+    expect(result).to be_instance_of(Saml::Kit::LogoutRequest)
+    expect(result.to_xml).to eql(subject.to_xml)
+  end
+end
spec/saml/builders/logout_response_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Builders::LogoutResponse do
+  subject { described_class.new(user, request, configuration: configuration) }
+  let(:configuration) { double(issuer: issuer)  }
+  let(:user) { double(:user, name_id_for: SecureRandom.uuid) }
+  let(:request) { Saml::Kit::Builders::LogoutRequest.new(user).build }
+  let(:issuer) { FFaker::Internet.http_url }
+  let(:destination) { FFaker::Internet.http_url }
+
+  describe "#build" do
+    it 'builds a logout response' do
+      travel_to 1.second.from_now
+
+      subject.destination = destination
+      result = subject.build
+      expect(result.id).to be_present
+      expect(result.issue_instant).to eql(Time.now.utc.iso8601)
+      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)
+      expect(result.destination).to eql(destination)
+    end
+  end
+end
spec/saml/builders/response_spec.rb
@@ -0,0 +1,132 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Builders::Response do
+  subject { described_class.new(user, request) }
+  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: Saml::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(want_assertions_signed: false, encryption_certificates: [Saml::Kit::Certificate.new(encryption_pem, use: :encryption)]) }
+  let(:encryption_pem) { Saml::Kit.configuration.stripped_encryption_certificate }
+  let(:issuer) { FFaker::Internet.uri("https") }
+
+  before :each do
+    allow(Saml::Kit.configuration).to receive(:issuer).and_return(issuer)
+  end
+
+  describe "#build" do
+    it 'builds a response with the request_id' do
+      expect(subject.build.request_id).to eql(request.id)
+    end
+
+    it 'builds a valid encrypted assertion' do
+      allow(Saml::Kit.configuration.registry).to receive(:metadata_for).with(issuer).and_return(provider)
+      allow(provider).to receive(:matches?).and_return(true)
+
+      subject.sign = true
+      subject.encrypt = true
+      result = subject.build
+      expect(result).to be_valid
+    end
+  end
+
+  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(:issuer).and_return(issuer)
+      hash = Hash.from_xml(subject.to_xml)
+
+      expect(hash['Response']['ID']).to be_present
+      expect(hash['Response']['Version']).to eql('2.0')
+      expect(hash['Response']['IssueInstant']).to eql(Time.now.utc.iso8601)
+      expect(hash['Response']['Destination']).to eql(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']['Assertion']['ID']).to be_present
+      expect(hash['Response']['Assertion']['IssueInstant']).to eql(Time.now.utc.iso8601)
+      expect(hash['Response']['Assertion']['Version']).to eql("2.0")
+      expect(hash['Response']['Assertion']['Issuer']).to eql(issuer)
+
+      expect(hash['Response']['Assertion']['Subject']['NameID']).to eql(user.name_id_for)
+      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['Method']).to eql("urn:oasis:names:tc:SAML:2.0:cm:bearer")
+      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['NotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
+      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['Recipient']).to eql(assertion_consumer_service_url)
+      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['InResponseTo']).to eql(request.id)
+
+      expect(hash['Response']['Assertion']['Conditions']['NotBefore']).to eql(0.seconds.ago.utc.iso8601)
+      expect(hash['Response']['Assertion']['Conditions']['NotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
+      expect(hash['Response']['Assertion']['Conditions']['AudienceRestriction']['Audience']).to eql(request.issuer)
+
+      expect(hash['Response']['Assertion']['AuthnStatement']['AuthnInstant']).to eql(Time.now.utc.iso8601)
+      expect(hash['Response']['Assertion']['AuthnStatement']['SessionNotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
+      expect(hash['Response']['Assertion']['AuthnStatement']['SessionIndex']).to eql(hash['Response']['Assertion']['ID'])
+      expect(hash['Response']['Assertion']['AuthnStatement']['AuthnContext']['AuthnContextClassRef']).to eql('urn:oasis:names:tc:SAML:2.0:ac:classes:Password')
+
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['Name']).to eql('email')
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['FriendlyName']).to eql('email')
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['NameFormat']).to eql('urn:oasis:names:tc:SAML:2.0:attrname-format:uri')
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['AttributeValue']).to eql(email)
+
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['Name']).to eql('created_at')
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['FriendlyName']).to eql('created_at')
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['NameFormat']).to eql('urn:oasis:names:tc:SAML:2.0:attrname-format:uri')
+      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['AttributeValue']).to be_present
+    end
+
+    it 'does not add a signature when the SP does not want assertions signed' do
+      builder = Saml::Kit::Builders::ServiceProviderMetadata.new
+      builder.want_assertions_signed = false
+      metadata = builder.build
+      allow(request).to receive(:provider).and_return(metadata)
+
+      hash = Hash.from_xml(subject.to_xml)
+      expect(hash['Response']['Signature']).to be_nil
+    end
+
+    it 'generates an EncryptedAssertion' do
+      subject.encrypt = true
+      result = Hash.from_xml(subject.to_xml)
+      expect(result['Response']['EncryptedAssertion']).to be_present
+      encrypted_assertion = result['Response']['EncryptedAssertion']
+      decrypted_assertion = Saml::Kit::XmlDecryption.new.decrypt(encrypted_assertion)
+      decrypted_hash = Hash.from_xml(decrypted_assertion)
+      expect(decrypted_hash['Assertion']).to be_present
+      expect(decrypted_hash['Assertion']['Issuer']).to be_present
+      expect(decrypted_hash['Assertion']['Subject']).to be_present
+      expect(decrypted_hash['Assertion']['Subject']['NameID']).to be_present
+      expect(decrypted_hash['Assertion']['Subject']['SubjectConfirmation']).to be_present
+      expect(decrypted_hash['Assertion']['Conditions']).to be_present
+      expect(decrypted_hash['Assertion']['Conditions']['AudienceRestriction']).to be_present
+      expect(decrypted_hash['Assertion']['AuthnStatement']).to be_present
+      expect(decrypted_hash['Assertion']['AuthnStatement']['AuthnContext']).to be_present
+      expect(decrypted_hash['Assertion']['AuthnStatement']['AuthnContext']['AuthnContextClassRef']).to be_present
+    end
+  end
+
+  describe "#destination" do
+    let(:assertion_consumer_service_url) { "https://#{FFaker::Internet.domain_name}/acs" }
+    let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: []) }
+    subject { described_class.new(user, request).build }
+
+    describe "when the request is signed and trusted" do
+      let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: SecureRandom.uuid, assertion_consumer_service_url: assertion_consumer_service_url, issuer: FFaker::Movie.title, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: nil, signed?: true, trusted?: true) }
+
+      it 'returns the ACS embedded in the request' do
+        expect(subject.destination).to eql(assertion_consumer_service_url)
+      end
+    end
+
+    describe "when the request is not trusted" do
+      let(:registered_acs_url) { FFaker::Internet.uri("https") }
+      let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: SecureRandom.uuid, assertion_consumer_service_url: assertion_consumer_service_url, issuer: FFaker::Movie.title, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: provider, signed?: true, trusted?: false) }
+      let(:provider) { instance_double(Saml::Kit::ServiceProviderMetadata, want_assertions_signed: false) }
+
+      it 'returns the registered ACS embedded in the metadata' do
+        allow(provider).to receive(:assertion_consumer_service_for).and_return(double(location: registered_acs_url))
+        expect(subject.destination).to eql(registered_acs_url)
+      end
+    end
+  end
+end
spec/saml/builders/service_provider_metadata_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Builders::ServiceProviderMetadata do
+  let(:assertion_consumer_service_url) { FFaker::Internet.http_url }
+  let(:entity_id) { FFaker::Internet.uri("https") }
+
+  it 'builds the service provider metadata' do
+    subject.entity_id = entity_id
+    subject.add_assertion_consumer_service(assertion_consumer_service_url, binding: :http_post)
+    subject.name_id_formats = [
+      Saml::Kit::Namespaces::PERSISTENT,
+      Saml::Kit::Namespaces::TRANSIENT,
+      Saml::Kit::Namespaces::EMAIL_ADDRESS,
+    ]
+    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']['ID']).to be_present
+    expect(result['EntityDescriptor']['entityID']).to eql(entity_id)
+    expect(result['EntityDescriptor']['SPSSODescriptor']['AuthnRequestsSigned']).to eql('true')
+    expect(result['EntityDescriptor']['SPSSODescriptor']['WantAssertionsSigned']).to eql('true')
+    expect(result['EntityDescriptor']['SPSSODescriptor']['protocolSupportEnumeration']).to eql('urn:oasis:names:tc:SAML:2.0:protocol')
+    expect(result['EntityDescriptor']['SPSSODescriptor']['NameIDFormat']).to match_array([
+      Saml::Kit::Namespaces::PERSISTENT,
+      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']['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['KeyInfo']['X509Data']['X509Certificate'] }).to match_array([
+      Saml::Kit.configuration.stripped_signing_certificate,
+      Saml::Kit.configuration.stripped_encryption_certificate,
+    ])
+  end
+end
spec/saml/authentication_request_spec.rb
@@ -2,52 +2,28 @@ require 'spec_helper'
 
 RSpec.describe Saml::Kit::AuthenticationRequest do
   subject { described_class.new(raw_xml) }
-  let(:id) { SecureRandom.uuid }
-  let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
+  let(:id) { Saml::Kit::Id.generate }
+  let(:assertion_consumer_service_url) { "https://#{FFaker::Internet.domain_name}/acs" }
   let(:issuer) { FFaker::Movie.title }
   let(:destination) { FFaker::Internet.http_url }
   let(:name_id_format) { Saml::Kit::Namespaces::EMAIL_ADDRESS }
   let(:raw_xml) do
-    builder = described_class::Builder.new
-    builder.id = id
-    builder.now = Time.now.utc
-    builder.issuer = issuer
-    builder.acs_url = acs_url
-    builder.name_id_format = name_id_format
-    builder.destination = destination
-    builder.to_xml
+    described_class.build do |builder|
+      builder.id = id
+      builder.now = Time.now.utc
+      builder.issuer = issuer
+      builder.assertion_consumer_service_url = assertion_consumer_service_url
+      builder.name_id_format = name_id_format
+      builder.destination = destination
+    end.to_xml
   end
 
   it { expect(subject.issuer).to eql(issuer) }
-  it { expect(subject.id).to eql("_#{id}") }
-  it { expect(subject.acs_url).to eql(acs_url) }
+  it { expect(subject.id).to eql(id) }
+  it { expect(subject.assertion_consumer_service_url).to eql(assertion_consumer_service_url) }
   it { expect(subject.name_id_format).to eql(name_id_format) }
   it { expect(subject.destination).to eql(destination) }
 
-  describe "#to_xml" do
-    subject { described_class::Builder.new(configuration: configuration) }
-    let(:configuration) do
-      config = Saml::Kit::Configuration.new
-      config.issuer = issuer
-      config
-    end
-    let(:issuer) { FFaker::Movie.title }
-    let(:acs_url) { "https://airport.dev/session/acs" }
-
-    it 'returns a valid authentication request' do
-      travel_to 1.second.from_now
-      subject.acs_url = acs_url
-      result = Hash.from_xml(subject.to_xml)
-
-      expect(result['AuthnRequest']['ID']).to be_present
-      expect(result['AuthnRequest']['Version']).to eql('2.0')
-      expect(result['AuthnRequest']['IssueInstant']).to eql(Time.now.utc.iso8601)
-      expect(result['AuthnRequest']['AssertionConsumerServiceURL']).to eql(acs_url)
-      expect(result['AuthnRequest']['Issuer']).to eql(issuer)
-      expect(result['AuthnRequest']['NameIDPolicy']['Format']).to eql(Saml::Kit::Namespaces::PERSISTENT)
-    end
-  end
-
   describe "#valid?" do
     let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
     let(:metadata) { instance_double(Saml::Kit::ServiceProviderMetadata) }
@@ -76,36 +52,34 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
     end
 
     it 'is invalid when not an AuthnRequest' do
-      xml = Saml::Kit::IdentityProviderMetadata::Builder.new.to_xml
+      xml = Saml::Kit::IdentityProviderMetadata.build.to_xml
       subject = described_class.new(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
-      builder = described_class::Builder.new
-      builder.issuer = issuer
-      builder.acs_url = acs_url
-      xml = builder.to_xml
-
       allow(metadata).to receive(:matches?).and_return(false)
-      subject = described_class.new(xml)
+      subject = described_class.build do |builder|
+        builder.issuer = issuer
+        builder.assertion_consumer_service_url = assertion_consumer_service_url
+      end
+
       expect(subject).to be_invalid
       expect(subject.errors[:fingerprint]).to be_present
     end
 
     it 'is invalid when the service provider is not known' do
       allow(registry).to receive(:metadata_for).and_return(nil)
-      builder = described_class::Builder.new
-      subject = described_class.new(builder.to_xml)
+      subject = described_class.build
       expect(subject).to be_invalid
       expect(subject.errors[:provider]).to be_present
     end
 
     it 'validates the schema of the request' do
-      id = SecureRandom.uuid
+      id = Saml::Kit::Id.generate
       signed_xml = Saml::Kit::Signature.sign(sign: true) do |xml, signature|
-        xml.tag!('samlp:AuthnRequest', "xmlns:samlp" => Saml::Kit::Namespaces::PROTOCOL, AssertionConsumerServiceURL: acs_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?"
@@ -117,12 +91,12 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
 
     it 'validates a request without a signature' do
       now = Time.now.utc
-raw_xml = <<-XML
-<samlp:AuthnRequest AssertionConsumerServiceURL='#{acs_url}' ID='_#{SecureRandom.uuid}' IssueInstant='#{now.iso8601}' Version='2.0' xmlns:saml='#{Saml::Kit::Namespaces::ASSERTION}' xmlns:samlp='#{Saml::Kit::Namespaces::PROTOCOL}'>
+      raw_xml = <<-XML
+<samlp:AuthnRequest AssertionConsumerServiceURL='#{assertion_consumer_service_url}' ID='#{Saml::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
+      XML
 
       subject = described_class.new(raw_xml)
       subject.signature_verified!
@@ -130,24 +104,56 @@ XML
     end
   end
 
-  describe "#acs_url" do
+  describe "#assertion_consumer_service_url" do
     let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
     let(:metadata) { instance_double(Saml::Kit::ServiceProviderMetadata) }
 
     it 'returns the ACS in the request' do
-      builder = described_class::Builder.new
-      builder.acs_url = acs_url
-      subject = builder.build
-      expect(subject.acs_url).to eql(acs_url)
+      subject = described_class.build do |builder|
+        builder.assertion_consumer_service_url = assertion_consumer_service_url
+      end
+      expect(subject.assertion_consumer_service_url).to eql(assertion_consumer_service_url)
     end
 
     it 'returns nil' do
-      builder = described_class::Builder.new
-      builder.issuer = issuer
-      builder.acs_url = nil
-      subject = builder.build
+      subject = described_class.build do |builder|
+        builder.assertion_consumer_service_url = nil
+      end
+
+      expect(subject.assertion_consumer_service_url).to be_nil
+    end
+  end
+
+  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|
+        builder.issuer = entity_id
+        builder.assertion_consumer_service_url = url
+      end
+
+      expect(result).to be_instance_of(described_class)
+      expect(result.issuer).to eql(entity_id)
+      expect(result.assertion_consumer_service_url).to eql(url)
+    end
+  end
+
+  describe "#response_for" do
+    let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: []) }
+    let(:provider) do
+      Saml::Kit::ServiceProviderMetadata.build do |x|
+        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)
 
-      expect(subject.acs_url).to be_nil
+      response = provider.assertion_consumer_service_for(binding: :http_post).deserialize(saml_params)
+      expect(response).to be_instance_of(Saml::Kit::Response)
     end
   end
 end
spec/saml/bindings_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Bindings 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('unknown')).to eql('unknown') }
+  end
+
+  describe ".create_for" do
+    subject { described_class }
+    let(:location) { FFaker::Internet.uri("https") }
+
+    it 'returns an HTTP redirect binding' do
+      expect(
+        subject.create_for(Saml::Kit::Bindings::HTTP_REDIRECT, location)
+      ).to be_instance_of(Saml::Kit::Bindings::HttpRedirect)
+    end
+
+    it 'returns an HTTP Post binding' do
+      expect(
+        subject.create_for(Saml::Kit::Bindings::HTTP_POST, location)
+      ).to be_instance_of(Saml::Kit::Bindings::HttpPost)
+    end
+
+    it 'returns an unknown binding' do
+      expect(
+        subject.create_for(Saml::Kit::Bindings::HTTP_ARTIFACT, location)
+      ).to be_instance_of(Saml::Kit::Bindings::Binding)
+    end
+  end
+end
spec/saml/default_registry_spec.rb
@@ -4,14 +4,14 @@ RSpec.describe Saml::Kit::DefaultRegistry do
   subject { described_class.new }
   let(:entity_id) { FFaker::Internet.http_url }
   let(:service_provider_metadata) do
-    builder = Saml::Kit::ServiceProviderMetadata::Builder.new
-    builder.entity_id = entity_id
-    builder.build
+    Saml::Kit::ServiceProviderMetadata.build do |builder|
+      builder.entity_id = entity_id
+    end
   end
   let(:identity_provider_metadata) do
-    builder = Saml::Kit::IdentityProviderMetadata::Builder.new
-    builder.entity_id = entity_id
-    builder.build
+    Saml::Kit::IdentityProviderMetadata.build do |builder|
+      builder.entity_id = entity_id
+    end
   end
 
   describe "#metadata_for" do
spec/saml/identity_provider_metadata_spec.rb
@@ -70,17 +70,18 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
 
   describe "#validate" do
     it 'valid when given valid identity provider metadata' do
-      builder = described_class::Builder.new
-      builder.attributes = [:email]
-      builder.add_single_sign_on_service(FFaker::Internet.http_url, binding: :http_post)
-      builder.add_single_sign_on_service(FFaker::Internet.http_url, binding: :http_redirect)
-      builder.add_single_logout_service(FFaker::Internet.http_url, binding: :http_post)
-      builder.add_single_logout_service(FFaker::Internet.http_url, binding: :http_redirect)
-      expect(builder.build).to be_valid
+      subject = described_class.build do |builder|
+        builder.attributes = [:email]
+        builder.add_single_sign_on_service(FFaker::Internet.http_url, binding: :http_post)
+        builder.add_single_sign_on_service(FFaker::Internet.http_url, binding: :http_redirect)
+        builder.add_single_logout_service(FFaker::Internet.http_url, binding: :http_post)
+        builder.add_single_logout_service(FFaker::Internet.http_url, binding: :http_redirect)
+      end
+      expect(subject).to be_valid
     end
 
     it 'is invalid, when given service provider metadata' do
-      service_provider_metadata = Saml::Kit::ServiceProviderMetadata::Builder.new.to_xml
+      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"))
@@ -121,10 +122,10 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
     let(:redirect_url) { FFaker::Internet.http_url }
 
     subject do
-      builder = Saml::Kit::IdentityProviderMetadata::Builder.new
-      builder.add_single_sign_on_service(redirect_url, binding: :http_redirect)
-      builder.add_single_sign_on_service(post_url, binding: :http_post)
-      builder.build
+      described_class.build do |builder|
+        builder.add_single_sign_on_service(redirect_url, binding: :http_redirect)
+        builder.add_single_sign_on_service(post_url, binding: :http_post)
+      end
     end
 
     it 'returns the POST binding' do
@@ -145,37 +146,37 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
   end
 
   describe "#want_authn_requests_signed" do
-    let(:builder) { described_class::Builder.new }
-
     it 'returns true when enabled' do
-      builder.want_authn_requests_signed = true
-      subject = builder.build
+      subject = described_class.build do |builder|
+        builder.want_authn_requests_signed = true
+      end
       expect(subject.want_authn_requests_signed).to be(true)
     end
 
     it 'returns false when disabled' do
-      builder.want_authn_requests_signed = false
-      subject = builder.build
+      subject = described_class.build do |builder|
+        builder.want_authn_requests_signed = false
+      end
       expect(subject.want_authn_requests_signed).to be(false)
     end
 
     it 'returns true when the attribute is missing' do
-      builder.want_authn_requests_signed = false
-      xml = builder.to_xml.gsub("WantAuthnRequestsSigned=\"false\"", "")
+      xml = described_class.build do |builder|
+        builder.want_authn_requests_signed = 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(:builder) { described_class::Builder.new }
-    let(:redirect_url) { FFaker::Internet.http_url }
-    let(:post_url) { FFaker::Internet.http_url }
-    let(:subject) { builder.build }
-
-    before :each do
-      builder.add_single_logout_service(redirect_url, binding: :http_redirect)
-      builder.add_single_logout_service(post_url, binding: :http_post)
+    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)
+        builder.add_single_logout_service(post_url, binding: :http_post)
+      end
     end
 
     it 'returns the location for the matching binding' do
@@ -188,53 +189,30 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
     end
   end
 
-  describe described_class::Builder do
-    subject { described_class.new }
-    let(:email) { FFaker::Internet.email }
-    let(:org_name) { FFaker::Movie.title }
-    let(:url) { "https://#{FFaker::Internet.domain_name}" }
-    let(:entity_id) { FFaker::Movie.title }
-
-    it 'builds a proper metadata' do
-      subject.contact_email = email
-      subject.entity_id = entity_id
-      subject.organization_name = org_name
-      subject.organization_url = url
-      subject.name_id_formats = [
-        Saml::Kit::Namespaces::PERSISTENT,
-        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"
-
-      result = Hash.from_xml(subject.build.to_xml)
-
-      expect(result['EntityDescriptor']['ID']).to be_present
-      expect(result['EntityDescriptor']['entityID']).to eql(entity_id)
-      expect(result['EntityDescriptor']['IDPSSODescriptor']['protocolSupportEnumeration']).to eql(Saml::Kit::Namespaces::PROTOCOL)
-      expect(result['EntityDescriptor']['IDPSSODescriptor']['WantAuthnRequestsSigned']).to eql('true')
-      expect(result['EntityDescriptor']['IDPSSODescriptor']['NameIDFormat']).to match_array([
-        Saml::Kit::Namespaces::PERSISTENT,
-        Saml::Kit::Namespaces::TRANSIENT,
-        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']['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")
-      certificates = result['EntityDescriptor']['IDPSSODescriptor']['KeyDescriptor'].map { |x| x['KeyInfo']['X509Data']['X509Certificate'] }
-      expect(certificates).to match_array([
-        Saml::Kit.configuration.stripped_signing_certificate,
-        Saml::Kit.configuration.stripped_encryption_certificate,
-      ])
-      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']['Company']).to eql("mailto:#{email}")
+  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|
+        builder.entity_id = entity_id
+        builder.add_single_sign_on_service(url, binding: :http_post)
+      end
+
+      expect(result).to be_instance_of(described_class)
+      expect(result.entity_id).to eql(entity_id)
+      expect(result.single_sign_on_service_for(binding: :http_post).location).to eql(url)
+    end
+  end
+
+  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)
+      end
+      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
   end
 end
spec/saml/invalid_document_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::InvalidDocument do
+  subject { described_class.new(xml) }
+  let(:xml) { "<xml></xml>" }
+
+  it 'is invalid' do
+    expect(subject).to be_invalid
+    expect(subject.errors[:base]).to be_present
+  end
+end
+
spec/saml/logout_request_spec.rb
@@ -1,14 +1,16 @@
 require 'spec_helper'
 
 RSpec.describe Saml::Kit::LogoutRequest do
-  subject { builder.build }
-  let(:builder) { described_class::Builder.new(user) }
+  subject { described_class.build(user) }
   let(:user) { double(:user, name_id_for: name_id) }
   let(:name_id) { SecureRandom.uuid }
 
   it 'parses the issuer' do
-    builder.issuer = FFaker::Internet.http_url
-    expect(subject.issuer).to eql(builder.issuer)
+    issuer = FFaker::Internet.uri("https")
+    subject = described_class.build(user) do |builder|
+      builder.issuer = issuer
+    end
+    expect(subject.issuer).to eql(issuer)
   end
 
   it 'parses the issue instant' do
@@ -21,8 +23,11 @@ RSpec.describe Saml::Kit::LogoutRequest do
   end
 
   it 'parses the destination' do
-    builder.destination = FFaker::Internet.http_url
-    expect(subject.destination).to eql(builder.destination)
+    destination = FFaker::Internet.uri("https")
+    subject = described_class.build(user) do |builder|
+      builder.destination = destination
+    end
+    expect(subject.destination).to eql(destination)
   end
 
   it 'parses the name_id' do
@@ -43,14 +48,16 @@ RSpec.describe Saml::Kit::LogoutRequest do
     end
 
     it 'is valid when left untampered' do
-      expect(builder.build).to be_valid
+      expect(subject).to be_valid
     end
 
     it 'is invalid if the document has been tampered with' do
-      builder.issuer = FFaker::Internet.http_url
-      raw_xml = builder.to_xml.gsub(builder.issuer, 'corrupt')
-      subject = described_class.new(raw_xml)
-      expect(subject).to be_invalid
+      issuer = FFaker::Internet.uri("https")
+      raw_xml = described_class.build(user) do |builder|
+        builder.issuer = issuer
+      end.to_xml.gsub(issuer, 'corrupt')
+
+      expect(described_class.new(raw_xml)).to be_invalid
     end
 
     it 'is invalid when blank' do
@@ -60,22 +67,19 @@ RSpec.describe Saml::Kit::LogoutRequest do
     end
 
     it 'is invalid when not a LogoutRequest' do
-      xml = Saml::Kit::IdentityProviderMetadata::Builder.new.to_xml
-      subject = described_class.new(xml)
+      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(metadata).to receive(:matches?).and_return(false)
-      subject = builder.build
       expect(subject).to be_invalid
       expect(subject.errors[:fingerprint]).to be_present
     end
 
     it 'is invalid when the provider is not known' do
       allow(registry).to receive(:metadata_for).and_return(nil)
-      subject = builder.build
       expect(subject).to be_invalid
       expect(subject.errors[:provider]).to be_present
     end
@@ -84,26 +88,28 @@ RSpec.describe Saml::Kit::LogoutRequest do
       allow(metadata).to receive(:matches?).and_return(true)
       allow(metadata).to receive(:single_logout_services).and_return([])
 
-      subject = builder.build
       expect(subject).to be_invalid
       expect(subject.errors[:single_logout_service]).to be_present
     end
 
     it 'is valid when a single lgout service url is available via the registry' do
-      builder.issuer = FFaker::Internet.http_url
-      allow(registry).to receive(:metadata_for).with(builder.issuer).and_return(metadata)
+      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.http_url)
+        Saml::Kit::Bindings::HttpPost.new(location: FFaker::Internet.uri("https"))
       ])
 
-      expect(builder.build).to be_valid
+      subject = described_class.build(user) do |builder|
+        builder.issuer = issuer
+      end
+      expect(subject).to be_valid
     end
 
     it 'validates the schema of the request' do
-      id = SecureRandom.uuid
+      id = Saml::Kit::Id.generate
       signed_xml = Saml::Kit::Signature.sign(sign: true) do |xml, signature|
-        xml.LogoutRequest ID: "_#{id}" do
+        xml.LogoutRequest ID: id do
           signature.template(id)
           xml.Fake do
             xml.NotAllowed "Huh?"
@@ -114,54 +120,21 @@ RSpec.describe Saml::Kit::LogoutRequest do
     end
   end
 
-  describe described_class::Builder do
-    subject { described_class.new(user) }
-    let(:user) { double(:user, name_id_for: name_id) }
-    let(:name_id) { SecureRandom.uuid }
-
-    it 'produces the expected xml' do
-      travel_to 1.second.from_now
-      subject.id = SecureRandom.uuid
-      subject.destination = FFaker::Internet.http_url
-      subject.issuer = FFaker::Internet.http_url
-      subject.name_id_format = Saml::Kit::Namespaces::TRANSIENT
-
-      result = subject.to_xml
-      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']['IssueInstant']).to eql(Time.now.utc.iso8601)
-      expect(xml_hash['LogoutRequest']['Destination']).to eql(subject.destination)
-
-      expect(xml_hash['LogoutRequest']['Issuer']).to eql(subject.issuer)
-      expect(xml_hash['LogoutRequest']['NameID']).to eql(name_id)
-      expect(result).to have_xpath("//samlp:LogoutRequest//saml:NameID[@Format=\"#{subject.name_id_format}\"]")
-    end
-
-    it 'includes a signature by default' do
-      xml_hash = Hash.from_xml(subject.to_xml)
-      expect(xml_hash['LogoutRequest']['Signature']).to be_present
+  describe "#response_for" do
+    let(:user) { double(:user, name_id_for: SecureRandom.uuid) }
+    let(:provider) do
+      Saml::Kit::IdentityProviderMetadata.build do |builder|
+        builder.add_single_logout_service(FFaker::Internet.uri("https"), binding: :http_post)
+      end
     end
 
-    it 'excludes a signature' do
-      subject.sign = false
-      xml_hash = Hash.from_xml(subject.to_xml)
-      expect(xml_hash['LogoutRequest']['Signature']).to be_nil
-    end
+    it 'serializes a logout response for a particular user' do
+      allow(subject).to receive(:provider).and_return(provider)
 
-    it 'builds a LogoutRequest' do
-      travel_to 1.second.from_now
-      result = subject.build
-      expect(result).to be_instance_of(Saml::Kit::LogoutRequest)
-      expect(result.to_xml).to eql(subject.to_xml)
-    end
-  end
-
-  describe "#response_for" do
-    it 'returns a logout response for a particular user' do
-      user = double(:user)
-      expect(subject.response_for(user)).to be_instance_of(Saml::Kit::LogoutResponse::Builder)
+      _, saml_params = subject.response_for(user, binding: :http_post)
+      response_binding = provider.single_logout_service_for(binding: :http_post)
+      result = response_binding.deserialize(saml_params)
+      expect(result).to be_instance_of(Saml::Kit::LogoutResponse)
     end
   end
 end
spec/saml/logout_response_spec.rb
@@ -1,34 +1,4 @@
 require 'spec_helper'
 
 RSpec.describe Saml::Kit::LogoutResponse do
-  describe described_class::Builder do
-    subject { described_class.new(user, request, configuration: configuration) }
-    let(:configuration) { double(issuer: issuer)  }
-    let(:user) { double(:user, name_id_for: SecureRandom.uuid) }
-    let(:request) { Saml::Kit::LogoutRequest::Builder.new(user).build }
-    let(:issuer) { FFaker::Internet.http_url }
-    let(:destination) { FFaker::Internet.http_url }
-    let(:registry) { double(:registry) }
-    let(:provider) { double(:provider) }
-    let(:binding) { double(:binding, location: destination) }
-
-    describe "#build" do
-      it 'builds a logout response' do
-        allow(configuration).to receive(:registry).and_return(registry)
-        allow(registry).to receive(:metadata_for).with(issuer).and_return(provider)
-        allow(provider).to receive(:single_logout_service_for).and_return(binding)
-
-        travel_to 1.second.from_now
-
-        result = subject.build
-        expect(result.id).to be_present
-        expect(result.issue_instant).to eql(Time.now.utc.iso8601)
-        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)
-        expect(result.destination).to eql(destination)
-      end
-    end
-  end
 end
spec/saml/response_spec.rb
@@ -1,100 +1,12 @@
 require 'spec_helper'
 
 RSpec.describe Saml::Kit::Response do
-  describe "#destination" do
-    let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
-    let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: []) }
-    subject { described_class::Builder.new(user, request).build }
-
-    describe "when the request is signed and trusted" do
-      let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: SecureRandom.uuid, acs_url: acs_url, issuer: FFaker::Movie.title, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: nil, signed?: true, trusted?: true) }
-
-      it 'returns the ACS embedded in the request' do
-        expect(subject.destination).to eql(acs_url)
-      end
-    end
-
-    describe "when the request is not trusted" do
-      let(:registered_acs_url) { FFaker::Internet.uri("https") }
-      let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: SecureRandom.uuid, acs_url: acs_url, issuer: FFaker::Movie.title, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: provider, signed?: true, trusted?: false) }
-      let(:provider) { instance_double(Saml::Kit::ServiceProviderMetadata, want_assertions_signed: false) }
-
-      it 'returns the registered ACS embedded in the metadata' do
-        allow(provider).to receive(:assertion_consumer_service_for).and_return(double(location: registered_acs_url))
-        expect(subject.destination).to eql(registered_acs_url)
-      end
-    end
-  end
-
-  describe "#to_xml" do
-    subject { described_class::Builder.new(user, request) }
-    let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: { email: email, created_at: Time.now.utc.iso8601 }) }
-    let(:request) { double(id: SecureRandom.uuid, acs_url: acs_url, issuer: FFaker::Movie.title, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: nil, trusted?: true, signed?: true) }
-    let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
-    let(:issuer) { FFaker::Movie.title }
-    let(:email) { FFaker::Internet.email }
-
-    it 'returns a proper response for the user' do
-      travel_to 1.second.from_now
-      allow(Saml::Kit.configuration).to receive(:issuer).and_return(issuer)
-      hash = Hash.from_xml(subject.to_xml)
-
-      expect(hash['Response']['ID']).to be_present
-      expect(hash['Response']['Version']).to eql('2.0')
-      expect(hash['Response']['IssueInstant']).to eql(Time.now.utc.iso8601)
-      expect(hash['Response']['Destination']).to eql(acs_url)
-      expect(hash['Response']['InResponseTo']).to eql(request.id)
-      expect(hash['Response']['Issuer']).to eql(issuer)
-      expect(hash['Response']['Status']['StatusCode']['Value']).to eql("urn:oasis:names:tc:SAML:2.0:status:Success")
-
-      expect(hash['Response']['Assertion']['ID']).to be_present
-      expect(hash['Response']['Assertion']['IssueInstant']).to eql(Time.now.utc.iso8601)
-      expect(hash['Response']['Assertion']['Version']).to eql("2.0")
-      expect(hash['Response']['Assertion']['Issuer']).to eql(issuer)
-
-      expect(hash['Response']['Assertion']['Subject']['NameID']).to eql(user.name_id_for)
-      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['Method']).to eql("urn:oasis:names:tc:SAML:2.0:cm:bearer")
-      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['NotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
-      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['Recipient']).to eql(acs_url)
-      expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['InResponseTo']).to eql(request.id)
-
-      expect(hash['Response']['Assertion']['Conditions']['NotBefore']).to eql(0.seconds.ago.utc.iso8601)
-      expect(hash['Response']['Assertion']['Conditions']['NotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
-      expect(hash['Response']['Assertion']['Conditions']['AudienceRestriction']['Audience']).to eql(request.issuer)
-
-      expect(hash['Response']['Assertion']['AuthnStatement']['AuthnInstant']).to eql(Time.now.utc.iso8601)
-      expect(hash['Response']['Assertion']['AuthnStatement']['SessionNotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
-      expect(hash['Response']['Assertion']['AuthnStatement']['SessionIndex']).to eql(hash['Response']['Assertion']['ID'])
-      expect(hash['Response']['Assertion']['AuthnStatement']['AuthnContext']['AuthnContextClassRef']).to eql('urn:oasis:names:tc:SAML:2.0:ac:classes:Password')
-
-      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['Name']).to eql('email')
-      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['FriendlyName']).to eql('email')
-      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['NameFormat']).to eql('urn:oasis:names:tc:SAML:2.0:attrname-format:uri')
-      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][0]['AttributeValue']).to eql(email)
-
-      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['Name']).to eql('created_at')
-      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['FriendlyName']).to eql('created_at')
-      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['NameFormat']).to eql('urn:oasis:names:tc:SAML:2.0:attrname-format:uri')
-      expect(hash['Response']['Assertion']['AttributeStatement']['Attribute'][1]['AttributeValue']).to be_present
-    end
-
-    it 'does not add a signature when the SP does not want assertions signed' do
-      builder = Saml::Kit::ServiceProviderMetadata::Builder.new
-      builder.want_assertions_signed = false
-      metadata = builder.build
-      allow(request).to receive(:provider).and_return(metadata)
-
-      hash = Hash.from_xml(subject.to_xml)
-      expect(hash['Response']['Signature']).to be_nil
-    end
-  end
-
   describe "#valid?" do
-    let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: "_#{SecureRandom.uuid}", issuer: FFaker::Internet.http_url, acs_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, provider: nil, signed?: true, trusted?: true) }
+    let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: Saml::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(:builder) { described_class::Builder.new(user, request) }
     let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
     let(:metadata) { instance_double(Saml::Kit::IdentityProviderMetadata) }
+    subject { described_class.build(user, request) }
 
     before :each do
       allow(Saml::Kit.configuration).to receive(:registry).and_return(registry)
@@ -104,7 +16,7 @@ RSpec.describe Saml::Kit::Response do
     it 'is valid' do
       allow(registry).to receive(:metadata_for).and_return(metadata)
       allow(metadata).to receive(:matches?).and_return(true)
-      expect(builder.build).to be_valid
+      expect(subject).to be_valid
     end
 
     it 'is invalid when blank' do
@@ -118,15 +30,16 @@ RSpec.describe Saml::Kit::Response do
       allow(registry).to receive(:metadata_for).and_return(metadata)
       allow(metadata).to receive(:matches?).and_return(true)
       status_code = FFaker::Movie.title
-      builder.status_code = status_code
-      subject = described_class.new(builder.to_xml.gsub(status_code, "TAMPERED"))
+      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)
-      xml = Saml::Kit::IdentityProviderMetadata::Builder.new.to_xml
-      subject = described_class.new(xml)
+      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
@@ -134,7 +47,6 @@ RSpec.describe Saml::Kit::Response do
     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)
-      subject = described_class.new(builder.to_xml)
       expect(subject).to be_invalid
       expect(subject.errors[:fingerprint]).to be_present
     end
@@ -142,9 +54,9 @@ RSpec.describe Saml::Kit::Response do
     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 = SecureRandom.uuid
+      id = Saml::Kit::Id.generate
       signed_xml = Saml::Kit::Signature.sign(sign: true) do |xml, signature|
-        xml.tag! "samlp:Response", "xmlns:samlp" => Saml::Kit::Namespaces::PROTOCOL, ID: "_#{id}" do
+        xml.tag! "samlp:Response", "xmlns:samlp" => Saml::Kit::Namespaces::PROTOCOL, ID: id do
           signature.template(id)
           xml.Fake do
             xml.NotAllowed "Huh?"
@@ -159,8 +71,9 @@ RSpec.describe Saml::Kit::Response do
     it 'validates the version' do
       allow(registry).to receive(:metadata_for).and_return(metadata)
       allow(metadata).to receive(:matches?).and_return(true)
-      builder.version = "1.1"
-      subject = described_class.new(builder.to_xml)
+      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
@@ -168,8 +81,9 @@ RSpec.describe Saml::Kit::Response do
     it 'validates the id' do
       allow(registry).to receive(:metadata_for).and_return(metadata)
       allow(metadata).to receive(:matches?).and_return(true)
-      builder.id = nil
-      subject = described_class.new(builder.to_xml)
+      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
@@ -177,8 +91,9 @@ RSpec.describe Saml::Kit::Response do
     it 'validates the status code' do
       allow(registry).to receive(:metadata_for).and_return(metadata)
       allow(metadata).to receive(:matches?).and_return(true)
-      builder.status_code = Saml::Kit::Namespaces::REQUESTER_ERROR
-      subject = described_class.new(builder.to_xml)
+      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
@@ -186,7 +101,9 @@ RSpec.describe Saml::Kit::Response do
     it 'validates the InResponseTo' do
       allow(registry).to receive(:metadata_for).and_return(metadata)
       allow(metadata).to receive(:matches?).and_return(true)
-      subject = described_class.new(builder.to_xml, request_id: SecureRandom.uuid)
+      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
@@ -195,7 +112,7 @@ RSpec.describe Saml::Kit::Response do
       allow(registry).to receive(:metadata_for).and_return(metadata)
       allow(metadata).to receive(:matches?).and_return(true)
 
-      subject = described_class.new(builder.to_xml)
+      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
@@ -205,7 +122,7 @@ RSpec.describe Saml::Kit::Response do
       allow(registry).to receive(:metadata_for).and_return(metadata)
       allow(metadata).to receive(:matches?).and_return(true)
 
-      subject = described_class.new(builder.to_xml)
+      subject = described_class.build(user, request)
       travel_to 5.seconds.ago
       expect(subject).to be_invalid
       expect(subject.errors[:base]).to be_present
@@ -215,9 +132,8 @@ RSpec.describe Saml::Kit::Response do
       allow(registry).to receive(:metadata_for).and_return(metadata)
       allow(metadata).to receive(:matches?).and_return(true)
 
-      allow(Saml::Kit.configuration).to receive(:issuer).and_return(FFaker::Internet.http_url)
-      allow(request).to receive(:issuer).and_return(FFaker::Internet.http_url)
-      subject = described_class.new(builder.to_xml)
+      allow(Saml::Kit.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
@@ -225,10 +141,10 @@ RSpec.describe Saml::Kit::Response do
 
     it 'is invalid' do
       now = Time.now.utc
-      destination = FFaker::Internet.http_url
+      destination = FFaker::Internet.uri("https")
       raw_xml = <<-XML
 <?xml version="1.0"?>
-<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="_#{SecureRandom.uuid}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{destination}" Consent="#{Saml::Kit::Namespaces::UNSPECIFIED}" InResponseTo="#{request.id}">
+<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{Saml::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}"/>
@@ -244,19 +160,19 @@ RSpec.describe Saml::Kit::Response do
 
   describe "#signed?" do
     let(:now) { Time.now.utc }
-    let(:id) { SecureRandom.uuid }
+    let(:id) { Saml::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">
+<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: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#"/>
@@ -282,12 +198,12 @@ RSpec.describe Saml::Kit::Response do
     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}">
+<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: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#"/>
@@ -303,7 +219,7 @@ RSpec.describe Saml::Kit::Response do
       </ds:X509Data>
     </KeyInfo>
   </ds:Signature>
-  <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="_#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
+  <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
 </samlp:Response>
       XML
       subject = described_class.new(xml)
@@ -313,8 +229,8 @@ RSpec.describe Saml::Kit::Response do
     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 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)
@@ -324,20 +240,20 @@ RSpec.describe Saml::Kit::Response do
 
   describe "#certificate" do
     let(:now) { Time.now.utc }
-    let(:id) { SecureRandom.uuid }
+    let(:id) { Saml::Kit::Id.generate }
     let(:url) { FFaker::Internet.uri("https") }
     let(:certificate) { FFaker::Movie.title }
 
     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">
+<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: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#"/>
@@ -363,12 +279,12 @@ RSpec.describe Saml::Kit::Response do
     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}">
+<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: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#"/>
@@ -384,7 +300,7 @@ RSpec.describe Saml::Kit::Response do
       </ds:X509Data>
     </KeyInfo>
   </ds:Signature>
-  <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="_#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
+  <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
 </samlp:Response>
       XML
       subject = described_class.new(xml)
@@ -394,8 +310,8 @@ RSpec.describe Saml::Kit::Response do
     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 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)
@@ -403,67 +319,15 @@ RSpec.describe Saml::Kit::Response do
     end
   end
 
-  describe described_class::Builder do
-    subject { described_class.new(user, request) }
-    let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: []) }
-    let(:request) { double(:request, id: "_#{SecureRandom.uuid}", acs_url: FFaker::Internet.http_url, provider: provider, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: issuer, signed?: true, trusted?: true) }
-    let(:provider) { double(want_assertions_signed: false, encryption_certificates: [Saml::Kit::Certificate.new(encryption_pem, use: :encryption)]) }
-    let(:encryption_pem) do
-      Saml::Kit.configuration.stripped_encryption_certificate
-    end
-    let(:issuer) { FFaker::Internet.uri("https") }
-
-    before :each do
-      allow(Saml::Kit.configuration).to receive(:issuer).and_return(issuer)
-    end
-
-    describe "#build" do
-      it 'builds a response with the request_id' do
-        expect(subject.build.request_id).to eql(request.id)
-      end
-
-      it 'builds a valid encrypted assertion' do
-        allow(Saml::Kit.configuration.registry).to receive(:metadata_for).with(issuer).and_return(provider)
-        allow(provider).to receive(:matches?).and_return(true)
-
-        subject.sign = true
-        subject.encrypt = true
-        result = subject.build
-        expect(result).to be_valid
-      end
-    end
-
-    describe "#to_xml" do
-      it 'generates an EncryptedAssertion' do
-        subject.encrypt = true
-        result = Hash.from_xml(subject.to_xml)
-        expect(result['Response']['EncryptedAssertion']).to be_present
-        encrypted_assertion = result['Response']['EncryptedAssertion']
-        decrypted_assertion = Saml::Kit::Cryptography.new.decrypt(encrypted_assertion)
-        decrypted_hash = Hash.from_xml(decrypted_assertion)
-        expect(decrypted_hash['Assertion']).to be_present
-        expect(decrypted_hash['Assertion']['Issuer']).to be_present
-        expect(decrypted_hash['Assertion']['Subject']).to be_present
-        expect(decrypted_hash['Assertion']['Subject']['NameID']).to be_present
-        expect(decrypted_hash['Assertion']['Subject']['SubjectConfirmation']).to be_present
-        expect(decrypted_hash['Assertion']['Conditions']).to be_present
-        expect(decrypted_hash['Assertion']['Conditions']['AudienceRestriction']).to be_present
-        expect(decrypted_hash['Assertion']['AuthnStatement']).to be_present
-        expect(decrypted_hash['Assertion']['AuthnStatement']['AuthnContext']).to be_present
-        expect(decrypted_hash['Assertion']['AuthnStatement']['AuthnContext']['AuthnContextClassRef']).to be_present
-      end
-    end
-  end
-
   describe "encrypted assertion" do
-    let(:id) { SecureRandom.uuid }
+    let(:id) { Saml::Kit::Id.generate }
     let(:now) { Time.now.utc }
-    let(:acs_url) { FFaker::Internet.uri("https") }
+    let(:assertion_consumer_service_url) { FFaker::Internet.uri("https") }
     let(:password) { FFaker::Movie.title }
     let(:assertion) do
       FFaker::Movie.title
       <<-XML
-<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="_11d39a7f-1b86-43ed-90d7-68090a857ca8" IssueInstant="2017-11-23T04:33:58Z" Version="2.0">
+<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="#{id}" IssueInstant="2017-11-23T04:33:58Z" Version="2.0">
  <Issuer>#{FFaker::Internet.uri("https")}</Issuer>
  <Subject>
    <NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">fdddf7ad-c4a4-443c-b96d-c953913b7b4e</NameID>
@@ -507,7 +371,7 @@ XML
       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="#{acs_url}" InResponseTo="_#{SecureRandom.uuid}">
+<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="#{Saml::Kit::Id.generate}">
   <saml:Issuer>#{FFaker::Internet.uri("https")}</saml:Issuer>
   <samlp:Status>
     <samlp:StatusCode Value="#{Saml::Kit::Namespaces::SUCCESS}"/>
@@ -536,4 +400,26 @@ XML
       expect(subject.attributes).to be_present
     end
   end
+
+  describe "parsing" do
+    let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: attributes) }
+    let(:request) { double(:request, id: Saml::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/service_provider_metadata_spec.rb
@@ -7,52 +7,15 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
   let(:logout_post_url) { FFaker::Internet.uri("https") }
   let(:logout_redirect_url) { FFaker::Internet.uri("https") }
 
-  describe described_class::Builder do
-    let(:acs_url) { FFaker::Internet.http_url }
-
-    it 'builds the service provider metadata' do
-      subject.entity_id = entity_id
-      subject.add_assertion_consumer_service(acs_url, binding: :http_post)
-      subject.name_id_formats = [
-        Saml::Kit::Namespaces::PERSISTENT,
-        Saml::Kit::Namespaces::TRANSIENT,
-        Saml::Kit::Namespaces::EMAIL_ADDRESS,
-      ]
-      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']['ID']).to be_present
-      expect(result['EntityDescriptor']['entityID']).to eql(entity_id)
-      expect(result['EntityDescriptor']['SPSSODescriptor']['AuthnRequestsSigned']).to eql('true')
-      expect(result['EntityDescriptor']['SPSSODescriptor']['WantAssertionsSigned']).to eql('true')
-      expect(result['EntityDescriptor']['SPSSODescriptor']['protocolSupportEnumeration']).to eql('urn:oasis:names:tc:SAML:2.0:protocol')
-      expect(result['EntityDescriptor']['SPSSODescriptor']['NameIDFormat']).to match_array([
-        Saml::Kit::Namespaces::PERSISTENT,
-        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']['Location']).to eql(acs_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['KeyInfo']['X509Data']['X509Certificate'] }).to match_array([
-        Saml::Kit.configuration.stripped_signing_certificate,
-        Saml::Kit.configuration.stripped_encryption_certificate,
-      ])
-    end
-  end
-
   describe described_class do
-    let(:builder) { described_class::Builder.new }
     subject do
-      builder.entity_id = entity_id
-      builder.add_assertion_consumer_service(acs_post_url, binding: :http_post)
-      builder.add_assertion_consumer_service(acs_redirect_url, binding: :http_redirect)
-      builder.add_single_logout_service(logout_post_url, binding: :http_post)
-      builder.add_single_logout_service(logout_redirect_url, binding: :http_redirect)
-      builder.build
+      described_class.build do |builder|
+        builder.entity_id = entity_id
+        builder.add_assertion_consumer_service(acs_post_url, binding: :http_post)
+        builder.add_assertion_consumer_service(acs_redirect_url, binding: :http_redirect)
+        builder.add_single_logout_service(logout_post_url, binding: :http_post)
+        builder.add_single_logout_service(logout_redirect_url, binding: :http_redirect)
+      end
     end
 
     it 'returns each of the certificates' do
@@ -89,13 +52,13 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
 
   describe "#validate" do
     let(:service_provider_metadata) do
-      builder = described_class::Builder.new
-      builder.entity_id = entity_id
-      builder.add_assertion_consumer_service(acs_post_url, binding: :http_post)
-      builder.add_assertion_consumer_service(acs_redirect_url, binding: :http_redirect)
-      builder.add_single_logout_service(logout_post_url, binding: :http_post)
-      builder.add_single_logout_service(logout_redirect_url, binding: :http_redirect)
-      builder.to_xml
+      described_class.build do |builder|
+        builder.entity_id = entity_id
+        builder.add_assertion_consumer_service(acs_post_url, binding: :http_post)
+        builder.add_assertion_consumer_service(acs_redirect_url, binding: :http_redirect)
+        builder.add_single_logout_service(logout_post_url, binding: :http_post)
+        builder.add_single_logout_service(logout_redirect_url, binding: :http_redirect)
+      end.to_xml
     end
 
     it 'valid when given valid service provider metadata' do
@@ -138,7 +101,7 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
     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="_#{SecureRandom.uuid}" entityID="#{entity_id}">
+<EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="#{Saml::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>
@@ -150,7 +113,7 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
   end
 
   describe "#matches?" do
-    subject { described_class::Builder.new.build }
+    subject { Saml::Kit::ServiceProviderMetadata.build }
 
     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']
@@ -164,4 +127,26 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
       expect(subject.matches?(fingerprint)).to be_falsey
     end
   end
+
+  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|
+        builder.entity_id = entity_id
+        builder.add_assertion_consumer_service(assertion_consumer_service_url, binding: :http_post)
+      end
+
+      expect(result).to be_instance_of(described_class)
+      expect(result.entity_id).to eql(entity_id)
+      expect(result.assertion_consumer_service_for(binding: :http_post).location).to eql(assertion_consumer_service_url)
+    end
+  end
+
+  describe "deprecations" do
+    it 'resolves the old builder constant' do
+      subject = Saml::Kit::ServiceProviderMetadata::Builder.new
+      expect(subject).to be_present
+    end
+  end
 end
spec/saml/signature_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Saml::Kit::Signature do
     config
   end
 
-  let(:reference_id) { SecureRandom.uuid }
+  let(:reference_id) { Saml::Kit::Id.generate }
   let(:rsa_key) { OpenSSL::PKey::RSA.new(2048) }
   let(:public_key) { rsa_key.public_key }
   let(:certificate) do
@@ -34,7 +34,7 @@ RSpec.describe Saml::Kit::Signature do
     options = {
       "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
       "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
-      ID: "_#{reference_id}",
+      ID: reference_id,
     }
     signed_xml = described_class.sign(sign: true, configuration: configuration) do |xml, signature|
       xml.tag!('samlp:AuthnRequest', options) do
@@ -49,7 +49,7 @@ RSpec.describe Saml::Kit::Signature do
     expect(signature['SignedInfo']['CanonicalizationMethod']['Algorithm']).to eql('http://www.w3.org/2001/10/xml-exc-c14n#')
     expect(signature['SignedInfo']['SignatureMethod']['Algorithm']).to eql("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256")
 
-    expect(signature['SignedInfo']['Reference']['URI']).to eql("#_#{reference_id}")
+    expect(signature['SignedInfo']['Reference']['URI']).to eql("##{reference_id}")
     expect(signature['SignedInfo']['Reference']['Transforms']['Transform']).to match_array([
       { "Algorithm" => "http://www.w3.org/2000/09/xmldsig#enveloped-signature" },
       { "Algorithm" => "http://www.w3.org/2001/10/xml-exc-c14n#" }
spec/saml/cryptography_spec.rb → spec/saml/xml_decryption_spec.rb
@@ -1,6 +1,6 @@
 require 'spec_helper'
 
-RSpec.describe Saml::Kit::Cryptography do
+RSpec.describe Saml::Kit::XmlDecryption do
   describe "#decrypt" do
     let(:secret) { FFaker::Movie.title }
     let(:password) { FFaker::Movie.title }
spec/saml/xml_spec.rb
@@ -6,13 +6,13 @@ RSpec.describe Saml::Kit::Xml do
     let(:logout_url) { "https://#{FFaker::Internet.domain_name}/logout" }
 
     let(:signed_xml) do
-      builder = Saml::Kit::ServiceProviderMetadata::Builder.new
-      builder.entity_id = FFaker::Movie.title
-      builder.add_assertion_consumer_service(login_url, binding: :http_post)
-      builder.add_assertion_consumer_service(login_url, binding: :http_redirect)
-      builder.add_single_logout_service(logout_url, binding: :http_post)
-      builder.add_single_logout_service(logout_url, binding: :http_redirect)
-      builder.to_xml
+      Saml::Kit::ServiceProviderMetadata.build do |builder|
+        builder.entity_id = FFaker::Movie.title
+        builder.add_assertion_consumer_service(login_url, binding: :http_post)
+        builder.add_assertion_consumer_service(login_url, binding: :http_redirect)
+        builder.add_single_logout_service(logout_url, binding: :http_post)
+        builder.add_single_logout_service(logout_url, binding: :http_redirect)
+      end.to_xml
     end
 
     it 'returns true, when the digest and signature is valid' do
spec/spec_helper.rb
@@ -1,3 +1,7 @@
+require 'simplecov'
+SimpleCov.start do
+  add_filter '/spec/'
+end
 require "bundler/setup"
 require "saml/kit"
 require "active_support/testing/time_helpers"
README.md
@@ -32,7 +32,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
 
 ## Contributing
 
-Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/saml-kit.
+Bug reports and pull requests are welcome on GitHub at https://github.com/mokhan/saml-kit.
 
 ## License
 
saml-kit.gemspec
@@ -23,13 +23,13 @@ Gem::Specification.new do |spec|
   spec.require_paths = ["lib"]
 
   spec.add_dependency "activemodel", ">= 4.2.0"
-  spec.add_dependency "activesupport", ">= 4.2.0"
   spec.add_dependency "builder", "~> 3.2"
   spec.add_dependency "nokogiri", "~> 1.8"
   spec.add_dependency "xmldsig", "~> 0.6"
   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 "ffaker", "~> 2.7"
+  spec.add_development_dependency "simplecov", "~> 0.15.1"
   spec.add_development_dependency "webmock", "~> 3.1"
 end