Comparing changes
v0.1.0
→
v0.2.0
67 commits
93 files changed
Commits
Changed files (93)
airport
app
assets
stylesheets
controllers
models
views
application
assertions
dashboard
layouts
registrations
sessions
config
initializers
db
spec
controllers
fixtures
models
proof
app
assets
stylesheets
views
application
layouts
saml-kit
lib
saml
kit
locales
spec
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"