Comparing changes

v0.2.11 v0.2.12
16 commits 30 files changed

Commits

30ddc4d bump version. mo 2017-12-19 18:11:34
4c103b3 hide doc for internal classes. mo 2017-12-19 18:09:51
6b91400 document LogoutRequest. mo 2017-12-19 17:56:37
618997d document the default registry. mo 2017-12-19 17:21:01
e3129d2 document AuthenticationRequest. mo 2017-12-19 17:15:03
09c7171 add documentation. mo 2017-12-19 16:50:50
6bbbe6d add example specs. mo 2017-12-19 15:59:32
a718816 update README. mo 2017-12-19 15:43:17
f373aeb create a logout request. mo 2017-12-19 04:59:48
ab1cc95 add Usage to readme. mo 2017-12-19 04:43:28
lib/saml/kit/builders/assertion.rb
@@ -45,7 +45,7 @@ module Saml
         def conditions_options
           {
             NotBefore: now.utc.iso8601,
-            NotOnOrAfter: configuration.session_timeout.from_now.utc.iso8601,
+            NotOnOrAfter: configuration.session_timeout.since(now).utc.iso8601,
           }
         end
 
@@ -53,7 +53,7 @@ module Saml
           {
             AuthnInstant: now.iso8601,
             SessionIndex: reference_id,
-            SessionNotOnOrAfter: 3.hours.since(now).utc.iso8601,
+            SessionNotOnOrAfter: configuration.session_timeout.since(now).utc.iso8601,
           }
         end
       end
lib/saml/kit/builders/authentication_request.rb
@@ -16,11 +16,13 @@ module Saml
           @version = "2.0"
         end
 
+        # @deprecated Use {#assertion_consumer_service_url} instead of this method.
         def acs_url
           Saml::Kit.deprecate("acs_url is deprecated. Use assertion_consumer_service_url instead")
           self.assertion_consumer_service_url
         end
 
+        # @deprecated Use {#assertion_consumer_service_url=} instead of this method.
         def acs_url=(value)
           Saml::Kit.deprecate("acs_url= is deprecated. Use assertion_consumer_service_url= instead")
           self.assertion_consumer_service_url = value
lib/saml/kit/builders/logout_response.rb
@@ -7,14 +7,13 @@ module Saml
         attr_reader :request
         attr_reader :configuration
 
-        def initialize(user, request, configuration: Saml::Kit.configuration)
+        def initialize(request, configuration: Saml::Kit.configuration)
           @configuration = configuration
           @id = Id.generate
           @issuer = configuration.issuer
           @now = Time.now.utc
           @request = request
           @status_code = Namespaces::SUCCESS
-          @user = user
           @version = "2.0"
         end
 
lib/saml/kit/authentication_request.rb
@@ -1,20 +1,46 @@
 module Saml
   module Kit
+    # This class can be used to parse a SAML AuthnRequest or generate one.
+    #
+    # To generate an AuthnRequest use the builder API.
+    #
+    #    request = AuthenticationRequest.build do |builder|
+    #      builder.name_id_format = [Saml::Kit::Namespaces::EMAIL_ADDRESS]
+    #    end
+    #
+    #    <?xml version="1.0" encoding="UTF-8"?>
+    #    <samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_ca3a0e72-9530-41f1-9518-c53716de88b2" Version="2.0" IssueInstant="2017-12-19T16:27:44Z" Destination="http://hartmann.info" AssertionConsumerServiceURL="https://carroll.com/acs">
+    #      <saml:Issuer>Day of the Dangerous Cousins</saml:Issuer>
+    #      <samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"/>
+    #    </samlp:AuthnRequest>
     class AuthenticationRequest < Document
       include Requestable
 
+      # Create an instance of an AuthnRequest document.
+      #
+      # @param xml [String] the raw xml.
+      # @param configuration [Saml::Kit::Configuration] defaults to the global configuration.
       def initialize(xml, configuration: Saml::Kit.configuration)
         super(xml, name: "AuthnRequest", configuration: configuration)
       end
 
+      # Extract the AssertionConsumerServiceURL from the AuthnRequest
+      #    <samlp:AuthnRequest AssertionConsumerServiceURL="https://carroll.com/acs"></samlp:AuthnRequest>
       def assertion_consumer_service_url
         to_h[name]['AssertionConsumerServiceURL']
       end
 
+      # Extract the NameIDPolicy from the AuthnRequest
+      #    <samlp:AuthnRequest>
+      #      <samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"/>
+      #    </samlp:AuthnRequest>
       def name_id_format
         to_h[name]['NameIDPolicy']['Format']
       end
 
+      # Generate a Response for a specific user.
+      # @param user [Object] this is a custom user object that can be used for generating a nameid and assertion attributes.
+      # @param binding [Symbol] the SAML binding to use `:http_post` or `:http_redirect`.
       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|
@@ -24,6 +50,7 @@ module Saml
         response_binding.serialize(builder, relay_state: relay_state)
       end
 
+      # @deprecated Use {#Saml::Kit::Builders::AuthenticationRequest} instead of this.
       Builder = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Saml::Kit::AuthenticationRequest::Builder', 'Saml::Kit::Builders::AuthenticationRequest')
     end
   end
lib/saml/kit/buildable.rb
@@ -4,15 +4,15 @@ module Saml
       extend ActiveSupport::Concern
 
       class_methods do
-        def build(*args, &block)
+        def build(*args, &block) # :yields builder
           builder(*args, &block).build
         end
 
-        def build_xml(*args, &block)
+        def build_xml(*args, &block) # :yields builder
           builder(*args, &block).to_xml
         end
 
-        def builder(*args)
+        def builder(*args) # :yields builder
           builder_class.new(*args).tap do |builder|
             yield builder if block_given?
           end
lib/saml/kit/composite_metadata.rb
@@ -1,6 +1,6 @@
 module Saml
   module Kit
-    class CompositeMetadata < Metadata
+    class CompositeMetadata < Metadata # :nodoc:
       include Enumerable
       attr_reader :service_provider, :identity_provider
 
lib/saml/kit/configuration.rb
@@ -1,12 +1,39 @@
 module Saml
   module Kit
+    # This class represents the main configuration that is use for generating SAML documents.
+    #
+    #   Saml::Kit::Configuration.new do |config|
+    #     config.issuer = "com:saml:kit"
+    #     config.signature_method = :SHA256
+    #     config.digest_method = :SHA256
+    #     config.registry = Saml::Kit::DefaultRegistry.new
+    #     config.session_timeout = 30.minutes
+    #     config.logger = Rails.logger
+    #   end
+    #
+    #   To specify global configuration it is best to do this in an initialize 
+    #   that runs at the start of the program.
+    #
+    #   Saml::Kit.configure do |configuration|
+    #     configuration.issuer = "https://www.example.com/saml/metadata"
+    #     configuration.generate_key_pair_for(use: :signing)
+    #     configuration.add_key_pair(ENV["X509_CERTIFICATE"], ENV["PRIVATE_KEY"], password: ENV['PRIVATE_KEY_PASSWORD'], use: :encryption)
+    #   end
     class Configuration
+      # The issuer or entity_id to use.
       attr_accessor :issuer
-      attr_accessor :signature_method, :digest_method
-      attr_accessor :registry, :session_timeout
+      # The signature method to use when generating signatures (See {SAML::Kit::Builders::XmlSignature::SIGNATURE_METHODS})
+      attr_accessor :signature_method
+      # The digest method to use when generating signatures (See {SAML::Kit::Builders::XmlSignature::DIGEST_METHODS})
+      attr_accessor :digest_method
+      # The metadata registry to use for searching for metadata associated with an issuer.
+      attr_accessor :registry
+      # The session timeout to use when generating an Assertion.
+      attr_accessor :session_timeout
+      # The logger to write log messages to.
       attr_accessor :logger
 
-      def initialize
+      def initialize # :yields configuration
         @signature_method = :SHA256
         @digest_method = :SHA256
         @registry = DefaultRegistry.new
@@ -16,42 +43,65 @@ module Saml
         yield self if block_given?
       end
 
-      def add_key_pair(certificate, private_key, password:, use: :signing)
+      # Add a key pair that can be used for either signing or encryption.
+      #
+      # @param certificate [String] the x509 certificate with public key.
+      # @param private_key [String] the plain text private key.
+      # @param password [String] the password to decrypt the private key.
+      # @param use [Symbol] the type of key pair, `:signing` or `:encryption`
+      def add_key_pair(certificate, private_key, password: '', use: :signing)
         @key_pairs.push(KeyPair.new(certificate, private_key, password, use.to_sym))
       end
 
+      # Generates a unique key pair that can be used for signing or encryption.
+      #
+      # @param use [Symbol] the type of key pair, `:signing` or `:encryption`
+      # @param password [String] the private key password to use.
       def generate_key_pair_for(use:, password: SecureRandom.uuid)
         certificate, private_key = SelfSignedCertificate.new(password).create
         add_key_pair(certificate, private_key, password: password, use: use)
       end
 
+      # Return each key pair for a specific use.
+      #
+      # @param use [Symbol] the type of key pair to return `nil`, `:signing` or `:encryption`
       def key_pairs(use: nil)
         use.present? ? @key_pairs.find_all { |x| x.for?(use) } : @key_pairs
       end
 
+      # Return each certificate for a specific use.
+      #
+      # @param use [Symbol] the type of key pair to return `nil`, `:signing` or `:encryption`
       def certificates(use: nil)
         key_pairs(use: use).flat_map(&:certificate)
       end
 
+      # Return each private for a specific use.
+      #
+      # @param use [Symbol] the type of key pair to return `nil`, `:signing` or `:encryption`
       def private_keys(use: :signing)
         key_pairs(use: use).flat_map(&:private_key)
       end
 
+      # @deprecated Use {#certificates} instead of this method.
       def encryption_certificate
         Saml::Kit.deprecate("encryption_certificate is deprecated. Use certificates(use: :encryption) instead")
         certificates(use: :encryption).last
       end
 
+      # @deprecated Use {#private_keys} instead of this method.
       def signing_private_key
         Saml::Kit.deprecate("signing_private_key is deprecated. Use private_keys(use: :signing) instead")
         private_keys(use: :signing).last
       end
 
+      # @deprecated Use {#private_keys} instead of this method.
       def encryption_private_key
         Saml::Kit.deprecate("encryption_private_key is deprecated. Use private_keys(use: :encryption) instead")
         private_keys(use: :encryption).last
       end
 
+      # Returns true if there is at least one signing certificate registered.
       def sign?
         certificates(use: :signing).any?
       end
lib/saml/kit/default_registry.rb
@@ -1,27 +1,38 @@
 module Saml
   module Kit
+    # The default metadata registry is used to fetch the metadata associated with an issuer or entity id.
+    # The metadata associated with an issuer is used to verify trust for any SAML documents that are received.
     class DefaultRegistry
       def initialize(items = {})
         @items = items
       end
 
+      # Register a metadata document
+      #
+      # @param metadata [Saml::Kit::Metadata] the metadata to register.
       def register(metadata)
         Saml::Kit.logger.debug(metadata.to_xml(pretty: true))
         @items[metadata.entity_id] = metadata
       end
 
+      # Register metadata via a remote URL.
+      # This will attempt to connect to the remove URL to download the metadata and register it in the registry.
+      #
+      # @param url [String] the url to download the metadata from.
+      # @param verify_ssl [Boolean] enable/disable SSL peer verification.
       def register_url(url, verify_ssl: true)
         content = HttpApi.new(url, verify_ssl: verify_ssl).get
         register(Saml::Kit::Metadata.from(content))
       end
 
+      # Returns the metadata document associated with an issuer or entityID.
+      #
+      # @param entity_id [String] the unique entityID/Issuer associated with metadata.
       def metadata_for(entity_id)
         @items[entity_id]
       end
 
-      class HttpApi
-        attr_reader :uri, :verify_ssl
-
+      class HttpApi # :nodoc:
         def initialize(url, verify_ssl: true)
           @uri = URI.parse(url)
           @verify_ssl = verify_ssl
@@ -37,6 +48,8 @@ module Saml
 
         private
 
+        attr_reader :uri, :verify_ssl
+
         def http
           http = Net::HTTP.new(uri.host, uri.port)
           http.read_timeout = 30
lib/saml/kit/document.rb
@@ -13,8 +13,6 @@ module Saml
       validate :must_be_expected_type
       validate :must_be_valid_version
 
-      attr_reader :content, :name, :configuration
-
       def initialize(xml, name:, configuration: Saml::Kit.configuration)
         @configuration = configuration
         @content = xml
@@ -22,39 +20,45 @@ module Saml
         @xml_hash = Hash.from_xml(xml) || {}
       end
 
+      # Returns the ID for the SAML document.
       def id
         to_h.fetch(name, {}).fetch('ID', nil)
       end
 
+      # Returns the Issuer for the SAML document.
       def issuer
         to_h.fetch(name, {}).fetch('Issuer', nil)
       end
 
+      # Returns the Version of the SAML document.
       def version
         to_h.fetch(name, {}).fetch('Version', {})
       end
 
+      # Returns the Destination of the SAML document.
       def destination
         to_h.fetch(name, {}).fetch('Destination', nil)
       end
 
+      # Returns the Destination of the SAML document.
       def issue_instant
-        to_h[name]['IssueInstant']
-      end
-
-      def expected_type?
-        return false if to_xml.blank?
-        to_h[name].present?
+        Time.parse(to_h[name]['IssueInstant'])
       end
 
+      # Returns the SAML document returned as a Hash.
       def to_h
         @xml_hash
       end
 
+      # Returns the SAML document as an XML string.
+      #
+      # @param pretty [Boolean] formats the xml or returns the raw xml.
       def to_xml(pretty: false)
         pretty ? Nokogiri::XML(content).to_xml(indent: 2) : content
       end
 
+      # Returns the SAML document as an XHTML string. 
+      # This is useful for rendering in a web page.
       def to_xhtml
         Nokogiri::XML(content, &:noblanks).to_xhtml
       end
@@ -64,6 +68,10 @@ module Saml
       end
 
       class << self
+        # 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?
@@ -80,7 +88,8 @@ module Saml
           InvalidDocument.new(xml)
         end
 
-        def builder_class
+        # @!visibility private
+        def builder_class # :nodoc:
           case name
           when Saml::Kit::Response.to_s
             Saml::Kit::Builders::Response
@@ -98,6 +107,8 @@ module Saml
 
       private
 
+      attr_reader :content, :name, :configuration
+
       def must_match_xsd
         matches_xsd?(PROTOCOL_XSD)
       end
@@ -108,6 +119,11 @@ module Saml
         errors[:base] << error_message(:invalid) unless expected_type?
       end
 
+      def expected_type?
+        return false if to_xml.blank?
+        to_h[name].present?
+      end
+
       def must_be_valid_version
         return unless expected_type?
         return if "2.0" == version
lib/saml/kit/fingerprint.rb
@@ -1,12 +1,18 @@
 module Saml
   module Kit
+    # This generates a fingerprint for an X509 Certificate.
     class Fingerprint
+      # The OpenSSL::X509::Certificate
       attr_reader :x509
 
       def initialize(raw_certificate)
         @x509 = Certificate.to_x509(raw_certificate)
       end
 
+      # Generates a formatted fingerprint using the specified hash algorithm.
+      #
+      # @param algorithm [OpenSSL::Digest] the openssl algorithm to use `OpenSSL::Digest::SHA256`, `OpenSSL::Digest::SHA1`.
+      # @return [String] in the format of `"BF:ED:C5:F1:6C:AB:F5:B2:15:1F:BF:BD:7D:68:1A:F9:A5:4E:4C:19:30:BC:6D:25:B1:8E:98:D4:23:FD:B4:09"`
       def algorithm(algorithm)
         pretty_fingerprint(algorithm.new.hexdigest(x509.to_der))
       end
lib/saml/kit/identity_provider_metadata.rb
@@ -1,10 +1,23 @@
 module Saml
   module Kit
+    # This class is used to parse the IDPSSODescriptor from a SAML metadata document.
+    #
+    #  <?xml version="1.0" encoding="UTF-8"?>
+    #  <EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_cfa24e2f-0ec0-4ee3-abb8-b2fcfe394c1c" entityID="">
+    #    <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+    #      <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://www.example.com/logout"/>
+    #      <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
+    #      <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://www.example.com/login"/>
+    #      <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://www.example.com/login"/>
+    #      <saml:Attribute Name="id"/>
+    #    </IDPSSODescriptor>
+    #  </EntityDescriptor>
     class IdentityProviderMetadata < Metadata
       def initialize(xml)
         super("IDPSSODescriptor", xml)
       end
 
+      # Returns the IDPSSODescriptor/@WantAuthnRequestsSigned attribute.
       def want_authn_requests_signed
         xpath = "/md:EntityDescriptor/md:#{name}"
         attribute = document.find_by(xpath).attribute("WantAuthnRequestsSigned")
@@ -12,14 +25,19 @@ module Saml
         attribute.text.downcase == "true"
       end
 
+      # Returns each of the SingleSignOnService elements.
       def single_sign_on_services
         services('SingleSignOnService')
       end
 
+      # Returns a SingleSignOnService elements with the specified binding.
+      #
+      # @param binding [Symbol] `:http_post` or `:http_redirect`.
       def single_sign_on_service_for(binding:)
         service_for(binding: binding, type: 'SingleSignOnService')
       end
 
+      # Returns each of the Attributes in the metadata.
       def attributes
         document.find_all("/md:EntityDescriptor/md:#{name}/saml:Attribute").map do |item|
           {
@@ -29,7 +47,13 @@ module Saml
         end
       end
 
-      def login_request_for(binding:, relay_state: nil, configuration: Saml::Kit.configuration)
+      # Creates a AuthnRequest document for the specified binding.
+      #
+      # @param binding [Symbol] `:http_post` or `:http_redirect`.
+      # @param relay_state [Object] The RelayState to include the returned SAML params.
+      # @param configuration [Saml::Kit::Configuration] the configuration to use for generating the request.
+      # @return [Array] The url and saml params encoded using the rules for the specified binding.
+      def login_request_for(binding:, relay_state: nil, configuration: Saml::Kit.configuration) # :yields builder
         builder = Saml::Kit::AuthenticationRequest.builder(configuration: configuration) do |x|
           x.embed_signature = want_authn_requests_signed
           yield x if block_given?
@@ -38,10 +62,12 @@ module Saml
         request_binding.serialize(builder, relay_state: relay_state)
       end
 
+      # @!visibility private
       def self.builder_class
         Saml::Kit::Builders::IdentityProviderMetadata
       end
 
+      # @deprecated Use {#Saml::Kit::Builders::IdentityProviderMetadata} instead of this.
       Builder = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Saml::Kit::IdentityProviderMetadata::Builder', 'Saml::Kit::Builders::IdentityProviderMetadata')
     end
   end
lib/saml/kit/key_pair.rb
@@ -1,6 +1,6 @@
 module Saml
   module Kit
-    class KeyPair
+    class KeyPair # :nodoc:
       attr_reader :certificate, :private_key, :use
 
       def initialize(certificate, private_key, password, use)
lib/saml/kit/logout_request.rb
@@ -1,5 +1,6 @@
 module Saml
   module Kit
+    # This class parses a LogoutRequest SAML document.
     class LogoutRequest < Document
       include Requestable
       validates_presence_of :single_logout_service, if: :expected_type?
@@ -8,25 +9,34 @@ module Saml
         super(xml, name: "LogoutRequest", configuration: configuration)
       end
 
+      # Returns the NameID value.
       def name_id
         to_h[name]['NameID']
       end
 
-      def single_logout_service
-        return if provider.nil?
-        urls = provider.single_logout_services
-        urls.first
-      end
-
-      def response_for(user, binding:, relay_state: nil)
-        builder = Saml::Kit::LogoutResponse.builder(user, self) do |x|
+      # Generates a Serialized LogoutResponse using the encoding rules for the specified binding.
+      #
+      # @param binding [Symbol] The binding to use `:http_redirect` or `:http_post`.
+      # @param relay_state [Object] The RelayState to include in the RelayState param.
+      # @return [Array] Returns an array with a url and Hash of parameters to return to the requestor.
+      def response_for(binding:, relay_state: nil)
+        builder = Saml::Kit::LogoutResponse.builder(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
 
+      # @deprecated Use {#Saml::Kit::Builders::LogoutRequest} instead of this.
       Builder = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Saml::Kit::LogoutRequest::Builder', 'Saml::Kit::Builders::LogoutRequest')
+
+      private
+
+      def single_logout_service
+        return if provider.nil?
+        urls = provider.single_logout_services
+        urls.first
+      end
     end
   end
 end
lib/saml/kit/logout_response.rb
@@ -1,5 +1,6 @@
 module Saml
   module Kit
+    # This class is used to parse a LogoutResponse SAML document.
     class LogoutResponse < Document
       include Respondable
 
@@ -8,6 +9,7 @@ module Saml
         super(xml, name: "LogoutResponse", configuration: configuration)
       end
 
+      # @deprecated Use {#Saml::Kit::Builders::LogoutResponse} instead of this.
       Builder = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('Saml::Kit::LogoutResponse::Builder', 'Saml::Kit::Builders::LogoutResponse')
     end
   end
lib/saml/kit/requestable.rb
@@ -3,6 +3,7 @@ module Saml
     module Requestable
       extend ActiveSupport::Concern
 
+      # @!visibility private
       def query_string_parameter
         'SAMLRequest'
       end
lib/saml/kit/respondable.rb
@@ -9,18 +9,22 @@ module Saml
         validate :must_match_request_id
       end
 
+      # @!visibility private
       def query_string_parameter
         'SAMLResponse'
       end
 
+      # Returns the /Status/StatusCode@Value
       def status_code
         to_h.fetch(name, {}).fetch('Status', {}).fetch('StatusCode', {}).fetch('Value', nil)
       end
 
+      # Returns the /InResponseTo attribute.
       def in_response_to
         to_h.fetch(name, {}).fetch('InResponseTo', nil)
       end
 
+      # Returns true if the Status code is #{Saml::Kit::Namespaces::SUCCESS}
       def success?
         Namespaces::SUCCESS == status_code
       end
lib/saml/kit/signatures.rb
@@ -1,37 +1,45 @@
 module Saml
   module Kit
-    class Signatures
+    class Signatures # :nodoc:
+      # @!visibility private
       attr_reader :configuration
 
+      # @!visibility private
       def initialize(configuration:)
         @configuration = configuration
       end
 
+      # @!visibility private
       def build(reference_id)
         return nil unless configuration.sign?
         Saml::Kit::Builders::XmlSignature.new(reference_id, configuration: configuration)
       end
 
+      # @!visibility private
       def complete(raw_xml)
         return raw_xml unless configuration.sign?
         private_key = configuration.private_keys(use: :signing).last
         Xmldsig::SignedDocument.new(raw_xml).sign(private_key)
       end
 
+      # @!visibility private
       def self.sign(xml: ::Builder::XmlMarkup.new, configuration: Saml::Kit.configuration)
         signatures = Saml::Kit::Signatures.new(configuration: configuration)
         yield xml, XmlSignatureTemplate.new(xml, signatures)
         signatures.complete(xml.target!)
       end
 
-      class XmlSignatureTemplate
+      class XmlSignatureTemplate # :nodoc:
+        # @!visibility private
         attr_reader :signatures, :xml
 
+        # @!visibility private
         def initialize(xml, signatures)
           @signatures = signatures
           @xml = xml
         end
 
+        # @!visibility private
         def template(reference_id)
           Template.new(signatures.build(reference_id)).to_xml(xml: xml)
         end
lib/saml/kit/translatable.rb
@@ -1,6 +1,7 @@
 module Saml
   module Kit
     module Translatable
+      # @!visibility private
       def error_message(attribute, type: :invalid)
         I18n.translate(attribute, scope: "saml/kit.errors.#{name}")
       end
lib/saml/kit/trustable.rb
@@ -13,6 +13,7 @@ module Saml
         signature_manually_verified || signature.present?
       end
 
+      # @!visibility private
       def signature
         xml_hash = to_h.fetch(name, {}).fetch('Signature', nil)
         xml_hash ? Signature.new(xml_hash) : nil
@@ -24,10 +25,12 @@ module Saml
         signature.trusted?(provider)
       end
 
+      # @!visibility private
       def provider
         configuration.registry.metadata_for(issuer)
       end
 
+      # @!visibility private
       def signature_verified!
         @signature_manually_verified = true
       end
lib/saml/kit/version.rb
@@ -1,5 +1,5 @@
 module Saml
   module Kit
-    VERSION = "0.2.11"
+    VERSION = "0.2.12"
   end
 end
lib/saml/kit/xml.rb
@@ -1,13 +1,13 @@
 module Saml
   module Kit
-    class Xml
+    class Xml # :nodoc:
       include ActiveModel::Validations
       NAMESPACES = {
         "NameFormat": Namespaces::ATTR_SPLAT,
         "ds": Namespaces::XMLDSIG,
         "md": Namespaces::METADATA,
         "saml": Namespaces::ASSERTION,
-        "pro": Namespaces::PROTOCOL,
+        "samlp": Namespaces::PROTOCOL,
       }.freeze
 
       attr_reader :raw_xml, :document
lib/saml/kit/xsd_validatable.rb
@@ -1,6 +1,7 @@
 module Saml
   module Kit
     module XsdValidatable
+      # @!visibility private
       def matches_xsd?(xsd)
         Dir.chdir(File.dirname(xsd)) do
           xsd = Nokogiri::XML::Schema(IO.read(xsd))
spec/saml/bindings/http_post_spec.rb
@@ -58,9 +58,8 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
     end
 
     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_class.new(user, request, configuration: configuration)
+      builder = Saml::Kit::LogoutResponse.builder_class.new(request, configuration: configuration)
       url, saml_params = subject.serialize(builder, relay_state: relay_state)
 
       expect(url).to eql(location)
spec/saml/bindings/http_redirect_spec.rb
@@ -95,9 +95,8 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
     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, 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(user, request))
+      url, _ = subject.serialize(Saml::Kit::LogoutResponse.builder(request))
       result = subject.deserialize(query_params_from(url))
       expect(result).to be_instance_of(Saml::Kit::LogoutResponse)
     end
spec/saml/builders/logout_response_spec.rb
@@ -1,7 +1,7 @@
 require 'spec_helper'
 
 RSpec.describe Saml::Kit::Builders::LogoutResponse do
-  subject { described_class.new(user, request) }
+  subject { described_class.new(request) }
   let(:user) { double(:user, name_id_for: SecureRandom.uuid) }
   let(:request) { Saml::Kit::Builders::LogoutRequest.new(user).build }
   let(:issuer) { FFaker::Internet.http_url }
@@ -15,7 +15,7 @@ RSpec.describe Saml::Kit::Builders::LogoutResponse do
       subject.destination = destination
       result = subject.build
       expect(result.id).to be_present
-      expect(result.issue_instant).to eql(Time.now.utc.iso8601)
+      expect(result.issue_instant).to eql(Time.now.utc)
       expect(result.version).to eql("2.0")
       expect(result.issuer).to eql(issuer)
       expect(result.status_code).to eql(Saml::Kit::Namespaces::SUCCESS)
spec/saml/logout_request_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe Saml::Kit::LogoutRequest do
 
   it 'parses the issue instant' do
     travel_to 1.second.from_now
-    expect(subject.issue_instant).to eql(Time.now.utc.iso8601)
+    expect(subject.issue_instant).to eql(Time.now.utc)
   end
 
   it 'parses the version' do
@@ -130,7 +130,6 @@ RSpec.describe Saml::Kit::LogoutRequest do
   end
 
   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)
@@ -140,7 +139,7 @@ RSpec.describe Saml::Kit::LogoutRequest do
     it 'serializes a logout response for a particular user' do
       allow(subject).to receive(:provider).and_return(provider)
 
-      _, saml_params = subject.response_for(user, binding: :http_post)
+      _, saml_params = subject.response_for(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)
spec/support/matchers/have_xml.rb
@@ -1,11 +1,6 @@
 RSpec::Matchers.define :have_xpath do |xpath|
-  SAML_NAMESPACES = {
-    'saml' => Saml::Kit::Namespaces::ASSERTION,
-    'samlp' => Saml::Kit::Namespaces::PROTOCOL,
-  }.freeze
-
   match do |actual|
-    xml_document(actual).xpath(xpath, SAML_NAMESPACES).any?
+    xml_document(actual).xpath(xpath, Saml::Kit::Xml::NAMESPACES).any?
   end
 
   failure_message do |actual|
spec/examples_spec.rb
@@ -0,0 +1,198 @@
+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,8 +1,10 @@
 # Saml::Kit
 
-Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/saml/kit`. To experiment with that code, run `bin/console` for an interactive prompt.
-
-TODO: Delete this and the text above, and describe your gem
+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
+create Service Provider Metadata, Identity Provider Metadata,
+AuthnRequest, Response, LogoutRequest, LogoutResponse documents.  
+It also supports generating signed and encrypted assertions.
 
 ## Installation
 
@@ -22,7 +24,219 @@ Or install it yourself as:
 
 ## Usage
 
-TODO: Write usage instructions here
+To specify a global configuration: (useful for a rails application)
+
+```ruby
+Saml::Kit.configure do |configuration|
+  configuration.issuer = ENV['ISSUER']
+  configuration.generate_key_pair_for(use: :signing)
+  configuration.generate_key_pair_for(use: :signing)
+end
+```
+
+### Metadata
+
+To generate metadata for an Identity Provider.
+
+```ruby
+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
+```
+
+Will produce something like:
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_efe0c000-8d0d-4406-96b8-61f649e004f6" entityID="">
+  <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+    <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://www.example.com/logout"/>
+    <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
+    <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://www.example.com/login"/>
+    <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://www.example.com/login"/>
+    <saml:Attribute Name="id"/>
+    <saml:Attribute Name="email"/>
+  </IDPSSODescriptor>
+  <Organization>
+    <OrganizationName xml:lang="en">Acme, Inc</OrganizationName>
+    <OrganizationDisplayName xml:lang="en">Acme, Inc</OrganizationDisplayName>
+    <OrganizationURL xml:lang="en">https://www.example.com</OrganizationURL>
+  </Organization>
+  <ContactPerson contactType="technical">
+    <Company>mailto:hi@example.com</Company>
+  </ContactPerson>
+</EntityDescriptor>
+```
+
+To generate service provider metadata:
+
+```xml
+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
+puts metadata.to_xml(pretty: true)
+```
+
+Will produce something like:
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_3ff5e4b3-4fce-4cc9-b278-6cb3a0a8cb10" entityID="">
+  <SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+    <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://www.example.com/logout"/>
+    <NameIDFormat>urn:oasis:names:tc:SAML: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>
+  <Organization>
+    <OrganizationName xml:lang="en">Acme, Inc</OrganizationName>
+    <OrganizationDisplayName xml:lang="en">Acme, Inc</OrganizationDisplayName>
+    <OrganizationURL xml:lang="en">https://www.example.com</OrganizationURL>
+  </Organization>
+  <ContactPerson contactType="technical">
+    <Company>mailto:hi@example.com</Company>
+  </ContactPerson>
+</EntityDescriptor>
+```
+
+To produce Metadata with an IDPSSODescriptor and SPSSODescriptor.
+
+```ruby
+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
+puts metadata.to_xml(pretty: true)
+```
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_a29a3a9d-ad16-4839-8f5d-a59daed6f3ce" entityID="">
+  <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+    <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://www.example.com/logout"/>
+    <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
+    <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://www.example.com/login"/>
+    <SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://www.example.com/login"/>
+    <saml:Attribute Name="id"/>
+    <saml:Attribute Name="email"/>
+  </IDPSSODescriptor>
+  <SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
+    <SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://www.example.com/logout"/>
+    <NameIDFormat>urn:oasis:names:tc:SAML: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>
+  <Organization>
+    <OrganizationName xml:lang="en">Acme, Inc</OrganizationName>
+    <OrganizationDisplayName xml:lang="en">Acme, Inc</OrganizationDisplayName>
+    <OrganizationURL xml:lang="en">https://www.example.com</OrganizationURL>
+  </Organization>
+  <ContactPerson contactType="technical">
+    <Company>mailto:hi@example.com</Company>
+  </ContactPerson>
+</EntityDescriptor>
+```
+
+### AuthnRequest
+
+To generate an Authentication Request choose the desired binding from
+the metadata and use it to serialize a request.
+
+```ruby
+idp = Saml::Kit::IdentityProviderMetadata.new(raw_xml)
+url, saml_params = idp.login_request_for(binding: :http_post)
+puts [url, saml_params].inspect
+# ["https://www.example.com/login", {"SAMLRequest"=>"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbHA6QXV0aG5SZXF1ZXN0IHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJfN2Y0YjkxZGMtNTMyNi00NjgzLTgyOWItYWViNzlkNjM0ZWYzIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNy0xMi0xOVQwNDo0ODoxMloiIERlc3RpbmF0aW9uPSJodHRwczovL3d3dy5leGFtcGxlLmNvbS9sb2dpbiI+PHNhbWw6SXNzdWVyLz48c2FtbHA6TmFtZUlEUG9saWN5IEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6cGVyc2lzdGVudCIvPjwvc2FtbHA6QXV0aG5SZXF1ZXN0Pg=="}]
+```
+
+### Response
+
+To generate a Response you will need a request object and the desired binding 
+to serialize a response. You will also need to specify a user
+object to create a response for.
+
+```ruby
+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)
+
+url, saml_params = saml_request.response_for(user, binding: :http_post)
+puts [url, saml_params].inspect
+# ["https://www.example.com/consume", {"SAMLResponse"=>"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48UmVzcG9uc2UgSUQ9Il9hZjFiNTg5Ni0wN2MzLTQ2Y2QtYTA5ZC0xOTRmZGNkNWZiZmYiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE3LTEyLTE5VDA1OjI5OjU0WiIgRGVzdGluYXRpb249Imh0dHBzOi8vd3d3LmV4YW1wbGUuY29tL2NvbnN1bWUiIENvbnNlbnQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjb25zZW50OnVuc3BlY2lmaWVkIiBJblJlc3BvbnNlVG89Il9mYzg5MjllOC0zY2ZkLTQ5YmQtOTgzNi0xNTRhZGYzOTEzZjYiIHhtbG5zPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiPjxJc3N1ZXIgeG1sbnM9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iLz48U3RhdHVzPjxTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L1N0YXR1cz48QXNzZXJ0aW9uIElEPSJfYjg4OWNmNzEtYTFmNS00ZWUxLWEzZTctMGM4ZTU5ZDY3ZTJkIiBJc3N1ZUluc3RhbnQ9IjIwMTctMTItMTlUMDU6Mjk6NTRaIiBWZXJzaW9uPSIyLjAiIHhtbG5zPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj48SXNzdWVyLz48U3ViamVjdD48TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6cGVyc2lzdGVudCI+Yjk2ODE1MDA
+```
+
+### LogoutRequest
+
+To create a logout request you will need to choose the desired binding
+from the metadata then generate a request for a specific user.
+
+```ruby
+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
+
+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)
+puts [url, saml_params].inspect
+# ["https://www.example.com/logout", {"SAMLRequest"=>"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48TG9nb3V0UmVxdWVzdCBJRD0iXzg3NjZiNTYyLTc2MzQtNDU4Zi04MzJmLTE4ODkwMjRlZDQ0MyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTctMTItMTlUMDQ6NTg6MThaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly93d3cuZXhhbXBsZS5jb20vbG9nb3V0IiB4bWxucz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIj48SXNzdWVyIHhtbG5zPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIi8+PE5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnBlcnNpc3RlbnQiIHhtbG5zPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5kODc3YWEzZS01YTUyLTRhODAtYTA3ZC1lM2U5YzBjNTA1Nzk8L05hbWVJRD48L0xvZ291dFJlcXVlc3Q+"}]
+```
+
+### LogoutResponse
+
+To generate a logout response, deserialize the logout request then
+generate a response from the request.
+
+```ruby
+idp = Saml::Kit::IdentityProviderMetadata.new(xml)
+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)
+url, saml_params = saml_request.response_for(binding: :http_post)
+puts [url, saml_params].inspect
+# ["https://www.example.com/logout", {"SAMLResponse"=>"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48TG9nb3V0UmVzcG9uc2UgeG1sbnM9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgSUQ9Il9kZDA2YmY5MC04ODI2LTQ5ZTMtYmYxNS1jYzAxMWJkNzU3NGEiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE3LTEyLTE5VDA1OjQyOjQyWiIgRGVzdGluYXRpb249Imh0dHBzOi8vd3d3LmV4YW1wbGUuY29tL2xvZ291dCIgSW5SZXNwb25zZVRvPSJfYmVhZjJiN2ItMDlmNC00ZmFkLWJkYmYtOWQ0ZDc1N2I5ZDU0Ij48SXNzdWVyIHhtbG5zPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIi8+PFN0YXR1cz48U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9TdGF0dXM+PC9Mb2dvdXRSZXNwb25zZT4="}]
+```
 
 ## Development
 
saml-kit.gemspec
@@ -18,6 +18,7 @@ Gem::Specification.new do |spec|
   spec.files         = `git ls-files -z`.split("\x0").reject do |f|
     f.match(%r{^(test|spec|features)/})
   end
+  spec.metadata["yard.run"] = "yri"
   spec.bindir        = "exe"
   spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
   spec.require_paths = ["lib"]