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