Comparing changes

v0.2.17 v0.2.18
20 commits 68 files changed

Commits

683bc52 bump version. mo 2017-12-24 05:20:03
7182e75 case -> hash lookup. mo 2017-12-24 05:16:21
6cf9160 try to improve readability. mo 2017-12-24 04:53:36
a447676 remove respond_to check. mo 2017-12-24 04:26:47
6d841c3 lazy load xml hash. mo 2017-12-24 04:14:12
8942f86 allow for clock drift. mo 2017-12-24 04:06:14
beecc8f embed rspec examples as doc. mo 2017-12-23 18:35:25
2231946 update response examples. mo 2017-12-23 18:26:11
e029742 embed metadata examples. mo 2017-12-23 18:08:43
e20f83d embed examples mo 2017-12-23 18:02:41
46e0e42 fix typo. mo 2017-12-23 17:47:58
b78d895 add logo. mo 2017-12-23 17:46:45
2c805d1 add gif. mo 2017-12-23 17:43:16
5a96fa6 split examples. mo 2017-12-23 17:40:40
ce2606a add project badges. mo 2017-12-23 17:31:23
lib/saml/kit/bindings/binding.rb
@@ -1,6 +1,7 @@
 module Saml
   module Kit
     module Bindings
+      # {include:file:spec/saml/bindings/binding_spec.rb}
       class Binding
         attr_reader :binding, :location
 
lib/saml/kit/bindings/http_post.rb
@@ -1,6 +1,7 @@
 module Saml
   module Kit
     module Bindings
+      # {include:file:spec/saml/bindings/http_post_spec.rb}
       class HttpPost < Binding
         include Serializable
 
lib/saml/kit/bindings/http_redirect.rb
@@ -1,6 +1,7 @@
 module Saml
   module Kit
     module Bindings
+      # {include:file:spec/saml/bindings/http_redirect_spec.rb}
       class HttpRedirect < Binding
         include Serializable
 
@@ -31,21 +32,26 @@ module Saml
 
         def ensure_valid_signature!(params, document)
           return if params[:Signature].blank? || params[:SigAlg].blank?
-
-          signature = decode(params[:Signature])
-          canonical_form = [:SAMLRequest, :SAMLResponse, :RelayState, :SigAlg].map do |key|
-            value = params[key]
-            value.present? ? "#{key}=#{value}" : nil
-          end.compact.join('&')
-
           return if document.provider.nil?
-          if document.provider.verify(algorithm_for(params[:SigAlg]), signature, canonical_form)
+
+          if document.provider.verify(
+              algorithm_for(params[:SigAlg]),
+              decode(params[:Signature]),
+              canonicalize(params)
+          )
             document.signature_verified!
           else
             raise ArgumentError.new("Invalid Signature")
           end
         end
 
+        def canonicalize(params)
+          [:SAMLRequest, :SAMLResponse, :RelayState, :SigAlg].map do |key|
+            value = params[key]
+            value.present? ? "#{key}=#{value}" : nil
+          end.compact.join('&')
+        end
+
         def algorithm_for(algorithm)
           case algorithm =~ /(rsa-)?sha(.*?)$/i && $2.to_i
           when 256
@@ -60,20 +66,13 @@ module Saml
         end
 
         def normalize(params)
-          if params.respond_to? :inject
-            params.inject({}) do |memo, (key, value)|
-              memo[key.to_sym] = value
-              memo
-            end
-          else
-            {
-              SAMLRequest: params['SAMLRequest'] || params[:SAMLRequest],
-              SAMLResponse: params['SAMLResponse'] || params[:SAMLResponse],
-              RelayState: params['RelayState'] || params[:RelayState],
-              Signature: params['Signature'] || params[:Signature],
-              SigAlg: params['SigAlg'] || params[:SigAlg],
-            }
-          end
+          {
+            SAMLRequest: params['SAMLRequest'] || params[:SAMLRequest],
+            SAMLResponse: params['SAMLResponse'] || params[:SAMLResponse],
+            RelayState: params['RelayState'] || params[:RelayState],
+            Signature: params['Signature'] || params[:Signature],
+            SigAlg: params['SigAlg'] || params[:SigAlg],
+          }
         end
       end
     end
lib/saml/kit/bindings/url_builder.rb
@@ -1,6 +1,7 @@
 module Saml
   module Kit
     module Bindings
+      # {include:file:spec/saml/bindings/url_builder_spec.rb}
       class UrlBuilder
         include Serializable
         attr_reader :configuration
lib/saml/kit/builders/authentication_request.rb
@@ -1,6 +1,7 @@
 module Saml
   module Kit
     module Builders
+      # {include:file:spec/saml/builders/authentication_request_spec.rb}
       class AuthenticationRequest
         include Saml::Kit::Templatable
         attr_accessor :id, :now, :issuer, :assertion_consumer_service_url, :name_id_format, :destination
lib/saml/kit/builders/identity_provider_metadata.rb
@@ -1,6 +1,7 @@
 module Saml
   module Kit
     module Builders
+      # {include:file:spec/saml/builders/identity_provider_metadata_spec.rb}
       class IdentityProviderMetadata
         include Saml::Kit::Templatable
         extend Forwardable
lib/saml/kit/builders/logout_request.rb
@@ -1,6 +1,7 @@
 module Saml
   module Kit
     module Builders
+      # {include:file:spec/saml/builders/logout_request_spec.rb}
       class LogoutRequest
         include Saml::Kit::Templatable
         attr_accessor :id, :destination, :issuer, :name_id_format, :now
lib/saml/kit/builders/logout_response.rb
@@ -1,6 +1,7 @@
 module Saml
   module Kit
     module Builders
+      # {include:file:spec/saml/builders/logout_response_spec.rb}
       class LogoutResponse
         include Saml::Kit::Templatable
         attr_accessor :id, :issuer, :version, :status_code, :now, :destination
lib/saml/kit/builders/metadata.rb
@@ -1,6 +1,7 @@
 module Saml
   module Kit
     module Builders
+      # {include:file:spec/saml/builders/metadata_spec.rb}
       class Metadata
         include Templatable
 
lib/saml/kit/builders/response.rb
@@ -1,6 +1,7 @@
 module Saml
   module Kit
     module Builders
+      # {include:file:spec/saml/builders/response_spec.rb}
       class Response
         include Templatable
         attr_reader :user, :request
lib/saml/kit/builders/service_provider_metadata.rb
@@ -1,6 +1,7 @@
 module Saml
   module Kit
     module Builders
+      # {include:file:spec/saml/builders/service_provider_metadata_spec.rb}
       class ServiceProviderMetadata
         include Saml::Kit::Templatable
         extend Forwardable
lib/saml/kit/assertion.rb
@@ -27,12 +27,12 @@ module Saml
         xml_hash ? Signature.new(xml_hash) : nil
       end
 
-      def expired?
-        Time.current > expired_at
+      def expired?(now = Time.current)
+        now > expired_at
       end
 
-      def active?
-        Time.current > started_at && !expired?
+      def active?(now = Time.current)
+        now > configuration.clock_drift.before(started_at) && !expired?
       end
 
       def attributes
@@ -69,9 +69,11 @@ module Saml
 
       private
 
+      attr_reader :configuration
+
       def assertion
         @assertion ||= if encrypted?
-          decrypted = XmlDecryption.new(configuration: @configuration).decrypt(@xml_hash['Response']['EncryptedAssertion'])
+          decrypted = XmlDecryption.new(configuration: configuration).decrypt(@xml_hash['Response']['EncryptedAssertion'])
           Saml::Kit.logger.debug(decrypted)
           Hash.from_xml(decrypted)['Assertion']
         else
@@ -91,7 +93,7 @@ module Saml
       end
 
       def must_match_issuer
-        unless audiences.include?(@configuration.issuer)
+        unless audiences.include?(configuration.issuer)
           errors[:audience] << error_message(:must_match_issuer)
         end
       end
lib/saml/kit/authentication_request.rb
@@ -13,6 +13,10 @@ module Saml
     #      <saml:Issuer>Day of the Dangerous Cousins</saml:Issuer>
     #      <samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"/>
     #    </samlp:AuthnRequest>
+    #
+    # Example:
+    #
+    # {include:file:spec/examples/authentication_request_spec.rb}
     class AuthenticationRequest < Document
       include Requestable
 
lib/saml/kit/certificate.rb
@@ -1,5 +1,6 @@
 module Saml
   module Kit
+    # {include:file:spec/saml/certificate_spec.rb}
     class Certificate
       BEGIN_CERT=/-----BEGIN CERTIFICATE-----/
       END_CERT=/-----END CERTIFICATE-----/
lib/saml/kit/configuration.rb
@@ -32,14 +32,17 @@ module Saml
       attr_accessor :session_timeout
       # The logger to write log messages to.
       attr_accessor :logger
+      # The total allowable clock drift for session timeout validation.
+      attr_accessor :clock_drift
 
       def initialize # :yields configuration
-        @signature_method = :SHA256
+        @clock_drift = 30.seconds
         @digest_method = :SHA256
+        @key_pairs = []
+        @logger = Logger.new(STDOUT)
         @registry = DefaultRegistry.new
         @session_timeout = 3.hours
-        @logger = Logger.new(STDOUT)
-        @key_pairs = []
+        @signature_method = :SHA256
         yield self if block_given?
       end
 
lib/saml/kit/default_registry.rb
@@ -26,7 +26,8 @@ module Saml
     #     configuration.registry = OnDemandRegistry.new(configuration.registry)
     #     configuration.logger = Rails.logger
     #   end
-
+    #
+    # {include:file:spec/saml/default_registry.rb}
     class DefaultRegistry
       def initialize(items = {})
         @items = items
lib/saml/kit/document.rb
@@ -17,37 +17,36 @@ module Saml
         @configuration = configuration
         @content = xml
         @name = name
-        @xml_hash = Hash.from_xml(xml) || {}
       end
 
       # Returns the ID for the SAML document.
       def id
-        to_h.fetch(name, {}).fetch('ID', nil)
+        root.fetch('ID', nil)
       end
 
       # Returns the Issuer for the SAML document.
       def issuer
-        to_h.fetch(name, {}).fetch('Issuer', nil)
+        root.fetch('Issuer', nil)
       end
 
       # Returns the Version of the SAML document.
       def version
-        to_h.fetch(name, {}).fetch('Version', {})
+        root.fetch('Version', {})
       end
 
       # Returns the Destination of the SAML document.
       def destination
-        to_h.fetch(name, {}).fetch('Destination', nil)
+        root.fetch('Destination', nil)
       end
 
       # Returns the Destination of the SAML document.
       def issue_instant
-        Time.parse(to_h[name]['IssueInstant'])
+        Time.parse(root['IssueInstant'])
       end
 
       # Returns the SAML document returned as a Hash.
       def to_h
-        @xml_hash
+        @xml_hash ||= Hash.from_xml(content) || {}
       end
 
       # Returns the SAML document as an XML string.
@@ -68,24 +67,28 @@ module Saml
       end
 
       class << self
+        XPATH = [
+          "/samlp:AuthnRequest",
+          "/samlp:LogoutRequest",
+          "/samlp:LogoutResponse",
+          "/samlp:Response",
+        ].join("|")
+
         # Returns the raw xml as a Saml::Kit SAML document.
         #
         # @param xml [String] the raw xml string.
         # @param configuration [Saml::Kit::Configuration] the configuration to use for unpacking the document.
         def to_saml_document(xml, configuration: Saml::Kit.configuration)
-          hash = Hash.from_xml(xml)
-          if hash['Response'].present?
-            Response.new(xml, configuration: configuration)
-          elsif hash['LogoutResponse'].present?
-            LogoutResponse.new(xml, configuration: configuration)
-          elsif hash['AuthnRequest'].present?
-            AuthenticationRequest.new(xml, configuration: configuration)
-          elsif hash['LogoutRequest'].present?
-            LogoutRequest.new(xml, configuration: configuration)
-          end
+          constructor = {
+            "AuthnRequest" => Saml::Kit::AuthenticationRequest,
+            "LogoutRequest" => Saml::Kit::LogoutRequest,
+            "LogoutResponse" => Saml::Kit::LogoutResponse,
+            "Response" => Saml::Kit::Response,
+          }[Saml::Kit::Xml.new(xml).find_by(XPATH).name] || InvalidDocument
+          constructor.new(xml, configuration: configuration)
         rescue => error
           Saml::Kit.logger.error(error)
-          InvalidDocument.new(xml)
+          InvalidDocument.new(xml, configuration: configuration)
         end
 
         # @!visibility private
@@ -109,18 +112,19 @@ module Saml
 
       attr_reader :content, :name, :configuration
 
+      def root
+        to_h.fetch(name, {})
+      end
+
       def must_match_xsd
         matches_xsd?(PROTOCOL_XSD)
       end
 
       def must_be_expected_type
-        return if to_h.nil?
-
         errors[:base] << error_message(:invalid) unless expected_type?
       end
 
       def expected_type?
-        return false if to_xml.blank?
         to_h[name].present?
       end
 
lib/saml/kit/fingerprint.rb
@@ -6,6 +6,8 @@ module Saml
     #
     #   puts Saml::Kit::Fingerprint.new(certificate).to_s
     #   # B7:AB:DC:BD:4D:23:58:65:FD:1A:99:0C:5F:89:EA:87:AD:F1:D7:83:34:7A:E9:E4:88:12:DD:46:1F:38:05:93
+    #
+    # {include:file:spec/saml/fingerprint_spec.rb}
     class Fingerprint
       # The OpenSSL::X509::Certificate
       attr_reader :x509
lib/saml/kit/identity_provider_metadata.rb
@@ -26,6 +26,10 @@ module Saml
     #   puts metadata.to_xml
     #
     # For more details on generating metadata see {Saml::Kit::Metadata}.
+    #
+    # Example:
+    #
+    # {include:file:spec/examples/identity_provider_metadata_spec.rb}
     class IdentityProviderMetadata < Metadata
       def initialize(xml)
         super("IDPSSODescriptor", xml)
lib/saml/kit/invalid_document.rb
@@ -1,13 +1,20 @@
 module Saml
   module Kit
+    # {include:file:spec/saml/invalid_document_spec.rb}
     class InvalidDocument < Document
       validate do |model|
         model.errors[:base] << model.error_message(:invalid)
       end
 
-      def initialize(xml)
+      def initialize(xml, configuration: nil)
         super(xml, name: "InvalidDocument")
       end
+
+      def to_h
+        super
+      rescue
+        {}
+      end
     end
   end
 end
lib/saml/kit/logout_request.rb
@@ -20,6 +20,8 @@ module Saml
     #   url, saml_params = document.response_for(binding: :http_post)
     #
     # See {#response_for} for more information.
+    #
+    # {include:file:spec/examples/logout_request_spec.rb}
     class LogoutRequest < Document
       include Requestable
       validates_presence_of :single_logout_service, if: :expected_type?
lib/saml/kit/logout_response.rb
@@ -3,6 +3,8 @@ module Saml
     # This class is used to parse a LogoutResponse SAML document.
     #
     #   document = Saml::Kit::LogoutResponse.new(raw_xml)
+    #
+    # {include:file:spec/examples/logout_response_spec.rb}
     class LogoutResponse < Document
       include Respondable
 
lib/saml/kit/metadata.rb
@@ -21,6 +21,7 @@ module Saml
     #
     # See {Saml::Kit::Builders::ServiceProviderMetadata} and {Saml::Kit::Builders::IdentityProviderMetadata}
     # for a list of options that can be specified.
+    # {include:file:spec/examples/metadata_spec.rb}
     class Metadata
       METADATA_XSD = File.expand_path("./xsd/saml-schema-metadata-2.0.xsd", File.dirname(__FILE__)).freeze
       include ActiveModel::Validations
lib/saml/kit/response.rb
@@ -1,5 +1,6 @@
 module Saml
   module Kit
+    # {include:file:spec/examples/response_spec.rb}
     class Response < Document
       include Respondable
       extend Forwardable
lib/saml/kit/service_provider_metadata.rb
@@ -1,5 +1,6 @@
 module Saml
   module Kit
+    # {include:file:spec/examples/service_provider_metadata_spec.rb}
     class ServiceProviderMetadata < Metadata
       def initialize(xml)
         super("SPSSODescriptor", xml)
lib/saml/kit/version.rb
@@ -1,5 +1,5 @@
 module Saml
   module Kit
-    VERSION = "0.2.17"
+    VERSION = "0.2.18"
   end
 end
lib/saml/kit/xml.rb
@@ -1,5 +1,6 @@
 module Saml
   module Kit
+    # {include:file:spec/saml/xml_spec.rb}
     class Xml # :nodoc:
       include ActiveModel::Validations
       NAMESPACES = {
@@ -59,24 +60,20 @@ module Saml
       end
 
       def validate_certificates(now = Time.current)
-        return unless document.at_xpath('//ds:Signature', Xmldsig::NAMESPACES).present?
+        return if find_by('//ds:Signature').nil?
 
         x509_certificates.each do |certificate|
-          if now < certificate.not_before
-            errors.add(:certificate, "Not valid before #{certificate.not_before}")
-          end
+          inactive = now < certificate.not_before
+          errors.add(:certificate, "Not valid before #{certificate.not_before}") if inactive
 
-          if now > certificate.not_after
-            errors.add(:certificate, "Not valid after #{certificate.not_after}")
-          end
+          expired = now > certificate.not_after
+          errors.add(:certificate, "Not valid after #{certificate.not_after}") if expired
         end
       end
 
       def x509_certificates
         xpath = "//ds:KeyInfo/ds:X509Data/ds:X509Certificate"
-        document.search(xpath, Xmldsig::NAMESPACES).map do |item|
-          Certificate.to_x509(item.text)
-        end
+        find_all(xpath).map { |item| Certificate.to_x509(item.text) }
       end
     end
   end
lib/saml/kit/xml_decryption.rb
@@ -1,5 +1,6 @@
 module Saml
   module Kit
+    # {include:file:spec/saml/xml_decryption_spec.rb}
     class XmlDecryption
       # The list of private keys to use to attempt to decrypt the document.
       attr_reader :private_keys
spec/examples/authentication_request_spec.rb
@@ -0,0 +1,27 @@
+RSpec.describe "Authentication Request" do
+  it 'produces an authentication request' do
+    xml = Saml::Kit::Metadata.build_xml do |builder|
+      builder.contact_email = 'hi@example.com'
+      builder.organization_name = "Acme, Inc"
+      builder.organization_url = 'https://www.example.com'
+      builder.build_identity_provider do |x|
+        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_post)
+        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_redirect)
+        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
+        x.name_id_formats = [ Saml::Kit::Namespaces::EMAIL_ADDRESS ]
+        x.attributes << :id
+        x.attributes << :email
+      end
+      builder.build_service_provider do |x|
+        x.add_assertion_consumer_service('https://www.example.com/consume', binding: :http_post)
+        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
+      end
+    end
+
+    idp = Saml::Kit::IdentityProviderMetadata.new(xml)
+    url, saml_params = idp.login_request_for(binding: :http_post)
+
+    expect(url).to eql("https://www.example.com/login")
+    expect(saml_params['SAMLRequest']).to be_present
+  end
+end
spec/examples/identity_provider_metadata_spec.rb
@@ -0,0 +1,20 @@
+RSpec.describe "Identity Provider Metadata" do
+  it 'produces identity provider metadata' do
+    xml = Saml::Kit::Metadata.build_xml do |builder|
+      builder.contact_email = 'hi@example.com'
+      builder.organization_name = "Acme, Inc"
+      builder.organization_url = 'https://www.example.com'
+      builder.build_identity_provider do |x|
+        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_post)
+        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_redirect)
+        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
+        x.name_id_formats = [ Saml::Kit::Namespaces::EMAIL_ADDRESS ]
+        x.attributes << :id
+        x.attributes << :email
+      end
+    end
+    expect(xml).to be_present
+    expect(xml).to have_xpath("//md:EntityDescriptor//md:IDPSSODescriptor")
+    expect(xml).to_not have_xpath("//md:EntityDescriptor//md:SPSSODescriptor")
+  end
+end
spec/examples/logout_request_spec.rb
@@ -0,0 +1,30 @@
+require_relative './user'
+
+RSpec.describe "Logout Request" do
+  let(:user) { User.new(id: SecureRandom.uuid, email: "hello@example.com") }
+
+  it 'produces a SAMLRequest' do
+    xml = Saml::Kit::Metadata.build_xml do |builder|
+      builder.contact_email = 'hi@example.com'
+      builder.organization_name = "Acme, Inc"
+      builder.organization_url = 'https://www.example.com'
+      builder.build_identity_provider do |x|
+        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_post)
+        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_redirect)
+        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
+        x.name_id_formats = [ Saml::Kit::Namespaces::EMAIL_ADDRESS ]
+        x.attributes << :id
+        x.attributes << :email
+      end
+      builder.build_service_provider do |x|
+        x.add_assertion_consumer_service('https://www.example.com/consume', binding: :http_post)
+        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
+      end
+    end
+
+    sp = Saml::Kit::IdentityProviderMetadata.new(xml)
+    url, saml_params = sp.logout_request_for(user, binding: :http_post)
+    expect(url).to eql("https://www.example.com/logout")
+    expect(saml_params['SAMLRequest']).to be_present
+  end
+end
spec/examples/logout_response_spec.rb
@@ -0,0 +1,39 @@
+require_relative './user'
+
+RSpec.describe "Logout Response" do
+  let(:user) { User.new(id: SecureRandom.uuid, email: "hello@example.com") }
+
+  it 'generates a logout response' do
+    xml = Saml::Kit::Metadata.build_xml do |builder|
+      builder.contact_email = 'hi@example.com'
+      builder.organization_name = "Acme, Inc"
+      builder.organization_url = 'https://www.example.com'
+      builder.build_identity_provider do |x|
+        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_post)
+        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_redirect)
+        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
+        x.name_id_formats = [ Saml::Kit::Namespaces::EMAIL_ADDRESS ]
+        x.attributes << :id
+        x.attributes << :email
+      end
+      builder.build_service_provider do |x|
+        x.add_assertion_consumer_service('https://www.example.com/consume', binding: :http_post)
+        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
+      end
+    end
+
+    idp = Saml::Kit::IdentityProviderMetadata.new(xml)
+    url, saml_params = idp.logout_request_for(user, binding: :http_post)
+    uri = URI.parse("#{url}?#{saml_params.map { |(x, y)| "#{x}=#{y}" }.join('&')}")
+
+    raw_params = Hash[uri.query.split("&amp;").map { |x| x.split("=", 2) }].symbolize_keys
+
+    binding = idp.single_logout_service_for(binding: :http_post)
+    saml_request = binding.deserialize(raw_params)
+    sp = Saml::Kit::ServiceProviderMetadata.new(xml)
+    allow(saml_request).to receive(:provider).and_return(sp)
+    url, saml_params = saml_request.response_for(binding: :http_post)
+    expect(url).to eql("https://www.example.com/logout")
+    expect(saml_params['SAMLResponse']).to be_present
+  end
+end
spec/examples/metadata_spec.rb
@@ -0,0 +1,44 @@
+RSpec.describe "Metadata" do
+  it 'consumes metadata' do
+    raw_xml = <<-XML
+<?xml version="1.0" encoding="UTF-8"?>
+<EntityDescriptor entityID="https://www.example.com/metadata" xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_50643868-c737-40c8-a30d-b5dc7f3c69d9">
+  <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+    <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:persistent</NameIDFormat>
+    <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://www.example.com/login"/>
+  </IDPSSODescriptor>
+  <SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+    <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>
+    <AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://www.example.com/consume" index="0" isDefault="true"/>
+  </SPSSODescriptor>
+</EntityDescriptor>
+    XML
+
+    metadata = Saml::Kit::Metadata.from(raw_xml)
+    expect(metadata.entity_id).to eql('https://www.example.com/metadata')
+  end
+
+  it 'produces metadata for a service provider and identity provider' do
+    metadata = Saml::Kit::Metadata.build do |builder|
+      builder.contact_email = 'hi@example.com'
+      builder.organization_name = "Acme, Inc"
+      builder.organization_url = 'https://www.example.com'
+      builder.build_identity_provider do |x|
+        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_post)
+        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_redirect)
+        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
+        x.name_id_formats = [ Saml::Kit::Namespaces::EMAIL_ADDRESS ]
+        x.attributes << :id
+        x.attributes << :email
+      end
+      builder.build_service_provider do |x|
+        x.add_assertion_consumer_service('https://www.example.com/consume', binding: :http_post)
+        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
+      end
+    end
+    xml = metadata.to_xml(pretty: true)
+    expect(xml).to be_present
+    expect(xml).to have_xpath("//md:EntityDescriptor//md:IDPSSODescriptor")
+    expect(xml).to have_xpath("//md:EntityDescriptor//md:SPSSODescriptor")
+  end
+end
spec/examples/response_spec.rb
@@ -0,0 +1,86 @@
+require_relative './user'
+
+RSpec.describe "Response" do
+  let(:user) { User.new(id: SecureRandom.uuid, email: "hello@example.com") }
+  let(:request) { Saml::Kit::AuthenticationRequest.build }
+
+  it 'consumes a Response' do
+    raw_xml = <<-XML
+<?xml version="1.0" encoding="UTF-8"?>
+<Response xmlns="urn:oasis:names:tc:SAML:2.0:protocol" ID="_32594448-5d41-4e5b-87c5-ee32ef1f14f7" Version="2.0" IssueInstant="2017-12-23T18:13:58Z" Destination="" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_55236abc-636f-41d1-8c0d-81c5384786dd">
+  <Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">https://www.example.com/metadata</Issuer>
+  <Status>
+    <StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
+  </Status>
+  <Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="_843f14bc-51e9-40d3-9861-23e59ccc8427" IssueInstant="2017-12-23T18:13:58Z" Version="2.0">
+    <Issuer>https://www.example.com/metadata</Issuer>
+    <Subject>
+      <NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent">ed215a85-597f-4e74-a892-ac83c386190b</NameID>
+      <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
+        <SubjectConfirmationData InResponseTo="_55236abc-636f-41d1-8c0d-81c5384786dd" NotOnOrAfter="2017-12-23T21:13:58Z" Recipient=""/>
+      </SubjectConfirmation>
+    </Subject>
+    <Conditions NotBefore="2017-12-23T18:13:58Z" NotOnOrAfter="2017-12-23T21:13:58Z">
+      <AudienceRestriction>
+        <Audience/>
+      </AudienceRestriction>
+    </Conditions>
+    <AuthnStatement AuthnInstant="2017-12-23T18:13:58Z" SessionIndex="_843f14bc-51e9-40d3-9861-23e59ccc8427" SessionNotOnOrAfter="2017-12-23T21:13:58Z">
+      <AuthnContext>
+        <AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef>
+      </AuthnContext>
+    </AuthnStatement>
+  </Assertion>
+</Response>
+    XML
+    response = Saml::Kit::Response.new(raw_xml)
+    expect(response.assertion.name_id).to eql('ed215a85-597f-4e74-a892-ac83c386190b')
+    expect(response.issuer).to eql("https://www.example.com/metadata")
+  end
+
+  it 'builds a Response document' do
+    response = Saml::Kit::Response.build(user, request) do |builder|
+      builder.issuer = "blah"
+    end
+
+    expect(response.issuer).to eql("blah")
+    expect(response.to_xml).to have_xpath("/samlp:Response/saml:Assertion/saml:Issuer[text()=\"blah\"]")
+  end
+
+  it 'generates a SAMLResponse' do
+    xml = Saml::Kit::Metadata.build_xml do |builder|
+      builder.contact_email = 'hi@example.com'
+      builder.organization_name = "Acme, Inc"
+      builder.organization_url = 'https://www.example.com'
+      builder.build_identity_provider do |x|
+        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_post)
+        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_redirect)
+        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
+        x.name_id_formats = [ Saml::Kit::Namespaces::EMAIL_ADDRESS ]
+        x.attributes << :id
+        x.attributes << :email
+      end
+      builder.build_service_provider do |x|
+        x.add_assertion_consumer_service('https://www.example.com/consume', binding: :http_post)
+        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
+      end
+    end
+
+    idp = Saml::Kit::IdentityProviderMetadata.new(xml)
+    url, saml_params = idp.login_request_for(binding: :http_post)
+    uri = URI.parse("#{url}?#{saml_params.map { |(x, y)| "#{x}=#{y}" }.join('&')}")
+
+    sp = Saml::Kit::ServiceProviderMetadata.new(xml)
+
+    binding = idp.single_sign_on_service_for(binding: :http_post)
+    raw_params = Hash[uri.query.split("&amp;").map { |x| x.split("=", 2) }].symbolize_keys
+    saml_request = binding.deserialize(raw_params)
+    allow(saml_request).to receive(:provider).and_return(sp)
+
+    url, saml_params = saml_request.response_for(user, binding: :http_post)
+
+    expect(url).to eql("https://www.example.com/consume")
+    expect(saml_params['SAMLResponse']).to be_present
+  end
+end
+
spec/examples/saml-kit.gif
Binary file
spec/examples/service_provider_metadata_spec.rb
@@ -0,0 +1,32 @@
+RSpec.describe "Service Provider Metadata" do
+  it 'consumes service provider_metadata' do
+    raw_xml = <<-XML
+<?xml version="1.0" encoding="UTF-8"?>
+<EntityDescriptor entityID="myEntityId" xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
+  <SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+    <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>
+  </SPSSODescriptor>
+</EntityDescriptor>
+    XML
+
+    metadata = Saml::Kit::ServiceProviderMetadata.new(raw_xml)
+    expect(metadata.entity_id).to eql('myEntityId')
+    expect(metadata.name_id_formats).to match_array([Saml::Kit::Namespaces::PERSISTENT])
+  end
+
+  it 'produces service provider metadata' do
+    metadata = Saml::Kit::Metadata.build do |builder|
+      builder.contact_email = 'hi@example.com'
+      builder.organization_name = "Acme, Inc"
+      builder.organization_url = 'https://www.example.com'
+      builder.build_service_provider do |x|
+        x.add_assertion_consumer_service('https://www.example.com/consume', binding: :http_post)
+        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
+      end
+    end
+    xml = metadata.to_xml(pretty: true)
+    expect(xml).to be_present
+    expect(xml).to_not have_xpath("//md:EntityDescriptor//md:IDPSSODescriptor")
+    expect(xml).to have_xpath("//md:EntityDescriptor//md:SPSSODescriptor")
+  end
+end
spec/examples/user.rb
@@ -0,0 +1,16 @@
+class User
+  attr_reader :id, :email
+
+  def initialize(id:, email:)
+    @id = id
+    @email = email
+  end
+
+  def name_id_for(name_id_format)
+    Saml::Kit::Namespaces::PERSISTENT == name_id_format ? id : email
+  end
+
+  def assertion_attributes_for(request)
+    request.trusted? ? { access_token: SecureRandom.uuid } : {}
+  end
+end
spec/saml/bindings/binding_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::Bindings::Binding do
   let(:location) { FFaker::Internet.http_url }
   subject { described_class.new(binding: Saml::Kit::Bindings::HTTP_ARTIFACT, location: location) }
spec/saml/bindings/http_post_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::Bindings::HttpPost do
   let(:location) { FFaker::Internet.uri("https") }
   subject { described_class.new(location: location) }
spec/saml/bindings/http_redirect_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::Bindings::HttpRedirect do
   let(:location) { FFaker::Internet.http_url }
   subject { described_class.new(location: location) }
spec/saml/bindings/url_builder_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::Bindings::UrlBuilder do
   describe "#build" do
     let(:xml) { "<xml></xml>" }
spec/saml/builders/authentication_request_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::Builders::AuthenticationRequest do
   subject { described_class.new(configuration: configuration) }
   let(:configuration) do
spec/saml/builders/identity_provider_metadata_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::Builders::IdentityProviderMetadata do
   subject { described_class.new(configuration: configuration) }
   let(:configuration) do
spec/saml/builders/logout_request_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::Builders::LogoutRequest do
   subject { described_class.new(user, configuration: configuration) }
   let(:user) { double(:user, name_id_for: name_id) }
spec/saml/builders/logout_response_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::Builders::LogoutResponse do
   subject { described_class.new(request) }
   let(:user) { double(:user, name_id_for: SecureRandom.uuid) }
spec/saml/builders/response_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::Builders::Response do
   subject { described_class.new(user, request, configuration: configuration) }
   let(:configuration) do
spec/saml/builders/service_provider_metadata_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::Builders::ServiceProviderMetadata do
   subject { described_class.new(configuration: configuration) }
   let(:configuration) do
spec/saml/assertion_spec.rb
@@ -0,0 +1,29 @@
+RSpec.describe Saml::Kit::Assertion do
+  describe "#active?" do
+    let(:configuration) do
+      Saml::Kit::Configuration.new do |config|
+        config.session_timeout = 30.minutes
+        config.clock_drift = 30.seconds
+      end
+    end
+
+    it 'is valid after a valid session window + drift' do
+      now = Time.current
+      travel_to now
+      xml_hash = {
+        'Response' => {
+          'Assertion' => {
+            'Conditions' => {
+              'NotBefore' => now.utc.iso8601,
+              'NotOnOrAfter' => configuration.session_timeout.since(now).iso8601,
+            }
+          }
+        }
+      }
+      subject = described_class.new(xml_hash, configuration: configuration)
+      travel_to (configuration.clock_drift - 1.second).before(now)
+      expect(subject).to be_active
+      expect(subject).to_not be_expired
+    end
+  end
+end
spec/saml/authentication_request_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::AuthenticationRequest do
   subject { described_class.new(raw_xml, configuration: configuration) }
   let(:id) { Saml::Kit::Id.generate }
spec/saml/bindings_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::Bindings do
   describe ".to_symbol" do
     subject { described_class }
spec/saml/certificate_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::Certificate do
   subject { described_class.new(certificate, use: :signing) }
   let(:certificate) do
spec/saml/composite_metadata_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::CompositeMetadata do
   subject { described_class.new(xml) }
   let(:post_binding) { Saml::Kit::Bindings::HTTP_POST  }
spec/saml/default_registry_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::DefaultRegistry do
   subject { described_class.new }
   let(:entity_id) { FFaker::Internet.http_url }
spec/saml/document_spec.rb
@@ -0,0 +1,52 @@
+RSpec.describe Saml::Kit::Document do
+  describe ".to_saml_document" do
+    subject { described_class }
+    let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: { id: SecureRandom.uuid }) }
+    let(: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) }
+
+    it 'returns a Response' do
+      xml = Saml::Kit::Response.build_xml(user, request)
+      result = subject.to_saml_document(xml)
+      expect(result).to be_instance_of(Saml::Kit::Response)
+    end
+
+    it 'returns a LogoutResponse' do
+      xml = Saml::Kit::LogoutResponse.build_xml(request)
+      result = subject.to_saml_document(xml)
+      expect(result).to be_instance_of(Saml::Kit::LogoutResponse)
+    end
+
+    it 'returns an AuthenticationRequest' do
+      xml = Saml::Kit::AuthenticationRequest.build_xml
+      result = subject.to_saml_document(xml)
+      expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
+    end
+
+    it 'returns a LogoutRequest' do
+      xml = Saml::Kit::LogoutRequest.build_xml(user)
+      result = subject.to_saml_document(xml)
+      expect(result).to be_instance_of(Saml::Kit::LogoutRequest)
+    end
+
+    it 'returns an invalid document' do
+      xml = <<-XML
+      <html>
+        <head></head>
+        <body></body>
+      </html>
+      XML
+      result = subject.to_saml_document(xml)
+      expect(result).to be_instance_of(Saml::Kit::InvalidDocument)
+    end
+
+    it 'returns an invalid document when the xml is not XML' do
+      result = subject.to_saml_document("NOT XML")
+      expect(result).to be_instance_of(Saml::Kit::InvalidDocument)
+    end
+
+    it 'returns an invalid document when given nil' do
+      result = subject.to_saml_document(nil)
+      expect(result).to be_instance_of(Saml::Kit::InvalidDocument)
+    end
+  end
+end
spec/saml/fingerprint_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::Fingerprint do
   describe "#sha" do
     it 'returns the SHA256' do
spec/saml/identity_provider_metadata_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::IdentityProviderMetadata do
   subject { described_class.new(raw_metadata) }
 
spec/saml/invalid_document_spec.rb
@@ -1,10 +1,12 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::InvalidDocument do
-  subject { described_class.new(xml) }
-  let(:xml) { "<xml></xml>" }
-
   it 'is invalid' do
+    subject = described_class.new("<xml></xml>")
+    expect(subject).to be_invalid
+    expect(subject.errors[:base]).to be_present
+  end
+
+  it 'is invalid with something that not xml' do
+    subject = described_class.new("NOT XML")
     expect(subject).to be_invalid
     expect(subject.errors[:base]).to be_present
   end
spec/saml/kit_spec.rb
@@ -1,5 +1,3 @@
-require "spec_helper"
-
 RSpec.describe Saml::Kit do
   it "has a version number" do
     expect(Saml::Kit::VERSION).not_to be nil
spec/saml/logout_request_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::LogoutRequest do
   subject { described_class.build(user, configuration: configuration) }
   let(:user) { double(:user, name_id_for: name_id) }
spec/saml/logout_response_spec.rb
@@ -1,4 +1,2 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::LogoutResponse do
 end
spec/saml/metadata_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::Metadata do
   describe ".from" do
     subject { described_class }
spec/saml/response_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::Response do
   describe "#valid?" do
     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) }
@@ -127,7 +125,7 @@ RSpec.describe Saml::Kit::Response do
       allow(metadata).to receive(:matches?).and_return(true)
 
       subject = described_class.build(user, request)
-      travel_to 5.seconds.ago
+      travel_to (Saml::Kit.configuration.clock_drift + 1.second).before(Time.now)
       expect(subject).to be_invalid
       expect(subject.errors[:base]).to be_present
     end
spec/saml/service_provider_metadata_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::ServiceProviderMetadata do
   let(:entity_id) { FFaker::Internet.uri("https") }
   let(:acs_post_url) { FFaker::Internet.uri("https") }
spec/saml/signatures_spec.rb
@@ -1,5 +1,3 @@
-require "spec_helper"
-
 RSpec.describe Saml::Kit::Signatures do
   let(:configuration) do
     config = Saml::Kit::Configuration.new
spec/saml/xml_decryption_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::XmlDecryption do
   describe "#decrypt" do
     let(:secret) { FFaker::Movie.title }
spec/saml/xml_spec.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 RSpec.describe Saml::Kit::Xml do
   describe "#valid_signature?" do
     let(:login_url) { "https://#{FFaker::Internet.domain_name}/login" }
spec/examples_spec.rb
@@ -1,198 +0,0 @@
-RSpec.describe "Examples" do
-  class User
-    attr_reader :id, :email
-
-    def initialize(id:, email:)
-      @id = id
-      @email = email
-    end
-
-    def name_id_for(name_id_format)
-      Saml::Kit::Namespaces::PERSISTENT == name_id_format ? id : email
-    end
-
-    def assertion_attributes_for(request)
-      request.trusted? ? { access_token: SecureRandom.uuid } : {}
-    end
-  end
-
-  let(:user) { User.new(id: SecureRandom.uuid, email: "hello@example.com") }
-
-  it 'produces identity provider metadata' do
-    xml = Saml::Kit::Metadata.build_xml do |builder|
-      builder.contact_email = 'hi@example.com'
-      builder.organization_name = "Acme, Inc"
-      builder.organization_url = 'https://www.example.com'
-      builder.build_identity_provider do |x|
-        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_post)
-        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_redirect)
-        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
-        x.name_id_formats = [ Saml::Kit::Namespaces::EMAIL_ADDRESS ]
-        x.attributes << :id
-        x.attributes << :email
-      end
-    end
-    expect(xml).to be_present
-    expect(xml).to have_xpath("//md:EntityDescriptor//md:IDPSSODescriptor")
-    expect(xml).to_not have_xpath("//md:EntityDescriptor//md:SPSSODescriptor")
-  end
-
-  it 'produces service provider metadata' do
-    metadata = Saml::Kit::Metadata.build do |builder|
-      builder.contact_email = 'hi@example.com'
-      builder.organization_name = "Acme, Inc"
-      builder.organization_url = 'https://www.example.com'
-      builder.build_service_provider do |x|
-        x.add_assertion_consumer_service('https://www.example.com/consume', binding: :http_post)
-        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
-      end
-    end
-    xml = metadata.to_xml(pretty: true)
-    expect(xml).to be_present
-    expect(xml).to_not have_xpath("//md:EntityDescriptor//md:IDPSSODescriptor")
-    expect(xml).to have_xpath("//md:EntityDescriptor//md:SPSSODescriptor")
-  end
-
-  it 'produces metadata for a service provider and identity provider' do
-    metadata = Saml::Kit::Metadata.build do |builder|
-      builder.contact_email = 'hi@example.com'
-      builder.organization_name = "Acme, Inc"
-      builder.organization_url = 'https://www.example.com'
-      builder.build_identity_provider do |x|
-        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_post)
-        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_redirect)
-        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
-        x.name_id_formats = [ Saml::Kit::Namespaces::EMAIL_ADDRESS ]
-        x.attributes << :id
-        x.attributes << :email
-      end
-      builder.build_service_provider do |x|
-        x.add_assertion_consumer_service('https://www.example.com/consume', binding: :http_post)
-        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
-      end
-    end
-    expect(metadata.to_xml(pretty: true)).to be_present
-    expect(metadata.to_xml(pretty: true)).to have_xpath("//md:EntityDescriptor//md:IDPSSODescriptor")
-    expect(metadata.to_xml(pretty: true)).to have_xpath("//md:EntityDescriptor//md:SPSSODescriptor")
-  end
-
-  it 'produces an authentication request' do
-    xml = Saml::Kit::Metadata.build_xml do |builder|
-      builder.contact_email = 'hi@example.com'
-      builder.organization_name = "Acme, Inc"
-      builder.organization_url = 'https://www.example.com'
-      builder.build_identity_provider do |x|
-        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_post)
-        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_redirect)
-        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
-        x.name_id_formats = [ Saml::Kit::Namespaces::EMAIL_ADDRESS ]
-        x.attributes << :id
-        x.attributes << :email
-      end
-      builder.build_service_provider do |x|
-        x.add_assertion_consumer_service('https://www.example.com/consume', binding: :http_post)
-        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
-      end
-    end
-
-    idp = Saml::Kit::IdentityProviderMetadata.new(xml)
-    url, saml_params = idp.login_request_for(binding: :http_post)
-
-    expect(url).to eql("https://www.example.com/login")
-    expect(saml_params['SAMLRequest']).to be_present
-  end
-
-  it 'produces a logout request' do
-    xml = Saml::Kit::Metadata.build_xml do |builder|
-      builder.contact_email = 'hi@example.com'
-      builder.organization_name = "Acme, Inc"
-      builder.organization_url = 'https://www.example.com'
-      builder.build_identity_provider do |x|
-        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_post)
-        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_redirect)
-        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
-        x.name_id_formats = [ Saml::Kit::Namespaces::EMAIL_ADDRESS ]
-        x.attributes << :id
-        x.attributes << :email
-      end
-      builder.build_service_provider do |x|
-        x.add_assertion_consumer_service('https://www.example.com/consume', binding: :http_post)
-        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
-      end
-    end
-
-    sp = Saml::Kit::IdentityProviderMetadata.new(xml)
-    url, saml_params = sp.logout_request_for(user, binding: :http_post)
-    expect(url).to eql("https://www.example.com/logout")
-    expect(saml_params['SAMLRequest']).to be_present
-  end
-
-  it 'generates a response' do
-    xml = Saml::Kit::Metadata.build_xml do |builder|
-      builder.contact_email = 'hi@example.com'
-      builder.organization_name = "Acme, Inc"
-      builder.organization_url = 'https://www.example.com'
-      builder.build_identity_provider do |x|
-        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_post)
-        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_redirect)
-        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
-        x.name_id_formats = [ Saml::Kit::Namespaces::EMAIL_ADDRESS ]
-        x.attributes << :id
-        x.attributes << :email
-      end
-      builder.build_service_provider do |x|
-        x.add_assertion_consumer_service('https://www.example.com/consume', binding: :http_post)
-        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
-      end
-    end
-
-    idp = Saml::Kit::IdentityProviderMetadata.new(xml)
-    url, saml_params = idp.login_request_for(binding: :http_post)
-    uri = URI.parse("#{url}?#{saml_params.map { |(x, y)| "#{x}=#{y}" }.join('&')}")
-
-    sp = Saml::Kit::ServiceProviderMetadata.new(xml)
-
-    binding = idp.single_sign_on_service_for(binding: :http_post)
-    raw_params = Hash[uri.query.split("&amp;").map { |x| x.split("=", 2) }].symbolize_keys
-    saml_request = binding.deserialize(raw_params)
-    allow(saml_request).to receive(:provider).and_return(sp)
-
-    url, saml_params = saml_request.response_for(user, binding: :http_post)
-    expect(url).to eql("https://www.example.com/consume")
-    expect(saml_params['SAMLResponse']).to be_present
-  end
-
-  it 'generates a logout response' do
-    xml = Saml::Kit::Metadata.build_xml do |builder|
-      builder.contact_email = 'hi@example.com'
-      builder.organization_name = "Acme, Inc"
-      builder.organization_url = 'https://www.example.com'
-      builder.build_identity_provider do |x|
-        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_post)
-        x.add_single_sign_on_service('https://www.example.com/login', binding: :http_redirect)
-        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
-        x.name_id_formats = [ Saml::Kit::Namespaces::EMAIL_ADDRESS ]
-        x.attributes << :id
-        x.attributes << :email
-      end
-      builder.build_service_provider do |x|
-        x.add_assertion_consumer_service('https://www.example.com/consume', binding: :http_post)
-        x.add_single_logout_service('https://www.example.com/logout', binding: :http_post)
-      end
-    end
-
-    idp = Saml::Kit::IdentityProviderMetadata.new(xml)
-    url, saml_params = idp.logout_request_for(user, binding: :http_post)
-    uri = URI.parse("#{url}?#{saml_params.map { |(x, y)| "#{x}=#{y}" }.join('&')}")
-
-    raw_params = Hash[uri.query.split("&amp;").map { |x| x.split("=", 2) }].symbolize_keys
-
-    binding = idp.single_logout_service_for(binding: :http_post)
-    saml_request = binding.deserialize(raw_params)
-    sp = Saml::Kit::ServiceProviderMetadata.new(xml)
-    allow(saml_request).to receive(:provider).and_return(sp)
-    url, saml_params = saml_request.response_for(binding: :http_post)
-    expect(url).to eql("https://www.example.com/logout")
-    expect(saml_params['SAMLResponse']).to be_present
-  end
-end
README.md
@@ -1,4 +1,8 @@
-# Saml::Kit
+![SAML-Kit](https://github.com/saml-kit/saml-kit/raw/master/spec/examples/saml-kit.gif)
+
+[![Gem Version](https://badge.fury.io/rb/saml-kit.svg)](https://rubygems.org/gems/saml-kit)
+[![Code Climate](https://codeclimate.com/github/saml-kit/saml-kit.svg)](https://codeclimate.com/github/saml-kit/saml-kit)
+[![Build Status](https://travis-ci.org/saml-kit/saml-kit.svg)](https://travis-ci.org/saml-kit/saml-kit)
 
 Saml::Kit is a library with the purpose of creating and consuming SAML
 documents. It supports the HTTP Post and HTTP Redirect bindings. It can
@@ -30,7 +34,8 @@ To specify a global configuration: (useful for a rails application)
 Saml::Kit.configure do |configuration|
   configuration.issuer = ENV['ISSUER']
   configuration.generate_key_pair_for(use: :signing)
-  configuration.generate_key_pair_for(use: :signing)
+  configuration.add_key_pair(ENV["CERTIFICATE"], ENV["PRIVATE_KEY"], passphrase: ENV['PASSPHRASE'], use: :signing)
+  configuration.generate_key_pair_for(use: :encryption)
 end
 ```
 
@@ -216,8 +221,8 @@ class User
 end
 
 user = User.new(id: SecureRandom.uuid, email: "hello@example.com")
-sp = Saml::Kit::IdentityProviderMetadata.new(xml)
-url, saml_params = sp.logout_request_for(user, binding: :http_post)
+idp = Saml::Kit::IdentityProviderMetadata.new(xml)
+url, saml_params = idp.logout_request_for(user, binding: :http_post)
 puts [url, saml_params].inspect
 # ["https://www.example.com/logout", {"SAMLRequest"=>"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48TG9nb3V0UmVxdWVzdCBJRD0iXzg3NjZiNTYyLTc2MzQtNDU4Zi04MzJmLTE4ODkwMjRlZDQ0MyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTctMTItMTlUMDQ6NTg6MThaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly93d3cuZXhhbXBsZS5jb20vbG9nb3V0IiB4bWxucz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIj48SXNzdWVyIHhtbG5zPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIi8+PE5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnBlcnNpc3RlbnQiIHhtbG5zPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5kODc3YWEzZS01YTUyLTRhODAtYTA3ZC1lM2U5YzBjNTA1Nzk8L05hbWVJRD48L0xvZ291dFJlcXVlc3Q+"}]
 ```