Comparing changes

v0.1.0 v0.2.0
67 commits 93 files changed

Commits

cb082c3 bump version mo 2017-11-26 19:25:52
0791bab extract ALGORITHMS contstants. mo 2017-11-26 18:36:47
315d8a9 extract multiple decryptors. mo 2017-11-26 18:28:04
5c1d8df cache decrypted assertion. mo 2017-11-26 17:54:52
5f3de38 fix some broken specs. mo 2017-11-25 06:07:20
e6357c5 verify encrypted assertion. mo 2017-11-25 06:02:08
e43af7c encrypt the assertion. mo 2017-11-25 05:20:21
076959d truncate the attribute value. mo 2017-11-25 00:08:52
e47b14a use friendly binding names. mo 2017-11-24 22:17:02
a450694 display response from IDP. mo 2017-11-24 21:44:34
ca5a56f use ternary operator. mo 2017-11-24 20:08:27
c5a6d48 remove commented out line. mo 2017-11-24 19:50:08
a57d63a remove dead code. mo 2017-11-24 19:49:42
4678182 fix registry. mo 2017-11-23 22:34:41
3dcb66d use active directory registry. mo 2017-11-23 22:31:03
0f28602 add metadatam model. mo 2017-11-23 22:14:32
e3a24dd decrypt an encrypted assertion. mo 2017-11-23 04:47:15
e808c0d add spec for xml decryption. mo 2017-11-23 04:12:28
dd5aa66 start working on crypto support. mo 2017-11-23 00:43:33
fcd9c9e :post -> :http_post mo 2017-11-22 21:19:50
80ef3ca extract bindings namespace. mo 2017-11-22 21:12:21
5312b07 extract bindings namespace. mo 2017-11-22 20:54:00
c582cac Merge branch 'backup-master' mo 2017-11-21 04:00:43
b2e7bc9 handle error from responder. mo 2017-11-20 23:35:56
290e5d6 rollback dependency to 4.2 mo 2017-11-20 16:30:02
33762c5 rename idp_metadata -> idp. mo 2017-11-20 02:10:03
da04229 display error correctly. mo 2017-11-20 00:30:01
8c0bf35 allow push to rubygems.org mo 2017-11-19 20:54:12
Changed files (93)
airport
proof
saml-kit
airport/app/assets/stylesheets/application.css
@@ -14,3 +14,19 @@
  *= require_tree .
  *= require_self
  */
+@keyframes spinner {
+  0% { transform: rotate(0deg) }
+  100% { transform: rotate(360deg) }
+}
+
+.spinner {
+  margin: auto;
+  width: 48px;
+  height: 48px;
+  animation: spinner 0.65s infinite steps(12);
+}
+
+.spinner svg {
+  width: 48px;
+  height: 48px;
+}
airport/app/controllers/application_controller.rb
@@ -1,18 +1,13 @@
 class ApplicationController < ActionController::Base
   protect_from_forgery with: :exception
-  helper_method :current_user
-  before_action :authenticate!
+  helper_method :current_user, :current_user?
 
-  def current_user
-    return nil unless session[:user].present?
-    @current_user ||= User.new(session[:user].with_indifferent_access)
+  def current_user(issuer = params[:entity_id])
+    return nil unless session[issuer].present?
+    User.new(session[issuer].with_indifferent_access)
   end
 
-  def current_user?
-    current_user.present?
-  end
-
-  def authenticate!
-    redirect_to new_session_path unless current_user?
+  def current_user?(issuer)
+    current_user(issuer).present?
   end
 end
airport/app/controllers/assertions_controller.rb
@@ -0,0 +1,29 @@
+class AssertionsController < ApplicationController
+  skip_before_action :verify_authenticity_token, only: [:create, :destroy]
+
+  def create
+    saml_binding = sp.assertion_consumer_service_for(binding: :http_post)
+    @saml_response = saml_binding.deserialize(params)
+    logger.debug(@saml_response.to_xml(pretty: true))
+    return render :error, status: :forbidden if @saml_response.invalid?
+
+    session[@saml_response.issuer] = { id: @saml_response.name_id }.merge(@saml_response.attributes)
+  end
+
+  def destroy
+    if params['SAMLRequest'].present?
+      # IDP initiated logout
+    elsif params['SAMLResponse'].present?
+      saml_binding = sp.single_logout_service_for(binding: :http_post)
+      @saml_response = saml_binding.deserialize(params)
+      raise ActiveRecordRecordInvalid.new(@saml_response) if @saml_response.invalid?
+      session[@saml_response.issuer] = nil
+    end
+  end
+
+  private
+
+  def sp
+    Sp.default(request)
+  end
+end
airport/app/controllers/dashboard_controller.rb
@@ -1,4 +0,0 @@
-class DashboardController < ApplicationController
-  def show
-  end
-end
airport/app/controllers/metadata_controller.rb
@@ -1,8 +1,8 @@
 class MetadataController < ApplicationController
-  skip_before_action :authenticate!
+  force_ssl if: :ssl_configured?
 
   def show
-    render xml: to_xml
+    render xml: to_xml, content_type: "application/samlmetadata+xml"
   end
 
   private
@@ -12,4 +12,8 @@ class MetadataController < ApplicationController
       Sp.default(request).to_xml
     end
   end
+
+  def ssl_configured?
+    !Rails.env.development?
+  end
 end
airport/app/controllers/registrations_controller.rb
@@ -0,0 +1,17 @@
+class RegistrationsController < ApplicationController
+  def index
+    @metadatum = Metadatum.all.limit(10)
+  end
+
+  def show
+    metadatum = Metadatum.find(params[:id])
+    render xml: metadatum.to_xml
+  end
+
+  def new
+  end
+
+  def create
+    Saml::Kit.configuration.registry.register_url(params[:url], verify_ssl: Rails.env.production?)
+  end
+end
airport/app/controllers/sessions_controller.rb
@@ -1,63 +1,44 @@
 class SessionsController < ApplicationController
-  skip_before_action :verify_authenticity_token, only: [:create, :destroy]
-  skip_before_action :authenticate!
-
   def new
-    builder = Saml::Kit::AuthenticationRequest::Builder.new
-    @relay_state = JSON.generate(redirect_to: '/')
-    # HTTP Redirect
-    # * URI
-    # * SigAlg
-    # * Signature
-    # * RelayState
-    redirect_binding = idp_metadata.single_sign_on_service_for(binding: :http_redirect)
-    @redirect_uri, _ = redirect_binding.serialize(builder, relay_state: @relay_state)
-
-    # HTTP POST
-    # * URI
-    # * SAMLRequest/SAMLResponse
-    post_binding = idp_metadata.single_sign_on_service_for(binding: :post)
-    @post_uri, @saml_params = post_binding.serialize(builder, relay_state: @relay_state)
+    @metadatum = Metadatum.all
   end
 
   def create
-    saml_binding = request_binding_for(request)
-    @saml_response = saml_binding.deserialize(params)
-    return render :error, status: :forbidden if @saml_response.invalid?
-
-    session[:user] = { id: @saml_response.name_id }.merge(@saml_response.attributes)
-    redirect_to dashboard_path
+    @saml_builder = builder_for(:login)
+    if :http_redirect == params[:binding].to_sym
+      redirect_binding = idp.single_sign_on_service_for(binding: :http_redirect)
+      @redirect_uri, _ = redirect_binding.serialize(@saml_builder, relay_state: relay_state)
+    else
+      post_binding = idp.single_sign_on_service_for(binding: :http_post)
+      @post_uri, @saml_params = post_binding.serialize(@saml_builder, relay_state: relay_state)
+    end
   end
 
   def destroy
-    if params['SAMLRequest'].present?
-      # IDP initiated logout
-    elsif params['SAMLResponse'].present?
-      saml_binding = request_binding_for(request)
-      saml_response = saml_binding.deserialize(params)
-      raise ActiveRecordRecordInvalid.new(saml_response) if saml_response.invalid?
-      reset_session
-      redirect_to new_session_path
-    else
-      saml_binding = idp_metadata.single_logout_service_for(binding: :post)
-      builder = Saml::Kit::LogoutRequest::Builder.new(current_user, sign: true)
-      @url, @saml_params = saml_binding.serialize(builder)
-      render layout: "spinner"
-    end
+    binding = :http_redirect == params[:binding].to_sym ? :http_redirect : :http_post
+    saml_binding = idp.single_logout_service_for(binding: binding)
+    @saml_builder = builder_for(:logout)
+    @url, @saml_params = saml_binding.serialize(@saml_builder)
   end
 
   private
 
-  def idp_metadata
-    Rails.configuration.x.idp_metadata
+  def idp(entity_id = params[:entity_id])
+    Saml::Kit.configuration.registry.metadata_for(params[:entity_id])
   end
 
-  def request_binding_for(request)
-    target_binding = request.post? ? :post : :http_redirect
-    sp.single_logout_service_for(binding: target_binding)
+  def relay_state
+    JSON.generate(redirect_to: '/')
   end
 
-  def sp
-    Sp.default(request)
+  def builder_for(type, entity_id: nil)
+    case type
+    when :login
+      builder = Saml::Kit::AuthenticationRequest::Builder.new
+      builder.acs_url = Sp.default(request).assertion_consumer_service_for(binding: :http_post).location
+      builder
+    when :logout
+      Saml::Kit::LogoutRequest::Builder.new(current_user)
+    end
   end
 end
airport/app/models/metadatum.rb
@@ -0,0 +1,30 @@
+class Metadatum < ApplicationRecord
+  def to_xml
+    to_saml.to_xml
+  end
+
+  def to_saml
+    Saml::Kit::IdentityProviderMetadata.new(metadata)
+  end
+
+  class << self
+    def register_url(url, verify_ssl: true)
+      content = Saml::Kit::DefaultRegistry::HttpApi.new(url, verify_ssl: verify_ssl).get
+      register(Saml::Kit::IdentityProviderMetadata.new(content))
+    end
+
+    def register(metadata)
+      record = Metadatum.find_or_create_by!(entity_id: metadata.entity_id)
+      record.metadata = metadata.to_xml
+      record.save!
+      metadata
+    end
+
+    def metadata_for(entity_id)
+      Metadatum.find_by!(entity_id: entity_id).to_saml
+    rescue ActiveRecord::RecordNotFound => error
+      Rails.logger.error(error)
+      nil
+    end
+  end
+end
airport/app/models/sp.rb
@@ -6,8 +6,8 @@ class Sp
         host = "#{request.protocol}#{request.host}:#{request.port}"
         builder = Saml::Kit::ServiceProviderMetadata::Builder.new
         builder.sign = false
-        builder.add_assertion_consumer_service(url_helpers.session_url(host: host), binding: :post)
-        builder.add_single_logout_service(url_helpers.logout_url(host: host), binding: :post)
+        builder.add_assertion_consumer_service(url_helpers.consume_url(host: host), binding: :http_post)
+        builder.add_single_logout_service(url_helpers.logout_url(host: host), binding: :http_post)
         builder.build
       end
     end
airport/app/views/application/_spinner.html.erb
@@ -0,0 +1,1 @@
+<div class="spinner"><svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 25 25"><defs><style>line{fill:none;stroke:#1a1a1a;stroke-linecap:round;stroke-miterlimit:10;stroke-width:2px;}.o25{opacity:0.25;}.o85{opacity:0.85;}.o7{opacity:0.7;}.o55{opacity:0.55;}.o35{opacity:0.35;}</style></defs><line x1="12.5" y1="2" x2="12.5" y2="7.05463"/><line class="o25" x1="12.5" y1="23" x2="12.5" y2="17.94537"/><line class="o85" x1="7.25" y1="3.40674" x2="9.77942" y2="7.78778"/><line class="o25" x1="17.75" y1="21.59326" x2="15.22058" y2="17.21222"/><line class="o25" x1="17.21222" y1="15.22058" x2="21.59326" y2="17.75"/><line class="o7" x1="7.78778" y1="9.77942" x2="3.40674" y2="7.25"/><line class="o25" x1="23" y1="12.5" x2="17.94537" y2="12.5"/><line class="o55" x1="7.05463" y1="12.5" x2="2" y2="12.5"/><line class="o35" x1="7.78778" y1="15.22058" x2="3.40674" y2="17.75"/><line class="o25" x1="21.59326" y1="7.25" x2="17.21222" y2="9.77942"/><line class="o25" x1="9.77942" y1="17.21222" x2="7.25" y2="21.59326"/><line class="o25" x1="17.75" y1="3.40674" x2="15.22058" y2="7.78778"/></svg></div>
airport/app/views/assertions/create.html.erb
@@ -0,0 +1,17 @@
+<div class="container">
+  <div class="row">
+    <div class="col">
+      <h1>Received Response</h1>
+      <%= render partial: "spinner" %>
+      <%= debug @saml_response.to_xhtml %>
+    </div>
+  </div>
+</div>
+
+<%= javascript_tag do %>
+  window.onload = function(){
+    window.setTimeout(function(){
+      window.location.href = '<%= registrations_path %>';
+    }, 5000);
+  };
+<% end %>
airport/app/views/assertions/destroy.html.erb
@@ -0,0 +1,17 @@
+<div class="container">
+  <div class="row">
+    <div class="col">
+      <h1>Received Response</h1>
+      <%= render partial: "spinner" %>
+      <%= debug @saml_response.to_xhtml %>
+    </div>
+  </div>
+</div>
+
+<%= javascript_tag do %>
+  window.onload = function(){
+    window.setTimeout(function(){
+      window.location.href = '<%= registrations_path %>';
+    }, 5000);
+  };
+<% end %>
airport/app/views/sessions/error.html.erb → airport/app/views/assertions/error.html.erb
File renamed without changes
airport/app/views/dashboard/show.html.erb
@@ -1,13 +0,0 @@
-<div class="container">
-  <div class="row">
-    <div class="col">
-      <h1>Welcome <%= current_user.email %></h1>
-      <ul>
-        <% current_user.attributes.each do |attribute| %>
-          <li> <%= attribute %> </li>
-        <% end %>
-      </ul>
-      <%= link_to "logout", session_path, method: :delete %>
-    </div>
-  </div>
-</div>
airport/app/views/layouts/spinner.html.erb
@@ -1,43 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
-    <style type="text/css" media="all">
-      html,
-      body {
-        width: 100%;
-        height: 100%;
-      }
-
-      body {
-        display: -ms-flexbox;
-        display: flex;
-        -ms-flex-align: center;
-        align-items: center;
-        -ms-flex-pack: center;
-        justify-content: center;
-      }
-
-      @keyframes spinner {
-        0% { transform: rotate(0deg) }
-        100% { transform: rotate(360deg) }
-      }
-
-      .spinner {
-        width: 48px;
-        height: 48px;
-        animation: spinner 0.65s infinite steps(12);
-      }
-
-      .spinner svg {
-        width: 48px;
-        height: 48px;
-      }
-    </style>
-  </head>
-  <body>
-    <div class="spinner"><svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 25 25"><defs><style>line{fill:none;stroke:#1a1a1a;stroke-linecap:round;stroke-miterlimit:10;stroke-width:2px;}.o25{opacity:0.25;}.o85{opacity:0.85;}.o7{opacity:0.7;}.o55{opacity:0.55;}.o35{opacity:0.35;}</style></defs><line x1="12.5" y1="2" x2="12.5" y2="7.05463"/><line class="o25" x1="12.5" y1="23" x2="12.5" y2="17.94537"/><line class="o85" x1="7.25" y1="3.40674" x2="9.77942" y2="7.78778"/><line class="o25" x1="17.75" y1="21.59326" x2="15.22058" y2="17.21222"/><line class="o25" x1="17.21222" y1="15.22058" x2="21.59326" y2="17.75"/><line class="o7" x1="7.78778" y1="9.77942" x2="3.40674" y2="7.25"/><line class="o25" x1="23" y1="12.5" x2="17.94537" y2="12.5"/><line class="o55" x1="7.05463" y1="12.5" x2="2" y2="12.5"/><line class="o35" x1="7.78778" y1="15.22058" x2="3.40674" y2="17.75"/><line class="o25" x1="21.59326" y1="7.25" x2="17.21222" y2="9.77942"/><line class="o25" x1="9.77942" y1="17.21222" x2="7.25" y2="21.59326"/><line class="o25" x1="17.75" y1="3.40674" x2="15.22058" y2="7.78778"/></svg></div>
-    <%= yield %>
-  </body>
-</html>
airport/app/views/registrations/create.js.erb
@@ -0,0 +1,1 @@
+window.location.href = '<%= registrations_path %>';
airport/app/views/registrations/index.html.erb
@@ -0,0 +1,48 @@
+<div class="container">
+  <div class="row">
+    <div class="col">
+      <%= link_to "register", new_registration_path %>
+      <table class="table">
+        <thead>
+          <tr>
+            <th>Entity Id</th>
+            <th>Login</th>
+            <th>Logout</th>
+          </tr>
+        </thead>
+        <tbody>
+          <% @metadatum.each do |metadata| %>
+            <tr>
+              <td>
+                <%= link_to metadata.entity_id, registration_path(metadata) %>
+                <ul>
+                  <% current_user(metadata.entity_id).attributes.each do |attribute| %>
+                    <li title="<%= attribute[1] %>"> <%= attribute[0] %> <%= attribute[1].truncate(100) %> </li>
+                  <% end if current_user?(metadata.entity_id) %>
+                </ul>
+              </td>
+              <td>
+                <% metadata.to_saml.single_sign_on_services.each do |service| %>
+                  <%= form_with url: session_path, data: { remote: false } do |form| %>
+                    <%= form.hidden_field :entity_id, value: metadata.entity_id %>
+                    <%= form.hidden_field :binding, value: Saml::Kit::Bindings.to_symbol(service.binding) %>
+                    <%= form.submit Saml::Kit::Bindings.to_symbol(service.binding).to_s.upcase %>
+                  <% end %>
+                <% end %>
+              </td>
+              <td>
+                <% metadata.to_saml.single_logout_services.each do |service| %>
+                  <%= form_with url: session_path, method: :delete, data: { remote: false } do |form| %>
+                    <%= form.hidden_field :entity_id, value: metadata.entity_id %>
+                    <%= form.hidden_field :binding, value: Saml::Kit::Bindings.to_symbol(service.binding) %>
+                    <%= form.submit Saml::Kit::Bindings.to_symbol(service.binding).to_s.upcase %>
+                  <% end %>
+                <% end %>
+              </td>
+            </tr>
+          <% end %>
+        </tbody>
+      </table>
+    </div>
+  </div>
+</div>
airport/app/views/registrations/new.html.erb
@@ -0,0 +1,13 @@
+<div class="container">
+  <div class="row">
+    <div class="col">
+      <h1>Register an Identity Provider</h1>
+
+      <%= form_with url: registrations_path do |form| %>
+        <%= form.label :url %>
+        <%= form.url_field :url, required: true %>
+        <%= form.submit %>
+      <% end %>
+    </div>
+  </div>
+</div>
airport/app/views/sessions/create.html.erb
@@ -0,0 +1,39 @@
+<% if :http_redirect == params[:binding].to_sym %>
+  <div class="container">
+    <div class="row">
+      <div class="col">
+        <h1>Redirecting to IDP</h1>
+        <%= render partial: 'spinner' %>
+        <%= debug @saml_builder.build.to_xhtml %>
+      </div>
+    </div>
+  </div>
+  <%= javascript_tag do %>
+    window.onload = function() {
+      window.setTimeout(function() {
+        window.location.href = '<%= @redirect_uri %>';
+      }, 5000);
+    };
+  <% end  %>
+<% else %>
+  <div class="container">
+    <div class="row">
+      <div class="col">
+        <h1>Sending Post to IDP</h1>
+        <%= render partial: 'spinner' %>
+        <%= form_tag(@post_uri, style: "position: absolute; left: -10000px; top: -10000px;") do %>
+          <%= @saml_params.each do |(key, value)| %>
+            <%= hidden_field_tag key, value %>
+          <% end %>
+        <% end %>
+        <%= debug @saml_builder.build.to_xhtml %>
+      </div>
+    </div>
+  </div>
+
+  <%= javascript_tag do %>
+    window.onload = function() {
+      window.setTimeout(function() { document.forms[0].submit(); }, 5000);
+    };
+  <% end %>
+<% end %>
airport/app/views/sessions/destroy.html.erb
@@ -1,12 +1,20 @@
-<h1>Loggout out of SP</h1>
-<%= form_tag(@url, style: "position: absolute; left: -10000px; top: -10000px;") do %>
-  <%= @saml_params.each do |(key, value)| %>
-    <%= hidden_field_tag key, value %>
-  <% end %>
-<% end %>
+<div class="container">
+  <div class="row">
+    <div class="col">
+      <h1>Loggout out of SP</h1>
+      <%= render partial: 'spinner' %>
+      <%= form_tag(@url, style: "position: absolute; left: -10000px; top: -10000px;") do %>
+        <%= @saml_params.each do |(key, value)| %>
+          <%= hidden_field_tag key, value %>
+        <% end %>
+      <% end %>
+      <%= debug @saml_builder.build.to_xhtml %>
+    </div>
+  </div>
+</div>
 
 <%= javascript_tag do %>
   window.onload = function() {
-    document.forms[0].submit();
+    window.setTimeout(function() { document.forms[0].submit(); }, 5000);
   };
 <% end %>
airport/app/views/sessions/new.html.erb
@@ -1,13 +1,10 @@
 <div class="container">
   <div class="row">
     <div class="col">
-      <%= link_to "Log in to IDP via redirect", @redirect_uri %>
-
-      <%= form_tag @post_uri.to_s, method: :post do %>
-        <% @saml_params.each do |(key, value)| %>
-          <%= hidden_field_tag key, value %>
-        <% end %>
-        <%= submit_tag "Log In to IDP via POST" %>
+      <%= form_with url: session_path, data: { remote: false } do |form| %>
+        <%= form.select "entity_id", @metadatum.pluck(:entity_id, :entity_id) %>
+        <%= form.select "binding", [["HTTP Post", :http_post], ["HTTP Redirect", :http_redirect]] %>
+        <%= form.submit "Log In to IDP" %>
       <% end %>
     </div>
   </div>
airport/config/initializers/saml_kit.rb
@@ -1,5 +1,4 @@
 Saml::Kit.configure do |configuration|
   configuration.issuer = ENV['ISSUER']
-  Rails.configuration.x.idp_metadata =
-    configuration.registry.register_url("#{ENV['AUTHENTICATION_HOST']}/metadata")
+  configuration.registry = Metadatum
 end
airport/config/routes.rb
@@ -1,8 +1,9 @@
 Rails.application.routes.draw do
-  get "dashboard", to: "dashboard#show", as: :dashboard
   resource :session, only: [:new, :create, :destroy]
-  post "/session/logout" => "sessions#destroy", as: :logout
+  resource :assertion, only: [:create, :destroy]
+  post "/assertions/consume" => "assertions#create", as: :consume
+  post "/assertions/logout" => "assertions#destroy", as: :logout
   resource :metadata, only: [:show]
-  resources :computers, only: [:index]
-  root to: "sessions#new"
+  resources :registrations, only: [:index, :show, :new, :create]
+  root to: "registrations#index"
 end
airport/db/migrate/20171123220807_create_metadata.rb
@@ -0,0 +1,11 @@
+class CreateMetadata < ActiveRecord::Migration[5.1]
+  def change
+    create_table :metadata do |t|
+      t.string :entity_id
+      t.text :metadata
+
+      t.timestamps
+    end
+    add_index :metadata, [:entity_id], unique: true
+  end
+end
airport/db/schema.rb
@@ -10,6 +10,14 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 0) do
+ActiveRecord::Schema.define(version: 20171123220807) do
+
+  create_table "metadata", force: :cascade do |t|
+    t.string "entity_id"
+    t.text "metadata"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["entity_id"], name: "index_metadata_on_entity_id", unique: true
+  end
 
 end
airport/spec/controllers/sessions_controller_spec.rb
@@ -1,44 +1,4 @@
 require 'rails_helper'
 
 describe SessionsController do
-  describe "#new" do
-    let(:relay_state) { CGI.escape(JSON.generate(inbound_path: "/")) }
-    let(:saml_request) { "blah" }
-    let(:auth_host) { "https://auth.dev/auth" }
-
-    it 'generates a saml request and redirects to the auth host' do
-      travel_to 1.seconds.from_now
-      allow(Saml::Kit::Request).to receive(:encode).and_return(saml_request)
-      #allow(Rails.configuration.x).to receive(:authentication_host).and_return(auth_host)
-
-      get :new
-
-      expect(response).to redirect_to(
-        [
-          auth_host,
-          "/session/new?SAMLRequest=",
-          saml_request,
-          "&RelayState=",
-          relay_state,
-        ].join
-      )
-    end
-  end
-
-  describe "#create" do
-    let(:saml_response) do
-      Saml::Kit::Response::Builder.new(user, auth_request).build.encode
-    end
-    let(:auth_request) { double(id: '1', issuer: 'issuer', acs_url: '')  }
-    let(:user) { double(uuid: user_id, assertion_attributes: { email: email, blah: 'blah' }) }
-    let(:email) { FFaker::Internet.email }
-    let(:user_id) { SecureRandom.uuid }
-
-    it 'logs the correct user in' do
-      post :create, params: { SAMLResponse: saml_response }
-
-      expect(session[:user]).to eql(id: user_id, 'email' => email, 'blah' => 'blah')
-      expect(response).to redirect_to(dashboard_path)
-    end
-  end
 end
airport/spec/fixtures/signed_response.xml
@@ -1,102 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://portal/sessions/acs" ID="id53441038949951161290410024" InResponseTo="_2aca46f5-47c5-4457-8bcf-09eb57352404" IssueInstant="2016-10-17T16:38:49.381Z" Version="2.0">
-  <saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">http://www.okta.com/exk8dx3jilpueVzpU0h7</saml2:Issuer>
-  <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
-    <ds:SignedInfo>
-      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
-      <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
-      <ds:Reference URI="#id53441038949951161290410024">
-        <ds:Transforms>
-          <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
-          <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
-        </ds:Transforms>
-        <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
-        <ds:DigestValue>Mu7QtaBFjUhvHOw91ef3d4sNTiA=</ds:DigestValue>
-      </ds:Reference>
-    </ds:SignedInfo>
-    <ds:SignatureValue>M45wBxK6/t8iRQgZ/NujTzaTOfQ8q1THre2v3trUcBtijVdDpK/4A3OSDasjGrCKLt84AMQyKoqwOCG+dGYgiLx9q/pKSxuhgVwjzordUMJTEzaH3QjUyuEFDMKuIdfR3rU3hm1oIRhe0U85hv1/GqNpaYuEXz+Ra+kydgaGWhbNwzAlV88bDWRCJIP6r+JA5XLwJnwuF7DE7Nj2qQoqgV8kntn2vZ8usijjUrB83rZrvFYTKrTQzZK6hYaFm4KF/A75VcvJ/VMnw+k8xBHmOMtfxI9WSppqrfpa2Wy6qKgwyrH0QdpJJeU2LoR6ejujSAEiBxjCMrWkfARmRZwujA==</ds:SignatureValue>
-    <ds:KeyInfo>
-      <ds:X509Data>
-        <ds:X509Certificate>
-          MIIDpDCCAoygAwIBAgIGAVea2yXEMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG
-          A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU
-          MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi05ODk4NDgxHDAaBgkqhkiG9w0BCQEW
-          DWluZm9Ab2t0YS5jb20wHhcNMTYxMDA2MTYzNjE1WhcNMjYxMDA2MTYzNzE1WjCBkjELMAkGA1UE
-          BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV
-          BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtOTg5ODQ4MRwwGgYJ
-          KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
-          j3XqgEw1CcsnHFYA8Sol/441SEFaf3gpDusagKBTKlLEfclMMmpP6VzjtndohUzyHFfDtRSjkvDj
-          4xMTPU5/eZ5jMPQpY1+y0ikVDVdxVPudZ0BQZeSWr2oharrGRBH/mdTBMv4KygXYy3qj8KRnuQrr
-          m0KRDwWKJh58/969sCWQPp2lThaBclf74ghOPg3JCcDSoZvH4yU8Y43S4Yg9q+On3sE/ZrQE4JgE
-          lbgPf1kgo30wx9IWUv5aUtIcHf2EegaB2N93y/rs2AzCWZXfeNCtRrGDY5i3vRntu7Bz2IV48g9n
-          1gFidCFQVckrc8gtD85sTPovZMwQJOlpHA5x/QIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQAdq/K0
-          D+hwHk+xaF/oB3qCtPDJWAY+izt4l0NHk4LPXhu1Kb+ikv1cfMQK7ltuSzg0EhXa32Hz+8iJctRR
-          VmkSxQaH/b0u37m6bob8AJtUe1sKVrSibL7ovMBJVJ6irre5MQ/SDgwKr4WZNl8f5Dgk0v0q2APl
-          KB1P9zGlZndWspsfxua8zvm7mBcfF0X+/5ar2wwHNyeaTChweyOhXFJnDRe3AzxFEZUsPV76ftyu
-          ZTNzF9hE0XP7BKbeuW5Im18jejMISODj/ayGgQbq93zHAf9xZnwrxjeGmFRDP48rVaCHpi/VX2AC
-          KD+eYRgY/kFJc/d0diiGR17H6XLVMWZ7
-        </ds:X509Certificate>
-      </ds:X509Data>
-    </ds:KeyInfo>
-  </ds:Signature>
-  <saml2p:Status xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol">
-    <saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
-  </saml2p:Status>
-  <saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="id5344103895060226950237596" IssueInstant="2016-10-17T16:38:49.381Z" Version="2.0">
-    <saml2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">http://www.okta.com/exk8dx3jilpueVzpU0h7</saml2:Issuer>
-    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
-      <ds:SignedInfo>
-        <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
-        <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
-        <ds:Reference URI="#id5344103895060226950237596">
-          <ds:Transforms>
-            <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
-            <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
-          </ds:Transforms>
-          <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
-          <ds:DigestValue>wXr5EGTWWaQUOT9aue8wd+NbEqM=</ds:DigestValue>
-        </ds:Reference>
-      </ds:SignedInfo>
-      <ds:SignatureValue>OUf0jZMuwc8NPfdhvuSegBtOFeSVy0dd84+jUfQGnDNynrDEvY27kgI4Sewka/+jBky8m0uRoFZgJew8B9kel8BAF4PgbAkbm5dpPm6N5LR5kFcaBwrhDDUtJpmCRflVpyiw8OXFPjc3Iif9uMHUePTUMfxH91vbKztTeeggPvxFePgQ4docKFVj/iHWWbMN/wxXRtb7Q+Aie/iqMS2yAJKZVenyh01Mh7faVz9pyJ8Y0RatQRx/ifQ9iER+0JLhyOb9t3LYsGzAhrb/uQgnMQiyAtQxuECjuTuZBOwlISvjEgRUXkbJrTz2bs3Gp0QbBgHXaB9dJnygFtCJfS/oYQ==</ds:SignatureValue>
-      <ds:KeyInfo>
-        <ds:X509Data>
-          <ds:X509Certificate>
-            MIIDpDCCAoygAwIBAgIGAVea2yXEMA0GCSqGSIb3DQEBBQUAMIGSMQswCQYDVQQGEwJVUzETMBEG
-            A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU
-            MBIGA1UECwwLU1NPUHJvdmlkZXIxEzARBgNVBAMMCmRldi05ODk4NDgxHDAaBgkqhkiG9w0BCQEW
-            DWluZm9Ab2t0YS5jb20wHhcNMTYxMDA2MTYzNjE1WhcNMjYxMDA2MTYzNzE1WjCBkjELMAkGA1UE
-            BhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xDTALBgNV
-            BAoMBE9rdGExFDASBgNVBAsMC1NTT1Byb3ZpZGVyMRMwEQYDVQQDDApkZXYtOTg5ODQ4MRwwGgYJ
-            KoZIhvcNAQkBFg1pbmZvQG9rdGEuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
-            j3XqgEw1CcsnHFYA8Sol/441SEFaf3gpDusagKBTKlLEfclMMmpP6VzjtndohUzyHFfDtRSjkvDj
-            4xMTPU5/eZ5jMPQpY1+y0ikVDVdxVPudZ0BQZeSWr2oharrGRBH/mdTBMv4KygXYy3qj8KRnuQrr
-            m0KRDwWKJh58/969sCWQPp2lThaBclf74ghOPg3JCcDSoZvH4yU8Y43S4Yg9q+On3sE/ZrQE4JgE
-            lbgPf1kgo30wx9IWUv5aUtIcHf2EegaB2N93y/rs2AzCWZXfeNCtRrGDY5i3vRntu7Bz2IV48g9n
-            1gFidCFQVckrc8gtD85sTPovZMwQJOlpHA5x/QIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQAdq/K0
-            D+hwHk+xaF/oB3qCtPDJWAY+izt4l0NHk4LPXhu1Kb+ikv1cfMQK7ltuSzg0EhXa32Hz+8iJctRR
-            VmkSxQaH/b0u37m6bob8AJtUe1sKVrSibL7ovMBJVJ6irre5MQ/SDgwKr4WZNl8f5Dgk0v0q2APl
-            KB1P9zGlZndWspsfxua8zvm7mBcfF0X+/5ar2wwHNyeaTChweyOhXFJnDRe3AzxFEZUsPV76ftyu
-            ZTNzF9hE0XP7BKbeuW5Im18jejMISODj/ayGgQbq93zHAf9xZnwrxjeGmFRDP48rVaCHpi/VX2AC
-            KD+eYRgY/kFJc/d0diiGR17H6XLVMWZ7
-          </ds:X509Certificate>
-        </ds:X509Data>
-      </ds:KeyInfo>
-    </ds:Signature>
-    <saml2:Subject xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
-      <saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">placeholder@example.com</saml2:NameID>
-      <saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
-        <saml2:SubjectConfirmationData InResponseTo="_2aca46f5-47c5-4457-8bcf-09eb57352404" NotOnOrAfter="2016-10-17T16:43:49.381Z" Recipient="https://portal/sessions/acs"/>
-      </saml2:SubjectConfirmation>
-    </saml2:Subject>
-    <saml2:Conditions NotBefore="2016-10-17T16:33:49.381Z" NotOnOrAfter="2016-10-17T16:43:49.381Z" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
-      <saml2:AudienceRestriction>
-        <saml2:Audience>https://portal/sessions/metadata</saml2:Audience>
-      </saml2:AudienceRestriction>
-    </saml2:Conditions>
-    <saml2:AuthnStatement AuthnInstant="2016-10-17T16:38:49.381Z" SessionIndex="_2aca46f5-47c5-4457-8bcf-09eb57352404" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
-      <saml2:AuthnContext>
-        <saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml2:AuthnContextClassRef>
-      </saml2:AuthnContext>
-    </saml2:AuthnStatement>
-  </saml2:Assertion>
-</saml2p:Response>
airport/spec/models/metadatum_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe Metadatum, type: :model do
+  pending "add some examples to (or delete) #{__FILE__}"
+end
airport/spec/factories.rb
@@ -1,8 +1,6 @@
 FactoryGirl.define do
-  sequence :saml_response do |n|
-    xml = IO.read("spec/fixtures/signed_response.xml")
-    xml.gsub!('2016-10-17T16:43:49.381Z', DateTime.now.iso8601)
-    xml.gsub!('https://portal', 'http://test.host')
-    xml
+  factory :metadatum do
+    entity_id FFaker::Internet.uri("https")
+    metadata Saml::Kit::IdentityProviderMetadata::Builder.new.to_xml
   end
 end
airport/.env
@@ -1,2 +1,2 @@
-AUTHENTICATION_HOST=http://localhost:4000
-ISSUER=airport.dev
+IDP_METADATA_URL=https://proof.dev/metadata
+ISSUER=https://airport.dev/metadata
airport/Gemfile.lock
@@ -2,8 +2,8 @@ PATH
   remote: ../saml-kit
   specs:
     saml-kit (0.1.0)
-      activemodel (~> 5.1)
-      activesupport (~> 5.1)
+      activemodel (>= 4.2.0)
+      activesupport (>= 4.2.0)
       builder (~> 3.2)
       nokogiri (~> 1.8)
       xmldsig (~> 0.6)
@@ -261,4 +261,4 @@ DEPENDENCIES
   webmock
 
 BUNDLED WITH
-   1.15.4
+   1.16.0
proof/app/assets/stylesheets/application.css
@@ -14,3 +14,20 @@
  *= require_tree .
  *= require_self
  */
+
+@keyframes spinner {
+  0% { transform: rotate(0deg) }
+  100% { transform: rotate(360deg) }
+}
+
+.spinner {
+  margin: auto;
+  width: 48px;
+  height: 48px;
+  animation: spinner 0.65s infinite steps(12);
+}
+
+.spinner svg {
+  width: 48px;
+  height: 48px;
+}
proof/app/controllers/application_controller.rb
@@ -5,4 +5,16 @@ class ApplicationController < ActionController::Base
     @model = model
     render template: "errors/#{status}", status: status
   end
+
+  def current_user
+    return nil if session[:user_id].blank?
+    @current_user ||= User.find(session[:user_id])
+  rescue ActiveRecord::RecordNotFound => error
+    logger.error(error)
+    nil
+  end
+
+  def current_user?
+    current_user.present?
+  end
 end
proof/app/controllers/metadata_controller.rb
@@ -1,6 +1,8 @@
 class MetadataController < ApplicationController
+  force_ssl if: :ssl_configured?
+
   def show
-    render xml: to_xml
+    render xml: to_xml, content_type: "application/samlmetadata+xml"
   end
 
   private
@@ -10,4 +12,8 @@ class MetadataController < ApplicationController
       Idp.default(request).to_xml
     end
   end
+
+  def ssl_configured?
+    !Rails.env.development?
+  end
 end
proof/app/controllers/sessions_controller.rb
@@ -1,37 +1,41 @@
 class SessionsController < ApplicationController
   skip_before_action :verify_authenticity_token, only: [:new, :destroy]
-  before_action :load_saml_request, only: [:new, :create, :destroy]
-  rescue_from ActiveRecord::RecordInvalid do |record|
-    render_error(:forbidden, model: record)
-  end
 
   def new
-    session[:SAMLRequest] ||= params[:SAMLRequest]
-    session[:RelayState] ||= params[:RelayState]
+    target_binding = request.post? ? :http_post : :http_redirect
+    binding = idp.single_sign_on_service_for(binding: target_binding)
+    @saml_request = binding.deserialize(raw_params)
+    return render_error(:forbidden, model: @saml_request) if @saml_request.invalid?
+    return post_back(@saml_request, current_user) if current_user?
+
+    session[:saml] = { params: raw_params.to_h, binding: target_binding }
   end
 
   def create
     if user = User.login(user_params[:email], user_params[:password])
-      reset_session
-      session[:user_id] = user.id
-      response_binding = @saml_request.provider.assertion_consumer_service_for(binding: :post)
-      @url, @saml_params = response_binding.serialize(@saml_request.response_for(user), relay_state: session[:RelayState])
-      render layout: "spinner"
+      binding = idp.single_sign_on_service_for(binding: session[:saml][:binding])
+      saml_request = binding.deserialize(session[:saml][:params])
+      return render_error(:forbidden, model: saml_request) if saml_request.invalid?
+
+      post_back(saml_request, user)
     else
-      redirect_to new_session_path, error: "Invalid Credentials"
+      flash[:error] = "Invalid Credentials"
+      render :new
     end
   end
 
   def destroy
-    if params['SAMLRequest'].present?
-      saml_request = load_saml_request
+    if saml_params[:SAMLRequest].present?
+      binding = idp.single_logout_service_for(binding: :http_post)
+      saml_request = binding.deserialize(raw_params).tap do |saml|
+        raise ActiveRecord::RecordInvalid.new(saml) if saml.invalid?
+      end
       user = User.find_by(uuid: saml_request.name_id)
-      response_binding = saml_request.provider.single_logout_service_for(binding: :post)
-      saml_response = saml_request.response_for(user)
-      @url, @saml_params = response_binding.serialize(saml_response, relay_state: params[:RelayState])
+      response_binding = saml_request.provider.single_logout_service_for(binding: :http_post)
+      @saml_response = saml_request.response_for(user)
+      @url, @saml_params = response_binding.serialize(@saml_response, relay_state: saml_params[:RelayState])
       reset_session
-      render layout: "spinner"
-    elsif params['SAMLResponse'].present?
+    elsif saml_params[:SAMLResponse].present?
     else
     end
   end
@@ -42,18 +46,28 @@ class SessionsController < ApplicationController
     params.require(:user).permit(:email, :password)
   end
 
-  def load_saml_request(raw_saml_request = session[:SAMLRequest] || params[:SAMLRequest])
-    @saml_request = request_binding_for(request).deserialize(params)
-    raise ActiveRecord::RecordInvalid.new(@saml_request) if @saml_request.invalid?
-    @saml_request
+  def saml_params
+    params.permit(:SAMLRequest, :SAMLResponse, :SAMLEncoding, :SigAlg, :Signature)
   end
 
   def idp
     Idp.default(request)
   end
 
-  def request_binding_for(request)
-    target_binding = request.post? ? :post : :http_redirect
-    idp.single_sign_on_service_for(binding: target_binding)
+  def raw_params
+    if request.post?
+      saml_params
+    else
+      Hash[request.query_string.split("&").map { |x| x.split("=", 2) }]
+    end
+  end
+
+  def post_back(saml_request, user)
+    response_binding = saml_request.provider.assertion_consumer_service_for(binding: :http_post)
+    @saml_response = saml_request.response_for(user)
+    @url, @saml_params = response_binding.serialize(@saml_response, relay_state: saml_params[:RelayState])
+    reset_session
+    session[:user_id] = user.id
+    render :create
   end
 end
proof/app/models/idp.rb
@@ -1,6 +1,5 @@
 class Idp
   class << self
-
     def default(request)
       @idp ||= begin
         host = "#{request.protocol}#{request.host}:#{request.port}"
@@ -10,9 +9,9 @@ class Idp
         builder.contact_email = 'hi@example.com'
         builder.organization_name = "Acme, Inc"
         builder.organization_url = url_helpers.root_url(host: host)
-        builder.add_single_sign_on_service(url_helpers.new_session_url(host: host), binding: :post)
+        builder.add_single_sign_on_service(url_helpers.new_session_url(host: host), binding: :http_post)
         builder.add_single_sign_on_service(url_helpers.new_session_url(host: host), binding: :http_redirect)
-        builder.add_single_logout_service(url_helpers.logout_url(host: host), binding: :post)
+        builder.add_single_logout_service(url_helpers.logout_url(host: host), binding: :http_post)
         builder.name_id_formats = [
           Saml::Kit::Namespaces::EMAIL_ADDRESS,
           Saml::Kit::Namespaces::PERSISTENT,
proof/app/models/user.rb
@@ -5,11 +5,7 @@ class User < ApplicationRecord
   end
 
   def name_id_for(name_id_format)
-    if Saml::Kit::Namespaces::PERSISTENT == name_id_format
-      uuid
-    else
-      email
-    end
+    Saml::Kit::Namespaces::PERSISTENT == name_id_format ? uuid : email
   end
 
   def assertion_attributes_for(request)
proof/app/views/application/_spinner.html.erb
@@ -0,0 +1,1 @@
+<div class="spinner"><svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 25 25"><defs><style>line{fill:none;stroke:#1a1a1a;stroke-linecap:round;stroke-miterlimit:10;stroke-width:2px;}.o25{opacity:0.25;}.o85{opacity:0.85;}.o7{opacity:0.7;}.o55{opacity:0.55;}.o35{opacity:0.35;}</style></defs><line x1="12.5" y1="2" x2="12.5" y2="7.05463"/><line class="o25" x1="12.5" y1="23" x2="12.5" y2="17.94537"/><line class="o85" x1="7.25" y1="3.40674" x2="9.77942" y2="7.78778"/><line class="o25" x1="17.75" y1="21.59326" x2="15.22058" y2="17.21222"/><line class="o25" x1="17.21222" y1="15.22058" x2="21.59326" y2="17.75"/><line class="o7" x1="7.78778" y1="9.77942" x2="3.40674" y2="7.25"/><line class="o25" x1="23" y1="12.5" x2="17.94537" y2="12.5"/><line class="o55" x1="7.05463" y1="12.5" x2="2" y2="12.5"/><line class="o35" x1="7.78778" y1="15.22058" x2="3.40674" y2="17.75"/><line class="o25" x1="21.59326" y1="7.25" x2="17.21222" y2="9.77942"/><line class="o25" x1="9.77942" y1="17.21222" x2="7.25" y2="21.59326"/><line class="o25" x1="17.75" y1="3.40674" x2="15.22058" y2="7.78778"/></svg></div>
proof/app/views/layouts/spinner.html.erb
@@ -1,43 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <meta charset="utf-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
-    <style type="text/css" media="all">
-      html,
-      body {
-        width: 100%;
-        height: 100%;
-      }
-
-      body {
-        display: -ms-flexbox;
-        display: flex;
-        -ms-flex-align: center;
-        align-items: center;
-        -ms-flex-pack: center;
-        justify-content: center;
-      }
-
-      @keyframes spinner {
-        0% { transform: rotate(0deg) }
-        100% { transform: rotate(360deg) }
-      }
-
-      .spinner {
-        width: 48px;
-        height: 48px;
-        animation: spinner 0.65s infinite steps(12);
-      }
-
-      .spinner svg {
-        width: 48px;
-        height: 48px;
-      }
-    </style>
-  </head>
-  <body onload="document.forms[0].submit();">
-    <div class="spinner"><svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 25 25"><defs><style>line{fill:none;stroke:#1a1a1a;stroke-linecap:round;stroke-miterlimit:10;stroke-width:2px;}.o25{opacity:0.25;}.o85{opacity:0.85;}.o7{opacity:0.7;}.o55{opacity:0.55;}.o35{opacity:0.35;}</style></defs><line x1="12.5" y1="2" x2="12.5" y2="7.05463"/><line class="o25" x1="12.5" y1="23" x2="12.5" y2="17.94537"/><line class="o85" x1="7.25" y1="3.40674" x2="9.77942" y2="7.78778"/><line class="o25" x1="17.75" y1="21.59326" x2="15.22058" y2="17.21222"/><line class="o25" x1="17.21222" y1="15.22058" x2="21.59326" y2="17.75"/><line class="o7" x1="7.78778" y1="9.77942" x2="3.40674" y2="7.25"/><line class="o25" x1="23" y1="12.5" x2="17.94537" y2="12.5"/><line class="o55" x1="7.05463" y1="12.5" x2="2" y2="12.5"/><line class="o35" x1="7.78778" y1="15.22058" x2="3.40674" y2="17.75"/><line class="o25" x1="21.59326" y1="7.25" x2="17.21222" y2="9.77942"/><line class="o25" x1="9.77942" y1="17.21222" x2="7.25" y2="21.59326"/><line class="o25" x1="17.75" y1="3.40674" x2="15.22058" y2="7.78778"/></svg></div>
-    <%= yield %>
-  </body>
-</html>
proof/app/views/sessions/_response.html.erb
@@ -0,0 +1,20 @@
+<div class="container">
+  <div class="row">
+    <div class="col">
+      <h1>Sending Response to Service Provider</h1>
+      <%= render partial: 'spinner' %>
+      <%= form_tag(@url, style: "position: absolute; left: -10000px; top: -10000px;") do %>
+        <% @saml_params.each do |(key, value)| %>
+          <%= hidden_field_tag key, value %>
+        <% end %>
+      <% end %>
+      <%= debug @saml_response.build.to_xhtml %>
+    </div>
+  </div>
+</div>
+
+<%= javascript_tag do %>
+  window.onload = function () {
+    window.setTimeout(function() { document.forms[0].submit(); }, 5000);
+  };
+<% end %>
proof/app/views/sessions/create.html.erb
@@ -1,12 +1,1 @@
-<%= form_tag(@url, style: "position: absolute; left: -10000px; top: -10000px;") do %>
-  <% @saml_params.each do |(key, value)| %>
-    <%= hidden_field_tag key, value %>
-  <% end %>
-  <%= submit_tag "Submit" %>
-<% end %>
-
-<%= javascript_tag do %>
-  window.onload= function () {
-    document.forms[0].submit();
-  };
-<% end %>
+<%= render partial: "response" %>
proof/app/views/sessions/destroy.html.erb
@@ -1,12 +1,1 @@
-<h1>Logging Out of IDP</h1>
-<%= form_tag(@url, style: "position: absolute; left: -10000px; top: -10000px;") do %>
-  <%= @saml_params.each do |(key, value)| %>
-    <%= hidden_field_tag key, value %>
-  <% end %>
-<% end %>
-
-<%= javascript_tag do %>
-  window.onload= function () {
-    document.forms[0].submit();
-  };
-<% end %>
+<%= render partial: "response" %>
proof/app/views/sessions/new.html.erb
@@ -9,10 +9,10 @@
         <div class="form-group">
           <%= form.password_field :password, class: 'form-control', placeholder: User.human_attribute_name(:password), required: :required %>
         </div>
-        <%= hidden_field_tag :SAMLRequest, params[:SAMLRequest] %>
-        <%= hidden_field_tag :RelayState, params[:RelayState] %>
         <%= form.button t('.login'), type: 'submit', class: 'btn btn-primary', data: { disable_with: t('.loading') } %>
       <% end %>
+
+      <%= debug @saml_request.to_xhtml %>
     </div>
   </div>
 </div>
proof/.env
@@ -1,1 +1,1 @@
-ISSUER=proof.dev
+ISSUER=http://proof.dev/metadata
proof/Gemfile.lock
@@ -2,8 +2,8 @@ PATH
   remote: ../saml-kit
   specs:
     saml-kit (0.1.0)
-      activemodel (~> 5.1)
-      activesupport (~> 5.1)
+      activemodel (>= 4.2.0)
+      activesupport (>= 4.2.0)
       builder (~> 3.2)
       nokogiri (~> 1.8)
       xmldsig (~> 0.6)
@@ -265,4 +265,4 @@ DEPENDENCIES
   webmock
 
 BUNDLED WITH
-   1.15.4
+   1.16.0
saml-kit/exe/saml-kit-decode-http-post
@@ -0,0 +1,8 @@
+#!/usr/bin/env ruby
+require 'saml/kit'
+
+saml = STDIN.read
+
+binding = Saml::Kit::Bindings::HttpPost.new(location: '')
+xml = binding.deserialize('SAMLRequest' => saml).to_xml
+puts Nokogiri::XML(xml).to_xml(indent: 2)
saml-kit/lib/saml/kit/bindings/binding.rb
@@ -0,0 +1,42 @@
+module Saml
+  module Kit
+    module Bindings
+      class Binding
+        attr_reader :binding, :location
+
+        def initialize(binding:, location:)
+          @binding = binding
+          @location = location
+        end
+
+        def binding?(other)
+          binding == other
+        end
+
+        def serialize(builder, relay_state: nil)
+          []
+        end
+
+        def deserialize(params)
+          raise ArgumentError.new("Unsupported binding")
+        end
+
+        def to_h
+          { binding: binding, location: location }
+        end
+
+        protected
+
+        def saml_param_from(params)
+          if params['SAMLRequest'].present?
+            params['SAMLRequest']
+          elsif params['SAMLResponse'].present?
+            params['SAMLResponse']
+          else
+            raise ArgumentError.new("SAMLRequest or SAMLResponse parameter is required.")
+          end
+        end
+      end
+    end
+  end
+end
saml-kit/lib/saml/kit/bindings/http_post.rb
@@ -0,0 +1,29 @@
+module Saml
+  module Kit
+    module Bindings
+      class HttpPost < Binding
+        include Serializable
+
+        def initialize(location:)
+          super(binding: Saml::Kit::Bindings::HTTP_POST, location: location)
+        end
+
+        def serialize(builder, relay_state: nil)
+          builder.sign = true
+          builder.destination = location
+          document = builder.build
+          saml_params = {
+            document.query_string_parameter => Base64.strict_encode64(document.to_xml),
+          }
+          saml_params['RelayState'] = relay_state if relay_state.present?
+          [location, saml_params]
+        end
+
+        def deserialize(params)
+          xml = decode(saml_param_from(params))
+          Saml::Kit::Document.to_saml_document(xml)
+        end
+      end
+    end
+  end
+end
saml-kit/lib/saml/kit/bindings/http_redirect.rb
@@ -0,0 +1,61 @@
+module Saml
+  module Kit
+    module Bindings
+      class HttpRedirect < Binding
+        include Serializable
+
+        def initialize(location:)
+          super(binding: Saml::Kit::Bindings::HTTP_REDIRECT, location: location)
+        end
+
+        def serialize(builder, relay_state: nil)
+          builder.sign = false
+          builder.destination = location
+          document = builder.build
+          [UrlBuilder.new.build(document, relay_state: relay_state), {}]
+        end
+
+        def deserialize(params)
+          document = deserialize_document_from!(params)
+          ensure_valid_signature!(params, document)
+          document.signature_verified!
+          document
+        end
+
+        private
+
+        def deserialize_document_from!(params)
+          xml = inflate(decode(unescape(saml_param_from(params))))
+          Saml::Kit.logger.debug(xml)
+          Saml::Kit::Document.to_saml_document(xml)
+        end
+
+        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('&')
+
+          valid = document.provider.verify(algorithm_for(params['SigAlg']), signature, canonical_form)
+          raise ArgumentError.new("Invalid Signature") unless valid
+        end
+
+        def algorithm_for(algorithm)
+          case algorithm =~ /(rsa-)?sha(.*?)$/i && $2.to_i
+          when 256
+            OpenSSL::Digest::SHA256.new
+          when 384
+            OpenSSL::Digest::SHA384.new
+          when 512
+            OpenSSL::Digest::SHA512.new
+          else
+            OpenSSL::Digest::SHA1.new
+          end
+        end
+      end
+    end
+  end
+end
saml-kit/lib/saml/kit/bindings/url_builder.rb
@@ -0,0 +1,40 @@
+module Saml
+  module Kit
+    module Bindings
+      class UrlBuilder
+        include Serializable
+
+        def initialize(private_key: Saml::Kit.configuration.signing_private_key)
+          @private_key = private_key
+        end
+
+        def build(saml_document, relay_state: nil)
+          payload = canonicalize(saml_document, relay_state)
+          "#{saml_document.destination}?#{payload}&Signature=#{signature_for(payload)}"
+        end
+
+        private
+
+        attr_reader :private_key
+
+        def signature_for(payload)
+          encode(private_key.sign(OpenSSL::Digest::SHA256.new, payload))
+        end
+
+        def canonicalize(saml_document, relay_state)
+          {
+            saml_document.query_string_parameter => serialize(saml_document.to_xml),
+            'RelayState' => relay_state,
+            'SigAlg' => Saml::Kit::Namespaces::SHA256,
+          }.map do |(key, value)|
+            value.present? ? "#{key}=#{escape(value)}" : nil
+          end.compact.join('&')
+        end
+
+        def serialize(value)
+          encode(deflate(value))
+        end
+      end
+    end
+  end
+end
saml-kit/lib/saml/kit/crypto/oaep_cipher.rb
@@ -0,0 +1,22 @@
+module Saml
+  module Kit
+    module Crypto
+      class OaepCipher
+        ALGORITHMS = {
+          'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p' => true,
+        }
+        def initialize(algorithm, key)
+          @key = key
+        end
+
+        def self.matches?(algorithm)
+          ALGORITHMS[algorithm]
+        end
+
+        def decrypt(cipher_text)
+          @key.private_decrypt(cipher_text, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
+        end
+      end
+    end
+  end
+end
saml-kit/lib/saml/kit/crypto/rsa_cipher.rb
@@ -0,0 +1,23 @@
+module Saml
+  module Kit
+    module Crypto
+      class RsaCipher
+        ALGORITHMS = {
+          'http://www.w3.org/2001/04/xmlenc#rsa-1_5' => true,
+        }
+
+        def initialize(algorithm, key)
+          @key = key
+        end
+
+        def self.matches?(algorithm)
+          ALGORITHMS[algorithm]
+        end
+
+        def decrypt(cipher_text)
+          @key.private_decrypt(cipher_text)
+        end
+      end
+    end
+  end
+end
saml-kit/lib/saml/kit/crypto/simple_cipher.rb
@@ -0,0 +1,38 @@
+module Saml
+  module Kit
+    module Crypto
+      class SimpleCipher
+        ALGORITHMS = {
+          'http://www.w3.org/2001/04/xmlenc#tripledes-cbc' => 'DES-EDE3-CBC',
+          'http://www.w3.org/2001/04/xmlenc#aes128-cbc' => 'AES-128-CBC',
+          'http://www.w3.org/2001/04/xmlenc#aes192-cbc' => 'AES-192-CBC',
+          'http://www.w3.org/2001/04/xmlenc#aes256-cbc' => 'AES-256-CBC',
+        }
+
+        def initialize(algorithm, private_key)
+          @algorithm = algorithm
+          @private_key = private_key
+        end
+
+        def self.matches?(algorithm)
+          ALGORITHMS[algorithm]
+        end
+
+        def decrypt(cipher_text)
+          cipher = OpenSSL::Cipher.new(ALGORITHMS[@algorithm])
+          cipher.decrypt
+          iv = cipher_text[0..cipher.iv_len-1]
+          data = cipher_text[cipher.iv_len..-1]
+          #cipher.padding = 0
+          cipher.key = @private_key
+          cipher.iv = iv
+
+          Saml::Kit.logger.debug ['-key', @private_key].inspect
+          Saml::Kit.logger.debug ['-iv', iv].inspect
+
+          cipher.update(data) + cipher.final
+        end
+      end
+    end
+  end
+end
saml-kit/lib/saml/kit/crypto/unknown_cipher.rb
@@ -0,0 +1,18 @@
+module Saml
+  module Kit
+    module Crypto
+      class UnknownCipher
+        def initialize(algorithm, key)
+        end
+
+        def self.matches?(algorithm)
+          true
+        end
+
+        def decrypt(cipher_text)
+          cipher_text
+        end
+      end
+    end
+  end
+end
saml-kit/lib/saml/kit/locales/en.yml
@@ -14,12 +14,13 @@ en:
       LogoutResponse:
         unregistered: "is unregistered."
       Response:
-        invalid: "must contain Response."
-        unregistered: "must originate from registered identity provider."
         expired: "must not be expired."
-        invalid_version: "must be 2.0."
+        invalid: "must contain Response."
+        invalid_fingerprint: "does not match."
         invalid_response_to: "must match request id."
+        invalid_version: "must be 2.0."
         must_match_issuer: "must match entityId."
+        unregistered: "must originate from registered identity provider."
       SPSSODescriptor:
         invalid: "must contain SPSSODescriptor."
         invalid_signature: "invalid signature."
saml-kit/lib/saml/kit/authentication_request.rb
@@ -2,18 +2,13 @@ module Saml
   module Kit
     class AuthenticationRequest < Document
       include Requestable
-      validates_presence_of :acs_url, if: :expected_type?
 
       def initialize(xml)
         super(xml, name: "AuthnRequest")
       end
 
       def acs_url
-        #if signed? && trusted?
-          to_h[name]['AssertionConsumerServiceURL'] || registered_acs_url(binding: :post)
-        #else
-          #registered_acs_url
-        #end
+        to_h[name]['AssertionConsumerServiceURL']
       end
 
       def name_id_format
@@ -26,11 +21,6 @@ module Saml
 
       private
 
-      def registered_acs_url(binding:)
-        return if provider.nil?
-        provider.assertion_consumer_service_for(binding: binding).try(:location)
-      end
-
       class Builder
         attr_accessor :id, :now, :issuer, :acs_url, :name_id_format, :sign, :destination
         attr_accessor :version
@@ -45,10 +35,10 @@ module Saml
         end
 
         def to_xml
-          Signature.sign(id, sign: sign) do |xml, signature|
+          Signature.sign(sign: sign) do |xml, signature|
             xml.tag!('samlp:AuthnRequest', request_options) do
               xml.tag!('saml:Issuer', issuer)
-              signature.template(xml)
+              signature.template(id)
               xml.tag!('samlp:NameIDPolicy', Format: name_id_format)
             end
           end
saml-kit/lib/saml/kit/binding.rb
@@ -1,40 +0,0 @@
-module Saml
-  module Kit
-    class Binding
-      attr_reader :binding, :location
-
-      def initialize(binding:, location:)
-        @binding = binding
-        @location = location
-      end
-
-      def binding?(other)
-        binding == other
-      end
-
-      def serialize(builder, relay_state: nil)
-        []
-      end
-
-      def deserialize(params)
-        raise ArgumentError.new("Unsupported binding")
-      end
-
-      def to_h
-        { binding: binding, location: location }
-      end
-
-      protected
-
-      def saml_param_from(params)
-        if params['SAMLRequest'].present?
-          params['SAMLRequest']
-        elsif params['SAMLResponse'].present?
-          params['SAMLResponse']
-        else
-          raise ArgumentError.new("SAMLRequest or SAMLResponse parameter is required.")
-        end
-      end
-    end
-  end
-end
saml-kit/lib/saml/kit/bindings.rb
@@ -0,0 +1,45 @@
+require "saml/kit/bindings/binding"
+require "saml/kit/bindings/http_post"
+require "saml/kit/bindings/http_redirect"
+require "saml/kit/bindings/url_builder"
+
+module Saml
+  module Kit
+    module Bindings
+      HTTP_ARTIFACT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact'
+      HTTP_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+      HTTP_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+      ALL = {
+        http_post: HTTP_POST,
+        http_redirect: HTTP_REDIRECT,
+        http_artifact: HTTP_ARTIFACT,
+      }
+
+      def self.binding_for(binding)
+        ALL[binding]
+      end
+
+      def self.to_symbol(binding)
+        case binding
+        when HTTP_REDIRECT
+          :http_redirect
+        when HTTP_POST
+          :http_post
+        else
+          binding
+        end
+      end
+
+      def self.create_for(binding, location)
+        case binding
+        when HTTP_REDIRECT
+          HttpRedirect.new(location: location)
+        when HTTP_POST
+          HttpPost.new(location: location)
+        else
+          Binding.new(binding: binding, location: location)
+        end
+      end
+    end
+  end
+end
saml-kit/lib/saml/kit/configuration.rb
@@ -7,6 +7,7 @@ module Saml
       attr_accessor :issuer
       attr_accessor :signature_method, :digest_method
       attr_accessor :signing_certificate_pem, :signing_private_key_pem, :signing_private_key_password
+      attr_accessor :encryption_certificate_pem, :encryption_private_key_pem, :encryption_private_key_password
       attr_accessor :registry, :session_timeout
       attr_accessor :logger
 
@@ -14,23 +15,43 @@ module Saml
         @signature_method = :SHA256
         @digest_method = :SHA256
         @signing_private_key_password = SecureRandom.uuid
+        @encryption_private_key_password = SecureRandom.uuid
         @signing_certificate_pem, @signing_private_key_pem = SelfSignedCertificate.new(@signing_private_key_password).create
+        @encryption_certificate_pem, @encryption_private_key_pem = SelfSignedCertificate.new(@encryption_private_key_password).create
         @registry = DefaultRegistry.new
         @session_timeout = 3.hours
         @logger = Logger.new(STDOUT)
       end
 
       def stripped_signing_certificate
-        signing_certificate_pem.to_s.gsub(BEGIN_CERT, '').gsub(END_CERT, '').gsub(/\n/, '')
+        normalize(signing_certificate_pem)
+      end
+
+      def stripped_encryption_certificate
+        normalize(encryption_certificate_pem)
       end
 
       def signing_x509
         OpenSSL::X509::Certificate.new(signing_certificate_pem)
       end
 
+      def encryption_x509
+        OpenSSL::X509::Certificate.new(encryption_certificate_pem)
+      end
+
       def signing_private_key
         OpenSSL::PKey::RSA.new(signing_private_key_pem, signing_private_key_password)
       end
+
+      def encryption_private_key
+        OpenSSL::PKey::RSA.new(encryption_private_key_pem, encryption_private_key_password)
+      end
+
+      private
+
+      def normalize(certificate)
+        certificate.to_s.gsub(BEGIN_CERT, '').gsub(END_CERT, '').gsub(/\n/, '')
+      end
     end
   end
 end
saml-kit/lib/saml/kit/crypto.rb
@@ -0,0 +1,16 @@
+require 'saml/kit/crypto/oaep_cipher'
+require 'saml/kit/crypto/rsa_cipher'
+require 'saml/kit/crypto/simple_cipher'
+require 'saml/kit/crypto/unknown_cipher'
+
+module Saml
+  module Kit
+    module Crypto
+      DECRYPTORS = [ SimpleCipher, RsaCipher, OaepCipher, UnknownCipher ]
+
+      def self.decryptor_for(algorithm, key)
+        DECRYPTORS.find { |x| x.matches?(algorithm) }.new(algorithm, key)
+      end
+    end
+  end
+end
saml-kit/lib/saml/kit/cryptography.rb
@@ -0,0 +1,30 @@
+module Saml
+  module Kit
+    class Cryptography
+      attr_reader :private_key
+
+      def initialize(private_key = Saml::Kit.configuration.encryption_private_key)
+        @private_key = private_key
+      end
+
+      def decrypt(data)
+        encrypt_data = data['EncryptedData']
+        symmetric_key = symmetric_key_from(encrypt_data)
+        cipher_text = Base64.decode64(encrypt_data["CipherData"]["CipherValue"])
+        to_plaintext(cipher_text, symmetric_key, encrypt_data["EncryptionMethod"]['Algorithm'])
+      end
+
+      private
+
+      def symmetric_key_from(encrypted_data)
+        encrypted_key = encrypted_data['KeyInfo']['EncryptedKey']
+        cipher_text = Base64.decode64(encrypted_key['CipherData']['CipherValue'])
+        to_plaintext(cipher_text, private_key, encrypted_key["EncryptionMethod"]['Algorithm'])
+      end
+
+      def to_plaintext(cipher_text, symmetric_key, algorithm)
+        return Crypto.decryptor_for(algorithm, symmetric_key).decrypt(cipher_text)
+      end
+    end
+  end
+end
saml-kit/lib/saml/kit/default_registry.rb
@@ -6,6 +6,7 @@ module Saml
       end
 
       def register(metadata)
+        Saml::Kit.logger.debug(metadata.to_xml(pretty: true))
         @items[metadata.entity_id] = metadata
       end
 
saml-kit/lib/saml/kit/document.rb
@@ -48,8 +48,12 @@ module Saml
         @xml_hash
       end
 
-      def to_xml
-        content
+      def to_xml(pretty: false)
+        pretty ? Nokogiri::XML(content).to_xml(indent: 2) : content
+      end
+
+      def to_xhtml
+        Nokogiri::XML(content, &:noblanks).to_xhtml
       end
 
       def to_s
saml-kit/lib/saml/kit/http_post_binding.rb
@@ -1,27 +0,0 @@
-module Saml
-  module Kit
-    class HttpPostBinding < Binding
-      include Serializable
-
-      def initialize(location:)
-        super(binding: Saml::Kit::Namespaces::HTTP_POST, location: location)
-      end
-
-      def serialize(builder, relay_state: nil)
-        builder.sign = true
-        builder.destination = location
-        document = builder.build
-        saml_params = {
-          document.query_string_parameter => Base64.strict_encode64(document.to_xml),
-        }
-        saml_params['RelayState'] = relay_state if relay_state.present?
-        [location, saml_params]
-      end
-
-      def deserialize(params)
-        xml = decode(saml_param_from(params))
-        Saml::Kit::Document.to_saml_document(xml)
-      end
-    end
-  end
-end
saml-kit/lib/saml/kit/http_redirect_binding.rb
@@ -1,58 +0,0 @@
-module Saml
-  module Kit
-    class HttpRedirectBinding < Binding
-      include Serializable
-
-      def initialize(location:)
-        super(binding: Saml::Kit::Namespaces::HTTP_REDIRECT, location: location)
-      end
-
-      def serialize(builder, relay_state: nil)
-        builder.sign = false
-        builder.destination = location
-        document = builder.build
-        [UrlBuilder.new.build(document, relay_state: relay_state), {}]
-      end
-
-      def deserialize(params)
-        document = deserialize_document_from!(params)
-        ensure_valid_signature!(params, document)
-        document
-      end
-
-      private
-
-      def deserialize_document_from!(params)
-        xml = inflate(decode(unescape(saml_param_from(params))))
-        Saml::Kit.logger.debug(xml)
-        Saml::Kit::Document.to_saml_document(xml)
-      end
-
-      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('&')
-
-        valid = document.provider.verify(algorithm_for(params['SigAlg']), signature, canonical_form)
-        raise ArgumentError.new("Invalid Signature") unless valid
-      end
-
-      def algorithm_for(algorithm)
-        case algorithm =~ /(rsa-)?sha(.*?)$/i && $2.to_i
-        when 256
-          OpenSSL::Digest::SHA256.new
-        when 384
-          OpenSSL::Digest::SHA384.new
-        when 512
-          OpenSSL::Digest::SHA512.new
-        else
-          OpenSSL::Digest::SHA1.new
-        end
-      end
-    end
-  end
-end
saml-kit/lib/saml/kit/identity_provider_metadata.rb
@@ -7,7 +7,7 @@ module Saml
 
       def want_authn_requests_signed
         xpath = "/md:EntityDescriptor/md:#{name}"
-        attribute = find_by(xpath).attribute("WantAuthnRequestsSigned")
+        attribute = document.find_by(xpath).attribute("WantAuthnRequestsSigned")
         return true if attribute.nil?
         attribute.text.downcase == "true"
       end
@@ -21,7 +21,7 @@ module Saml
       end
 
       def attributes
-        find_all("/md:EntityDescriptor/md:#{name}/saml:Attribute").map do |item|
+        document.find_all("/md:EntityDescriptor/md:#{name}/saml:Attribute").map do |item|
           {
             format: item.attribute("NameFormat").try(:value),
             name: item.attribute("Name").value,
@@ -48,19 +48,19 @@ module Saml
           @want_authn_requests_signed = true
         end
 
-        def add_single_sign_on_service(url, binding: :post)
-          @single_sign_on_urls.push(location: url, binding: Namespaces.binding_for(binding))
+        def add_single_sign_on_service(url, binding: :http_post)
+          @single_sign_on_urls.push(location: url, binding: Bindings.binding_for(binding))
         end
 
-        def add_single_logout_service(url, binding: :post)
-          @logout_urls.push(location: url, binding: Namespaces.binding_for(binding))
+        def add_single_logout_service(url, binding: :http_post)
+          @logout_urls.push(location: url, binding: Bindings.binding_for(binding))
         end
 
         def to_xml
-          Signature.sign(id, sign: sign) do |xml, signature|
+          Signature.sign(sign: sign) do |xml, signature|
             xml.instruct!
             xml.EntityDescriptor entity_descriptor_options do
-              signature.template(xml)
+              signature.template(id)
               xml.IDPSSODescriptor idp_sso_descriptor_options do
                 xml.KeyDescriptor use: "signing" do
                   xml.KeyInfo "xmlns": Namespaces::XMLDSIG do
@@ -112,8 +112,8 @@ module Saml
 
         def idp_sso_descriptor_options
           {
+            WantAuthnRequestsSigned: want_authn_requests_signed,
             protocolSupportEnumeration: Namespaces::PROTOCOL,
-            WantAuthnRequestsSigned: want_authn_requests_signed
           }
         end
       end
saml-kit/lib/saml/kit/logout_request.rb
@@ -40,11 +40,11 @@ module Saml
         end
 
         def to_xml
-          Signature.sign(id, sign: sign) do |xml, signature|
+          Signature.sign(sign: sign) do |xml, signature|
             xml.instruct!
             xml.LogoutRequest logout_request_options do
               xml.Issuer({ xmlns: Namespaces::ASSERTION }, issuer)
-              signature.template(xml)
+              signature.template(id)
               xml.NameID name_id_options, user.name_id_for(name_id_format)
             end
           end
saml-kit/lib/saml/kit/logout_response.rb
@@ -25,15 +25,15 @@ module Saml
           @issuer = configuration.issuer
           provider = configuration.registry.metadata_for(@issuer)
           if provider
-            @destination = provider.single_logout_service_for(binding: :post).try(:location)
+            @destination = provider.single_logout_service_for(binding: :http_post).try(:location)
           end
         end
 
         def to_xml
-          Signature.sign(id, sign: sign) do |xml, signature|
+          Signature.sign(sign: sign) do |xml, signature|
             xml.LogoutResponse logout_response_options do
               xml.Issuer(issuer, xmlns: Namespaces::ASSERTION)
-              signature.template(xml)
+              signature.template(id)
               xml.Status do
                 xml.StatusCode Value: status_code
               end
saml-kit/lib/saml/kit/metadata.rb
@@ -3,14 +3,7 @@ module Saml
     class Metadata
       include ActiveModel::Validations
       include XsdValidatable
-
       METADATA_XSD = File.expand_path("./xsd/saml-schema-metadata-2.0.xsd", File.dirname(__FILE__)).freeze
-      NAMESPACES = {
-        "NameFormat": Namespaces::ATTR_SPLAT,
-        "ds": Namespaces::XMLDSIG,
-        "md": Namespaces::METADATA,
-        "saml": Namespaces::ASSERTION,
-      }.freeze
 
       validates_presence_of :metadata
       validate :must_contain_descriptor
@@ -27,16 +20,16 @@ module Saml
       end
 
       def entity_id
-        find_by("/md:EntityDescriptor/@entityID").value
+        document.find_by("/md:EntityDescriptor/@entityID").value
       end
 
       def name_id_formats
-        find_all("/md:EntityDescriptor/md:#{name}/md:NameIDFormat").map(&:text)
+        document.find_all("/md:EntityDescriptor/md:#{name}/md:NameIDFormat").map(&:text)
       end
 
       def certificates
-        @certificates ||= find_all("/md:EntityDescriptor/md:#{name}/md:KeyDescriptor").map do |item|
-          cert = item.at_xpath("./ds:KeyInfo/ds:X509Data/ds:X509Certificate", NAMESPACES).text
+        @certificates ||= document.find_all("/md:EntityDescriptor/md:#{name}/md:KeyDescriptor").map do |item|
+          cert = item.at_xpath("./ds:KeyInfo/ds:X509Data/ds:X509Certificate", Xml::NAMESPACES).text
           {
             text: cert,
             fingerprint: Fingerprint.new(cert).algorithm(hash_algorithm),
@@ -54,15 +47,15 @@ module Saml
       end
 
       def services(type)
-        find_all("/md:EntityDescriptor/md:#{name}/md:#{type}").map do |item|
+        document.find_all("/md:EntityDescriptor/md:#{name}/md:#{type}").map do |item|
           binding = item.attribute("Binding").value
           location = item.attribute("Location").value
-          binding_for(binding, location)
+          Saml::Kit::Bindings.create_for(binding, location)
         end
       end
 
       def service_for(binding:, type:)
-        binding = Saml::Kit::Namespaces.binding_for(binding)
+        binding = Saml::Kit::Bindings.binding_for(binding)
         services(type).find { |x| x.binding?(binding) }
       end
 
@@ -78,6 +71,7 @@ module Saml
         if :signing == use.to_sym
           hash_value = fingerprint.algorithm(hash_algorithm)
           signing_certificates.find do |signing_certificate|
+            Saml::Kit.logger.debug [hash_value, signing_certificate[:fingerprint]].inspect
             hash_value == signing_certificate[:fingerprint]
           end
         end
@@ -87,8 +81,8 @@ module Saml
         @xml_hash ||= Hash.from_xml(to_xml)
       end
 
-      def to_xml
-        @xml
+      def to_xml(pretty: false)
+        document.to_xml(pretty: pretty)
       end
 
       def to_s
@@ -116,19 +110,11 @@ module Saml
       private
 
       def document
-        @document ||= Nokogiri::XML(@xml)
-      end
-
-      def find_by(xpath)
-        document.at_xpath(xpath, NAMESPACES)
-      end
-
-      def find_all(xpath)
-        document.search(xpath, NAMESPACES)
+        @document ||= Xml.new(xml)
       end
 
       def metadata
-        find_by("/md:EntityDescriptor/md:#{name}").present?
+        document.find_by("/md:EntityDescriptor/md:#{name}").present?
       end
 
       def must_contain_descriptor
@@ -155,17 +141,6 @@ module Saml
         end
         result
       end
-
-      def binding_for(binding, location)
-        case binding
-        when Namespaces::HTTP_REDIRECT
-          Saml::Kit::HttpRedirectBinding.new(location: location)
-        when Namespaces::POST
-          Saml::Kit::HttpPostBinding.new(location: location)
-        else
-          Saml::Kit::Binding.new(binding: binding, location: location)
-        end
-      end
     end
   end
 end
saml-kit/lib/saml/kit/namespaces.rb
@@ -7,9 +7,6 @@ module Saml
       BEARER = "urn:oasis:names:tc:SAML:2.0:cm:bearer"
       EMAIL_ADDRESS = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
       ENVELOPED_SIG = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
-      HTTP_ARTIFACT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact'
-      HTTP_POST = POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
-      HTTP_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
       METADATA = "urn:oasis:names:tc:SAML:2.0:metadata"
       PASSWORD = "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
       PASSWORD_PROTECTED = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
@@ -32,16 +29,7 @@ module Saml
       URI = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
       VERSION_MISMATCH_ERROR = "urn:oasis:names:tc:SAML:2.0:status:VersionMismatch"
       XMLDSIG = "http://www.w3.org/2000/09/xmldsig#"
-
-      def self.binding_for(binding)
-        if :post == binding
-          Namespaces::HTTP_POST
-        elsif :http_redirect == binding
-          Namespaces::HTTP_REDIRECT
-        else
-          nil
-        end
-      end
+      XMLENC = "http://www.w3.org/2001/04/xmlenc#"
     end
   end
 end
saml-kit/lib/saml/kit/respondable.rb
@@ -21,6 +21,10 @@ module Saml
         to_h.fetch(name, {}).fetch('InResponseTo', nil)
       end
 
+      def success?
+        Namespaces::SUCCESS == status_code
+      end
+
       private
 
       def must_match_request_id
saml-kit/lib/saml/kit/response.rb
@@ -12,7 +12,7 @@ module Saml
       end
 
       def name_id
-        to_h.fetch(name, {}).fetch('Assertion', {}).fetch('Subject', {}).fetch('NameID', nil)
+        assertion.fetch('Subject', {}).fetch('NameID', nil)
       end
 
       def [](key)
@@ -20,18 +20,19 @@ module Saml
       end
 
       def attributes
-        @attributes ||= Hash[to_h.fetch(name, {}).fetch('Assertion', {}).fetch('AttributeStatement', {}).fetch('Attribute', []).map do |item|
-          [item['Name'].to_sym, item['AttributeValue']]
-        end].with_indifferent_access
+        @attributes ||= Hash[
+          assertion.fetch('AttributeStatement', {}).fetch('Attribute', []).map do |item|
+            [item['Name'].to_sym, item['AttributeValue']]
+          end
+        ].with_indifferent_access
       end
 
-
       def started_at
-        parse_date(to_h.fetch(name, {}).fetch('Assertion', {}).fetch('Conditions', {}).fetch('NotBefore', nil))
+        parse_date(assertion.fetch('Conditions', {}).fetch('NotBefore', nil))
       end
 
       def expired_at
-        parse_date(to_h.fetch(name, {}).fetch('Assertion', {}).fetch('Conditions', {}).fetch('NotOnOrAfter', nil))
+        parse_date(assertion.fetch('Conditions', {}).fetch('NotOnOrAfter', nil))
       end
 
       def expired?
@@ -42,15 +43,42 @@ module Saml
         Time.current > started_at && !expired?
       end
 
+      def encrypted?
+        to_h[name]['EncryptedAssertion'].present?
+      end
+
+      def assertion
+        @assertion =
+          begin
+            if encrypted?
+              decrypted = Cryptography.new.decrypt(to_h.fetch(name, {}).fetch('EncryptedAssertion', {}))
+              Saml::Kit.logger.debug(decrypted)
+              Hash.from_xml(decrypted)['Assertion']
+            else
+              to_h.fetch(name, {}).fetch('Assertion', {})
+            end
+          end
+      end
+
+      def signed?
+        super || assertion.fetch('Signature', nil).present?
+      end
+
+      def certificate
+        super || assertion.fetch('Signature', {}).fetch('KeyInfo', {}).fetch('X509Data', {}).fetch('X509Certificate', nil)
+      end
+
       private
 
       def must_be_active_session
         return unless expected_type?
+        return unless success?
         errors[:base] << error_message(:expired) unless active?
       end
 
       def must_match_issuer
         return unless expected_type?
+        return unless success?
 
         unless audiences.include?(Saml::Kit.configuration.issuer)
           errors[:audience] << error_message(:must_match_issuer)
@@ -58,7 +86,7 @@ module Saml
       end
 
       def audiences
-        Array(to_h[name]['Assertion']['Conditions']['AudienceRestriction']['Audience'])
+        Array(assertion['Conditions']['AudienceRestriction']['Audience'])
       rescue => error
         Saml::Kit.logger.error(error)
         []
@@ -75,7 +103,7 @@ module Saml
         attr_reader :user, :request
         attr_accessor :id, :reference_id, :now
         attr_accessor :version, :status_code
-        attr_accessor :issuer, :sign, :destination
+        attr_accessor :issuer, :sign, :destination, :encrypt
 
         def initialize(user, request)
           @user = user
@@ -86,8 +114,9 @@ module Saml
           @version = "2.0"
           @status_code = Namespaces::SUCCESS
           @issuer = configuration.issuer
-          @destination = request.acs_url
+          @destination = destination_for(request)
           @sign = want_assertions_signed
+          @encrypt = false
         end
 
         def want_assertions_signed
@@ -98,38 +127,51 @@ module Saml
         end
 
         def to_xml
-          Signature.sign(id, sign: sign) do |xml, signature|
+          Signature.sign(sign: sign) do |xml, signature|
             xml.Response response_options do
               xml.Issuer(issuer, xmlns: Namespaces::ASSERTION)
-              signature.template(xml)
+              signature.template(id)
               xml.Status do
                 xml.StatusCode Value: status_code
               end
-              xml.Assertion(assertion_options) do
-                xml.Issuer issuer
-                xml.Subject do
-                  xml.NameID user.name_id_for(request.name_id_format), Format: request.name_id_format
-                  xml.SubjectConfirmation Method: Namespaces::BEARER do
-                    xml.SubjectConfirmationData "", subject_confirmation_data_options
-                  end
+              assertion(xml, signature)
+            end
+          end
+        end
+
+        def build
+          Response.new(to_xml, request_id: request.id)
+        end
+
+        private
+
+        def assertion(xml, signature)
+          with_encryption(xml) do |xml|
+            xml.Assertion(assertion_options) do
+              xml.Issuer issuer
+              signature.template(reference_id) unless encrypt
+              xml.Subject do
+                xml.NameID user.name_id_for(request.name_id_format), Format: request.name_id_format
+                xml.SubjectConfirmation Method: Namespaces::BEARER do
+                  xml.SubjectConfirmationData "", subject_confirmation_data_options
                 end
-                xml.Conditions conditions_options do
-                  xml.AudienceRestriction do
-                    xml.Audience request.issuer
-                  end
+              end
+              xml.Conditions conditions_options do
+                xml.AudienceRestriction do
+                  xml.Audience request.issuer
                 end
-                xml.AuthnStatement authn_statement_options do
-                  xml.AuthnContext do
-                    xml.AuthnContextClassRef Namespaces::PASSWORD
-                  end
+              end
+              xml.AuthnStatement authn_statement_options do
+                xml.AuthnContext do
+                  xml.AuthnContextClassRef Namespaces::PASSWORD
                 end
-                assertion_attributes = user.assertion_attributes_for(request)
-                if assertion_attributes.any?
-                  xml.AttributeStatement do
-                    assertion_attributes.each do |key, value|
-                      xml.Attribute Name: key, NameFormat: Namespaces::URI, FriendlyName: key do
-                        xml.AttributeValue value.to_s
-                      end
+              end
+              assertion_attributes = user.assertion_attributes_for(request)
+              if assertion_attributes.any?
+                xml.AttributeStatement do
+                  assertion_attributes.each do |key, value|
+                    xml.Attribute Name: key, NameFormat: Namespaces::URI, FriendlyName: key do
+                      xml.AttributeValue value.to_s
                     end
                   end
                 end
@@ -138,11 +180,52 @@ module Saml
           end
         end
 
-        def build
-          Response.new(to_xml, request_id: request.id)
+        def with_encryption(xml)
+          if encrypt
+            temp = ::Builder::XmlMarkup.new
+            yield temp
+            raw_xml_to_encrypt = temp.target!
+
+            encryption_certificate = OpenSSL::X509::Certificate.new(Base64.decode64(request.provider.encryption_certificates.first[:text]))
+            public_key = encryption_certificate.public_key
+
+            cipher = OpenSSL::Cipher.new('AES-256-CBC')
+            cipher.encrypt
+            key = cipher.random_key
+            iv = cipher.random_iv
+            encrypted = cipher.update(raw_xml_to_encrypt) + cipher.final
+
+            Saml::Kit.logger.debug ['+iv', iv].inspect
+            Saml::Kit.logger.debug ['+key', key].inspect
+
+            xml.EncryptedAssertion xmlns: Namespaces::ASSERTION do
+              xml.EncryptedData xmlns: Namespaces::XMLENC do
+                xml.EncryptionMethod Algorithm: "http://www.w3.org/2001/04/xmlenc#aes256-cbc"
+                xml.KeyInfo xmlns: Namespaces::XMLDSIG do
+                  xml.EncryptedKey xmlns: Namespaces::XMLENC do
+                    xml.EncryptionMethod Algorithm: "http://www.w3.org/2001/04/xmlenc#rsa-1_5"
+                    xml.CipherData do
+                      xml.CipherValue Base64.encode64(public_key.public_encrypt(key))
+                    end
+                  end
+                end
+                xml.CipherData do
+                  xml.CipherValue Base64.encode64(iv + encrypted)
+                end
+              end
+            end
+          else
+            yield xml
+          end
         end
 
-        private
+        def destination_for(request)
+          if request.signed? && request.trusted?
+            request.acs_url || request.provider.assertion_consumer_service_for(binding: :http_post).try(:location)
+          else
+            request.provider.assertion_consumer_service_for(binding: :http_post).try(:location)
+          end
+        end
 
         def configuration
           Saml::Kit.configuration
saml-kit/lib/saml/kit/service_provider_metadata.rb
@@ -14,7 +14,7 @@ module Saml
       end
 
       def want_assertions_signed
-        attribute = find_by("/md:EntityDescriptor/md:#{name}").attribute("WantAssertionsSigned")
+        attribute = document.find_by("/md:EntityDescriptor/md:#{name}").attribute("WantAssertionsSigned")
         attribute.text.downcase == "true"
       end
 
@@ -35,19 +35,19 @@ module Saml
           @want_assertions_signed = true
         end
 
-        def add_assertion_consumer_service(url, binding: :post)
-          @acs_urls.push(location: url, binding: Namespaces.binding_for(binding))
+        def add_assertion_consumer_service(url, binding: :http_post)
+          @acs_urls.push(location: url, binding: Bindings.binding_for(binding))
         end
 
-        def add_single_logout_service(url, binding: :post)
-          @logout_urls.push(location: url, binding: Namespaces.binding_for(binding))
+        def add_single_logout_service(url, binding: :http_post)
+          @logout_urls.push(location: url, binding: Bindings.binding_for(binding))
         end
 
         def to_xml
-          Signature.sign(id, sign: sign) do |xml, signature|
+          Signature.sign(sign: sign) do |xml, signature|
             xml.instruct!
             xml.EntityDescriptor entity_descriptor_options do
-              signature.template(xml)
+              signature.template(id)
               xml.SPSSODescriptor descriptor_options do
                 if @configuration.signing_certificate_pem.present?
                   xml.KeyDescriptor use: "signing" do
@@ -58,6 +58,15 @@ module Saml
                     end
                   end
                 end
+                if @configuration.encryption_certificate_pem.present?
+                  xml.KeyDescriptor use: "encryption" do
+                    xml.KeyInfo "xmlns": Namespaces::XMLDSIG do
+                      xml.X509Data do
+                        xml.X509Certificate @configuration.stripped_encryption_certificate
+                      end
+                    end
+                  end
+                end
                 logout_urls.each do |item|
                   xml.SingleLogoutService Binding: item[:binding], Location: item[:location]
                 end
saml-kit/lib/saml/kit/signature.rb
@@ -16,17 +16,19 @@ module Saml
         SHA512: "http://www.w3.org/2001/04/xmlenc#sha512",
       }.freeze
 
-      attr_reader :configuration, :reference_id, :sign
+      attr_reader :configuration, :sign, :xml
 
-      def initialize(reference_id, configuration: Saml::Kit.configuration, sign: true)
-        @reference_id = reference_id
+      def initialize(xml, configuration:, sign: true)
+        @xml = xml
         @configuration = configuration
         @sign = sign
+        @reference_ids = []
       end
 
-      def template(xml = ::Builder::XmlMarkup.new)
+      def template(reference_id)
         return unless sign
         return if reference_id.blank?
+        @reference_ids << reference_id
 
         xml.Signature "xmlns" => Namespaces::XMLDSIG do
           xml.SignedInfo do
@@ -50,19 +52,20 @@ module Saml
         end
       end
 
-      def finalize(xml)
-        if sign && reference_id.present?
-          document = Xmldsig::SignedDocument.new(xml.target!)
-          document.sign(private_key)
-        else
-          xml.target!
+      def finalize
+        return xml.target! unless sign
+
+        raw_xml = xml.target!
+        @reference_ids.each do |reference_id|
+          raw_xml = Xmldsig::SignedDocument.new(raw_xml).sign(private_key)
         end
+        raw_xml
       end
 
-      def self.sign(id, sign: true, xml: ::Builder::XmlMarkup.new)
-        signature = new(id, sign: sign)
+      def self.sign(sign: true, xml: ::Builder::XmlMarkup.new, configuration: Saml::Kit.configuration)
+        signature = new(xml, sign: sign, configuration: configuration)
         yield xml, signature
-        signature.finalize(xml)
+        signature.finalize
       end
 
       private
saml-kit/lib/saml/kit/trustable.rb
@@ -4,8 +4,9 @@ module Saml
       extend ActiveSupport::Concern
 
       included do
-        validate :must_have_valid_signature
+        validate :must_have_valid_signature, unless: :signature_manually_verified
         validate :must_be_registered
+        validate :must_be_trusted, unless: :signature_manually_verified
       end
 
       def certificate
@@ -19,7 +20,7 @@ module Saml
       end
 
       def signed?
-        to_h[name]['Signature'].present?
+        to_h.fetch(name, {}).fetch('Signature', nil).present?
       end
 
       def trusted?
@@ -36,8 +37,14 @@ module Saml
         Saml::Kit.configuration.registry
       end
 
+      def signature_verified!
+        @signature_manually_verified = true
+      end
+
       private
 
+      attr_reader :signature_manually_verified
+
       def must_have_valid_signature
         return if to_xml.blank?
 
@@ -50,10 +57,11 @@ module Saml
 
       def must_be_registered
         return unless expected_type?
-        if provider.nil?
-          errors[:provider] << error_message(:unregistered)
-          return
-        end
+        return if provider.present?
+        errors[:provider] << error_message(:unregistered)
+      end
+
+      def must_be_trusted
         return if trusted?
         errors[:fingerprint] << error_message(:invalid_fingerprint)
       end
saml-kit/lib/saml/kit/url_builder.rb
@@ -1,38 +0,0 @@
-module Saml
-  module Kit
-    class UrlBuilder
-      include Serializable
-
-      def initialize(private_key: Saml::Kit.configuration.signing_private_key)
-        @private_key = private_key
-      end
-
-      def build(saml_document, relay_state: nil)
-        payload = canonicalize(saml_document, relay_state)
-        "#{saml_document.destination}?#{payload}&Signature=#{signature_for(payload)}"
-      end
-
-      private
-
-      attr_reader :private_key
-
-      def signature_for(payload)
-        encode(private_key.sign(OpenSSL::Digest::SHA256.new, payload))
-      end
-
-      def canonicalize(saml_document, relay_state)
-        {
-          saml_document.query_string_parameter => serialize(saml_document.to_xml),
-          'RelayState' => relay_state,
-          'SigAlg' => Saml::Kit::Namespaces::SHA256,
-        }.map do |(key, value)|
-          value.present? ? "#{key}=#{escape(value)}" : nil
-        end.compact.join('&')
-      end
-
-      def serialize(value)
-        encode(deflate(value))
-      end
-    end
-  end
-end
saml-kit/lib/saml/kit/version.rb
@@ -1,5 +1,5 @@
 module Saml
   module Kit
-    VERSION = "0.1.0"
+    VERSION = "0.2.0"
   end
 end
saml-kit/lib/saml/kit/xml.rb
@@ -2,6 +2,12 @@ module Saml
   module Kit
     class Xml
       include ActiveModel::Validations
+      NAMESPACES = {
+        "NameFormat": Namespaces::ATTR_SPLAT,
+        "ds": Namespaces::XMLDSIG,
+        "md": Namespaces::METADATA,
+        "saml": Namespaces::ASSERTION,
+      }.freeze
 
       attr_reader :raw_xml, :document
 
@@ -10,9 +16,7 @@ module Saml
 
       def initialize(raw_xml)
         @raw_xml = raw_xml
-        @document = Nokogiri::XML(raw_xml, nil, nil, Nokogiri::XML::ParseOptions::STRICT) do |config|
-          config.noblanks
-        end
+        @document = Nokogiri::XML(raw_xml)
       end
 
       def x509_certificates
@@ -22,6 +26,18 @@ module Saml
         end
       end
 
+      def find_by(xpath)
+        document.at_xpath(xpath, NAMESPACES)
+      end
+
+      def find_all(xpath)
+        document.search(xpath, NAMESPACES)
+      end
+
+      def to_xml(pretty: true)
+        pretty ? document.to_xml(indent: 2) : raw_xml
+      end
+
       private
 
       def validate_signatures
saml-kit/lib/saml/kit.rb
@@ -3,6 +3,7 @@ require "saml/kit/version"
 require "active_model"
 require "active_support/core_ext/date/calculations"
 require "active_support/core_ext/hash/conversions"
+require "active_support/core_ext/hash/indifferent_access"
 require "active_support/core_ext/numeric/time"
 require "active_support/duration"
 require "builder"
@@ -21,14 +22,14 @@ require "saml/kit/trustable"
 require "saml/kit/document"
 
 require "saml/kit/authentication_request"
-require "saml/kit/binding"
+require "saml/kit/bindings"
 require "saml/kit/configuration"
+require "saml/kit/crypto"
+require "saml/kit/cryptography"
 require "saml/kit/default_registry"
 require "saml/kit/fingerprint"
 require "saml/kit/logout_response"
 require "saml/kit/logout_request"
-require "saml/kit/http_post_binding"
-require "saml/kit/http_redirect_binding"
 require "saml/kit/metadata"
 require "saml/kit/response"
 require "saml/kit/identity_provider_metadata"
@@ -36,7 +37,6 @@ require "saml/kit/invalid_document"
 require "saml/kit/self_signed_certificate"
 require "saml/kit/service_provider_metadata"
 require "saml/kit/signature"
-require "saml/kit/url_builder"
 require "saml/kit/xml"
 
 I18n.load_path += Dir[File.expand_path("kit/locales/*.yml", File.dirname(__FILE__))]
saml-kit/spec/saml/binding_spec.rb → saml-kit/spec/saml/bindings/binding_spec.rb
@@ -1,8 +1,8 @@
 require 'spec_helper'
 
-RSpec.describe Saml::Kit::Binding do
+RSpec.describe Saml::Kit::Bindings::Binding do
   let(:location) { FFaker::Internet.http_url }
-  subject { Saml::Kit::Binding.new(binding: Saml::Kit::Namespaces::HTTP_ARTIFACT, location: location) }
+  subject { described_class.new(binding: Saml::Kit::Bindings::HTTP_ARTIFACT, location: location) }
 
   describe "#serialize" do
     it 'ignores other bindings' do
saml-kit/spec/saml/http_post_binding_spec.rb → saml-kit/spec/saml/bindings/http_post_spec.rb
@@ -1,8 +1,8 @@
 require 'spec_helper'
 
-RSpec.describe Saml::Kit::HttpPostBinding do
+RSpec.describe Saml::Kit::Bindings::HttpPost do
   let(:location) { FFaker::Internet.http_url }
-  subject { Saml::Kit::HttpPostBinding.new(location: location) }
+  subject { described_class.new(location: location) }
 
   describe "#serialize" do
     let(:relay_state) { "ECHO" }
@@ -76,7 +76,7 @@ RSpec.describe Saml::Kit::HttpPostBinding do
 
     it 'deserializes to a Response' do
       user = double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: [])
-      request = double(:request, id: SecureRandom.uuid, provider: nil, acs_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: FFaker::Internet.http_url)
+      request = double(:request, id: SecureRandom.uuid, provider: nil, acs_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: FFaker::Internet.http_url, signed?: true, trusted?: true)
       builder = Saml::Kit::Response::Builder.new(user, request)
       _, params = subject.serialize(builder)
       result = subject.deserialize(params)
saml-kit/spec/saml/http_redirect_binding_spec.rb → saml-kit/spec/saml/bindings/http_redirect_spec.rb
@@ -1,8 +1,8 @@
 require 'spec_helper'
 
-RSpec.describe Saml::Kit::HttpRedirectBinding do
+RSpec.describe Saml::Kit::Bindings::HttpRedirect do
   let(:location) { FFaker::Internet.http_url }
-  subject { Saml::Kit::HttpRedirectBinding.new(location: location) }
+  subject { described_class.new(location: location) }
 
   describe "#serialize" do
     let(:relay_state) { "ECHO" }
@@ -47,7 +47,7 @@ RSpec.describe Saml::Kit::HttpRedirectBinding do
 
     it 'deserializes the SAMLResponse to a Response' do
       user = double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: [])
-      request = double(:request, id: SecureRandom.uuid, provider: nil, acs_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: issuer)
+      request = double(:request, id: SecureRandom.uuid, provider: nil, acs_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: issuer, signed?: true, trusted?: true)
       url, _ = subject.serialize(Saml::Kit::Response::Builder.new(user, request))
       result = subject.deserialize(query_params_from(url))
       expect(result).to be_instance_of(Saml::Kit::Response)
@@ -81,5 +81,17 @@ RSpec.describe Saml::Kit::HttpRedirectBinding do
         subject.deserialize(query_params)
       end.to raise_error(/Invalid Signature/)
     end
+
+    it 'returns a signed document, when a signature is missing' do
+      builder = Saml::Kit::ServiceProviderMetadata::Builder.new
+      builder.add_assertion_consumer_service(FFaker::Internet.http_url, binding: :http_post)
+      provider = builder.build
+      allow(Saml::Kit.configuration.registry).to receive(:metadata_for).with(issuer).and_return(provider)
+
+      url, _ = subject.serialize(Saml::Kit::AuthenticationRequest::Builder.new)
+      result = subject.deserialize(query_params_from(url))
+      expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
+      expect(result).to be_valid
+    end
   end
 end
saml-kit/spec/saml/url_builder_spec.rb → saml-kit/spec/saml/bindings/url_builder_spec.rb
@@ -1,6 +1,6 @@
 require 'spec_helper'
 
-RSpec.describe Saml::Kit::UrlBuilder do
+RSpec.describe Saml::Kit::Bindings::UrlBuilder do
   describe "#build" do
     let(:xml) { "<xml></xml>" }
     let(:destination) { FFaker::Internet.http_url }
saml-kit/spec/saml/authentication_request_spec.rb
@@ -102,51 +102,31 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
       expect(subject.errors[:provider]).to be_present
     end
 
-    it 'is invalid when an assertion consumer service url is not provided' do
-      allow(metadata).to receive(:matches?).and_return(true)
-      allow(metadata).to receive(:assertion_consumer_service_for).and_return(nil)
-
-      builder = described_class::Builder.new
-      builder.acs_url = nil
-      xml = builder.to_xml
-
-      subject = described_class.new(xml)
-      expect(subject).to be_invalid
-      expect(subject.errors[:acs_url]).to be_present
-    end
-
-    it 'is valid when an the ACS is available via the registry' do
-      allow(registry).to receive(:metadata_for).with(issuer)
-        .and_return(metadata)
-      allow(metadata).to receive(:matches?).and_return(true)
-      allow(metadata).to receive(:assertion_consumer_service_for).and_return(
-        Saml::Kit::HttpPostBinding.new(location: acs_url)
-      )
-
-      builder = described_class::Builder.new
-      builder.issuer = issuer
-      builder.acs_url = nil
-      xml = builder.to_xml
-
-      expect(described_class.new(xml)).to be_valid
-    end
-
     it 'validates the schema of the request' do
-      xml = ::Builder::XmlMarkup.new
       id = SecureRandom.uuid
-      options = {
-        "xmlns:samlp" => Saml::Kit::Namespaces::PROTOCOL,
-        AssertionConsumerServiceURL: acs_url,
-        ID: "_#{id}",
-      }
-      signature = Saml::Kit::Signature.new(id)
-      xml.tag!('samlp:AuthnRequest', options) do
-        signature.template(xml)
-        xml.Fake do
-          xml.NotAllowed "Huh?"
+      signed_xml = Saml::Kit::Signature.sign(sign: true) do |xml, signature|
+        xml.tag!('samlp:AuthnRequest', "xmlns:samlp" => Saml::Kit::Namespaces::PROTOCOL, AssertionConsumerServiceURL: acs_url, ID: "_#{id}") do
+          signature.template(id)
+          xml.Fake do
+            xml.NotAllowed "Huh?"
+          end
         end
       end
-      expect(described_class.new(signature.finalize(xml))).to be_invalid
+      expect(described_class.new(signed_xml)).to be_invalid
+    end
+
+    it 'validates a request without a signature' do
+      now = Time.now.utc
+raw_xml = <<-XML
+<samlp:AuthnRequest AssertionConsumerServiceURL='#{acs_url}' ID='_#{SecureRandom.uuid}' IssueInstant='#{now.iso8601}' Version='2.0' xmlns:saml='#{Saml::Kit::Namespaces::ASSERTION}' xmlns:samlp='#{Saml::Kit::Namespaces::PROTOCOL}'>
+  <saml:Issuer>#{issuer}</saml:Issuer>
+  <samlp:NameIDPolicy AllowCreate='true' Format='#{Saml::Kit::Namespaces::EMAIL_ADDRESS}'/>
+</samlp:AuthnRequest>
+XML
+
+      subject = described_class.new(raw_xml)
+      subject.signature_verified!
+      expect(subject).to be_valid
     end
   end
 
@@ -161,19 +141,13 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
       expect(subject.acs_url).to eql(acs_url)
     end
 
-    it 'returns the registered ACS url' do
+    it 'returns nil' do
       builder = described_class::Builder.new
       builder.issuer = issuer
       builder.acs_url = nil
       subject = builder.build
 
-      allow(Saml::Kit.configuration).to receive(:registry).and_return(registry)
-      allow(registry).to receive(:metadata_for).and_return(metadata)
-      allow(registry).to receive(:metadata_for).with(issuer).and_return(metadata)
-      allow(metadata).to receive(:assertion_consumer_service_for).and_return(
-        Saml::Kit::HttpPostBinding.new(location: acs_url)
-      )
-      expect(subject.acs_url).to eql(acs_url)
+      expect(subject.acs_url).to be_nil
     end
   end
 end
saml-kit/spec/saml/cryptography_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+RSpec.describe Saml::Kit::Cryptography do
+  describe "#decrypt" do
+    let(:secret) { FFaker::Movie.title }
+    let(:password) { FFaker::Movie.title }
+
+    it 'decrypts the data' do
+      certificate_pem, private_key_pem = Saml::Kit::SelfSignedCertificate.new(password).create
+      public_key = OpenSSL::X509::Certificate.new(certificate_pem).public_key
+      private_key = OpenSSL::PKey::RSA.new(private_key_pem, password)
+
+      cipher = OpenSSL::Cipher.new('AES-128-CBC')
+      cipher.encrypt
+      key = cipher.random_key
+      iv = cipher.random_iv
+      encrypted = cipher.update(secret) + cipher.final
+
+      data = {
+        "EncryptedData"=> {
+          "xmlns:xenc"=>"http://www.w3.org/2001/04/xmlenc#",
+          "xmlns:dsig"=>"http://www.w3.org/2000/09/xmldsig#",
+          "Type"=>"http://www.w3.org/2001/04/xmlenc#Element",
+          "EncryptionMethod"=> { 
+            "Algorithm"=>"http://www.w3.org/2001/04/xmlenc#aes128-cbc"
+          },
+          "KeyInfo"=> {
+            "xmlns:dsig"=>"http://www.w3.org/2000/09/xmldsig#",
+            "EncryptedKey"=> {
+              "EncryptionMethod"=>{ 
+                "Algorithm"=>"http://www.w3.org/2001/04/xmlenc#rsa-1_5"
+              },
+              "CipherData"=>{
+                "CipherValue"=> Base64.encode64(public_key.public_encrypt(key))
+              }
+            }
+          },
+          "CipherData"=>{
+            "CipherValue"=> Base64.encode64(iv + encrypted)
+          }
+        }
+      }
+      subject = described_class.new(private_key)
+      decrypted = subject.decrypt(data)
+      expect(decrypted.strip).to eql(secret)
+    end
+  end
+end
saml-kit/spec/saml/identity_provider_metadata_spec.rb
@@ -14,8 +14,8 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
     it do
       location = "https://dev.oktapreview.com/app/example/1/sso/saml"
       expect(subject.single_sign_on_services.map(&:to_h)).to match_array([
-        { binding: Saml::Kit::Namespaces::HTTP_POST, location: location },
-        { binding: Saml::Kit::Namespaces::HTTP_REDIRECT, location: location },
+        { binding: Saml::Kit::Bindings::HTTP_POST, location: location },
+        { binding: Saml::Kit::Bindings::HTTP_REDIRECT, location: location },
       ])
     end
     it { expect(subject.single_logout_services).to be_empty }
@@ -47,15 +47,15 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
     it do
       location = "https://www.example.com/adfs/ls/"
       expect(subject.single_sign_on_services.map(&:to_h)).to match_array([
-        { location: location, binding: Saml::Kit::Namespaces::HTTP_REDIRECT },
-        { location: location, binding: Saml::Kit::Namespaces::HTTP_POST },
+        { location: location, binding: Saml::Kit::Bindings::HTTP_REDIRECT },
+        { location: location, binding: Saml::Kit::Bindings::HTTP_POST },
       ])
     end
     it do
       location = "https://www.example.com/adfs/ls/"
       expect(subject.single_logout_services.map(&:to_h)).to match_array([
-        { location: location, binding: Saml::Kit::Namespaces::HTTP_REDIRECT },
-        { location: location, binding: Saml::Kit::Namespaces::HTTP_POST },
+        { location: location, binding: Saml::Kit::Bindings::HTTP_REDIRECT },
+        { location: location, binding: Saml::Kit::Bindings::HTTP_POST },
       ])
     end
     it do
@@ -71,9 +71,9 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
     it 'valid when given valid identity provider metadata' do
       builder = described_class::Builder.new
       builder.attributes = [:email]
-      builder.add_single_sign_on_service(FFaker::Internet.http_url, binding: :post)
+      builder.add_single_sign_on_service(FFaker::Internet.http_url, binding: :http_post)
       builder.add_single_sign_on_service(FFaker::Internet.http_url, binding: :http_redirect)
-      builder.add_single_logout_service(FFaker::Internet.http_url, binding: :post)
+      builder.add_single_logout_service(FFaker::Internet.http_url, binding: :http_post)
       builder.add_single_logout_service(FFaker::Internet.http_url, binding: :http_redirect)
       expect(builder.build).to be_valid
     end
@@ -122,20 +122,20 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
     subject do
       builder = Saml::Kit::IdentityProviderMetadata::Builder.new
       builder.add_single_sign_on_service(redirect_url, binding: :http_redirect)
-      builder.add_single_sign_on_service(post_url, binding: :post)
+      builder.add_single_sign_on_service(post_url, binding: :http_post)
       builder.build
     end
 
     it 'returns the POST binding' do
-      result = subject.single_sign_on_service_for(binding: :post)
+      result = subject.single_sign_on_service_for(binding: :http_post)
       expect(result.location).to eql(post_url)
-      expect(result.binding).to eql(Saml::Kit::Namespaces::POST)
+      expect(result.binding).to eql(Saml::Kit::Bindings::HTTP_POST)
     end
 
     it 'returns the HTTP_REDIRECT binding' do
       result = subject.single_sign_on_service_for(binding: :http_redirect)
       expect(result.location).to eql(redirect_url)
-      expect(result.binding).to eql(Saml::Kit::Namespaces::HTTP_REDIRECT)
+      expect(result.binding).to eql(Saml::Kit::Bindings::HTTP_REDIRECT)
     end
 
     it 'returns nil if the binding cannot be found' do
@@ -174,11 +174,11 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
 
     before :each do
       builder.add_single_logout_service(redirect_url, binding: :http_redirect)
-      builder.add_single_logout_service(post_url, binding: :post)
+      builder.add_single_logout_service(post_url, binding: :http_post)
     end
 
     it 'returns the location for the matching binding' do
-      expect(subject.single_logout_service_for(binding: :post).location).to eql(post_url)
+      expect(subject.single_logout_service_for(binding: :http_post).location).to eql(post_url)
       expect(subject.single_logout_service_for(binding: :http_redirect).location).to eql(redirect_url)
     end
 
@@ -205,7 +205,7 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
         Saml::Kit::Namespaces::EMAIL_ADDRESS,
       ]
       subject.add_single_sign_on_service("https://www.example.com/login", binding: :http_redirect)
-      subject.add_single_logout_service("https://www.example.com/logout", binding: :post)
+      subject.add_single_logout_service("https://www.example.com/logout", binding: :http_post)
       subject.attributes << "id"
 
       result = Hash.from_xml(subject.build.to_xml)
@@ -219,9 +219,9 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
         Saml::Kit::Namespaces::TRANSIENT,
         Saml::Kit::Namespaces::EMAIL_ADDRESS,
       ])
-      expect(result['EntityDescriptor']['IDPSSODescriptor']['SingleSignOnService']['Binding']).to eql(Saml::Kit::Namespaces::HTTP_REDIRECT)
+      expect(result['EntityDescriptor']['IDPSSODescriptor']['SingleSignOnService']['Binding']).to eql(Saml::Kit::Bindings::HTTP_REDIRECT)
       expect(result['EntityDescriptor']['IDPSSODescriptor']['SingleSignOnService']['Location']).to eql("https://www.example.com/login")
-      expect(result['EntityDescriptor']['IDPSSODescriptor']['SingleLogoutService']['Binding']).to eql(Saml::Kit::Namespaces::HTTP_POST)
+      expect(result['EntityDescriptor']['IDPSSODescriptor']['SingleLogoutService']['Binding']).to eql(Saml::Kit::Bindings::HTTP_POST)
       expect(result['EntityDescriptor']['IDPSSODescriptor']['SingleLogoutService']['Location']).to eql("https://www.example.com/logout")
       expect(result['EntityDescriptor']['IDPSSODescriptor']['Attribute']['Name']).to eql("id")
       expect(result['EntityDescriptor']['IDPSSODescriptor']['KeyDescriptor']['KeyInfo']['X509Data']['X509Certificate']).to eql(Saml::Kit.configuration.stripped_signing_certificate)
saml-kit/spec/saml/logout_request_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe Saml::Kit::LogoutRequest do
       allow(registry).to receive(:metadata_for).and_return(metadata)
       allow(metadata).to receive(:matches?).and_return(true)
       allow(metadata).to receive(:single_logout_services).and_return([
-        Saml::Kit::HttpPostBinding.new(location: FFaker::Internet.http_url)
+        Saml::Kit::Bindings::HttpPost.new(location: FFaker::Internet.http_url)
       ])
     end
 
@@ -94,7 +94,7 @@ RSpec.describe Saml::Kit::LogoutRequest do
       allow(registry).to receive(:metadata_for).with(builder.issuer).and_return(metadata)
       allow(metadata).to receive(:matches?).and_return(true)
       allow(metadata).to receive(:single_logout_services).and_return([
-        Saml::Kit::HttpPostBinding.new(location: FFaker::Internet.http_url)
+        Saml::Kit::Bindings::HttpPost.new(location: FFaker::Internet.http_url)
       ])
 
       expect(builder.build).to be_valid
@@ -102,15 +102,15 @@ RSpec.describe Saml::Kit::LogoutRequest do
 
     it 'validates the schema of the request' do
       id = SecureRandom.uuid
-      signature = Saml::Kit::Signature.new(id)
-      xml = ::Builder::XmlMarkup.new
-      xml.LogoutRequest ID: "_#{id}" do
-        signature.template(xml)
-        xml.Fake do
-          xml.NotAllowed "Huh?"
+      signed_xml = Saml::Kit::Signature.sign(sign: true) do |xml, signature|
+        xml.LogoutRequest ID: "_#{id}" do
+          signature.template(id)
+          xml.Fake do
+            xml.NotAllowed "Huh?"
+          end
         end
       end
-      expect(described_class.new(signature.finalize(xml))).to be_invalid
+      expect(described_class.new(signed_xml)).to be_invalid
     end
   end
 
saml-kit/spec/saml/response_spec.rb
@@ -3,19 +3,33 @@ require 'spec_helper'
 RSpec.describe Saml::Kit::Response do
   describe "#destination" do
     let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
-    let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: { }) }
-    let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: SecureRandom.uuid, acs_url: acs_url, issuer: FFaker::Movie.title, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: nil) }
+    let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: []) }
     subject { described_class::Builder.new(user, request).build }
 
-    it 'returns the acs_url' do
-      expect(subject.destination).to eql(acs_url)
+    describe "when the request is signed and trusted" do
+      let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: SecureRandom.uuid, acs_url: acs_url, issuer: FFaker::Movie.title, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: nil, signed?: true, trusted?: true) }
+
+      it 'returns the ACS embedded in the request' do
+        expect(subject.destination).to eql(acs_url)
+      end
+    end
+
+    describe "when the request is not trusted" do
+      let(:registered_acs_url) { FFaker::Internet.uri("https") }
+      let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: SecureRandom.uuid, acs_url: acs_url, issuer: FFaker::Movie.title, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: provider, signed?: true, trusted?: false) }
+      let(:provider) { instance_double(Saml::Kit::ServiceProviderMetadata, want_assertions_signed: false) }
+
+      it 'returns the registered ACS embedded in the metadata' do
+        allow(provider).to receive(:assertion_consumer_service_for).and_return(double(location: registered_acs_url))
+        expect(subject.destination).to eql(registered_acs_url)
+      end
     end
   end
 
   describe "#to_xml" do
     subject { described_class::Builder.new(user, request) }
     let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: { email: email, created_at: Time.now.utc.iso8601 }) }
-    let(:request) { double(id: SecureRandom.uuid, acs_url: acs_url, issuer: FFaker::Movie.title, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: nil) }
+    let(:request) { double(id: SecureRandom.uuid, acs_url: acs_url, issuer: FFaker::Movie.title, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: nil, trusted?: true, signed?: true) }
     let(:acs_url) { "https://#{FFaker::Internet.domain_name}/acs" }
     let(:issuer) { FFaker::Movie.title }
     let(:email) { FFaker::Internet.email }
@@ -76,7 +90,7 @@ RSpec.describe Saml::Kit::Response do
   end
 
   describe "#valid?" do
-    let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: "_#{SecureRandom.uuid}", issuer: FFaker::Internet.http_url, acs_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, provider: nil) }
+    let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: "_#{SecureRandom.uuid}", issuer: FFaker::Internet.http_url, acs_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, provider: nil, signed?: true, trusted?: true) }
     let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: { id: SecureRandom.uuid }) }
     let(:builder) { described_class::Builder.new(user, request) }
     let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
@@ -94,6 +108,7 @@ RSpec.describe Saml::Kit::Response do
     end
 
     it 'is invalid when blank' do
+      allow(registry).to receive(:metadata_for).and_return(nil)
       subject = described_class.new("")
       expect(subject).to be_invalid
       expect(subject.errors[:content]).to be_present
@@ -109,6 +124,7 @@ RSpec.describe Saml::Kit::Response do
     end
 
     it 'is invalid when not a Response' do
+      allow(registry).to receive(:metadata_for).and_return(nil)
       xml = Saml::Kit::IdentityProviderMetadata::Builder.new.to_xml
       subject = described_class.new(xml)
       expect(subject).to be_invalid
@@ -126,17 +142,16 @@ RSpec.describe Saml::Kit::Response do
     it 'validates the schema of the response' do
       allow(registry).to receive(:metadata_for).and_return(metadata)
       allow(metadata).to receive(:matches?).and_return(true)
-      xml = ::Builder::XmlMarkup.new
       id = SecureRandom.uuid
-      options = { "xmlns:samlp" => Saml::Kit::Namespaces::PROTOCOL, ID: "_#{id}", }
-      signature = Saml::Kit::Signature.new(id)
-      xml.tag!("samlp:Response", options) do
-        signature.template(xml)
-        xml.Fake do
-          xml.NotAllowed "Huh?"
+      signed_xml = Saml::Kit::Signature.sign(sign: true) do |xml, signature|
+        xml.tag! "samlp:Response", "xmlns:samlp" => Saml::Kit::Namespaces::PROTOCOL, ID: "_#{id}" do
+          signature.template(id)
+          xml.Fake do
+            xml.NotAllowed "Huh?"
+          end
         end
       end
-      subject = described_class.new(signature.finalize(xml))
+      subject = described_class.new(signed_xml)
       expect(subject).to be_invalid
       expect(subject.errors[:base]).to be_present
     end
@@ -207,17 +222,318 @@ RSpec.describe Saml::Kit::Response do
       expect(subject).to be_invalid
       expect(subject.errors[:audience]).to be_present
     end
+
+    it 'is invalid' do
+      now = Time.now.utc
+      destination = FFaker::Internet.http_url
+      raw_xml = <<-XML
+<?xml version="1.0"?>
+<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="_#{SecureRandom.uuid}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{destination}" Consent="#{Saml::Kit::Namespaces::UNSPECIFIED}" InResponseTo="#{request.id}">
+  <Issuer xmlns="#{Saml::Kit::Namespaces::ASSERTION}">#{request.issuer}</Issuer>
+  <samlp:Status>
+    <samlp:StatusCode Value="#{Saml::Kit::Namespaces::RESPONDER_ERROR}"/>
+  </samlp:Status>
+</samlp:Response>
+      XML
+
+      allow(registry).to receive(:metadata_for).with(request.issuer).and_return(metadata)
+      subject = described_class.new(raw_xml)
+      expect(subject).to be_invalid
+    end
+  end
+
+  describe "#signed?" do
+    let(:now) { Time.now.utc }
+    let(:id) { SecureRandom.uuid }
+    let(:url) { FFaker::Internet.uri("https") }
+
+    it 'returns true when the Assertion is signed' do
+      xml = <<-XML
+<?xml version="1.0"?>
+<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="_#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
+  <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="_#{id}" IssueInstant="#{now.iso8601}" Version="2.0">
+    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+      <ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+        <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+        <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+        <ds:Reference URI="#_#{id}">
+          <ds:Transforms>
+            <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
+            <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+          </ds:Transforms>
+          <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+          <ds:DigestValue></ds:DigestValue>
+        </ds:Reference>
+      </ds:SignedInfo>
+      <ds:SignatureValue></ds:SignatureValue>
+      <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+        <ds:X509Data>
+          <ds:X509Certificate></ds:X509Certificate>
+        </ds:X509Data>
+      </KeyInfo>
+    </ds:Signature>
+  </Assertion>
+</samlp:Response>
+      XML
+      subject = described_class.new(xml)
+      expect(subject).to be_signed
+    end
+
+    it 'returns true when the Response is signed' do
+      xml = <<-XML
+<?xml version="1.0"?>
+<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="_#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
+  <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+    <ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+      <ds:Reference URI="#_#{id}">
+        <ds:Transforms>
+          <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
+          <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+        </ds:Transforms>
+        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+        <ds:DigestValue></ds:DigestValue>
+      </ds:Reference>
+    </ds:SignedInfo>
+    <ds:SignatureValue></ds:SignatureValue>
+    <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+      <ds:X509Data>
+        <ds:X509Certificate></ds:X509Certificate>
+      </ds:X509Data>
+    </KeyInfo>
+  </ds:Signature>
+  <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="_#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
+</samlp:Response>
+      XML
+      subject = described_class.new(xml)
+      expect(subject).to be_signed
+    end
+
+    it 'returns false when there is no signature' do
+      xml = <<-XML
+<?xml version="1.0"?>
+<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="_#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
+  <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="_#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
+</samlp:Response>
+      XML
+      subject = described_class.new(xml)
+      expect(subject).to_not be_signed
+    end
+  end
+
+  describe "#certificate" do
+    let(:now) { Time.now.utc }
+    let(:id) { SecureRandom.uuid }
+    let(:url) { FFaker::Internet.uri("https") }
+    let(:certificate) { FFaker::Movie.title }
+
+    it 'returns the certificate when the Assertion is signed' do
+      xml = <<-XML
+<?xml version="1.0"?>
+<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="_#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
+  <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="_#{id}" IssueInstant="#{now.iso8601}" Version="2.0">
+    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+      <ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+        <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+        <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+        <ds:Reference URI="#_#{id}">
+          <ds:Transforms>
+            <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
+            <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+          </ds:Transforms>
+          <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+          <ds:DigestValue></ds:DigestValue>
+        </ds:Reference>
+      </ds:SignedInfo>
+      <ds:SignatureValue></ds:SignatureValue>
+      <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+        <ds:X509Data>
+          <ds:X509Certificate>#{certificate}</ds:X509Certificate>
+        </ds:X509Data>
+      </KeyInfo>
+    </ds:Signature>
+  </Assertion>
+</samlp:Response>
+      XML
+      subject = described_class.new(xml)
+      expect(subject.certificate).to eql(certificate)
+    end
+
+    it 'returns the certificate when the Response is signed' do
+      xml = <<-XML
+<?xml version="1.0"?>
+<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="_#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
+  <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+    <ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+      <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+      <ds:Reference URI="#_#{id}">
+        <ds:Transforms>
+          <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
+          <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
+        </ds:Transforms>
+        <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+        <ds:DigestValue></ds:DigestValue>
+      </ds:Reference>
+    </ds:SignedInfo>
+    <ds:SignatureValue></ds:SignatureValue>
+    <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
+      <ds:X509Data>
+        <ds:X509Certificate>#{certificate}</ds:X509Certificate>
+      </ds:X509Data>
+    </KeyInfo>
+  </ds:Signature>
+  <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="_#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
+</samlp:Response>
+      XML
+      subject = described_class.new(xml)
+      expect(subject.certificate).to eql(certificate)
+    end
+
+    it 'returns nil when there is no signature' do
+      xml = <<-XML
+<?xml version="1.0"?>
+<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="_#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{url}" Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified" InResponseTo="_#{SecureRandom.uuid}">
+  <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="_#{id}" IssueInstant="#{now.iso8601}" Version="2.0"></Assertion>
+</samlp:Response>
+      XML
+      subject = described_class.new(xml)
+      expect(subject.certificate).to be_nil
+    end
   end
 
   describe described_class::Builder do
     subject { described_class.new(user, request) }
     let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: []) }
-    let(:request) { double(:request, id: SecureRandom.uuid, acs_url: FFaker::Internet.http_url, provider: nil, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: FFaker::Internet.http_url) }
+    let(:request) { double(:request, id: "_#{SecureRandom.uuid}", acs_url: FFaker::Internet.http_url, provider: provider, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: issuer, signed?: true, trusted?: true) }
+    let(:provider) { double(want_assertions_signed: false, encryption_certificates: [{ text: encryption_pem }]) }
+    let(:encryption_pem) do
+      Saml::Kit.configuration.stripped_encryption_certificate
+    end
+    let(:issuer) { FFaker::Internet.uri("https") }
+
+    before :each do
+      allow(Saml::Kit.configuration).to receive(:issuer).and_return(issuer)
+    end
 
     describe "#build" do
       it 'builds a response with the request_id' do
         expect(subject.build.request_id).to eql(request.id)
       end
+
+      it 'builds a valid encrypted assertion' do
+        allow(Saml::Kit.configuration.registry).to receive(:metadata_for).with(issuer).and_return(provider)
+        allow(provider).to receive(:matches?).and_return(true)
+
+        subject.sign = true
+        subject.encrypt = true
+        result = subject.build
+        expect(result).to be_valid
+      end
+    end
+
+    describe "#to_xml" do
+      it 'generates an EncryptedAssertion' do
+        subject.encrypt = true
+        result = Hash.from_xml(subject.to_xml)
+        expect(result['Response']['EncryptedAssertion']).to be_present
+        encrypted_assertion = result['Response']['EncryptedAssertion']
+        decrypted_assertion = Saml::Kit::Cryptography.new.decrypt(encrypted_assertion)
+        decrypted_hash = Hash.from_xml(decrypted_assertion)
+        expect(decrypted_hash['Assertion']).to be_present
+        expect(decrypted_hash['Assertion']['Issuer']).to be_present
+        expect(decrypted_hash['Assertion']['Subject']).to be_present
+        expect(decrypted_hash['Assertion']['Subject']['NameID']).to be_present
+        expect(decrypted_hash['Assertion']['Subject']['SubjectConfirmation']).to be_present
+        expect(decrypted_hash['Assertion']['Conditions']).to be_present
+        expect(decrypted_hash['Assertion']['Conditions']['AudienceRestriction']).to be_present
+        expect(decrypted_hash['Assertion']['AuthnStatement']).to be_present
+        expect(decrypted_hash['Assertion']['AuthnStatement']['AuthnContext']).to be_present
+        expect(decrypted_hash['Assertion']['AuthnStatement']['AuthnContext']['AuthnContextClassRef']).to be_present
+      end
+    end
+  end
+
+  describe "encrypted assertion" do
+    let(:id) { SecureRandom.uuid }
+    let(:now) { Time.now.utc }
+    let(:acs_url) { FFaker::Internet.uri("https") }
+    let(:password) { FFaker::Movie.title }
+    let(:assertion) do
+      FFaker::Movie.title
+      <<-XML
+<Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion" ID="_11d39a7f-1b86-43ed-90d7-68090a857ca8" IssueInstant="2017-11-23T04:33:58Z" Version="2.0">
+ <Issuer>#{FFaker::Internet.uri("https")}</Issuer>
+ <Subject>
+   <NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">fdddf7ad-c4a4-443c-b96d-c953913b7b4e</NameID>
+   <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
+     <SubjectConfirmationData InResponseTo="cc8c4131-9336-4d1a-82f2-4ad92abeee22" NotOnOrAfter="2017-11-23T07:33:58Z" Recipient="https://westyundt.ca/acs"/>
+   </SubjectConfirmation>
+ </Subject>
+ <Conditions NotBefore="2017-11-23T04:33:58Z" NotOnOrAfter="2017-11-23T07:33:58Z">
+   <AudienceRestriction>
+     <Audience>American Wolves</Audience>
+   </AudienceRestriction>
+ </Conditions>
+ <AuthnStatement AuthnInstant="2017-11-23T04:33:58Z" SessionIndex="_11d39a7f-1b86-43ed-90d7-68090a857ca8" SessionNotOnOrAfter="2017-11-23T07:33:58Z">
+   <AuthnContext>
+     <AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</AuthnContextClassRef>
+   </AuthnContext>
+ </AuthnStatement>
+ <AttributeStatement>
+   <Attribute Name="email" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="email">
+     <AttributeValue>sidney_bayer@nienowemmerich.com</AttributeValue>
+   </Attribute>
+   <Attribute Name="created_at" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri" FriendlyName="created_at">
+     <AttributeValue>2017-11-23T04:33:58Z</AttributeValue>
+   </Attribute>
+ </AttributeStatement>
+</Assertion>
+XML
+    end
+
+    it 'parses the encrypted assertion' do
+      certificate_pem, private_key_pem = Saml::Kit::SelfSignedCertificate.new(password).create
+      public_key = OpenSSL::X509::Certificate.new(certificate_pem).public_key
+      private_key = OpenSSL::PKey::RSA.new(private_key_pem, password)
+
+      allow(Saml::Kit.configuration).to receive(:encryption_private_key).and_return(private_key)
+
+      cipher = OpenSSL::Cipher.new('AES-128-CBC')
+      cipher.encrypt
+      key = cipher.random_key
+      iv = cipher.random_iv
+      encrypted = cipher.update(assertion) + cipher.final
+
+      xml = <<-XML
+<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" xmlns:saml="#{Saml::Kit::Namespaces::ASSERTION}" ID="_#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{acs_url}" InResponseTo="_#{SecureRandom.uuid}">
+  <saml:Issuer>#{FFaker::Internet.uri("https")}</saml:Issuer>
+  <samlp:Status>
+    <samlp:StatusCode Value="#{Saml::Kit::Namespaces::SUCCESS}"/>
+  </samlp:Status>
+  <saml:EncryptedAssertion>
+    <xenc:EncryptedData xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
+    <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
+    <dsig:KeyInfo xmlns:dsig="http://www.w3.org/2000/09/xmldsig#">
+      <xenc:EncryptedKey>
+        <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
+        <xenc:CipherData>
+          <xenc:CipherValue>#{Base64.encode64(public_key.public_encrypt(key))}</xenc:CipherValue>
+        </xenc:CipherData>
+      </xenc:EncryptedKey>
+    </dsig:KeyInfo>
+    <xenc:CipherData>
+      <xenc:CipherValue>#{Base64.encode64(iv + encrypted)}</xenc:CipherValue>
+    </xenc:CipherData>
+    </xenc:EncryptedData>
+  </saml:EncryptedAssertion>
+</samlp:Response>
+XML
+
+      subject = described_class.new(xml)
+      expect(subject).to be_encrypted
+      expect(subject.attributes).to be_present
     end
   end
 end
saml-kit/spec/saml/service_provider_metadata_spec.rb
@@ -1,11 +1,11 @@
 require 'spec_helper'
 
 RSpec.describe Saml::Kit::ServiceProviderMetadata do
-  let(:entity_id) { FFaker::Internet.http_url }
-  let(:acs_post_url) { FFaker::Internet.http_url }
-  let(:acs_redirect_url) { FFaker::Internet.http_url }
-  let(:logout_post_url) { FFaker::Internet.http_url }
-  let(:logout_redirect_url) { FFaker::Internet.http_url }
+  let(:entity_id) { FFaker::Internet.uri("https") }
+  let(:acs_post_url) { FFaker::Internet.uri("https") }
+  let(:acs_redirect_url) { FFaker::Internet.uri("https") }
+  let(:logout_post_url) { FFaker::Internet.uri("https") }
+  let(:logout_redirect_url) { FFaker::Internet.uri("https") }
 
   describe described_class::Builder do
     let(:acs_url) { FFaker::Internet.http_url }
@@ -26,7 +26,7 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
     XML
     it 'builds the service provider metadata' do
       subject.entity_id = entity_id
-      subject.add_assertion_consumer_service(acs_url, binding: :post)
+      subject.add_assertion_consumer_service(acs_url, binding: :http_post)
       subject.name_id_formats = [
         Saml::Kit::Namespaces::PERSISTENT,
         Saml::Kit::Namespaces::TRANSIENT,
@@ -50,8 +50,11 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
       expect(result['EntityDescriptor']['SPSSODescriptor']['AssertionConsumerService']['isDefault']).to eql('true')
       expect(result['EntityDescriptor']['SPSSODescriptor']['AssertionConsumerService']['index']).to eql('0')
       expect(result['EntityDescriptor']['Signature']).to be_present
-      expect(result['EntityDescriptor']['SPSSODescriptor']['KeyDescriptor']['use']).to eql("signing")
-      expect(result['EntityDescriptor']['SPSSODescriptor']['KeyDescriptor']['KeyInfo']['X509Data']['X509Certificate']).to eql(Saml::Kit.configuration.stripped_signing_certificate)
+      expect(result['EntityDescriptor']['SPSSODescriptor']['KeyDescriptor'].map { |x| x['use'] }).to match_array(['signing', 'encryption'])
+      expect(result['EntityDescriptor']['SPSSODescriptor']['KeyDescriptor'].map { |x| x['KeyInfo']['X509Data']['X509Certificate'] }).to match_array([
+        Saml::Kit.configuration.stripped_signing_certificate,
+        Saml::Kit.configuration.stripped_encryption_certificate,
+      ])
     end
   end
 
@@ -59,35 +62,41 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
     let(:builder) { described_class::Builder.new }
     subject do
       builder.entity_id = entity_id
-      builder.add_assertion_consumer_service(acs_post_url, binding: :post)
+      builder.add_assertion_consumer_service(acs_post_url, binding: :http_post)
       builder.add_assertion_consumer_service(acs_redirect_url, binding: :http_redirect)
-      builder.add_single_logout_service(logout_post_url, binding: :post)
+      builder.add_single_logout_service(logout_post_url, binding: :http_post)
       builder.add_single_logout_service(logout_redirect_url, binding: :http_redirect)
       builder.build
     end
 
     it 'returns each of the certificates' do
       expected_sha256 = OpenSSL::Digest::SHA256.new.hexdigest(Saml::Kit.configuration.signing_x509.to_der)
+      expected_encryption_sha256 = OpenSSL::Digest::SHA256.new.hexdigest(Saml::Kit.configuration.encryption_x509.to_der)
       expect(subject.certificates).to match_array([
         {
           fingerprint: expected_sha256.upcase.scan(/../).join(":"),
           use: :signing,
           text: Saml::Kit.configuration.stripped_signing_certificate
-        }
+        },
+        {
+          fingerprint: expected_encryption_sha256.upcase.scan(/../).join(":"),
+          use: :encryption,
+          text: Saml::Kit.configuration.stripped_encryption_certificate
+        },
       ])
     end
 
     it 'returns each acs url and binding' do
       expect(subject.assertion_consumer_services.map(&:to_h)).to match_array([
-        { location: acs_post_url, binding: Saml::Kit::Namespaces::POST },
-        { location: acs_redirect_url, binding: Saml::Kit::Namespaces::HTTP_REDIRECT },
+        { location: acs_post_url, binding: Saml::Kit::Bindings::HTTP_POST },
+        { location: acs_redirect_url, binding: Saml::Kit::Bindings::HTTP_REDIRECT },
       ])
     end
 
     it 'returns each logout url and binding' do
       expect(subject.single_logout_services.map(&:to_h)).to match_array([
-        { location: logout_post_url, binding: Saml::Kit::Namespaces::POST },
-        { location: logout_redirect_url, binding: Saml::Kit::Namespaces::HTTP_REDIRECT },
+        { location: logout_post_url, binding: Saml::Kit::Bindings::HTTP_POST },
+        { location: logout_redirect_url, binding: Saml::Kit::Bindings::HTTP_REDIRECT },
       ])
     end
 
@@ -106,9 +115,9 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
     let(:service_provider_metadata) do
       builder = described_class::Builder.new
       builder.entity_id = entity_id
-      builder.add_assertion_consumer_service(acs_post_url, binding: :post)
+      builder.add_assertion_consumer_service(acs_post_url, binding: :http_post)
       builder.add_assertion_consumer_service(acs_redirect_url, binding: :http_redirect)
-      builder.add_single_logout_service(logout_post_url, binding: :post)
+      builder.add_single_logout_service(logout_post_url, binding: :http_post)
       builder.add_single_logout_service(logout_redirect_url, binding: :http_redirect)
       builder.to_xml
     end
@@ -149,6 +158,19 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
       expect(subject).to be_invalid
       expect(subject.errors[:base]).to include("invalid signature.")
     end
+
+    it 'is invalid when 0 ACS endpoints are specified' do
+      xml = <<-XML
+<?xml version="1.0" encoding="UTF-8"?>
+<EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="_#{SecureRandom.uuid}" entityID="#{entity_id}">
+  <SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
+    <SingleLogoutService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}"/>
+    <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
+  </SPSSODescriptor>
+</EntityDescriptor>
+      XML
+      expect(described_class.new(xml)).to be_invalid
+    end
   end
 
   describe "#matches?" do
saml-kit/spec/saml/signature_spec.rb
@@ -1,7 +1,6 @@
 require "spec_helper"
 
 RSpec.describe Saml::Kit::Signature do
-  subject { described_class.new(reference_id, configuration: configuration) }
   let(:configuration) do
     config = Saml::Kit::Configuration.new
     config.signing_certificate_pem = certificate
@@ -32,17 +31,18 @@ RSpec.describe Saml::Kit::Signature do
   let(:password) { "password" }
 
   it 'generates a signature' do
-    xml = ::Builder::XmlMarkup.new
     options = {
       "xmlns:samlp" => "urn:oasis:names:tc:SAML:2.0:protocol",
       "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion",
       ID: "_#{reference_id}",
     }
-    xml.tag!('samlp:AuthnRequest', options) do
-      subject.template(xml)
-      xml.tag!('saml:Issuer', "MyEntityID")
+    signed_xml = described_class.sign(sign: true, configuration: configuration) do |xml, signature|
+      xml.tag!('samlp:AuthnRequest', options) do
+        signature.template(reference_id)
+        xml.tag!('saml:Issuer', "MyEntityID")
+      end
     end
-    result = Hash.from_xml(subject.finalize(xml))
+    result = Hash.from_xml(signed_xml)
 
     signature = result["AuthnRequest"]["Signature"]
     expect(signature['xmlns']).to eql("http://www.w3.org/2000/09/xmldsig#")
@@ -63,13 +63,13 @@ RSpec.describe Saml::Kit::Signature do
   end
 
   it 'does not add a signature' do
-    subject = described_class.new(reference_id, sign: false, configuration: configuration)
-    xml = ::Builder::XmlMarkup.new
-    xml.AuthnRequest do
-      subject.template(xml)
-      xml.Issuer "MyEntityID"
+    signed_xml = described_class.sign(sign: false, configuration: configuration) do |xml, signature|
+      xml.AuthnRequest do
+        signature.template(reference_id)
+        xml.Issuer "MyEntityID"
+      end
     end
-    result = Hash.from_xml(subject.finalize(xml))
+    result = Hash.from_xml(signed_xml)
     expect(result['AuthnRequest']).to be_present
     expect(result["AuthnRequest"]["Signature"]).to be_nil
   end
saml-kit/spec/saml/xml_spec.rb
@@ -8,9 +8,9 @@ RSpec.describe Saml::Kit::Xml do
     let(:signed_xml) do
       builder = Saml::Kit::ServiceProviderMetadata::Builder.new
       builder.entity_id = FFaker::Movie.title
-      builder.add_assertion_consumer_service(login_url, binding: :post)
+      builder.add_assertion_consumer_service(login_url, binding: :http_post)
       builder.add_assertion_consumer_service(login_url, binding: :http_redirect)
-      builder.add_single_logout_service(logout_url, binding: :post)
+      builder.add_single_logout_service(logout_url, binding: :http_post)
       builder.add_single_logout_service(logout_url, binding: :http_redirect)
       builder.to_xml
     end
saml-kit/spec/spec_helper.rb
@@ -4,7 +4,7 @@ require "active_support/testing/time_helpers"
 require "ffaker"
 require "webmock/rspec"
 
-Saml::Kit.configuration.logger.level = :fatal
+Saml::Kit.configuration.logger.level = Logger::FATAL
 
 Dir[File.join(Dir.pwd, 'spec/support/**/*.rb')].each { |f| require f }
 RSpec.configure do |config|
saml-kit/saml-kit.gemspec
@@ -15,15 +15,6 @@ Gem::Specification.new do |spec|
   spec.license       = "MIT"
   spec.required_ruby_version = '>= 2.2.0'
 
-  # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
-  # to allow pushing to a single host or delete this section to allow pushing to any host.
-  if spec.respond_to?(:metadata)
-    spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
-  else
-    raise "RubyGems 2.0 or newer is required to protect against " \
-      "public gem pushes."
-  end
-
   spec.files         = `git ls-files -z`.split("\x0").reject do |f|
     f.match(%r{^(test|spec|features)/})
   end
@@ -31,8 +22,8 @@ Gem::Specification.new do |spec|
   spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
   spec.require_paths = ["lib"]
 
-  spec.add_dependency "activemodel", "~> 5.1"
-  spec.add_dependency "activesupport", "~> 5.1"
+  spec.add_dependency "activemodel", ">= 4.2.0"
+  spec.add_dependency "activesupport", ">= 4.2.0"
   spec.add_dependency "builder", "~> 3.2"
   spec.add_dependency "nokogiri", "~> 1.8"
   spec.add_dependency "xmldsig", "~> 0.6"