Comparing changes
v1.0.6
→
v1.0.7
49 commits
92 files changed
Commits
172a4ac
reproduce defect where signature is not parsed from encrypted assertion.
2018-02-15 20:03:58
Changed files (92)
lib
saml
kit
bindings
builders
locales
rspec
spec
saml
kit
builders
support
bin/cibuild
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+# script/cibuild: Setup environment for CI to run tests. This is primarily
+# designed to run on the continuous integration server.
+
+set -e
+
+cd "$(dirname "$0")/.."
+
+echo "Started at…"
+date "+%H:%M:%S"
+
+# GC customizations
+export RUBY_GC_MALLOC_LIMIT=79000000
+export RUBY_GC_HEAP_INIT_SLOTS=800000
+export RUBY_HEAP_FREE_MIN=100000
+export RUBY_HEAP_SLOTS_INCREMENT=400000
+export RUBY_HEAP_SLOTS_GROWTH_FACTOR=1
+
+gem install bundler --no-ri --no-rdoc --conservative
+# run tests
+ruby -v
+bin/test
bin/console
@@ -1,7 +1,7 @@
#!/usr/bin/env ruby
-require "bundler/setup"
-require "saml/kit"
+require 'bundler/setup'
+require 'saml/kit'
# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.
@@ -10,5 +10,5 @@ require "saml/kit"
# require "pry"
# Pry.start
-require "irb"
+require 'irb'
IRB.start(__FILE__)
bin/lint
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+set -e
+
+[ -z "$DEBUG" ] || set -x
+
+echo "==> Running setup…"
+date "+%H:%M:%S"
+bin/setup
+
+echo "==> Running linters…"
+date "+%H:%M:%S"
+bundle exec rake rubocop
bin/setup
@@ -3,6 +3,6 @@ set -euo pipefail
IFS=$'\n\t'
set -vx
-bundle install
+bundle check || bundle install --jobs $(nproc)
# Do any other automated setup that you need to do here
bin/test
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+# script/test: Run test suite for application. Optionally pass in a path to an
+# individual test file to run a single test.
+
+
+set -e
+
+cd "$(dirname "$0")/.."
+
+[ -z "$DEBUG" ] || set -x
+
+echo "==> Running setup…"
+date "+%H:%M:%S"
+bin/setup
+
+echo "==> Running tests…"
+date "+%H:%M:%S"
+bundle exec rake spec
exe/saml-kit-create-self-signed-certificate
@@ -3,22 +3,22 @@ require 'saml/kit'
Saml::Kit.deprecate("Use the 'saml-kit-cli' gem instead. saml-kit-create-self-signed-certificate")
-puts "Enter Passphrase:"
+puts 'Enter Passphrase:'
passphrase = STDIN.read.strip
certificate, private_key = ::Xml::Kit::SelfSignedCertificate.new.create(passphrase: passphrase)
-puts "** BEGIN File Format **"
+puts '** BEGIN File Format **'
print certificate
puts private_key
-puts "***********************"
+puts '***********************'
puts
-puts "*** BEGIN ENV Format **"
+puts '*** BEGIN ENV Format **'
puts certificate.inspect
puts private_key.inspect
-puts "***********************"
+puts '***********************'
puts
-puts "Private Key Passphrase:"
+puts 'Private Key Passphrase:'
puts passphrase.inspect
exe/saml-kit-decode-http-redirect
@@ -6,9 +6,13 @@ Saml::Kit.deprecate("Use the 'saml-kit-cli' gem instead. saml-kit-decode-http-re
input = STDIN.read
binding = Saml::Kit::Bindings::HttpRedirect.new(location: '')
-uri = URI.parse(input) rescue nil
+uri = begin
+ URI.parse(input)
+ rescue StandardError
+ nil
+ end
if uri
- query_params = Hash[uri.query.split('&').map { |x| x.split('=', 2) }]
+ query_params = Hash[uri.query.split('&').map { |x| x.split('=', 2) }]
puts binding.deserialize(query_params).to_xml(pretty: true)
else
puts binding.deserialize('SAMLRequest' => input).to_xml(pretty: true)
lib/saml/kit/bindings/binding.rb
@@ -14,12 +14,12 @@ module Saml
binding == other
end
- def serialize(builder, relay_state: nil)
+ def serialize(*)
[]
end
- def deserialize(params)
- raise ArgumentError.new("Unsupported binding")
+ def deserialize(_params)
+ raise ArgumentError, 'Unsupported binding'
end
def to_h
@@ -27,7 +27,7 @@ module Saml
end
def ==(other)
- self.to_s == other.to_s
+ to_s == other.to_s
end
def eql?(other)
@@ -58,7 +58,7 @@ module Saml
elsif parameters[:SAMLResponse].present?
parameters[:SAMLResponse]
else
- raise ArgumentError.new("SAMLRequest or SAMLResponse parameter is required.")
+ raise ArgumentError, 'SAMLRequest or SAMLResponse parameter is required.'
end
end
end
lib/saml/kit/bindings/http_redirect.rb
@@ -17,7 +17,7 @@ module Saml
end
def deserialize(params, configuration: Saml::Kit.configuration)
- parameters = normalize(params)
+ parameters = normalize(params_to_hash(params))
document = deserialize_document_from!(parameters, configuration)
ensure_valid_signature!(parameters, document)
document
@@ -35,25 +35,25 @@ module Saml
return if document.provider.nil?
if document.provider.verify(
- algorithm_for(params[:SigAlg]),
- decode(params[:Signature]),
- canonicalize(params)
+ algorithm_for(params[:SigAlg]),
+ decode(params[:Signature]),
+ canonicalize(params)
)
document.signature_verified!
else
- raise ArgumentError.new("Invalid Signature")
+ raise ArgumentError, 'Invalid Signature'
end
end
def canonicalize(params)
- [:SAMLRequest, :SAMLResponse, :RelayState, :SigAlg].map do |key|
+ %i[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
+ case algorithm =~ /(rsa-)?sha(.*?)$/i && Regexp.last_match(2).to_i
when 256
OpenSSL::Digest::SHA256.new
when 384
@@ -74,6 +74,11 @@ module Saml
SigAlg: params['SigAlg'] || params[:SigAlg],
}
end
+
+ def params_to_hash(value)
+ return value unless value.is_a?(String)
+ Hash[URI.parse(value).query.split('&').map { |x| x.split('=', 2) }]
+ end
end
end
end
lib/saml/kit/bindings/url_builder.rb
@@ -17,7 +17,7 @@ module Saml
else
payload = to_query_string(
saml_document.query_string_parameter => serialize(saml_document.to_xml),
- 'RelayState' => relay_state,
+ 'RelayState' => relay_state
)
"#{saml_document.destination}?#{payload}"
end
@@ -34,7 +34,7 @@ module Saml
to_query_string(
saml_document.query_string_parameter => serialize(saml_document.to_xml),
'RelayState' => relay_state,
- 'SigAlg' => ::Xml::Kit::Namespaces::SHA256,
+ 'SigAlg' => ::Xml::Kit::Namespaces::SHA256
)
end
lib/saml/kit/builders/templates/assertion.builder
@@ -4,7 +4,7 @@ xml.Assertion(assertion_options) do
xml.Subject do
xml.NameID name_id, Format: name_id_format
xml.SubjectConfirmation Method: Saml::Kit::Namespaces::BEARER do
- xml.SubjectConfirmationData "", subject_confirmation_data_options
+ xml.SubjectConfirmationData '', subject_confirmation_data_options
end
end
xml.Conditions conditions_options do
lib/saml/kit/builders/templates/metadata.builder
@@ -4,11 +4,11 @@ xml.EntityDescriptor entity_descriptor_options do
render identity_provider, xml: xml
render service_provider, xml: xml
xml.Organization do
- xml.OrganizationName organization_name, 'xml:lang': "en"
- xml.OrganizationDisplayName organization_name, 'xml:lang': "en"
- xml.OrganizationURL organization_url, 'xml:lang': "en"
+ xml.OrganizationName organization_name, 'xml:lang': 'en'
+ xml.OrganizationDisplayName organization_name, 'xml:lang': 'en'
+ xml.OrganizationURL organization_url, 'xml:lang': 'en'
end
- xml.ContactPerson contactType: "technical" do
+ xml.ContactPerson contactType: 'technical' do
xml.Company "mailto:#{contact_email}"
end
end
lib/saml/kit/builders/templates/service_provider_metadata.builder
@@ -12,6 +12,6 @@ xml.SPSSODescriptor descriptor_options do
xml.NameIDFormat format
end
acs_urls.each_with_index do |item, index|
- xml.AssertionConsumerService Binding: item[:binding], Location: item[:location], index: index, isDefault: index == 0 ? true : false
+ xml.AssertionConsumerService Binding: item[:binding], Location: item[:location], index: index, isDefault: index.zero?
end
end
lib/saml/kit/builders/assertion.rb
@@ -25,6 +25,10 @@ module Saml
user.assertion_attributes_for(request)
end
+ def signing_key_pair
+ super || @response_builder.signing_key_pair
+ end
+
private
def assertion_options
lib/saml/kit/builders/authentication_request.rb
@@ -15,7 +15,7 @@ module Saml
@issuer = configuration.entity_id
@name_id_format = Namespaces::PERSISTENT
@now = Time.now.utc
- @version = "2.0"
+ @version = '2.0'
end
def build
@@ -26,8 +26,8 @@ module Saml
def request_options
options = {
- "xmlns:samlp" => Namespaces::PROTOCOL,
- "xmlns:saml" => Namespaces::ASSERTION,
+ 'xmlns:samlp' => Namespaces::PROTOCOL,
+ 'xmlns:saml' => Namespaces::ASSERTION,
ID: id,
Version: version,
IssueInstant: now.utc.iso8601,
lib/saml/kit/builders/logout_request.rb
@@ -16,7 +16,7 @@ module Saml
@issuer = configuration.entity_id
@name_id_format = Saml::Kit::Namespaces::PERSISTENT
@now = Time.now.utc
- @version = "2.0"
+ @version = '2.0'
end
def build
lib/saml/kit/builders/logout_response.rb
@@ -16,7 +16,7 @@ module Saml
@now = Time.now.utc
@request = request
@status_code = Namespaces::SUCCESS
- @version = "2.0"
+ @version = '2.0'
end
def build
lib/saml/kit/builders/response.rb
@@ -17,9 +17,10 @@ module Saml
@id = ::Xml::Kit::Id.generate
@reference_id = ::Xml::Kit::Id.generate
@now = Time.now.utc
- @version = "2.0"
+ @version = '2.0'
@status_code = Namespaces::SUCCESS
@issuer = configuration.entity_id
+ @encryption_certificate = request.try(:provider).try(:encryption_certificates).try(:last)
@encrypt = encryption_certificate.present?
@configuration = configuration
end
@@ -28,13 +29,6 @@ module Saml
Saml::Kit::Response.new(to_xml, request_id: request.id, configuration: configuration)
end
- def encryption_certificate
- request.provider.encryption_certificates.first
- rescue => error
- Saml::Kit.logger.error(error)
- nil
- end
-
def assertion
@assertion ||=
begin
lib/saml/kit/locales/en.yml
@@ -3,12 +3,13 @@ en:
saml/kit:
errors:
Assertion:
+ cannot_decrypt: "cannot be decrypted."
expired: "must not be expired."
must_match_issuer: "must match entityId."
must_contain_single_assertion: "must contain single Assertion."
AuthnRequest:
invalid: "must contain AuthnRequest."
- invalid_fingerprint: "does not match."
+ invalid_fingerprint: "is not registered."
unregistered: "is unregistered."
IDPSSODescriptor:
invalid: "must contain IDPSSODescriptor."
@@ -16,16 +17,24 @@ en:
InvalidDocument:
invalid: "must contain valid SAMLRequest"
LogoutRequest:
- invalid_fingerprint: "does not match."
+ invalid_fingerprint: "is not registered."
unregistered: "is unregistered."
LogoutResponse:
unregistered: "is unregistered."
+ NullAssertion:
+ invalid: "is missing."
Response:
invalid: "must contain Response."
- invalid_fingerprint: "does not match."
+ invalid_fingerprint: "is not registered."
invalid_response_to: "must match request id."
invalid_version: "must be 2.0."
unregistered: "must originate from registered identity provider."
+ must_contain_single_assertion: "must contain a single Assertion."
+ Signature:
+ certificate: "Not valid before %{not_before}. Not valid after %{not_after}."
+ digest_value: "is invalid."
+ empty: "is missing."
+ signature: "is invalid."
SPSSODescriptor:
invalid: "must contain SPSSODescriptor."
invalid_signature: "invalid signature."
lib/saml/kit/rspec/have_query_param.rb
@@ -6,7 +6,7 @@ RSpec::Matchers.define :have_query_param do |key|
end
def query_params_from(url)
- Hash[query_for(url).split("&").map { |x| x.split('=', 2) }]
+ Hash[query_for(url).split('&').map { |x| x.split('=', 2) }]
end
def uri_for(url)
lib/saml/kit/assertion.rb
@@ -3,17 +3,29 @@ module Saml
class Assertion
include ActiveModel::Validations
include Translatable
-
- validate :must_match_issuer
- validate :must_be_active_session
+ XPATH = [
+ '/samlp:Response/saml:Assertion',
+ '/samlp:Response/saml:EncryptedAssertion'
+ ].join('|')
+
+ validate :must_be_decryptable
+ validate :must_match_issuer, if: :decryptable?
+ validate :must_be_active_session, if: :decryptable?
+ validate :must_have_valid_signature, if: :decryptable?
attr_reader :name
attr_accessor :occurred_at
- def initialize(xml_hash, configuration: Saml::Kit.configuration)
- @name = "Assertion"
- @xml_hash = xml_hash
+ def initialize(node, configuration: Saml::Kit.configuration, private_keys: [])
+ @name = 'Assertion'
+ @node = node
+ @xml_hash = hash_from(node)['Response'] || {}
@configuration = configuration
@occurred_at = Time.current
+ decrypt!(::Xml::Kit::Decryption.new(
+ private_keys: (
+ configuration.private_keys(use: :encryption) + private_keys
+ ).uniq
+ ))
end
def issuer
@@ -29,8 +41,7 @@ module Saml
end
def signature
- xml_hash = assertion.fetch('Signature', nil)
- xml_hash ? Signature.new(xml_hash) : nil
+ @signature ||= Signature.new(at_xpath('./ds:Signature'))
end
def expired?(now = occurred_at)
@@ -47,7 +58,7 @@ module Saml
begin
attrs = assertion.fetch('AttributeStatement', {}).fetch('Attribute', [])
items = if attrs.is_a? Hash
- [[attrs["Name"], attrs["AttributeValue"]]]
+ [[attrs['Name'], attrs['AttributeValue']]]
else
attrs.map { |item| [item['Name'], item['AttributeValue']] }
end
@@ -65,57 +76,88 @@ module Saml
def audiences
Array(assertion['Conditions']['AudienceRestriction']['Audience'])
- rescue => error
+ rescue StandardError => error
Saml::Kit.logger.error(error)
[]
end
def encrypted?
- @xml_hash.fetch('Response', {}).fetch('EncryptedAssertion', nil).present?
+ @xml_hash.fetch('EncryptedAssertion', nil).present?
+ end
+
+ def decryptable?
+ return true unless encrypted?
+ !@cannot_decrypt
end
def present?
assertion.present?
end
+ def to_xml(pretty: false)
+ pretty ? @node.to_xml(indent: 2) : @node.to_s
+ end
+
private
attr_reader :configuration
def assertion
@assertion ||=
- if encrypted?
- private_keys = configuration.private_keys(use: :encryption)
- decryptor = ::Xml::Kit::Decryption.new(private_keys: private_keys)
- decrypted = decryptor.decrypt_hash(@xml_hash['Response']['EncryptedAssertion'])
- Saml::Kit.logger.debug(decrypted)
- Hash.from_xml(decrypted)['Assertion']
- else
- result = @xml_hash.fetch('Response', {}).fetch('Assertion', {})
+ begin
+ result = (hash_from(@node)['Response'] || {})['Assertion']
return result if result.is_a?(Hash)
-
- errors[:assertion] << error_message(:must_contain_single_assertion)
{}
end
end
+ def decrypt!(decryptor)
+ return unless encrypted?
+
+ encrypted_assertion = @node.at_xpath('./xmlenc:EncryptedData', Saml::Kit::Document::NAMESPACES)
+ @node = decryptor.decrypt_node(encrypted_assertion)
+ rescue Xml::Kit::DecryptionError => error
+ @cannot_decrypt = true
+ Saml::Kit.logger.error(error)
+ end
+
def parse_date(value)
DateTime.parse(value)
- rescue => error
+ rescue StandardError => error
Saml::Kit.logger.error(error)
Time.at(0).to_datetime
end
def must_match_issuer
- unless audiences.include?(configuration.entity_id)
- errors[:audience] << error_message(:must_match_issuer)
- end
+ return if audiences.include?(configuration.entity_id)
+ errors[:audience] << error_message(:must_match_issuer)
end
def must_be_active_session
return if active?
errors[:base] << error_message(:expired)
end
+
+ def must_have_valid_signature
+ return if !signed? || signature.valid?
+
+ signature.errors.each do |attribute, message|
+ errors.add(attribute, message)
+ end
+ end
+
+ def must_be_decryptable
+ errors.add(:base, error_message(:cannot_decrypt)) unless decryptable?
+ end
+
+ def at_xpath(xpath)
+ @node.at_xpath(xpath, Saml::Kit::Document::NAMESPACES)
+ end
+
+ def hash_from(node)
+ return {} if node.nil?
+ Hash.from_xml(node.document.root.to_s) || {}
+ end
end
end
end
lib/saml/kit/authentication_request.rb
@@ -25,7 +25,7 @@ module Saml
# @param xml [String] the raw xml.
# @param configuration [Saml::Kit::Configuration] defaults to the global configuration.
def initialize(xml, configuration: Saml::Kit.configuration)
- super(xml, name: "AuthnRequest", configuration: configuration)
+ super(xml, name: 'AuthnRequest', configuration: configuration)
end
# Extract the AssertionConsumerServiceURL from the AuthnRequest
lib/saml/kit/bindings.rb
@@ -1,19 +1,19 @@
-require "saml/kit/bindings/binding"
-require "saml/kit/bindings/http_post"
-require "saml/kit/bindings/http_redirect"
-require "saml/kit/bindings/url_builder"
+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"
+ HTTP_ARTIFACT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact'.freeze
+ HTTP_POST = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'.freeze
+ HTTP_REDIRECT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'.freeze
ALL = {
http_post: HTTP_POST,
http_redirect: HTTP_REDIRECT,
http_artifact: HTTP_ARTIFACT,
- }
+ }.freeze
def self.binding_for(binding)
ALL[binding]
lib/saml/kit/buildable.rb
@@ -4,19 +4,19 @@ module Saml
extend ActiveSupport::Concern
class_methods do
- def build(*args) # :yields builder
+ def build(*args)
builder(*args) do |builder|
yield builder if block_given?
end.build
end
- def build_xml(*args) # :yields builder
+ def build_xml(*args)
builder(*args) do |builder|
yield builder if block_given?
end.to_xml
end
- def builder(*args) # :yields builder
+ def builder(*args)
builder_class.new(*args).tap do |builder|
yield builder if block_given?
end
lib/saml/kit/composite_metadata.rb
@@ -5,7 +5,7 @@ module Saml
attr_reader :service_provider, :identity_provider
def initialize(xml)
- super("IDPSSODescriptor", xml)
+ super('IDPSSODescriptor', xml)
@metadatum = [
Saml::Kit::ServiceProviderMetadata.new(xml),
Saml::Kit::IdentityProviderMetadata.new(xml),
@@ -13,10 +13,10 @@ module Saml
end
def services(type)
- xpath = map { |x| "//md:EntityDescriptor/md:#{x.name}/md:#{type}" }.join("|")
+ xpath = map { |x| "//md:EntityDescriptor/md:#{x.name}/md:#{type}" }.join('|')
document.find_all(xpath).map do |item|
- binding = item.attribute("Binding").value
- location = item.attribute("Location").value
+ binding = item.attribute('Binding').value
+ location = item.attribute('Location').value
Saml::Kit::Bindings.create_for(binding, location)
end
end
@@ -30,12 +30,16 @@ module Saml
end
def method_missing(name, *args)
- if target = find { |x| x.respond_to?(name) }
+ if (target = find { |x| x.respond_to?(name) })
target.public_send(name, *args)
else
super
end
end
+
+ def respond_to_missing?(method, *)
+ find { |x| x.respond_to?(method) }
+ end
end
end
end
lib/saml/kit/configuration.rb
@@ -20,7 +20,7 @@ module Saml
# configuration.add_key_pair(ENV["X509_CERTIFICATE"], ENV["PRIVATE_KEY"], passphrase: ENV['PRIVATE_KEY_PASSPHRASE'], use: :encryption)
# end
class Configuration
- USES = [:signing, :encryption]
+ USES = %i[signing encryption].freeze
# The issuer to use in requests or responses from this entity to use.
attr_accessor :entity_id
# The signature method to use when generating signatures (See {Saml::Kit::Builders::XmlSignature::SIGNATURE_METHODS})
@@ -36,7 +36,7 @@ module Saml
# The total allowable clock drift for session timeout validation.
attr_accessor :clock_drift
- def initialize # :yields configuration
+ def initialize
@clock_drift = 30.seconds
@digest_method = :SHA256
@key_pairs = []
@@ -85,7 +85,7 @@ module Saml
# Return each private for a specific use.
#
# @param use [Symbol] the type of key pair to return `nil`, `:signing` or `:encryption`
- def private_keys(use: :signing)
+ def private_keys(use: nil)
key_pairs(use: use).flat_map(&:private_key)
end
@@ -97,10 +97,10 @@ module Saml
private
def ensure_proper_use!(use)
- unless USES.include?(use)
- error_message = "Use must be either :signing or :encryption"
- raise ArgumentError.new(error_message)
- end
+ return if USES.include?(use)
+
+ error_message = 'Use must be either :signing or :encryption'
+ raise ArgumentError, error_message
end
end
end
lib/saml/kit/default_registry.rb
@@ -62,7 +62,7 @@ module Saml
# Yields each registered [Saml::Kit::Metadata] to the block.
def each
- @items.each do |key, value|
+ @items.each_value do |value|
yield value
end
end
lib/saml/kit/document.rb
@@ -1,19 +1,20 @@
module Saml
module Kit
class Document
- PROTOCOL_XSD = File.expand_path("./xsd/saml-schema-protocol-2.0.xsd", File.dirname(__FILE__)).freeze
+ include ActiveModel::Validations
+ include XsdValidatable
+ include Translatable
+ include Trustable
+ include Buildable
+ PROTOCOL_XSD = File.expand_path('./xsd/saml-schema-protocol-2.0.xsd', File.dirname(__FILE__)).freeze
NAMESPACES = {
"NameFormat": ::Saml::Kit::Namespaces::ATTR_SPLAT,
"ds": ::Xml::Kit::Namespaces::XMLDSIG,
"md": ::Saml::Kit::Namespaces::METADATA,
"saml": ::Saml::Kit::Namespaces::ASSERTION,
"samlp": ::Saml::Kit::Namespaces::PROTOCOL,
+ 'xmlenc' => ::Xml::Kit::Namespaces::XMLENC,
}.freeze
- include ActiveModel::Validations
- include XsdValidatable
- include Translatable
- include Trustable
- include Buildable
validates_presence_of :content
validates_presence_of :id
validate :must_match_xsd
@@ -60,13 +61,28 @@ module Saml
#
# @param pretty [Boolean] formats the xml or returns the raw xml.
def to_xml(pretty: false)
- pretty ? Nokogiri::XML(content).to_xml(indent: 2) : content
+ pretty ? to_nokogiri.to_xml(indent: 2) : content
end
- # Returns the SAML document as an XHTML string.
+ # Returns the SAML document as an XHTML string.
# This is useful for rendering in a web page.
def to_xhtml
- Nokogiri::XML(content, &:noblanks).to_xhtml
+ Nokogiri::XML(to_xml, &:noblanks).to_xhtml
+ end
+
+ # @!visibility private
+ def to_nokogiri
+ @nokogiri ||= Nokogiri::XML(content)
+ end
+
+ # @!visibility private
+ def at_xpath(xpath)
+ to_nokogiri.at_xpath(xpath, NAMESPACES)
+ end
+
+ # @!visibility private
+ def search(xpath)
+ to_nokogiri.search(xpath, NAMESPACES)
end
def to_s
@@ -75,11 +91,11 @@ module Saml
class << self
XPATH = [
- "/samlp:AuthnRequest",
- "/samlp:LogoutRequest",
- "/samlp:LogoutResponse",
- "/samlp:Response",
- ].join("|")
+ '/samlp:AuthnRequest',
+ '/samlp:LogoutRequest',
+ '/samlp:LogoutResponse',
+ '/samlp:Response',
+ ].join('|')
# Returns the raw xml as a Saml::Kit SAML document.
#
@@ -87,16 +103,16 @@ module Saml
# @param configuration [Saml::Kit::Configuration] the configuration to use for unpacking the document.
def to_saml_document(xml, configuration: Saml::Kit.configuration)
xml_document = ::Xml::Kit::Document.new(xml, namespaces: {
- "samlp": ::Saml::Kit::Namespaces::PROTOCOL
- })
+ "samlp": ::Saml::Kit::Namespaces::PROTOCOL
+ })
constructor = {
- "AuthnRequest" => Saml::Kit::AuthenticationRequest,
- "LogoutRequest" => Saml::Kit::LogoutRequest,
- "LogoutResponse" => Saml::Kit::LogoutResponse,
- "Response" => Saml::Kit::Response,
+ 'AuthnRequest' => Saml::Kit::AuthenticationRequest,
+ 'LogoutRequest' => Saml::Kit::LogoutRequest,
+ 'LogoutResponse' => Saml::Kit::LogoutResponse,
+ 'Response' => Saml::Kit::Response,
}[xml_document.find_by(XPATH).name] || InvalidDocument
constructor.new(xml, configuration: configuration)
- rescue => error
+ rescue StandardError => error
Saml::Kit.logger.error(error)
InvalidDocument.new(xml, configuration: configuration)
end
@@ -113,7 +129,7 @@ module Saml
when Saml::Kit::LogoutRequest.to_s
Saml::Kit::Builders::LogoutRequest
else
- raise ArgumentError.new("Unknown SAML Document #{name}")
+ raise ArgumentError, "Unknown SAML Document #{name}"
end
end
end
@@ -140,7 +156,7 @@ module Saml
def must_be_valid_version
return unless expected_type?
- return if "2.0" == version
+ return if version == '2.0'
errors[:version] << error_message(:invalid_version)
end
end
lib/saml/kit/identity_provider_metadata.rb
@@ -32,15 +32,15 @@ module Saml
# {include:file:spec/examples/identity_provider_metadata_spec.rb}
class IdentityProviderMetadata < Metadata
def initialize(xml)
- super("IDPSSODescriptor", xml)
+ super('IDPSSODescriptor', xml)
end
# Returns the IDPSSODescriptor/@WantAuthnRequestsSigned attribute.
def want_authn_requests_signed
xpath = "/md:EntityDescriptor/md:#{name}"
- attribute = document.find_by(xpath).attribute("WantAuthnRequestsSigned")
+ attribute = document.find_by(xpath).attribute('WantAuthnRequestsSigned')
return true if attribute.nil?
- attribute.text.downcase == "true"
+ attribute.text.casecmp('true').zero?
end
# Returns each of the SingleSignOnService elements.
@@ -59,8 +59,8 @@ module Saml
def attributes
document.find_all("/md:EntityDescriptor/md:#{name}/saml:Attribute").map do |item|
{
- format: item.attribute("NameFormat").try(:value),
- name: item.attribute("Name").value,
+ format: item.attribute('NameFormat').try(:value),
+ name: item.attribute('Name').value,
}
end
end
@@ -71,7 +71,7 @@ module Saml
# @param relay_state [Object] The RelayState to include the returned SAML params.
# @param configuration [Saml::Kit::Configuration] the configuration to use for generating the request.
# @return [Array] The url and saml params encoded using the rules for the specified binding.
- def login_request_for(binding:, relay_state: nil, configuration: Saml::Kit.configuration) # :yields builder
+ def login_request_for(binding:, relay_state: nil, configuration: Saml::Kit.configuration)
builder = Saml::Kit::AuthenticationRequest.builder(configuration: configuration) do |x|
x.embed_signature = want_authn_requests_signed
yield x if block_given?
lib/saml/kit/invalid_document.rb
@@ -7,12 +7,12 @@ module Saml
end
def initialize(xml, configuration: nil)
- super(xml, name: "InvalidDocument")
+ super(xml, name: 'InvalidDocument')
end
def to_h
super
- rescue
+ rescue StandardError
{}
end
end
lib/saml/kit/logout_request.rb
@@ -31,7 +31,7 @@ module Saml
# @param xml [String] The raw xml string.
# @param configuration [Saml::Kit::Configuration] the configuration to use.
def initialize(xml, configuration: Saml::Kit.configuration)
- super(xml, name: "LogoutRequest", configuration: configuration)
+ super(xml, name: 'LogoutRequest', configuration: configuration)
end
# Returns the NameID value.
lib/saml/kit/logout_response.rb
@@ -10,7 +10,7 @@ module Saml
def initialize(xml, request_id: nil, configuration: Saml::Kit.configuration)
@request_id = request_id
- super(xml, name: "LogoutResponse", configuration: configuration)
+ super(xml, name: 'LogoutResponse', configuration: configuration)
end
end
end
lib/saml/kit/metadata.rb
@@ -23,7 +23,11 @@ module Saml
# 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
+ include XsdValidatable
+ include Translatable
+ include Buildable
+ METADATA_XSD = File.expand_path('./xsd/saml-schema-metadata-2.0.xsd', File.dirname(__FILE__)).freeze
NAMESPACES = {
"NameFormat": Namespaces::ATTR_SPLAT,
"ds": ::Xml::Kit::Namespaces::XMLDSIG,
@@ -31,10 +35,6 @@ module Saml
"saml": Namespaces::ASSERTION,
"samlp": Namespaces::PROTOCOL,
}.freeze
- include ActiveModel::Validations
- include XsdValidatable
- include Translatable
- include Buildable
validates_presence_of :metadata
validate :must_contain_descriptor
@@ -50,7 +50,7 @@ module Saml
# Returns the /EntityDescriptor/@entityID
def entity_id
- document.find_by("/md:EntityDescriptor/@entityID").value
+ document.find_by('/md:EntityDescriptor/@entityID').value
end
# Returns the supported NameIDFormats.
@@ -60,23 +60,23 @@ module Saml
# Returns the Organization Name
def organization_name
- document.find_by("/md:EntityDescriptor/md:Organization/md:OrganizationName").try(:text)
+ document.find_by('/md:EntityDescriptor/md:Organization/md:OrganizationName').try(:text)
end
# Returns the Organization URL
def organization_url
- document.find_by("/md:EntityDescriptor/md:Organization/md:OrganizationURL").try(:text)
+ document.find_by('/md:EntityDescriptor/md:Organization/md:OrganizationURL').try(:text)
end
# Returns the Company
def contact_person_company
- document.find_by("/md:EntityDescriptor/md:ContactPerson/md:Company").try(:text)
+ document.find_by('/md:EntityDescriptor/md:ContactPerson/md:Company').try(:text)
end
# Returns each of the X509 certificates.
def certificates
@certificates ||= document.find_all("/md:EntityDescriptor/md:#{name}/md:KeyDescriptor").map do |item|
- cert = item.at_xpath("./ds:KeyInfo/ds:X509Data/ds:X509Certificate", NAMESPACES).text
+ cert = item.at_xpath('./ds:KeyInfo/ds:X509Data/ds:X509Certificate', NAMESPACES).text
attribute = item.attribute('use')
use = attribute.nil? ? nil : item.attribute('use').value
::Xml::Kit::Certificate.new(cert, use: use)
@@ -98,8 +98,8 @@ module Saml
# @param type [String] the type of service. .E.g. `AssertionConsumerServiceURL`
def services(type)
document.find_all("/md:EntityDescriptor/md:#{name}/md:#{type}").map do |item|
- binding = item.attribute("Binding").value
- location = item.attribute("Location").value
+ binding = item.attribute('Binding').value
+ location = item.attribute('Location').value
Saml::Kit::Bindings.create_for(binding, location)
end
end
@@ -179,25 +179,31 @@ module Saml
end
end
- # Creates a `{Saml::Kit::Metadata}` object from a raw XML [String].
- #
- # @param content [String] the raw metadata XML.
- # @return [Saml::Kit::Metadata] the metadata document or subclass.
- def self.from(content)
- hash = Hash.from_xml(content)
- entity_descriptor = hash["EntityDescriptor"]
- if entity_descriptor.key?("SPSSODescriptor") && entity_descriptor.key?("IDPSSODescriptor")
- Saml::Kit::CompositeMetadata.new(content)
- elsif entity_descriptor.keys.include?("SPSSODescriptor")
- Saml::Kit::ServiceProviderMetadata.new(content)
- elsif entity_descriptor.keys.include?("IDPSSODescriptor")
- Saml::Kit::IdentityProviderMetadata.new(content)
+ def signature
+ @signature ||= Signature.new(at_xpath('/md:EntityDescriptor/ds:Signature'))
+ end
+
+ class << self
+ # Creates a `{Saml::Kit::Metadata}` object from a raw XML [String].
+ #
+ # @param content [String] the raw metadata XML.
+ # @return [Saml::Kit::Metadata] the metadata document or subclass.
+ def from(content)
+ hash = Hash.from_xml(content)
+ entity_descriptor = hash['EntityDescriptor']
+ if entity_descriptor.key?('SPSSODescriptor') && entity_descriptor.key?('IDPSSODescriptor')
+ Saml::Kit::CompositeMetadata.new(content)
+ elsif entity_descriptor.keys.include?('SPSSODescriptor')
+ Saml::Kit::ServiceProviderMetadata.new(content)
+ elsif entity_descriptor.keys.include?('IDPSSODescriptor')
+ Saml::Kit::IdentityProviderMetadata.new(content)
+ end
end
- end
- # @!visibility private
- def self.builder_class
- Saml::Kit::Builders::Metadata
+ # @!visibility private
+ def builder_class
+ Saml::Kit::Builders::Metadata
+ end
end
private
@@ -208,6 +214,10 @@ module Saml
@document ||= ::Xml::Kit::Document.new(xml, namespaces: NAMESPACES)
end
+ def at_xpath(xpath)
+ document.find_by(xpath)
+ end
+
def metadata
document.find_by("/md:EntityDescriptor/md:#{name}").present?
end
@@ -221,20 +231,12 @@ module Saml
end
def must_have_valid_signature
- return if to_xml.blank?
-
- unless valid_signature?
- errors[:base] << error_message(:invalid_signature)
- end
- end
+ return unless signature.present?
- def valid_signature?
- xml = document
- result = xml.valid?
- xml.errors.each do |error|
- errors[:base] << error
+ signature.valid?
+ signature.errors.each do |attribute, error|
+ errors[attribute] << error
end
- result
end
end
end
lib/saml/kit/namespaces.rb
@@ -1,32 +1,32 @@
module Saml
module Kit
module Namespaces
- SAML_2_0 = "urn:oasis:names:tc:SAML:2.0"
- SAML_1_1 = "urn:oasis:names:tc:SAML:1.1"
- ATTR_NAME_FORMAT = "#{SAML_2_0}:attrname-format"
- NAME_ID_FORMAT_1_1 = "#{SAML_1_1}:nameid-format"
- NAME_ID_FORMAT_2_0 = "#{SAML_2_0}:nameid-format"
- STATUS = "#{SAML_2_0}:status"
+ SAML_2_0 = 'urn:oasis:names:tc:SAML:2.0'.freeze
+ SAML_1_1 = 'urn:oasis:names:tc:SAML:1.1'.freeze
+ ATTR_NAME_FORMAT = "#{SAML_2_0}:attrname-format".freeze
+ NAME_ID_FORMAT_1_1 = "#{SAML_1_1}:nameid-format".freeze
+ NAME_ID_FORMAT_2_0 = "#{SAML_2_0}:nameid-format".freeze
+ STATUS = "#{SAML_2_0}:status".freeze
- ASSERTION = "#{SAML_2_0}:assertion"
- ATTR_SPLAT = "#{ATTR_NAME_FORMAT}:*"
- BASIC = "#{ATTR_NAME_FORMAT}:basic"
- BEARER = "#{SAML_2_0}:cm:bearer"
- EMAIL_ADDRESS = "#{NAME_ID_FORMAT_1_1}:emailAddress"
- INVALID_NAME_ID_POLICY = "#{STATUS}:InvalidNameIDPolicy"
- METADATA = "#{SAML_2_0}:metadata"
- PASSWORD = "#{SAML_2_0}:ac:classes:Password"
- PASSWORD_PROTECTED = "#{SAML_2_0}:ac:classes:PasswordProtectedTransport"
- PERSISTENT = "#{NAME_ID_FORMAT_2_0}:persistent"
- PROTOCOL = "#{SAML_2_0}:protocol"
- REQUESTER_ERROR = "#{STATUS}:Requester"
- RESPONDER_ERROR = "#{STATUS}:Responder"
- SUCCESS = "#{STATUS}:Success"
- TRANSIENT = "#{NAME_ID_FORMAT_2_0}:transient"
- UNSPECIFIED = "#{SAML_2_0}:consent:unspecified"
- UNSPECIFIED_NAMEID = "#{NAME_ID_FORMAT_1_1}:unspecified"
- URI = "#{ATTR_NAME_FORMAT}:uri"
- VERSION_MISMATCH_ERROR = "#{STATUS}:VersionMismatch"
+ ASSERTION = "#{SAML_2_0}:assertion".freeze
+ ATTR_SPLAT = "#{ATTR_NAME_FORMAT}:*".freeze
+ BASIC = "#{ATTR_NAME_FORMAT}:basic".freeze
+ BEARER = "#{SAML_2_0}:cm:bearer".freeze
+ EMAIL_ADDRESS = "#{NAME_ID_FORMAT_1_1}:emailAddress".freeze
+ INVALID_NAME_ID_POLICY = "#{STATUS}:InvalidNameIDPolicy".freeze
+ METADATA = "#{SAML_2_0}:metadata".freeze
+ PASSWORD = "#{SAML_2_0}:ac:classes:Password".freeze
+ PASSWORD_PROTECTED = "#{SAML_2_0}:ac:classes:PasswordProtectedTransport".freeze
+ PERSISTENT = "#{NAME_ID_FORMAT_2_0}:persistent".freeze
+ PROTOCOL = "#{SAML_2_0}:protocol".freeze
+ REQUESTER_ERROR = "#{STATUS}:Requester".freeze
+ RESPONDER_ERROR = "#{STATUS}:Responder".freeze
+ SUCCESS = "#{STATUS}:Success".freeze
+ TRANSIENT = "#{NAME_ID_FORMAT_2_0}:transient".freeze
+ UNSPECIFIED = "#{SAML_2_0}:consent:unspecified".freeze
+ UNSPECIFIED_NAMEID = "#{NAME_ID_FORMAT_1_1}:unspecified".freeze
+ URI = "#{ATTR_NAME_FORMAT}:uri".freeze
+ VERSION_MISMATCH_ERROR = "#{STATUS}:VersionMismatch".freeze
end
end
end
lib/saml/kit/null_assertion.rb
@@ -0,0 +1,17 @@
+module Saml
+ module Kit
+ class NullAssertion
+ include ActiveModel::Validations
+ include Translatable
+ validate :invalid
+
+ def invalid
+ errors[:assertion].push(error_message(:invalid))
+ end
+
+ def name
+ 'NullAssertion'
+ end
+ end
+ end
+end
lib/saml/kit/respondable.rb
@@ -33,10 +33,9 @@ module Saml
def must_match_request_id
return if request_id.nil?
+ return if in_response_to == request_id
- if in_response_to != request_id
- errors[:in_response_to] << error_message(:invalid_response_to)
- end
+ errors[:in_response_to] << error_message(:invalid_response_to)
end
end
end
lib/saml/kit/response.rb
@@ -8,14 +8,23 @@ module Saml
def_delegators :assertion, :name_id, :[], :attributes
validate :must_be_valid_assertion
+ validate :must_contain_single_assertion
def initialize(xml, request_id: nil, configuration: Saml::Kit.configuration)
@request_id = request_id
- super(xml, name: "Response", configuration: configuration)
+ super(xml, name: 'Response', configuration: configuration)
end
- def assertion
- @assertion ||= Saml::Kit::Assertion.new(to_h, configuration: @configuration)
+ def assertion(private_keys = configuration.private_keys(use: :encryption))
+ @assertion ||=
+ begin
+ node = assertion_nodes.last
+ if node.nil?
+ Saml::Kit::NullAssertion.new
+ else
+ Saml::Kit::Assertion.new(node, configuration: @configuration, private_keys: private_keys)
+ end
+ end
end
private
@@ -23,9 +32,19 @@ module Saml
def must_be_valid_assertion
assertion.valid?
assertion.errors.each do |attribute, error|
- self.errors[attribute] << error
+ attribute = :assertion if attribute == :base
+ errors[attribute] << error
end
end
+
+ def must_contain_single_assertion
+ return if assertion_nodes.count <= 1
+ errors[:base] << error_message(:must_contain_single_assertion)
+ end
+
+ def assertion_nodes
+ search(Saml::Kit::Assertion::XPATH)
+ end
end
end
end
lib/saml/kit/service_provider_metadata.rb
@@ -3,7 +3,7 @@ module Saml
# {include:file:spec/examples/service_provider_metadata_spec.rb}
class ServiceProviderMetadata < Metadata
def initialize(xml)
- super("SPSSODescriptor", xml)
+ super('SPSSODescriptor', xml)
end
# Returns each of the AssertionConsumerService bindings.
@@ -20,9 +20,9 @@ module Saml
# Returns true when the metadata demands that Assertions must be signed.
def want_assertions_signed
- attribute = document.find_by("/md:EntityDescriptor/md:#{name}").attribute("WantAssertionsSigned")
+ attribute = document.find_by("/md:EntityDescriptor/md:#{name}").attribute('WantAssertionsSigned')
return true if attribute.nil?
- attribute.text.downcase == "true"
+ attribute.text.casecmp('true').zero?
end
# @!visibility private
lib/saml/kit/signature.rb
@@ -2,14 +2,21 @@ module Saml
module Kit
class Signature
include ActiveModel::Validations
+ include Translatable
- def initialize(xml_hash)
- @xml_hash = xml_hash
+ validate :validate_signature
+ validate :validate_certificate
+
+ attr_reader :name
+
+ def initialize(node)
+ @name = 'Signature'
+ @node = node
end
# Returns the embedded X509 Certificate
def certificate
- value = to_h.fetch('KeyInfo', {}).fetch('X509Data', {}).fetch('X509Certificate', nil)
+ value = at_xpath('./ds:KeyInfo/ds:X509Data/ds:X509Certificate').try(:text)
return if value.nil?
::Xml::Kit::Certificate.new(value, use: :signing)
end
@@ -20,9 +27,72 @@ module Saml
metadata.matches?(certificate.fingerprint, use: :signing)
end
+ def digest_value
+ at_xpath('./ds:SignedInfo/ds:Reference/ds:DigestValue').try(:text)
+ end
+
+ def digest_method
+ at_xpath('./ds:SignedInfo/ds:Reference/ds:DigestMethod/@Algorithm').try(:value)
+ end
+
+ def signature_value
+ at_xpath('./ds:SignatureValue').try(:text)
+ end
+
+ def signature_method
+ at_xpath('./ds:SignedInfo/ds:SignatureMethod/@Algorithm').try(:value)
+ end
+
+ def canonicalization_method
+ at_xpath('./ds:SignedInfo/ds:CanonicalizationMethod/@Algorithm').try(:value)
+ end
+
+ def transforms
+ node.search('./ds:SignedInfo/ds:Reference/ds:Transforms/ds:Transform/@Algorithm', Saml::Kit::Document::NAMESPACES).try(:map, &:value)
+ end
+
# Returns the XML Hash.
def to_h
- @xml_hash
+ @xml_hash ||= present? ? Hash.from_xml(to_xml)['Signature'] : {}
+ end
+
+ def present?
+ node
+ end
+
+ def to_xml
+ node.to_s
+ end
+
+ private
+
+ attr_reader :node
+
+ def validate_signature
+ return errors[:base].push(error_message(:empty)) if certificate.nil?
+
+ signature = Xmldsig::Signature.new(@node, 'ID=$uri or @Id')
+ return if signature.valid?(certificate.x509)
+ signature.errors.each do |attribute|
+ errors.add(attribute, error_message(attribute))
+ end
+ end
+
+ def validate_certificate(now = Time.now.utc)
+ return unless certificate.present?
+ return if certificate.active?(now)
+
+ message = error_message(
+ :certificate,
+ not_before: certificate.not_before,
+ not_after: certificate.not_after
+ )
+ errors.add(:certificate, message)
+ end
+
+ def at_xpath(xpath)
+ return nil unless node
+ node.at_xpath(xpath, Saml::Kit::Document::NAMESPACES)
end
end
end
lib/saml/kit/translatable.rb
@@ -2,8 +2,9 @@ module Saml
module Kit
module Translatable
# @!visibility private
- def error_message(attribute, type: :invalid)
- I18n.translate(attribute, scope: "saml/kit.errors.#{name}")
+ def error_message(attribute, options = {})
+ default_options = { scope: "saml/kit.errors.#{name}" }
+ I18n.translate(attribute, default_options.merge(options))
end
end
end
lib/saml/kit/trustable.rb
@@ -16,8 +16,7 @@ module Saml
# @!visibility private
def signature
- xml_hash = to_h.fetch(name, {}).fetch('Signature', nil)
- xml_hash ? Signature.new(xml_hash) : nil
+ @signature ||= Signature.new(at_xpath("/samlp:#{name}/ds:Signature"))
end
# Returns true when documents is signed and the signing certificate belongs to a known service entity.
@@ -43,16 +42,10 @@ module Saml
def must_have_valid_signature
return if to_xml.blank?
+ return unless signature.present?
- xml = ::Xml::Kit::Document.new(to_xml, namespaces: {
- "NameFormat": Namespaces::ATTR_SPLAT,
- "ds": ::Xml::Kit::Namespaces::XMLDSIG,
- "md": Namespaces::METADATA,
- "saml": Namespaces::ASSERTION,
- "samlp": Namespaces::PROTOCOL,
- })
- xml.valid?
- xml.errors.each do |attribute, error|
+ signature.valid?
+ signature.errors.each do |attribute, error|
errors[attribute] << error
end
end
lib/saml/kit/version.rb
@@ -1,5 +1,5 @@
module Saml
module Kit
- VERSION = "1.0.6"
+ VERSION = '1.0.7'.freeze
end
end
lib/saml/kit/xml_templatable.rb
@@ -4,16 +4,21 @@ module Saml
include ::Xml::Kit::Templatable
def template_path
- root_path = File.expand_path(File.dirname(__FILE__))
- template_name = "#{self.class.name.split("::").last.underscore}.builder"
- File.join(root_path, "builders/templates/", template_name)
+ root_path = __dir__
+ template_name = "#{self.class.name.split('::').last.underscore}.builder"
+ File.join(root_path, 'builders/templates/', template_name)
end
# Returns true if an embedded signature is requested and at least one signing certificate is available via the configuration.
def sign?
return configuration.sign? if embed_signature.nil?
(embed_signature && configuration.sign?) ||
- (embed_signature && @signing_key_pair.present?)
+ (embed_signature && signing_key_pair.present?)
+ end
+
+ def encrypt_with(key_pair)
+ self.encrypt = true
+ self.encryption_certificate = key_pair.certificate
end
def digest_method
@@ -25,7 +30,7 @@ module Saml
end
def signing_key_pair
- configuration.key_pairs(use: :signing).last
+ @signing_key_pair || configuration.key_pairs(use: :signing).last
end
end
end
lib/saml/kit.rb
@@ -1,46 +1,49 @@
-require "saml/kit/version"
+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/deprecation"
-require "active_support/duration"
-require "forwardable"
-require "logger"
-require "net/http"
-require "nokogiri"
-require "securerandom"
-require "xml/kit"
+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/deprecation'
+require 'active_support/duration'
+require 'forwardable'
+require 'logger'
+require 'net/http'
+require 'nokogiri'
+require 'securerandom'
+require 'uri'
+require 'xml/kit'
-require "saml/kit/buildable"
-require "saml/kit/builders"
-require "saml/kit/namespaces"
-require "saml/kit/serializable"
-require "saml/kit/xsd_validatable"
-require "saml/kit/respondable"
-require "saml/kit/requestable"
-require "saml/kit/trustable"
-require "saml/kit/translatable"
-require "saml/kit/document"
+require 'saml/kit/buildable'
+require 'saml/kit/builders'
+require 'saml/kit/namespaces'
+require 'saml/kit/serializable'
+require 'saml/kit/xsd_validatable'
+require 'saml/kit/respondable'
+require 'saml/kit/requestable'
+require 'saml/kit/trustable'
+require 'saml/kit/translatable'
+require 'saml/kit/document'
-require "saml/kit/assertion"
-require "saml/kit/authentication_request"
-require "saml/kit/bindings"
-require "saml/kit/configuration"
-require "saml/kit/default_registry"
-require "saml/kit/logout_response"
-require "saml/kit/logout_request"
-require "saml/kit/metadata"
-require "saml/kit/composite_metadata"
-require "saml/kit/response"
-require "saml/kit/identity_provider_metadata"
-require "saml/kit/invalid_document"
-require "saml/kit/service_provider_metadata"
-require "saml/kit/signature"
+require 'saml/kit/assertion'
+require 'saml/kit/authentication_request'
+require 'saml/kit/bindings'
+require 'saml/kit/configuration'
+require 'saml/kit/default_registry'
+require 'saml/kit/logout_response'
+require 'saml/kit/logout_request'
+require 'saml/kit/metadata'
+require 'saml/kit/null_assertion'
+require 'saml/kit/composite_metadata'
+require 'saml/kit/response'
+require 'saml/kit/identity_provider_metadata'
+require 'saml/kit/invalid_document'
+require 'saml/kit/service_provider_metadata'
+require 'saml/kit/signature'
-I18n.load_path += Dir[File.expand_path("kit/locales/*.yml", File.dirname(__FILE__))]
+I18n.load_path +=
+ Dir[File.expand_path('kit/locales/*.yml', File.dirname(__FILE__))]
module Saml
module Kit
spec/examples/logout_request_spec.rb
@@ -1,7 +1,7 @@
-require_relative './user'
+require_relative './principal'
RSpec.describe "Logout Request" do
- let(:user) { User.new(id: SecureRandom.uuid, email: "hello@example.com") }
+ let(:user) { Principal.new(id: SecureRandom.uuid, email: "hello@example.com") }
it 'produces a SAMLRequest' do
xml = Saml::Kit::Metadata.build_xml do |builder|
spec/examples/logout_response_spec.rb
@@ -1,7 +1,7 @@
-require_relative './user'
+require_relative './principal'
RSpec.describe "Logout Response" do
- let(:user) { User.new(id: SecureRandom.uuid, email: "hello@example.com") }
+ let(:user) { Principal.new(id: SecureRandom.uuid, email: "hello@example.com") }
it 'generates a logout response' do
xml = Saml::Kit::Metadata.build_xml do |builder|
spec/examples/user.rb → spec/examples/principal.rb
@@ -1,4 +1,4 @@
-class User
+class Principal
attr_reader :id, :email
def initialize(id:, email:)
spec/examples/response_spec.rb
@@ -1,7 +1,7 @@
-require_relative './user'
+require_relative './principal'
RSpec.describe "Response" do
- let(:user) { User.new(id: SecureRandom.uuid, email: "hello@example.com") }
+ let(:user) { Principal.new(id: SecureRandom.uuid, email: "hello@example.com") }
let(:request) { Saml::Kit::AuthenticationRequest.build }
it 'consumes a Response' do
spec/saml/bindings/binding_spec.rb → spec/saml/kit/bindings/binding_spec.rb
@@ -1,17 +1,18 @@
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) }
- describe "#serialize" do
+ let(:location) { FFaker::Internet.http_url }
+
+ describe '#serialize' do
it 'ignores other bindings' do
expect(subject.serialize(Saml::Kit::AuthenticationRequest)).to be_empty
end
end
- describe "#deserialize" do
+ describe '#deserialize' do
it 'raises an error' do
expect do
- subject.deserialize('SAMLRequest' => "CORRUPT")
+ subject.deserialize('SAMLRequest' => 'CORRUPT')
end.to raise_error(/Unsupported binding/)
end
end
spec/saml/bindings/http_post_spec.rb → spec/saml/kit/bindings/http_post_spec.rb
@@ -1,27 +1,28 @@
RSpec.describe Saml::Kit::Bindings::HttpPost do
- let(:location) { FFaker::Internet.uri("https") }
subject { described_class.new(location: location) }
- describe "equality" do
+ let(:location) { FFaker::Internet.uri('https') }
+
+ describe 'equality' do
it 'is referentially equal' do
expect(subject).to eql(subject)
end
it 'is equal by value' do
expect(subject).to eql(
- Saml::Kit::Bindings::HttpPost.new(location: location)
+ described_class.new(location: location)
)
end
it 'is not equal' do
- expect(subject).to_not eql(
- described_class.new(location: FFaker::Internet.uri("https"))
+ expect(subject).not_to eql(
+ described_class.new(location: FFaker::Internet.uri('https'))
)
end
end
- describe "#serialize" do
- let(:relay_state) { "ECHO" }
+ describe '#serialize' do
+ let(:relay_state) { 'ECHO' }
let(:configuration) do
Saml::Kit::Configuration.new do |config|
config.generate_key_pair_for(use: :signing)
@@ -42,7 +43,7 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
end
it 'returns a SAMLRequest for a LogoutRequest' do
- user = double(:user, name_id_for: SecureRandom.uuid)
+ user = User.new
builder = Saml::Kit::LogoutRequest.builder_class.new(user, configuration: configuration)
url, saml_params = subject.serialize(builder, relay_state: relay_state)
@@ -74,11 +75,11 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
url, saml_params = subject.serialize(builder)
expect(url).to eql(location)
- expect(saml_params.keys).to_not include('RelayState')
+ expect(saml_params.keys).not_to include('RelayState')
end
end
- describe "#deserialize" do
+ describe '#deserialize' do
it 'deserializes to an AuthnRequest' do
builder = Saml::Kit::AuthenticationRequest.builder_class.new
_, params = subject.serialize(builder)
@@ -87,7 +88,7 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
end
it 'deserializes to a LogoutRequest' do
- user = double(:user, name_id_for: SecureRandom.uuid)
+ user = User.new
builder = Saml::Kit::LogoutRequest.builder_class.new(user)
_, params = subject.serialize(builder)
result = subject.deserialize(params)
@@ -95,8 +96,8 @@ RSpec.describe Saml::Kit::Bindings::HttpPost do
end
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, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: FFaker::Internet.http_url, signed?: true, trusted?: true)
+ user = User.new
+ request = instance_double(Saml::Kit::AuthenticationRequest, id: SecureRandom.uuid, provider: nil, assertion_consumer_service_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_class.new(user, request)
_, params = subject.serialize(builder)
result = subject.deserialize(params)
spec/saml/bindings/http_redirect_spec.rb → spec/saml/kit/bindings/http_redirect_spec.rb
@@ -1,9 +1,10 @@
RSpec.describe Saml::Kit::Bindings::HttpRedirect do
- let(:location) { FFaker::Internet.http_url }
subject { described_class.new(location: location) }
- describe "#serialize" do
- let(:relay_state) { "ECHO" }
+ let(:location) { FFaker::Internet.http_url }
+
+ describe '#serialize' do
+ let(:relay_state) { 'ECHO' }
let(:configuration) do
Saml::Kit::Configuration.new do |config|
config.generate_key_pair_for(use: :signing)
@@ -12,7 +13,7 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
it 'encodes the request using the HTTP-Redirect encoding' do
builder = Saml::Kit::AuthenticationRequest.builder(configuration: configuration)
- url, _ = subject.serialize(builder, relay_state: relay_state)
+ url, = subject.serialize(builder, relay_state: relay_state)
expect(url).to start_with(location)
expect(url).to have_query_param('SAMLRequest')
expect(url).to have_query_param('SigAlg')
@@ -20,28 +21,34 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
end
end
- describe "#deserialize" do
+ describe '#deserialize' do
let(:entity_id) { FFaker::Internet.http_url }
let(:provider) { Saml::Kit::IdentityProviderMetadata.build }
- before :each do
+ before do
allow(Saml::Kit.configuration.registry).to receive(:metadata_for).with(entity_id).and_return(provider)
allow(Saml::Kit.configuration).to receive(:entity_id).and_return(entity_id)
end
it 'deserializes the SAMLRequest to an AuthnRequest' do
- url, _ = subject.serialize(Saml::Kit::AuthenticationRequest.builder)
+ url, = subject.serialize(Saml::Kit::AuthenticationRequest.builder)
result = subject.deserialize(query_params_from(url))
expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
end
+ it 'deserializes the raw query_string to an AuthnRequest' do
+ url, = subject.serialize(Saml::Kit::AuthenticationRequest.builder, relay_state: 'HELLO')
+ result = subject.deserialize(url)
+ expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
+ end
+
it 'deserializes the SAMLRequest to an AuthnRequest with symbols for keys' do
configuration = Saml::Kit::Configuration.new do |config|
config.entity_id = entity_id
config.generate_key_pair_for(use: :signing)
end
provider = Saml::Kit::IdentityProviderMetadata.build(configuration: configuration)
- url, _ = subject.serialize(Saml::Kit::AuthenticationRequest.builder(configuration: configuration))
+ url, = subject.serialize(Saml::Kit::AuthenticationRequest.builder(configuration: configuration))
allow(configuration.registry).to receive(:metadata_for).with(entity_id).and_return(provider)
result = subject.deserialize(query_params_from(url).symbolize_keys, configuration: configuration)
@@ -50,64 +57,66 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
expect(result).to be_trusted
end
- it 'deserializes the SAMLRequest to an AuthnRequest with symbols for keys' do
- url, _ = subject.serialize(Saml::Kit::AuthenticationRequest.builder)
+ it 'deserializes the SAMLRequest to an AuthnRequest with symbolized keys' do
+ url, = subject.serialize(Saml::Kit::AuthenticationRequest.builder)
result = subject.deserialize(query_params_from(url).symbolize_keys)
expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
end
it 'deserializes the SAMLRequest to an AuthnRequest when given a custom params object' do
class Parameters
+ attr_reader :params
+
def initialize(params)
@params = params
end
def [](key)
- @params[key]
+ params[key]
end
end
- url, _ = subject.serialize(Saml::Kit::AuthenticationRequest.builder)
+ url, = subject.serialize(Saml::Kit::AuthenticationRequest.builder)
result = subject.deserialize(Parameters.new(query_params_from(url)))
expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
end
it 'deserializes the SAMLRequest to a LogoutRequest' do
- user = double(:user, name_id_for: SecureRandom.uuid)
- url, _ = subject.serialize(Saml::Kit::LogoutRequest.builder(user))
+ user = User.new
+ url, = subject.serialize(Saml::Kit::LogoutRequest.builder(user))
result = subject.deserialize(query_params_from(url))
expect(result).to be_instance_of(Saml::Kit::LogoutRequest)
end
it 'returns an invalid request when the SAMLRequest is invalid' do
expect do
- subject.deserialize({ 'SAMLRequest' => "nonsense" })
+ subject.deserialize('SAMLRequest' => 'nonsense')
end.to raise_error(Zlib::DataError)
end
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, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: entity_id, signed?: true, trusted?: true)
- url, _ = subject.serialize(Saml::Kit::Response.builder(user, request))
+ user = User.new
+ request = instance_double(Saml::Kit::AuthenticationRequest, id: SecureRandom.uuid, provider: nil, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: entity_id, signed?: true, trusted?: true)
+ url, = subject.serialize(Saml::Kit::Response.builder(user, request))
result = subject.deserialize(query_params_from(url))
expect(result).to be_instance_of(Saml::Kit::Response)
end
it 'deserializes the SAMLResponse to a LogoutResponse' do
- request = double(:request, id: SecureRandom.uuid, provider: provider, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, issuer: FFaker::Internet.http_url)
- url, _ = subject.serialize(Saml::Kit::LogoutResponse.builder(request))
+ request = instance_double(Saml::Kit::LogoutRequest, id: SecureRandom.uuid, provider: provider, issuer: FFaker::Internet.http_url)
+ url, = subject.serialize(Saml::Kit::LogoutResponse.builder(request))
result = subject.deserialize(query_params_from(url))
expect(result).to be_instance_of(Saml::Kit::LogoutResponse)
end
it 'raises an error when the content is invalid' do
expect do
- subject.deserialize('SAMLResponse' => "nonsense")
+ subject.deserialize('SAMLResponse' => 'nonsense')
end.to raise_error(Zlib::DataError)
end
it 'raises an error when a saml parameter is not specified' do
expect do
- subject.deserialize({ })
+ subject.deserialize({})
end.to raise_error(ArgumentError)
end
@@ -116,7 +125,7 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
config.entity_id = entity_id
config.generate_key_pair_for(use: :signing)
end
- url, _ = subject.serialize(
+ url, = subject.serialize(
Saml::Kit::AuthenticationRequest.builder(configuration: configuration) do |x|
x.embed_signature = true
end
@@ -134,7 +143,7 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
end
allow(Saml::Kit.configuration.registry).to receive(:metadata_for).with(entity_id).and_return(provider)
- url, _ = subject.serialize(Saml::Kit::AuthenticationRequest.builder)
+ url, = subject.serialize(Saml::Kit::AuthenticationRequest.builder)
result = subject.deserialize(query_params_from(url))
expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
expect(result).to be_valid
@@ -144,14 +153,14 @@ RSpec.describe Saml::Kit::Bindings::HttpRedirect do
configuration = Saml::Kit::Configuration.new do |config|
config.generate_key_pair_for(use: :signing)
end
- url, _ = subject.serialize(Saml::Kit::AuthenticationRequest.builder(configuration: configuration))
+ url, = subject.serialize(Saml::Kit::AuthenticationRequest.builder(configuration: configuration))
other_configuration = Saml::Kit::Configuration.new
allow(other_configuration.registry).to receive(:metadata_for).and_return(nil)
result = subject.deserialize(query_params_from(url), configuration: other_configuration)
- expect(result).to_not be_signed
- expect(result).to_not be_trusted
+ expect(result).not_to be_signed
+ expect(result).not_to be_trusted
end
end
end
spec/saml/bindings/url_builder_spec.rb → spec/saml/kit/bindings/url_builder_spec.rb
@@ -1,6 +1,6 @@
RSpec.describe Saml::Kit::Bindings::UrlBuilder do
- describe "#build" do
- let(:xml) { "<xml></xml>" }
+ describe '#build' do
+ let(:xml) { '<xml></xml>' }
let(:destination) { FFaker::Internet.http_url }
let(:relay_state) { FFaker::Movie.title }
@@ -12,6 +12,7 @@ RSpec.describe Saml::Kit::Bindings::UrlBuilder do
].each do |(response_type, query_string_parameter)|
describe response_type.to_s do
subject { described_class.new(configuration: configuration) }
+
let(:configuration) do
Saml::Kit::Configuration.new do |config|
config.generate_key_pair_for(use: :signing)
@@ -21,12 +22,12 @@ RSpec.describe Saml::Kit::Bindings::UrlBuilder do
let(:response) { instance_double(response_type, destination: destination, to_xml: xml, query_string_parameter: query_string_parameter) }
def to_query_params(url)
- Hash[URI.parse(url).query.split("&").map { |x| x.split('=', 2) }]
+ Hash[URI.parse(url).query.split('&').map { |x| x.split('=', 2) }]
end
it 'returns a url containing the target location' do
result_uri = URI.parse(subject.build(response))
- expect(result_uri.scheme).to eql("http")
+ expect(result_uri.scheme).to eql('http')
expect(result_uri.host).to eql(URI.parse(destination).host)
end
@@ -34,7 +35,7 @@ RSpec.describe Saml::Kit::Bindings::UrlBuilder do
result = subject.build(response, relay_state: relay_state)
query_params = to_query_params(result)
level = Zlib::BEST_COMPRESSION
- expected = CGI.escape(Base64.encode64(Zlib::Deflate.deflate(xml, level)[2..-5]).gsub(/\n/, ''))
+ expected = CGI.escape(Base64.encode64(Zlib::Deflate.deflate(xml, level)[2..-5]).delete("\n"))
expect(result).to include("#{query_string_parameter}=#{expected}")
expect(query_params[query_string_parameter]).to eql(expected)
end
spec/saml/builders/authentication_request_spec.rb → spec/saml/kit/builders/authentication_request_spec.rb
@@ -1,14 +1,15 @@
RSpec.describe Saml::Kit::Builders::AuthenticationRequest do
subject { described_class.new(configuration: configuration) }
+
let(:configuration) do
config = Saml::Kit::Configuration.new
config.entity_id = issuer
config
end
- describe "#to_xml" do
+ describe '#to_xml' do
let(:issuer) { FFaker::Movie.title }
- let(:assertion_consumer_service_url) { "https://airport.dev/session/acs" }
+ let(:assertion_consumer_service_url) { 'https://airport.dev/session/acs' }
it 'returns a valid authentication request' do
travel_to 1.second.from_now
spec/saml/builders/identity_provider_metadata_spec.rb → spec/saml/kit/builders/identity_provider_metadata_spec.rb
@@ -1,5 +1,6 @@
RSpec.describe Saml::Kit::Builders::IdentityProviderMetadata do
subject { described_class.new(configuration: configuration) }
+
let(:configuration) do
Saml::Kit::Configuration.new do |config|
config.generate_key_pair_for(use: :signing)
@@ -8,7 +9,7 @@ RSpec.describe Saml::Kit::Builders::IdentityProviderMetadata do
end
let(:email) { FFaker::Internet.email }
let(:org_name) { FFaker::Movie.title }
- let(:url) { FFaker::Internet.uri("https") }
+ let(:url) { FFaker::Internet.uri('https') }
let(:entity_id) { FFaker::Movie.title }
it 'builds a proper metadata' do
@@ -21,9 +22,9 @@ RSpec.describe Saml::Kit::Builders::IdentityProviderMetadata do
Saml::Kit::Namespaces::TRANSIENT,
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: :http_post)
- subject.attributes << "id"
+ 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: :http_post)
+ subject.attributes << 'id'
result = Hash.from_xml(subject.build.to_xml)
@@ -37,17 +38,17 @@ RSpec.describe Saml::Kit::Builders::IdentityProviderMetadata do
Saml::Kit::Namespaces::EMAIL_ADDRESS,
])
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']['SingleSignOnService']['Location']).to eql('https://www.example.com/login')
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']['SingleLogoutService']['Location']).to eql('https://www.example.com/logout')
+ expect(result['EntityDescriptor']['IDPSSODescriptor']['Attribute']['Name']).to eql('id')
certificates = result['EntityDescriptor']['IDPSSODescriptor']['KeyDescriptor'].map { |x| x['KeyInfo']['X509Data']['X509Certificate'] }
expected_certificates = configuration.certificates.map(&:stripped)
expect(certificates).to match_array(expected_certificates)
expect(result['EntityDescriptor']['Organization']['OrganizationName']).to eql(org_name)
expect(result['EntityDescriptor']['Organization']['OrganizationDisplayName']).to eql(org_name)
expect(result['EntityDescriptor']['Organization']['OrganizationURL']).to eql(url)
- expect(result['EntityDescriptor']['ContactPerson']['contactType']).to eql("technical")
+ expect(result['EntityDescriptor']['ContactPerson']['contactType']).to eql('technical')
expect(result['EntityDescriptor']['ContactPerson']['Company']).to eql("mailto:#{email}")
end
end
spec/saml/builders/logout_request_spec.rb → spec/saml/kit/builders/logout_request_spec.rb
@@ -1,6 +1,7 @@
RSpec.describe Saml::Kit::Builders::LogoutRequest do
subject { described_class.new(user, configuration: configuration) }
- let(:user) { double(:user, name_id_for: name_id) }
+
+ let(:user) { User.new(name_id: name_id) }
let(:name_id) { SecureRandom.uuid }
let(:configuration) do
Saml::Kit::Configuration.new do |config|
@@ -19,7 +20,7 @@ RSpec.describe Saml::Kit::Builders::LogoutRequest do
xml_hash = Hash.from_xml(result)
expect(xml_hash['LogoutRequest']['ID']).to eql(subject.id)
- expect(xml_hash['LogoutRequest']['Version']).to eql("2.0")
+ expect(xml_hash['LogoutRequest']['Version']).to eql('2.0')
expect(xml_hash['LogoutRequest']['IssueInstant']).to eql(Time.now.utc.iso8601)
expect(xml_hash['LogoutRequest']['Destination']).to eql(subject.destination)
spec/saml/builders/logout_response_spec.rb → spec/saml/kit/builders/logout_response_spec.rb
@@ -1,11 +1,12 @@
RSpec.describe Saml::Kit::Builders::LogoutResponse do
subject { described_class.new(request) }
- let(:user) { double(:user, name_id_for: SecureRandom.uuid) }
+
+ let(:user) { User.new }
let(:request) { Saml::Kit::Builders::LogoutRequest.new(user).build }
let(:issuer) { FFaker::Internet.http_url }
let(:destination) { FFaker::Internet.http_url }
- describe "#build" do
+ describe '#build' do
it 'builds a logout response' do
travel_to 1.second.from_now
@@ -14,7 +15,7 @@ RSpec.describe Saml::Kit::Builders::LogoutResponse do
result = subject.build
expect(result.id).to be_present
expect(result.issue_instant).to eql(Time.now.utc)
- expect(result.version).to eql("2.0")
+ expect(result.version).to eql('2.0')
expect(result.issuer).to eql(issuer)
expect(result.status_code).to eql(Saml::Kit::Namespaces::SUCCESS)
expect(result.in_response_to).to eql(request.id)
spec/saml/builders/metadata_spec.rb → spec/saml/kit/builders/metadata_spec.rb
@@ -1,7 +1,8 @@
RSpec.describe Saml::Kit::Builders::Metadata do
- describe ".build" do
+ describe '.build' do
subject { Saml::Kit::Metadata }
- let(:url) { FFaker::Internet.uri("https") }
+
+ let(:url) { FFaker::Internet.uri('https') }
it 'builds metadata for a service provider' do
result = subject.build do |builder|
@@ -57,7 +58,7 @@ RSpec.describe Saml::Kit::Builders::Metadata do
3.times { config.generate_key_pair_for(use: :signing) }
end
metadata = Saml::Kit::Metadata.build(configuration: configuration) do |builder|
- builder.entity_id = FFaker::Internet.uri("https")
+ builder.entity_id = FFaker::Internet.uri('https')
builder.build_identity_provider do |x|
x.embed_signature = true
x.add_single_sign_on_service(url, binding: :http_post)
spec/saml/builders/response_spec.rb → spec/saml/kit/builders/response_spec.rb
@@ -1,5 +1,6 @@
RSpec.describe Saml::Kit::Builders::Response do
subject { described_class.new(user, request, configuration: configuration) }
+
let(:configuration) do
Saml::Kit::Configuration.new do |config|
config.entity_id = issuer
@@ -8,13 +9,13 @@ RSpec.describe Saml::Kit::Builders::Response do
end
end
let(:email) { FFaker::Internet.email }
- let(:assertion_consumer_service_url) { FFaker::Internet.uri("https") }
- let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: { email: email, created_at: Time.now.utc.iso8601 }) }
- let(:request) { double(:request, id: Xml::Kit::Id.generate, assertion_consumer_service_url: assertion_consumer_service_url, issuer: issuer, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: provider, trusted?: true, signed?: true) }
- let(:provider) { double(:provider, want_assertions_signed: false, encryption_certificates: [configuration.certificates(use: :encryption).last] ) }
- let(:issuer) { FFaker::Internet.uri("https") }
+ let(:assertion_consumer_service_url) { FFaker::Internet.uri('https') }
+ let(:user) { User.new(attributes: { email: email, created_at: Time.now.utc.iso8601 }) }
+ let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: Xml::Kit::Id.generate, assertion_consumer_service_url: assertion_consumer_service_url, issuer: issuer, name_id_format: Saml::Kit::Namespaces::EMAIL_ADDRESS, provider: provider, trusted?: true, signed?: true) }
+ let(:provider) { instance_double(Saml::Kit::ServiceProviderMetadata, want_assertions_signed: false, encryption_certificates: [configuration.certificates(use: :encryption).last]) }
+ let(:issuer) { FFaker::Internet.uri('https') }
- describe "#build" do
+ describe '#build' do
it 'builds a response with the request_id' do
expect(subject.build.request_id).to eql(request.id)
end
@@ -37,7 +38,7 @@ RSpec.describe Saml::Kit::Builders::Response do
end
end
- describe "#to_xml" do
+ describe '#to_xml' do
it 'returns a proper response for the user' do
travel_to 1.second.from_now
allow(Saml::Kit.configuration).to receive(:entity_id).and_return(issuer)
@@ -51,15 +52,15 @@ RSpec.describe Saml::Kit::Builders::Response do
expect(hash['Response']['Destination']).to eql(assertion_consumer_service_url)
expect(hash['Response']['InResponseTo']).to eql(request.id)
expect(hash['Response']['Issuer']).to eql(issuer)
- expect(hash['Response']['Status']['StatusCode']['Value']).to eql("urn:oasis:names:tc:SAML:2.0:status:Success")
+ expect(hash['Response']['Status']['StatusCode']['Value']).to eql('urn:oasis:names:tc:SAML:2.0:status:Success')
expect(hash['Response']['Assertion']['ID']).to be_present
expect(hash['Response']['Assertion']['IssueInstant']).to eql(Time.now.utc.iso8601)
- expect(hash['Response']['Assertion']['Version']).to eql("2.0")
+ expect(hash['Response']['Assertion']['Version']).to eql('2.0')
expect(hash['Response']['Assertion']['Issuer']).to eql(issuer)
- expect(hash['Response']['Assertion']['Subject']['NameID']).to eql(user.name_id_for)
- expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['Method']).to eql("urn:oasis:names:tc:SAML:2.0:cm:bearer")
+ expect(hash['Response']['Assertion']['Subject']['NameID']).to eql(user.name_id)
+ expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['Method']).to eql('urn:oasis:names:tc:SAML:2.0:cm:bearer')
expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['NotOnOrAfter']).to eql(3.hours.from_now.utc.iso8601)
expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['Recipient']).to eql(assertion_consumer_service_url)
expect(hash['Response']['Assertion']['Subject']['SubjectConfirmation']['SubjectConfirmationData']['InResponseTo']).to eql(request.id)
@@ -145,13 +146,13 @@ RSpec.describe Saml::Kit::Builders::Response do
subject.embed_signature = false
result = Saml::Kit::Response.new(subject.to_xml, configuration: configuration)
- expect(result).to_not be_signed
- expect(result.assertion).to_not be_signed
+ expect(result).not_to be_signed
+ expect(result.assertion).not_to be_signed
expect(result.assertion).to be_encrypted
end
end
- describe ".build" do
+ describe '.build' do
let(:configuration) do
Saml::Kit::Configuration.new do |config|
config.entity_id = issuer
spec/saml/builders/service_provider_metadata_spec.rb → spec/saml/kit/builders/service_provider_metadata_spec.rb
@@ -1,5 +1,6 @@
RSpec.describe Saml::Kit::Builders::ServiceProviderMetadata do
subject { described_class.new(configuration: configuration) }
+
let(:configuration) do
Saml::Kit::Configuration.new do |config|
config.generate_key_pair_for(use: :signing)
@@ -9,8 +10,8 @@ RSpec.describe Saml::Kit::Builders::ServiceProviderMetadata do
let(:assertion_consumer_service_url) { FFaker::Internet.http_url }
let(:email) { FFaker::Internet.email }
let(:org_name) { FFaker::Movie.title }
- let(:url) { FFaker::Internet.uri("https") }
- let(:entity_id) { FFaker::Internet.uri("https") }
+ let(:url) { FFaker::Internet.uri('https') }
+ let(:entity_id) { FFaker::Internet.uri('https') }
it 'builds the service provider metadata' do
subject.contact_email = email
@@ -25,7 +26,7 @@ RSpec.describe Saml::Kit::Builders::ServiceProviderMetadata do
]
result = Hash.from_xml(subject.build.to_xml)
- expect(result['EntityDescriptor']['xmlns']).to eql("urn:oasis:names:tc:SAML:2.0:metadata")
+ expect(result['EntityDescriptor']['xmlns']).to eql('urn:oasis:names:tc:SAML:2.0:metadata')
expect(result['EntityDescriptor']['ID']).to be_present
expect(result['EntityDescriptor']['entityID']).to eql(entity_id)
expect(result['EntityDescriptor']['SPSSODescriptor']['AuthnRequestsSigned']).to eql('true')
@@ -36,18 +37,18 @@ RSpec.describe Saml::Kit::Builders::ServiceProviderMetadata do
Saml::Kit::Namespaces::TRANSIENT,
Saml::Kit::Namespaces::EMAIL_ADDRESS,
])
- expect(result['EntityDescriptor']['SPSSODescriptor']['AssertionConsumerService']['Binding']).to eql("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST")
+ expect(result['EntityDescriptor']['SPSSODescriptor']['AssertionConsumerService']['Binding']).to eql('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST')
expect(result['EntityDescriptor']['SPSSODescriptor']['AssertionConsumerService']['Location']).to eql(assertion_consumer_service_url)
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'].map { |x| x['use'] }).to match_array(['signing', 'encryption'])
+ expect(result['EntityDescriptor']['SPSSODescriptor']['KeyDescriptor'].map { |x| x['use'] }).to match_array(%w[signing encryption])
expected_certificates = configuration.certificates.map(&:stripped)
expect(result['EntityDescriptor']['SPSSODescriptor']['KeyDescriptor'].map { |x| x['KeyInfo']['X509Data']['X509Certificate'] }).to match_array(expected_certificates)
expect(result['EntityDescriptor']['Organization']['OrganizationName']).to eql(org_name)
expect(result['EntityDescriptor']['Organization']['OrganizationDisplayName']).to eql(org_name)
expect(result['EntityDescriptor']['Organization']['OrganizationURL']).to eql(url)
- expect(result['EntityDescriptor']['ContactPerson']['contactType']).to eql("technical")
+ expect(result['EntityDescriptor']['ContactPerson']['contactType']).to eql('technical')
expect(result['EntityDescriptor']['ContactPerson']['Company']).to eql("mailto:#{email}")
end
end
spec/saml/kit/assertion_spec.rb
@@ -0,0 +1,204 @@
+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
+ not_on_or_after = configuration.session_timeout.since(now).iso8601
+ xml = <<-XML.strip_heredoc
+ <Response>
+ <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{Xml::Kit::Id.generate}" IssueInstant="#{now.iso8601}" Version="2.0">
+ <Issuer>#{FFaker::Internet.uri('https')}</Issuer>
+ <Subject>
+ <NameID Format="#{Saml::Kit::Namespaces::PERSISTENT}">#{SecureRandom.uuid}</NameID>
+ <SubjectConfirmation Method="#{Saml::Kit::Namespaces::BEARER}">
+ <SubjectConfirmationData InResponseTo="#{SecureRandom.uuid}" NotOnOrAfter="#{not_on_or_after}" Recipient="#{FFaker::Internet.uri('https')}"/>
+ </SubjectConfirmation>
+ </Subject>
+ <Conditions NotBefore="#{now.utc.iso8601}" NotOnOrAfter="#{not_on_or_after}">
+ <AudienceRestriction>
+ <Audience>#{FFaker::Internet.uri('https')}</Audience>
+ </AudienceRestriction>
+ </Conditions>
+ <AuthnStatement AuthnInstant="#{now.utc.iso8601}" SessionIndex="#{Xml::Kit::Id.generate}" SessionNotOnOrAfter="#{not_on_or_after}">
+ <AuthnContext>
+ <AuthnContextClassRef>#{Saml::Kit::Namespaces::PASSWORD}</AuthnContextClassRef>
+ </AuthnContext>
+ </AuthnStatement>
+ </Assertion>
+ </Response>
+XML
+ subject = described_class.new(Nokogiri::XML(xml), configuration: configuration)
+ travel_to((configuration.clock_drift - 1.second).before(now))
+ expect(subject).to be_active
+ expect(subject).not_to be_expired
+ end
+
+ it 'interprets integers correctly' do
+ configuration.clock_drift = 30
+ now = Time.current
+ travel_to now
+ not_before = now.utc.iso8601
+ not_after = configuration.session_timeout.since(now).iso8601
+ xml = <<-XML.strip_heredoc
+ <Response>
+ <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{Xml::Kit::Id.generate}" IssueInstant="#{now.iso8601}" Version="2.0">
+ <Issuer>#{FFaker::Internet.uri('https')}</Issuer>
+ <Subject>
+ <NameID Format="#{Saml::Kit::Namespaces::PERSISTENT}">#{SecureRandom.uuid}</NameID>
+ <SubjectConfirmation Method="#{Saml::Kit::Namespaces::BEARER}">
+ <SubjectConfirmationData InResponseTo="#{SecureRandom.uuid}" NotOnOrAfter="#{not_after}" Recipient="#{FFaker::Internet.uri('https')}"/>
+ </SubjectConfirmation>
+ </Subject>
+ <Conditions NotBefore="#{not_before}" NotOnOrAfter="#{not_after}">
+ <AudienceRestriction>
+ <Audience>#{FFaker::Internet.uri('https')}</Audience>
+ </AudienceRestriction>
+ </Conditions>
+ <AuthnStatement AuthnInstant="#{now.utc.iso8601}" SessionIndex="#{Xml::Kit::Id.generate}" SessionNotOnOrAfter="#{not_after}">
+ <AuthnContext>
+ <AuthnContextClassRef>#{Saml::Kit::Namespaces::PASSWORD}</AuthnContextClassRef>
+ </AuthnContext>
+ </AuthnStatement>
+ </Assertion>
+ </Response>
+XML
+ subject = described_class.new(Nokogiri::XML(xml), configuration: configuration)
+ expect(subject).to be_active
+ expect(subject).not_to be_expired
+ end
+ end
+
+ describe '#present?' do
+ it 'returns false when the assertion is empty' do
+ subject = described_class.new(nil)
+ expect(subject).not_to be_present
+ end
+
+ it 'returns true when the assertion is present' do
+ not_before = Time.now.utc.iso8601
+ not_after = 10.minutes.from_now.iso8601
+ xml = <<-XML.strip_heredoc
+ <Response>
+ <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{Xml::Kit::Id.generate}" IssueInstant="#{Time.now.iso8601}" Version="2.0">
+ <Issuer>#{FFaker::Internet.uri('https')}</Issuer>
+ <Subject>
+ <NameID Format="#{Saml::Kit::Namespaces::PERSISTENT}">#{SecureRandom.uuid}</NameID>
+ <SubjectConfirmation Method="#{Saml::Kit::Namespaces::BEARER}">
+ <SubjectConfirmationData InResponseTo="#{SecureRandom.uuid}" NotOnOrAfter="#{not_after}" Recipient="#{FFaker::Internet.uri('https')}"/>
+ </SubjectConfirmation>
+ </Subject>
+ <Conditions NotBefore="#{not_before}" NotOnOrAfter="#{not_after}">
+ <AudienceRestriction>
+ <Audience>#{FFaker::Internet.uri('https')}</Audience>
+ </AudienceRestriction>
+ </Conditions>
+ <AuthnStatement AuthnInstant="#{Time.now.utc.iso8601}" SessionIndex="#{Xml::Kit::Id.generate}" SessionNotOnOrAfter="#{not_after}">
+ <AuthnContext>
+ <AuthnContextClassRef>#{Saml::Kit::Namespaces::PASSWORD}</AuthnContextClassRef>
+ </AuthnContext>
+ </AuthnStatement>
+ </Assertion>
+ </Response>
+XML
+ subject = described_class.new(Nokogiri::XML(xml))
+ expect(subject).to be_present
+ end
+ end
+
+ describe '#signed?' do
+ let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: ::Xml::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) }
+ let(:user) { User.new(attributes: { id: SecureRandom.uuid }) }
+
+ it 'detects a signature in an encrypted assertion' do
+ encryption_key_pair = Xml::Kit::KeyPair.generate(use: :encryption)
+ response = Saml::Kit::Response.build(user, request) do |x|
+ x.sign_with(Xml::Kit::KeyPair.generate(use: :signing))
+ x.encrypt_with(encryption_key_pair)
+ end
+ assertion = response.assertion([encryption_key_pair.private_key])
+ expect(response).to be_signed
+ expect(assertion).to be_signed
+ end
+ end
+
+ describe '#to_xml' do
+ let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: ::Xml::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) }
+ let(:user) { User.new(attributes: { id: SecureRandom.uuid }) }
+
+ it 'returns the decrypted xml' do
+ encryption_key_pair = Xml::Kit::KeyPair.generate(use: :encryption)
+ response = Saml::Kit::Response.build(user, request) do |x|
+ x.sign_with(Xml::Kit::KeyPair.generate(use: :signing))
+ x.encrypt_with(encryption_key_pair)
+ end
+ assertion = response.assertion([encryption_key_pair.private_key])
+ expect(assertion.to_xml).not_to include('EncryptedAssertion')
+ expect(assertion.to_xml).to include('Assertion')
+ end
+ end
+
+ describe '#valid?' do
+ let(:entity_id) { FFaker::Internet.uri('https') }
+ let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: ::Xml::Kit::Id.generate, issuer: entity_id, assertion_consumer_service_url: FFaker::Internet.http_url, name_id_format: Saml::Kit::Namespaces::PERSISTENT, provider: nil, signed?: true, trusted?: true) }
+ let(:name_id) { SecureRandom.uuid }
+ let(:user) { User.new(name_id: name_id, attributes: { id: SecureRandom.uuid }) }
+ let(:registry) { instance_double(Saml::Kit::DefaultRegistry, metadata_for: idp) }
+ let(:idp) { Saml::Kit::IdentityProviderMetadata.build(configuration: configuration) }
+ let(:configuration) do
+ Saml::Kit::Configuration.new do |x|
+ x.entity_id = entity_id
+ x.generate_key_pair_for(use: :signing)
+ end
+ end
+
+ before do
+ allow(configuration.registry).to receive(:metadata_for).with(entity_id).and_return(idp)
+ end
+
+ it 'is invalid when the encrypted signature is invalid' do
+ xml = Saml::Kit::Response.build_xml(user, request, configuration: configuration)
+ altered = xml.gsub(name_id, 'altered')
+ document = Nokogiri::XML(altered)
+ assertion = document.at_xpath('/samlp:Response/saml:Assertion', Saml::Kit::Document::NAMESPACES)
+ key_pair = Xml::Kit::KeyPair.generate(use: :encryption)
+ encrypted = Xml::Kit::Encryption.new(assertion.to_xml, key_pair.public_key).to_xml
+ response = Saml::Kit::Response.new(encrypted, configuration: configuration)
+ expect(response.assertion([key_pair.private_key])).to be_invalid
+ end
+
+ it 'is valid when the encrypted signature is valid' do
+ key_pair = Xml::Kit::KeyPair.generate(use: :encryption)
+ response = Saml::Kit::Response.build(user, request, configuration: configuration) do |x|
+ x.encrypt_with(key_pair)
+ end
+ expect(response.assertion([key_pair.private_key])).to be_valid
+ end
+
+ it 'is invalid when the assertion signature is invalid' do
+ xml = Saml::Kit::Response.build_xml(user, request, configuration: configuration)
+ altered = xml.gsub(name_id, 'altered')
+ response = Saml::Kit::Response.new(altered, configuration: configuration)
+ expect(response.assertion).to be_invalid
+ expect(response.assertion.errors[:digest_value]).to match_array(['is invalid.'])
+ end
+
+ it 'is invalid when the response signature is invalid' do
+ xml = Saml::Kit::Response.build_xml(user, request, configuration: configuration)
+ altered = xml.gsub('StatusCode', 'ALTERED')
+ response = Saml::Kit::Response.new(altered, configuration: configuration)
+ expect(response).to be_invalid
+ end
+
+ it 'is valid' do
+ response = Saml::Kit::Response.build(user, request, configuration: configuration)
+ expect(response.assertion).to be_valid
+ end
+ end
+end
spec/saml/authentication_request_spec.rb → spec/saml/kit/authentication_request_spec.rb
@@ -1,5 +1,6 @@
RSpec.describe Saml::Kit::AuthenticationRequest do
subject { described_class.new(raw_xml, configuration: configuration) }
+
let(:id) { Xml::Kit::Id.generate }
let(:assertion_consumer_service_url) { "https://#{FFaker::Internet.domain_name}/acs" }
let(:issuer) { FFaker::Movie.title }
@@ -27,11 +28,11 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
it { expect(subject.name_id_format).to eql(name_id_format) }
it { expect(subject.destination).to eql(destination) }
- describe "#valid?" do
+ describe '#valid?' do
let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
let(:metadata) { Saml::Kit::ServiceProviderMetadata.build(configuration: configuration) }
- before :each do
+ before do
allow(configuration).to receive(:registry).and_return(registry)
allow(registry).to receive(:metadata_for).and_return(metadata)
end
@@ -82,10 +83,10 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
id = Xml::Kit::Id.generate
key_pair = ::Xml::Kit::KeyPair.generate(use: :signing)
signed_xml = ::Xml::Kit::Signatures.sign(key_pair: key_pair) do |xml, signature|
- xml.tag!('samlp:AuthnRequest', "xmlns:samlp" => Saml::Kit::Namespaces::PROTOCOL, AssertionConsumerServiceURL: assertion_consumer_service_url, ID: id) do
+ xml.tag!('samlp:AuthnRequest', 'xmlns:samlp' => Saml::Kit::Namespaces::PROTOCOL, AssertionConsumerServiceURL: assertion_consumer_service_url, ID: id) do
signature.template(id)
xml.Fake do
- xml.NotAllowed "Huh?"
+ xml.NotAllowed 'Huh?'
end
end
end
@@ -94,11 +95,11 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
it 'validates a request without a signature' do
now = Time.now.utc
- raw_xml = <<-XML
-<samlp:AuthnRequest AssertionConsumerServiceURL='#{assertion_consumer_service_url}' ID='#{Xml::Kit::Id.generate}' 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>
+ raw_xml = <<-XML.strip_heredoc
+ <samlp:AuthnRequest AssertionConsumerServiceURL='#{assertion_consumer_service_url}' ID='#{Xml::Kit::Id.generate}' 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, configuration: configuration)
@@ -108,11 +109,11 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
it 'is valid when there is no signature, and the issuer is registered' do
now = Time.now.utc
- raw_xml = <<-XML
-<samlp:AuthnRequest AssertionConsumerServiceURL='#{assertion_consumer_service_url}' ID='#{Xml::Kit::Id.generate}' 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::PERSISTENT}'/>
-</samlp:AuthnRequest>
+ raw_xml = <<-XML.strip_heredoc
+ <samlp:AuthnRequest AssertionConsumerServiceURL='#{assertion_consumer_service_url}' ID='#{Xml::Kit::Id.generate}' 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::PERSISTENT}'/>
+ </samlp:AuthnRequest>
XML
allow(registry).to receive(:metadata_for).with(issuer).and_return(metadata)
@@ -122,11 +123,11 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
it 'is invalid when there is no signature, and the issuer is not registered' do
now = Time.now.utc
- raw_xml = <<-XML
-<samlp:AuthnRequest AssertionConsumerServiceURL='#{assertion_consumer_service_url}' ID='#{Xml::Kit::Id.generate}' 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::PERSISTENT}'/>
-</samlp:AuthnRequest>
+ raw_xml = <<-XML.strip_heredoc
+ <samlp:AuthnRequest AssertionConsumerServiceURL='#{assertion_consumer_service_url}' ID='#{Xml::Kit::Id.generate}' 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::PERSISTENT}'/>
+ </samlp:AuthnRequest>
XML
allow(registry).to receive(:metadata_for).with(issuer).and_return(nil)
@@ -134,7 +135,7 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
expect(subject).to be_invalid
end
- context "when the certificate is expired" do
+ context 'when the certificate is expired' do
let(:expired_certificate) do
certificate = OpenSSL::X509::Certificate.new
certificate.public_key = private_key.public_key
@@ -145,7 +146,7 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
let(:private_key) { OpenSSL::PKey::RSA.new(2048) }
let(:digest_algorithm) { OpenSSL::Digest::SHA256.new }
- before :each do
+ before do
expired_certificate.sign(private_key, digest_algorithm)
end
@@ -162,7 +163,7 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
end
end
- describe "#assertion_consumer_service_url" do
+ describe '#assertion_consumer_service_url' do
let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
let(:metadata) { instance_double(Saml::Kit::ServiceProviderMetadata) }
@@ -182,9 +183,9 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
end
end
- describe ".build" do
- let(:url) { FFaker::Internet.uri("https") }
- let(:entity_id) { FFaker::Internet.uri("https") }
+ describe '.build' do
+ let(:url) { FFaker::Internet.uri('https') }
+ let(:entity_id) { FFaker::Internet.uri('https') }
it 'provides a nice API for building metadata' do
result = described_class.build do |builder|
@@ -198,17 +199,17 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
end
end
- describe "#response_for" do
- let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: []) }
+ describe '#response_for' do
+ let(:user) { User.new }
let(:provider) do
Saml::Kit::ServiceProviderMetadata.build do |x|
- x.add_assertion_consumer_service(FFaker::Internet.uri("https"), binding: :http_post)
+ x.add_assertion_consumer_service(FFaker::Internet.uri('https'), binding: :http_post)
end
end
it 'serializes a response' do
allow(subject).to receive(:provider).and_return(provider)
- url, saml_params = subject.response_for(user, binding: :http_post, relay_state: FFaker::Movie.title)
+ _url, saml_params = subject.response_for(user, binding: :http_post, relay_state: FFaker::Movie.title)
response = provider.assertion_consumer_service_for(binding: :http_post).deserialize(saml_params)
expect(response).to be_instance_of(Saml::Kit::Response)
@@ -220,7 +221,7 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
config.generate_key_pair_for(use: :signing)
end
key_pair = configuration.key_pairs(use: :signing).first
- url, saml_params = subject.response_for(user, binding: :http_post, configuration: configuration) do |builder|
+ _url, saml_params = subject.response_for(user, binding: :http_post, configuration: configuration) do |builder|
builder.sign_with(key_pair)
end
@@ -228,4 +229,16 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
expect(response).to be_instance_of(Saml::Kit::Response)
end
end
+
+ describe '#signature.valid?' do
+ it 'returns true when the signature is valid' do
+ expect(subject.signature).to be_valid
+ end
+
+ it 'returns false when the signature does not match' do
+ raw_xml.gsub!(issuer, 'corrupt')
+ subject = described_class.new(raw_xml, configuration: configuration)
+ expect(subject.signature).not_to be_valid
+ end
+ end
end
spec/saml/bindings_spec.rb → spec/saml/kit/bindings_spec.rb
@@ -1,15 +1,16 @@
RSpec.describe Saml::Kit::Bindings do
- describe ".to_symbol" do
+ describe '.to_symbol' do
subject { described_class }
- it { expect(subject.to_symbol(Saml::Kit::Bindings::HTTP_POST)).to eql(:http_post) }
- it { expect(subject.to_symbol(Saml::Kit::Bindings::HTTP_REDIRECT)).to eql(:http_redirect) }
+ it { expect(subject.to_symbol(Saml::Kit::Bindings::HTTP_POST)).to be(:http_post) }
+ it { expect(subject.to_symbol(Saml::Kit::Bindings::HTTP_REDIRECT)).to be(:http_redirect) }
it { expect(subject.to_symbol('unknown')).to eql('unknown') }
end
- describe ".create_for" do
+ describe '.create_for' do
subject { described_class }
- let(:location) { FFaker::Internet.uri("https") }
+
+ let(:location) { FFaker::Internet.uri('https') }
it 'returns an HTTP redirect binding' do
expect(
spec/saml/composite_metadata_spec.rb → spec/saml/kit/composite_metadata_spec.rb
@@ -1,72 +1,73 @@
RSpec.describe Saml::Kit::CompositeMetadata do
subject { described_class.new(xml) }
- let(:post_binding) { Saml::Kit::Bindings::HTTP_POST }
+
+ let(:post_binding) { Saml::Kit::Bindings::HTTP_POST }
let(:redirect_binding) { Saml::Kit::Bindings::HTTP_REDIRECT }
- let(:sign_on_service) { FFaker::Internet.uri("https") }
- let(:assertion_consumer_service) { FFaker::Internet.uri("https") }
- let(:sp_logout_service) { FFaker::Internet.uri("https") }
- let(:idp_logout_service) { FFaker::Internet.uri("https") }
- let(:entity_id) { FFaker::Internet.uri("https") }
+ let(:sign_on_service) { FFaker::Internet.uri('https') }
+ let(:assertion_consumer_service) { FFaker::Internet.uri('https') }
+ let(:sp_logout_service) { FFaker::Internet.uri('https') }
+ let(:idp_logout_service) { FFaker::Internet.uri('https') }
+ let(:entity_id) { FFaker::Internet.uri('https') }
let(:sp_signing_certificate) { ::Xml::Kit::KeyPair.generate(use: :signing).certificate }
let(:sp_encryption_certificate) { ::Xml::Kit::KeyPair.generate(use: :encryption).certificate }
let(:idp_signing_certificate) { ::Xml::Kit::KeyPair.generate(use: :signing).certificate }
let(:idp_encryption_certificate) { ::Xml::Kit::KeyPair.generate(use: :encryption).certificate }
let(:xml) do
- <<-XML
-<EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="#{::Xml::Kit::Id.generate}" entityID="#{entity_id}">
- <SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
- <KeyDescriptor use="signing">
- <KeyInfo xmlns="#{::Xml::Kit::Namespaces::XMLDSIG}">
- <X509Data>
- <X509Certificate>#{sp_signing_certificate.stripped}</X509Certificate>
- </X509Data>
- </KeyInfo>
- </KeyDescriptor>
- <KeyDescriptor use="encryption">
- <KeyInfo xmlns="#{::Xml::Kit::Namespaces::XMLDSIG}">
- <X509Data>
- <X509Certificate>#{sp_encryption_certificate.stripped}</X509Certificate>
- </X509Data>
- </KeyInfo>
- </KeyDescriptor>
- <SingleLogoutService Binding="#{post_binding}" Location="#{sp_logout_service}"/>
- <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
- <AssertionConsumerService Binding="#{post_binding}" Location="#{assertion_consumer_service}" index="0" isDefault="true"/>
- </SPSSODescriptor>
- <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
- <KeyDescriptor use="signing">
- <KeyInfo xmlns="#{::Xml::Kit::Namespaces::XMLDSIG}">
- <X509Data>
- <X509Certificate>#{idp_signing_certificate.stripped}</X509Certificate>
- </X509Data>
- </KeyInfo>
- </KeyDescriptor>
- <KeyDescriptor use="encryption">
- <KeyInfo xmlns="#{::Xml::Kit::Namespaces::XMLDSIG}">
- <X509Data>
- <X509Certificate>#{idp_encryption_certificate.stripped}</X509Certificate>
- </X509Data>
- </KeyInfo>
- </KeyDescriptor>
- <SingleLogoutService Binding="#{post_binding}" Location="#{idp_logout_service}"/>
- <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
- <SingleSignOnService Binding="#{post_binding}" Location="#{sign_on_service}"/>
- <SingleSignOnService Binding="#{redirect_binding}" Location="#{sign_on_service}"/>
- <Attribute xmlns="#{Saml::Kit::Namespaces::ASSERTION}" Name="id" ></Attribute>
- </IDPSSODescriptor>
- <Organization>
- <OrganizationName xml:lang="en">Acme, Inc</OrganizationName>
- <OrganizationDisplayName xml:lang="en">Acme, Inc</OrganizationDisplayName>
- <OrganizationURL xml:lang="en">http://localhost:5000/</OrganizationURL>
- </Organization>
- <ContactPerson contactType="technical">
- <Company>mailto:hi@example.com</Company>
- </ContactPerson>
-</EntityDescriptor>
+ <<-XML.strip_heredoc
+ <EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="#{::Xml::Kit::Id.generate}" entityID="#{entity_id}">
+ <SPSSODescriptor AuthnRequestsSigned="false" WantAssertionsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
+ <KeyDescriptor use="signing">
+ <KeyInfo xmlns="#{::Xml::Kit::Namespaces::XMLDSIG}">
+ <X509Data>
+ <X509Certificate>#{sp_signing_certificate.stripped}</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+ <KeyDescriptor use="encryption">
+ <KeyInfo xmlns="#{::Xml::Kit::Namespaces::XMLDSIG}">
+ <X509Data>
+ <X509Certificate>#{sp_encryption_certificate.stripped}</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+ <SingleLogoutService Binding="#{post_binding}" Location="#{sp_logout_service}"/>
+ <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
+ <AssertionConsumerService Binding="#{post_binding}" Location="#{assertion_consumer_service}" index="0" isDefault="true"/>
+ </SPSSODescriptor>
+ <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
+ <KeyDescriptor use="signing">
+ <KeyInfo xmlns="#{::Xml::Kit::Namespaces::XMLDSIG}">
+ <X509Data>
+ <X509Certificate>#{idp_signing_certificate.stripped}</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+ <KeyDescriptor use="encryption">
+ <KeyInfo xmlns="#{::Xml::Kit::Namespaces::XMLDSIG}">
+ <X509Data>
+ <X509Certificate>#{idp_encryption_certificate.stripped}</X509Certificate>
+ </X509Data>
+ </KeyInfo>
+ </KeyDescriptor>
+ <SingleLogoutService Binding="#{post_binding}" Location="#{idp_logout_service}"/>
+ <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
+ <SingleSignOnService Binding="#{post_binding}" Location="#{sign_on_service}"/>
+ <SingleSignOnService Binding="#{redirect_binding}" Location="#{sign_on_service}"/>
+ <Attribute xmlns="#{Saml::Kit::Namespaces::ASSERTION}" Name="id" ></Attribute>
+ </IDPSSODescriptor>
+ <Organization>
+ <OrganizationName xml:lang="en">Acme, Inc</OrganizationName>
+ <OrganizationDisplayName xml:lang="en">Acme, Inc</OrganizationDisplayName>
+ <OrganizationURL xml:lang="en">http://localhost:5000/</OrganizationURL>
+ </Organization>
+ <ContactPerson contactType="technical">
+ <Company>mailto:hi@example.com</Company>
+ </ContactPerson>
+ </EntityDescriptor>
XML
end
- describe "#single_sign_on_services" do
+ describe '#single_sign_on_services' do
it 'returns the single sign on services from the idp' do
expect(subject.single_sign_on_services).to match_array([
Saml::Kit::Bindings::HttpPost.new(location: sign_on_service),
@@ -75,7 +76,7 @@ RSpec.describe Saml::Kit::CompositeMetadata do
end
end
- describe "#single_sign_on_service_for" do
+ describe '#single_sign_on_service_for' do
it 'returns the post binding' do
expect(subject.single_sign_on_service_for(binding: :http_post)).to eql(
Saml::Kit::Bindings::HttpPost.new(location: sign_on_service)
@@ -138,7 +139,7 @@ RSpec.describe Saml::Kit::CompositeMetadata do
end
it do
expect(subject.service_for(type: 'AssertionConsumerService', binding: :http_post)).to eql(
- Saml::Kit::Bindings::HttpPost.new(location: assertion_consumer_service),
+ Saml::Kit::Bindings::HttpPost.new(location: assertion_consumer_service)
)
end
it do
@@ -149,7 +150,7 @@ RSpec.describe Saml::Kit::CompositeMetadata do
end
it do
expect(subject.service_for(type: 'SingleSignOnService', binding: :http_post)).to eql(
- Saml::Kit::Bindings::HttpPost.new(location: sign_on_service),
+ Saml::Kit::Bindings::HttpPost.new(location: sign_on_service)
)
end
@@ -166,8 +167,5 @@ RSpec.describe Saml::Kit::CompositeMetadata do
)
end
- it do
- user = double(:user, name_id_for: SecureRandom.uuid)
- expect(subject.logout_request_for(user)).to be_present
- end
+ specify { expect(subject.logout_request_for(User.new)).to be_present }
end
spec/saml/configuration_spec.rb → spec/saml/kit/configuration_spec.rb
@@ -1,5 +1,5 @@
RSpec.describe Saml::Kit::Configuration do
- describe "#generate_key_pair_for" do
+ describe '#generate_key_pair_for' do
subject { described_class.new }
it 'raises an error when the use is not known' do
@@ -10,17 +10,18 @@ RSpec.describe Saml::Kit::Configuration do
it 'generates a signing key pair' do
subject.generate_key_pair_for(use: :signing)
- expect(subject.key_pairs(use: :signing).count).to eql(1)
+ expect(subject.key_pairs(use: :signing).count).to be(1)
end
it 'generates an encryption key pair' do
subject.generate_key_pair_for(use: :encryption)
- expect(subject.key_pairs(use: :encryption).count).to eql(1)
+ expect(subject.key_pairs(use: :encryption).count).to be(1)
end
end
- describe "#add_key_pair" do
+ describe '#add_key_pair' do
subject { described_class.new }
+
let(:certificate) do
certificate = OpenSSL::X509::Certificate.new
certificate.public_key = private_key.public_key
@@ -36,12 +37,12 @@ RSpec.describe Saml::Kit::Configuration do
it 'adds a signing key pair' do
subject.add_key_pair(certificate.to_pem, private_key.export, use: :signing)
- expect(subject.key_pairs(use: :signing).count).to eql(1)
+ expect(subject.key_pairs(use: :signing).count).to be(1)
end
it 'adds an encryption key pair' do
subject.add_key_pair(certificate.to_pem, private_key.export, use: :encryption)
- expect(subject.key_pairs(use: :encryption).count).to eql(1)
+ expect(subject.key_pairs(use: :encryption).count).to be(1)
end
end
end
spec/saml/kit/default_registry_spec.rb
@@ -0,0 +1,94 @@
+RSpec.describe Saml::Kit::DefaultRegistry do
+ subject { described_class.new }
+
+ let(:entity_id) { FFaker::Internet.http_url }
+ let(:service_provider_metadata) do
+ Saml::Kit::ServiceProviderMetadata.build do |builder|
+ builder.entity_id = entity_id
+ end
+ end
+ let(:identity_provider_metadata) do
+ Saml::Kit::IdentityProviderMetadata.build do |builder|
+ builder.entity_id = entity_id
+ end
+ end
+
+ describe '#metadata_for' do
+ it 'returns the metadata for the entity_id' do
+ subject.register(service_provider_metadata)
+ expect(subject.metadata_for(entity_id)).to eql(service_provider_metadata)
+ end
+ end
+
+ describe '#register_url' do
+ let(:url) { FFaker::Internet.http_url }
+
+ it 'fetches the SP metadata from a remote url and registers it' do
+ stub_request(:get, url)
+ .to_return(status: 200, body: service_provider_metadata.to_xml)
+ subject.register_url(url)
+
+ result = subject.metadata_for(entity_id)
+ expect(result).to be_present
+ expect(result).to be_instance_of(Saml::Kit::ServiceProviderMetadata)
+ end
+
+ it 'fetches the IDP metadata from a remote url' do
+ stub_request(:get, url)
+ .to_return(status: 200, body: identity_provider_metadata.to_xml)
+ subject.register_url(url)
+
+ result = subject.metadata_for(entity_id)
+ expect(result).to be_present
+ expect(result).to be_instance_of(Saml::Kit::IdentityProviderMetadata)
+ end
+
+ it 'registers metadata that serves as both an IDP and SP' do
+ xml = <<-XML.strip_heredoc
+ <EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="#{::Xml::Kit::Id.generate}" 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>
+ <AssertionConsumerService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri('https')}" index="0" isDefault="true"/>
+ </SPSSODescriptor>
+ <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
+ <SingleLogoutService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri('https')}"/>
+ <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
+ <SingleSignOnService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri('https')}"/>
+ <SingleSignOnService Binding="#{Saml::Kit::Bindings::HTTP_REDIRECT}" Location="#{FFaker::Internet.uri('https')}"/>
+ </IDPSSODescriptor>
+ <Organization>
+ <OrganizationName xml:lang="en">Acme, Inc</OrganizationName>
+ <OrganizationDisplayName xml:lang="en">Acme, Inc</OrganizationDisplayName>
+ <OrganizationURL xml:lang="en">http://localhost:5000/</OrganizationURL>
+ </Organization>
+ <ContactPerson contactType="technical">
+ <Company>mailto:hi@example.com</Company>
+ </ContactPerson>
+ </EntityDescriptor>
+ XML
+ stub_request(:get, url).to_return(status: 200, body: xml)
+ subject.register_url(url)
+
+ result = subject.metadata_for(entity_id)
+ expect(result).to be_present
+ expect(result).to be_instance_of(Saml::Kit::CompositeMetadata)
+ end
+ end
+
+ describe '#each' do
+ it 'yields each registered metadata' do
+ idp = Saml::Kit::IdentityProviderMetadata.build do |config|
+ config.entity_id = 'idp'
+ end
+ sp = Saml::Kit::ServiceProviderMetadata.build do |config|
+ config.entity_id = 'sp'
+ end
+
+ subject.register(idp)
+ subject.register(sp)
+
+ expect(subject.map(&:to_xml)).to match_array([idp.to_xml, sp.to_xml])
+ end
+ end
+end
spec/saml/document_spec.rb → spec/saml/kit/document_spec.rb
@@ -1,7 +1,8 @@
RSpec.describe Saml::Kit::Document do
- describe ".to_saml_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(:user) { User.new(attributes: { id: SecureRandom.uuid }) }
let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: Xml::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
@@ -40,7 +41,7 @@ RSpec.describe Saml::Kit::Document do
end
it 'returns an invalid document when the xml is not XML' do
- result = subject.to_saml_document("NOT XML")
+ result = subject.to_saml_document('NOT XML')
expect(result).to be_instance_of(Saml::Kit::InvalidDocument)
end
spec/saml/identity_provider_metadata_spec.rb → spec/saml/kit/identity_provider_metadata_spec.rb
@@ -1,16 +1,16 @@
RSpec.describe Saml::Kit::IdentityProviderMetadata do
subject { described_class.new(raw_metadata) }
- context "okta metadata" do
- let(:raw_metadata) { IO.read("spec/fixtures/metadata/okta.xml") }
+ describe 'okta metadata' do
+ let(:raw_metadata) { IO.read('spec/fixtures/metadata/okta.xml') }
let(:certificate) do
Hash.from_xml(raw_metadata)['EntityDescriptor']['IDPSSODescriptor']['KeyDescriptor']['KeyInfo']['X509Data']['X509Certificate']
end
- it { expect(subject.entity_id).to eql("http://www.okta.com/1") }
- it { expect(subject.name_id_formats).to match_array([ Saml::Kit::Namespaces::EMAIL_ADDRESS, Saml::Kit::Namespaces::UNSPECIFIED_NAMEID ]) }
+ it { expect(subject.entity_id).to eql('http://www.okta.com/1') }
+ it { expect(subject.name_id_formats).to match_array([Saml::Kit::Namespaces::EMAIL_ADDRESS, Saml::Kit::Namespaces::UNSPECIFIED_NAMEID]) }
it do
- location = "https://dev.oktapreview.com/app/example/1/sso/saml"
+ 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::Bindings::HTTP_POST, location: location },
{ binding: Saml::Kit::Bindings::HTTP_REDIRECT, location: location },
@@ -18,15 +18,15 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
end
it { expect(subject.single_logout_services).to be_empty }
it do
- fingerprint = "9F:74:13:3B:BC:5A:7B:8B:2D:4F:8B:EF:1E:88:EB:D1:AE:BC:19:BF:CA:19:C6:2F:0F:4B:31:1D:68:98:B0:1B"
+ fingerprint = '9F:74:13:3B:BC:5A:7B:8B:2D:4F:8B:EF:1E:88:EB:D1:AE:BC:19:BF:CA:19:C6:2F:0F:4B:31:1D:68:98:B0:1B'
expect(subject.certificates).to match_array([::Xml::Kit::Certificate.new(certificate, use: :signing)])
expect(subject.certificates.first.fingerprint.to_s).to eql(fingerprint)
end
it { expect(subject.attributes).to be_empty }
end
- context "active directory" do
- let(:raw_metadata) { IO.read("spec/fixtures/metadata/ad_2012.xml") }
+ describe 'active directory' do
+ let(:raw_metadata) { IO.read('spec/fixtures/metadata/ad_2012.xml') }
let(:xml_hash) { Hash.from_xml(raw_metadata) }
let(:signing_certificate) do
xml_hash['EntityDescriptor']['IDPSSODescriptor']['KeyDescriptor'].find { |x| x['use'] == 'signing' }['KeyInfo']['X509Data']['X509Certificate']
@@ -35,7 +35,7 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
xml_hash['EntityDescriptor']['IDPSSODescriptor']['KeyDescriptor'].find { |x| x['use'] == 'encryption' }['KeyInfo']['X509Data']['X509Certificate']
end
- it { expect(subject.entity_id).to eql("http://www.example.com/adfs/services/trust") }
+ it { expect(subject.entity_id).to eql('http://www.example.com/adfs/services/trust') }
it do
expect(subject.name_id_formats).to match_array([
Saml::Kit::Namespaces::EMAIL_ADDRESS,
@@ -44,14 +44,14 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
])
end
it do
- location = "https://www.example.com/adfs/ls/"
+ location = 'https://www.example.com/adfs/ls/'
expect(subject.single_sign_on_services.map(&:to_h)).to match_array([
{ 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/"
+ location = 'https://www.example.com/adfs/ls/'
expect(subject.single_logout_services.map(&:to_h)).to match_array([
{ location: location, binding: Saml::Kit::Bindings::HTTP_REDIRECT },
{ location: location, binding: Saml::Kit::Bindings::HTTP_POST },
@@ -66,7 +66,7 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
it { expect(subject.attributes).to be_present }
end
- describe "#validate" do
+ describe '#validate' do
it 'valid when given valid identity provider metadata' do
subject = described_class.build do |builder|
builder.attributes = [:email]
@@ -81,13 +81,13 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
it 'is invalid, when given service provider metadata' do
service_provider_metadata = Saml::Kit::ServiceProviderMetadata.build.to_xml
subject = described_class.new(service_provider_metadata)
- expect(subject).to_not be_valid
- expect(subject.errors[:base]).to include(I18n.translate("saml/kit.errors.IDPSSODescriptor.invalid"))
+ expect(subject).not_to be_valid
+ expect(subject.errors[:base]).to include(I18n.translate('saml/kit.errors.IDPSSODescriptor.invalid'))
end
it 'is invalid, when the metadata is nil' do
subject = described_class.new(nil)
- expect(subject).to_not be_valid
+ expect(subject).not_to be_valid
expect(subject.errors[:metadata]).to include("can't be blank")
end
@@ -100,25 +100,24 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
end
end
subject = described_class.new(xml.target!)
- expect(subject).to_not be_valid
+ expect(subject).not_to be_valid
expect(subject.errors[:base][0]).to include("1:0: ERROR: Element '{urn:oasis:names:tc:SAML:2.0:metadata}EntityDescriptor'")
end
it 'is invalid, when the signature is invalid' do
old_url = 'https://www.example.com/adfs/ls/'
new_url = 'https://myserver.com/hacked'
- metadata_xml = IO.read("spec/fixtures/metadata/ad_2012.xml").gsub(old_url, new_url)
+ metadata_xml = IO.read('spec/fixtures/metadata/ad_2012.xml').gsub(old_url, new_url)
subject = described_class.new(metadata_xml)
expect(subject).to be_invalid
- expect(subject.errors[:base]).to include("invalid signature.")
+ expect(subject.errors[:base]).to be_empty
+ expect(subject.errors[:digest_value]).to match_array(['is invalid.'])
+ expect(subject.errors[:signature]).to match_array(['is invalid.'])
end
end
- describe "#single_sign_on_service_for" do
- let(:post_url) { FFaker::Internet.http_url }
- let(:redirect_url) { FFaker::Internet.http_url }
-
+ describe '#single_sign_on_service_for' do
subject do
described_class.build do |builder|
builder.add_single_sign_on_service(redirect_url, binding: :http_redirect)
@@ -126,6 +125,9 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
end
end
+ let(:post_url) { FFaker::Internet.http_url }
+ let(:redirect_url) { FFaker::Internet.http_url }
+
it 'returns the POST binding' do
result = subject.single_sign_on_service_for(binding: :http_post)
expect(result.location).to eql(post_url)
@@ -143,7 +145,7 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
end
end
- describe "#want_authn_requests_signed" do
+ describe '#want_authn_requests_signed' do
it 'returns true when enabled' do
subject = described_class.build do |builder|
builder.want_authn_requests_signed = true
@@ -161,15 +163,15 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
it 'returns true when the attribute is missing' do
xml = described_class.build do |builder|
builder.want_authn_requests_signed = false
- end.to_xml.gsub("WantAuthnRequestsSigned=\"false\"", "")
+ end.to_xml.gsub('WantAuthnRequestsSigned="false"', '')
subject = described_class.new(xml)
expect(subject.want_authn_requests_signed).to be(true)
end
end
- describe "#single_logout_service_for" do
- let(:redirect_url) { FFaker::Internet.uri("https") }
- let(:post_url) { FFaker::Internet.uri("https") }
+ describe '#single_logout_service_for' do
+ let(:redirect_url) { FFaker::Internet.uri('https') }
+ let(:post_url) { FFaker::Internet.uri('https') }
let(:subject) do
described_class.build do |builder|
builder.add_single_logout_service(redirect_url, binding: :http_redirect)
@@ -187,9 +189,9 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
end
end
- describe ".build" do
- let(:url) { FFaker::Internet.uri("https") }
- let(:entity_id) { FFaker::Internet.uri("https") }
+ describe '.build' do
+ let(:url) { FFaker::Internet.uri('https') }
+ let(:entity_id) { FFaker::Internet.uri('https') }
it 'provides a nice API for building metadata' do
result = described_class.build do |builder|
@@ -203,12 +205,12 @@ RSpec.describe Saml::Kit::IdentityProviderMetadata do
end
end
- describe "#login_request_for" do
+ describe '#login_request_for' do
it 'returns a serialized login request' do
subject = described_class.build do |x|
- x.add_single_sign_on_service(FFaker::Internet.uri("https"), binding: :http_post)
+ x.add_single_sign_on_service(FFaker::Internet.uri('https'), binding: :http_post)
end
- url, saml_params = subject.login_request_for(binding: :http_post, relay_state: FFaker::Movie.title)
+ _url, saml_params = subject.login_request_for(binding: :http_post, relay_state: FFaker::Movie.title)
result = subject.single_sign_on_service_for(binding: :http_post).deserialize(saml_params)
expect(result).to be_instance_of(Saml::Kit::AuthenticationRequest)
end
spec/saml/invalid_document_spec.rb → spec/saml/kit/invalid_document_spec.rb
@@ -1,14 +1,13 @@
RSpec.describe Saml::Kit::InvalidDocument do
it 'is invalid' do
- subject = described_class.new("<xml></xml>")
+ 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")
+ subject = described_class.new('NOT XML')
expect(subject).to be_invalid
expect(subject.errors[:base]).to be_present
end
end
-
spec/saml/kit_spec.rb → spec/saml/kit/kit_spec.rb
@@ -1,5 +1,5 @@
RSpec.describe Saml::Kit do
- it "has a version number" do
+ it 'has a version number' do
expect(Saml::Kit::VERSION).not_to be nil
end
end
spec/saml/logout_request_spec.rb → spec/saml/kit/logout_request_spec.rb
@@ -1,8 +1,9 @@
RSpec.describe Saml::Kit::LogoutRequest do
subject { described_class.build(user, configuration: configuration) }
- let(:user) { double(:user, name_id_for: name_id) }
+
+ let(:user) { User.new(name_id: name_id) }
let(:name_id) { SecureRandom.uuid }
- let(:entity_id) { FFaker::Internet.uri("https") }
+ let(:entity_id) { FFaker::Internet.uri('https') }
let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
let(:configuration) do
Saml::Kit::Configuration.new do |config|
@@ -25,11 +26,11 @@ RSpec.describe Saml::Kit::LogoutRequest do
end
it 'parses the version' do
- expect(subject.version).to eql("2.0")
+ expect(subject.version).to eql('2.0')
end
it 'parses the destination' do
- destination = FFaker::Internet.uri("https")
+ destination = FFaker::Internet.uri('https')
subject = described_class.build(user, configuration: configuration) do |builder|
builder.destination = destination
end
@@ -40,15 +41,15 @@ RSpec.describe Saml::Kit::LogoutRequest do
expect(subject.name_id).to eql(name_id)
end
- describe "#valid?" do
+ describe '#valid?' do
let(:metadata) do
Saml::Kit::ServiceProviderMetadata.build(configuration: configuration) do |builder|
builder.entity_id = entity_id
- builder.add_single_logout_service(FFaker::Internet.uri("https"), binding: :http_post)
+ builder.add_single_logout_service(FFaker::Internet.uri('https'), binding: :http_post)
end
end
- before :each do
+ before do
allow(registry).to receive(:metadata_for).and_return(metadata)
end
@@ -57,7 +58,7 @@ RSpec.describe Saml::Kit::LogoutRequest do
end
it 'is invalid if the document has been tampered with' do
- issuer = FFaker::Internet.uri("https")
+ issuer = FFaker::Internet.uri('https')
raw_xml = described_class.build(user, configuration: configuration) do |builder|
builder.issuer = issuer
end.to_xml.gsub(issuer, 'corrupt')
@@ -98,11 +99,11 @@ RSpec.describe Saml::Kit::LogoutRequest do
end
it 'is valid when a single logout service url is available via the registry' do
- issuer = FFaker::Internet.uri("https")
+ issuer = FFaker::Internet.uri('https')
allow(registry).to receive(:metadata_for).with(issuer).and_return(metadata)
allow(metadata).to receive(:matches?).and_return(true)
allow(metadata).to receive(:single_logout_services).and_return([
- Saml::Kit::Bindings::HttpPost.new(location: FFaker::Internet.uri("https"))
+ Saml::Kit::Bindings::HttpPost.new(location: FFaker::Internet.uri('https'))
])
subject = described_class.build(user, configuration: configuration) do |builder|
@@ -118,7 +119,7 @@ RSpec.describe Saml::Kit::LogoutRequest do
xml.LogoutRequest ID: id do
signature.template(id)
xml.Fake do
- xml.NotAllowed "Huh?"
+ xml.NotAllowed 'Huh?'
end
end
end
@@ -126,10 +127,10 @@ RSpec.describe Saml::Kit::LogoutRequest do
end
end
- describe "#response_for" do
+ describe '#response_for' do
let(:provider) do
Saml::Kit::IdentityProviderMetadata.build do |builder|
- builder.add_single_logout_service(FFaker::Internet.uri("https"), binding: :http_post)
+ builder.add_single_logout_service(FFaker::Internet.uri('https'), binding: :http_post)
end
end
spec/saml/kit/logout_response_spec.rb
@@ -0,0 +1,5 @@
+RSpec.describe Saml::Kit::LogoutResponse do
+ it 'exists' do
+ expect(described_class).to be(described_class)
+ end
+end
spec/saml/kit/metadata_spec.rb
@@ -0,0 +1,89 @@
+RSpec.describe Saml::Kit::Metadata do
+ describe '.from' do
+ subject { described_class }
+
+ it 'returns an identity provider metadata' do
+ xml = described_class.build_xml do |x|
+ x.build_identity_provider
+ end
+ expect(subject.from(xml)).to be_instance_of(Saml::Kit::IdentityProviderMetadata)
+ end
+
+ it 'returns a service provider metadata' do
+ xml = described_class.build_xml do |x|
+ x.build_service_provider
+ end
+ expect(subject.from(xml)).to be_instance_of(Saml::Kit::ServiceProviderMetadata)
+ end
+
+ it 'generates a full metadata in a reasonable amount of time' do
+ expect do
+ described_class.build_xml do |x|
+ x.build_identity_provider
+ x.build_service_provider
+ end
+ end.to perform_under(10).ms
+ end
+
+ it 'returns a composite' do
+ xml = <<-XML.strip_heredoc
+ <EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="#{Xml::Kit::Id.generate}" entityID="#{FFaker::Internet.uri('https')}">
+ <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>
+ <AssertionConsumerService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri('https')}" index="0" isDefault="true"/>
+ </SPSSODescriptor>
+ <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
+ <SingleLogoutService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri('https')}"/>
+ <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
+ <SingleSignOnService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri('https')}"/>
+ <SingleSignOnService Binding="#{Saml::Kit::Bindings::HTTP_REDIRECT}" Location="#{FFaker::Internet.uri('https')}"/>
+ </IDPSSODescriptor>
+ <Organization>
+ <OrganizationName xml:lang="en">Acme, Inc</OrganizationName>
+ <OrganizationDisplayName xml:lang="en">Acme, Inc</OrganizationDisplayName>
+ <OrganizationURL xml:lang="en">http://localhost:5000/</OrganizationURL>
+ </Organization>
+ <ContactPerson contactType="technical">
+ <Company>mailto:hi@example.com</Company>
+ </ContactPerson>
+ </EntityDescriptor>
+ XML
+ result = subject.from(xml)
+ expect(result).to be_present
+
+ expect(result.single_sign_on_services.count).to be(2)
+ expect(result.assertion_consumer_services.count).to be(1)
+ expect(result.single_logout_services.count).to be(2)
+ expect(result.organization_name).to eql('Acme, Inc')
+ expect(result.organization_url).to eql('http://localhost:5000/')
+ expect(result.contact_person_company).to eql('mailto:hi@example.com')
+ end
+ end
+
+ describe '#certificates' do
+ it 'returns each certificate when missing a "use"' do
+ configuration = Saml::Kit::Configuration.new do |config|
+ config.generate_key_pair_for(use: :signing)
+ end
+ xml = described_class.build_xml(configuration: configuration) do |x|
+ x.embed_signature = false
+ x.build_identity_provider
+ end
+ modified_xml = xml.gsub(/use/, 'misuse')
+ subject = described_class.from(modified_xml)
+ expect(subject.certificates.count).to be(1)
+ end
+ end
+
+ describe '#signature' do
+ it 'returns the signature' do
+ subject = described_class.build do |x|
+ x.sign_with(::Xml::Kit::KeyPair.generate(use: :signing))
+ x.build_identity_provider
+ end
+
+ expect(subject.signature).to be_present
+ end
+ end
+end
spec/saml/kit/response_spec.rb
@@ -0,0 +1,536 @@
+RSpec.describe Saml::Kit::Response do
+ describe '#valid?' do
+ subject { described_class.build(user, request, configuration: configuration) }
+
+ let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: ::Xml::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) }
+ let(:user) { User.new(attributes: { id: SecureRandom.uuid }) }
+ let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
+ let(:metadata) { instance_double(Saml::Kit::IdentityProviderMetadata) }
+
+ let(:configuration) do
+ Saml::Kit::Configuration.new do |config|
+ config.entity_id = request.issuer
+ config.registry = registry
+ config.generate_key_pair_for(use: :signing)
+ end
+ end
+
+ it 'is valid' do
+ allow(registry).to receive(:metadata_for).and_return(metadata)
+ allow(metadata).to receive(:matches?).and_return(true)
+ expect(subject).to be_valid
+ 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
+ expect(subject.errors[:assertion]).to match_array(['is missing.'])
+ end
+
+ it 'is invalid if the document has been tampered with' do
+ allow(registry).to receive(:metadata_for).and_return(metadata)
+ allow(metadata).to receive(:matches?).and_return(true)
+ status_code = FFaker::Movie.title
+ xml = described_class.build(user, request) do |builder|
+ builder.status_code = status_code
+ end.to_xml.gsub(status_code, 'TAMPERED')
+ subject = described_class.new(xml)
+ expect(subject).to be_invalid
+ end
+
+ it 'is invalid when not a Response' do
+ allow(registry).to receive(:metadata_for).and_return(nil)
+ subject = described_class.new(Saml::Kit::IdentityProviderMetadata.build.to_xml)
+ expect(subject).to be_invalid
+ expect(subject.errors[:base]).to include(subject.error_message(:invalid))
+ end
+
+ it 'is invalid when the fingerprint of the certificate does not match the registered fingerprint' do
+ allow(registry).to receive(:metadata_for).and_return(metadata)
+ allow(metadata).to receive(:matches?).and_return(false)
+ expect(subject).to be_invalid
+ expect(subject.errors[:fingerprint]).to be_present
+ end
+
+ 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)
+ id = Xml::Kit::Id.generate
+ key_pair = ::Xml::Kit::KeyPair.generate(use: :signing)
+ signed_xml = ::Xml::Kit::Signatures.sign(key_pair: key_pair) 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(signed_xml)
+ expect(subject).to be_invalid
+ expect(subject.errors[:base]).to be_present
+ end
+
+ it 'validates the version' do
+ allow(registry).to receive(:metadata_for).and_return(metadata)
+ allow(metadata).to receive(:matches?).and_return(true)
+ subject = described_class.build(user, request) do |builder|
+ builder.version = '1.1'
+ end
+ expect(subject).to be_invalid
+ expect(subject.errors[:version]).to be_present
+ end
+
+ it 'validates the id' do
+ allow(registry).to receive(:metadata_for).and_return(metadata)
+ allow(metadata).to receive(:matches?).and_return(true)
+ subject = described_class.build(user, request) do |builder|
+ builder.id = nil
+ end
+ expect(subject).to be_invalid
+ expect(subject.errors[:id]).to be_present
+ end
+
+ it 'validates the status code' do
+ allow(registry).to receive(:metadata_for).and_return(metadata)
+ allow(metadata).to receive(:matches?).and_return(true)
+ subject = described_class.build(user, request) do |builder|
+ builder.status_code = Saml::Kit::Namespaces::REQUESTER_ERROR
+ end
+ expect(subject).to be_invalid
+ expect(subject.errors[:status_code]).to be_present
+ end
+
+ it 'validates the InResponseTo' do
+ allow(registry).to receive(:metadata_for).and_return(metadata)
+ allow(metadata).to receive(:matches?).and_return(true)
+ xml = described_class.build(user, request).to_xml
+ subject = described_class.new(xml, request_id: SecureRandom.uuid)
+
+ expect(subject).to be_invalid
+ expect(subject.errors[:in_response_to]).to be_present
+ end
+
+ it 'is invalid after a valid session window' do
+ allow(registry).to receive(:metadata_for).and_return(metadata)
+ allow(metadata).to receive(:matches?).and_return(true)
+
+ subject = described_class.build(user, request)
+ travel_to Saml::Kit.configuration.session_timeout.from_now + 5.seconds
+ expect(subject).not_to be_valid
+ expect(subject.errors[:assertion]).to match_array(['must not be expired.'])
+ end
+
+ it 'is invalid before the valid session window' do
+ allow(registry).to receive(:metadata_for).and_return(metadata)
+ allow(metadata).to receive(:matches?).and_return(true)
+
+ subject = described_class.build(user, request)
+ travel_to((Saml::Kit.configuration.clock_drift + 1.second).before(Time.now))
+ expect(subject).to be_invalid
+ expect(subject.errors[:assertion]).to match_array(['must not be expired.'])
+ end
+
+ it 'is invalid when the audience does not match the expected issuer' do
+ allow(registry).to receive(:metadata_for).and_return(metadata)
+ allow(metadata).to receive(:matches?).and_return(true)
+
+ allow(configuration).to receive(:issuer).and_return(FFaker::Internet.uri('https'))
+ allow(request).to receive(:issuer).and_return(FFaker::Internet.uri('https'))
+
+ expect(subject).to be_invalid
+ expect(subject.errors[:audience]).to be_present
+ end
+
+ it 'is invalid' do
+ now = Time.now.utc
+ destination = FFaker::Internet.uri('https')
+ raw_xml = <<-XML.strip_heredoc
+ <?xml version="1.0"?>
+ <samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{Xml::Kit::Id.generate}" 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
+
+ it 'is invalid when there are 2 assertions' do
+ id = Xml::Kit::Id.generate
+ issuer = FFaker::Internet.uri('https')
+ key_pair = ::Xml::Kit::KeyPair.generate(use: :signing)
+ response_options = {
+ ID: id,
+ Version: '2.0',
+ IssueInstant: Time.now.iso8601,
+ Consent: Saml::Kit::Namespaces::UNSPECIFIED,
+ InResponseTo: request.id,
+ xmlns: Saml::Kit::Namespaces::PROTOCOL,
+ }
+ assertion_options = {
+ ID: Xml::Kit::Id.generate,
+ IssueInstant: Time.now.iso8601,
+ Version: '2.0',
+ xmlns: Saml::Kit::Namespaces::ASSERTION,
+ }
+ raw_xml = ::Xml::Kit::Signatures.sign(key_pair: key_pair) do |xml, signature|
+ xml.instruct!
+ xml.Response response_options do
+ xml.Issuer(issuer, xmlns: Saml::Kit::Namespaces::ASSERTION)
+ xml.Status do
+ xml.StatusCode Value: Saml::Kit::Namespaces::SUCCESS
+ end
+ xml.Assertion(assertion_options) do
+ xml.Issuer issuer
+ signature.template(assertion_options[:ID])
+ xml.Subject do
+ xml.NameID FFaker::Internet.email, Format: Saml::Kit::Namespaces::EMAIL_ADDRESS
+ xml.SubjectConfirmation Method: Saml::Kit::Namespaces::BEARER do
+ xml.SubjectConfirmationData '', InResponseTo: request.id, NotOnOrAfter: 3.hours.from_now.utc.iso8601, Recipient: FFaker::Internet.uri('https')
+ end
+ end
+ xml.Conditions NotBefore: Time.now.utc.iso8601, NotOnOrAfter: 3.hours.from_now.utc.iso8601 do
+ xml.AudienceRestriction do
+ xml.Audience request.issuer
+ end
+ end
+ xml.AuthnStatement AuthnInstant: Time.now.iso8601, SessionIndex: assertion_options[:ID], SessionNotOnOrAfter: 3.hours.from_now.utc.iso8601 do
+ xml.AuthnContext do
+ xml.AuthnContextClassRef Saml::Kit::Namespaces::PASSWORD
+ end
+ end
+ end
+ new_options = assertion_options.merge(ID: Xml::Kit::Id.generate)
+ xml.Assertion(new_options) do
+ xml.Issuer issuer
+ xml.Subject do
+ xml.NameID FFaker::Internet.email, Format: Saml::Kit::Namespaces::EMAIL_ADDRESS
+ xml.SubjectConfirmation Method: Saml::Kit::Namespaces::BEARER do
+ xml.SubjectConfirmationData '', InResponseTo: request.id, NotOnOrAfter: 3.hours.from_now.utc.iso8601, Recipient: FFaker::Internet.uri('https')
+ end
+ end
+ xml.Conditions NotBefore: Time.now.utc.iso8601, NotOnOrAfter: 3.hours.from_now.utc.iso8601 do
+ xml.AudienceRestriction do
+ xml.Audience request.issuer
+ end
+ end
+ xml.AuthnStatement AuthnInstant: Time.now.iso8601, SessionIndex: new_options[:ID], SessionNotOnOrAfter: 3.hours.from_now.utc.iso8601 do
+ xml.AuthnContext do
+ xml.AuthnContextClassRef Saml::Kit::Namespaces::PASSWORD
+ end
+ end
+ end
+ end
+ end
+ subject = described_class.new(raw_xml)
+ expect(subject).not_to be_valid
+ expect(subject.errors.full_messages).to include('must contain a single Assertion.')
+ end
+
+ it 'is invalid when the assertion has a signature and has been tampered with' do
+ user = User.new(attributes: { token: SecureRandom.uuid })
+ request = Saml::Kit::AuthenticationRequest.build
+ document = described_class.build(user, request, configuration: configuration) do |x|
+ x.embed_signature = false
+ x.assertion.embed_signature = true
+ end
+
+ altered_xml = document.to_xml.gsub(/token/, 'heck')
+ subject = described_class.new(altered_xml)
+ expect(subject).not_to be_valid
+ expect(subject.errors[:digest_value]).to be_present
+ end
+
+ it 'is invalid when we do not have a private key to decrypt the assertion' do
+ xml = described_class.build_xml(user, request) do |x|
+ x.encrypt_with(::Xml::Kit::KeyPair.generate(use: :encryption))
+ end
+
+ subject = described_class.new(xml)
+ expect(subject).to be_invalid
+ expect(subject.errors[:assertion]).to match_array(['cannot be decrypted.'])
+ end
+ end
+
+ describe '#signed?' do
+ let(:now) { Time.now.utc }
+ let(:id) { Xml::Kit::Id.generate }
+ let(:url) { FFaker::Internet.uri('https') }
+
+ it 'returns true when the Assertion is signed' do
+ xml = <<-XML.strip_heredoc
+ <?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).not_to be_signed
+ expect(subject.assertion).to be_signed
+ end
+
+ it 'returns true when the Response is signed' do
+ xml = <<-XML.strip_heredoc
+ <?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.strip_heredoc
+ <?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).not_to be_signed
+ end
+ end
+
+ describe '#certificate' do
+ let(:now) { Time.now.utc }
+ let(:id) { Xml::Kit::Id.generate }
+ let(:url) { FFaker::Internet.uri('https') }
+ let(:certificate) do
+ ::Xml::Kit::Certificate.new(
+ ::Xml::Kit::SelfSignedCertificate.new.create(passphrase: 'password')[0],
+ use: :signing
+ )
+ end
+
+ it 'returns the certificate when the Assertion is signed' do
+ xml = <<-XML.strip_heredoc
+ <?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.stripped}</ds:X509Certificate>
+ </ds:X509Data>
+ </KeyInfo>
+ </ds:Signature>
+ </Assertion>
+ </samlp:Response>
+ XML
+ subject = described_class.new(xml)
+ expect(subject.signature).not_to be_present
+ expect(subject.assertion.signature).to be_present
+ expect(subject.assertion.signature.certificate.stripped).to eql(certificate.stripped)
+ end
+
+ it 'returns the certificate when the Response is signed' do
+ xml = <<-XML.strip_heredoc
+ <?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.signature.certificate).to eql(certificate)
+ end
+
+ it 'returns nil when there is no signature' do
+ xml = <<-XML.strip_heredoc
+ <?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.signature).not_to be_present
+ end
+ end
+
+ describe 'encrypted assertion' do
+ let(:id) { Xml::Kit::Id.generate }
+ let(:now) { Time.now.utc }
+ let(:assertion_consumer_service_url) { FFaker::Internet.uri('https') }
+ let(:password) { FFaker::Movie.title }
+ let(:email) { FFaker::Internet.email }
+ let(:created_at) { Time.now }
+ let(:assertion) do
+ <<-XML.strip_heredoc
+ <Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="2017-11-23T04:33:58Z" Version="2.0">
+ <Issuer>#{FFaker::Internet.uri('https')}</Issuer>
+ <Subject>
+ <NameID Format="#{Saml::Kit::Namespaces::PERSISTENT}">#{SecureRandom.uuid}</NameID>
+ <SubjectConfirmation Method="#{Saml::Kit::Namespaces::BEARER}">
+ <SubjectConfirmationData InResponseTo="#{SecureRandom.uuid}" 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>#{Saml::Kit::Namespaces::PASSWORD}</AuthnContextClassRef>
+ </AuthnContext>
+ </AuthnStatement>
+ <AttributeStatement>
+ <Attribute Name="email" NameFormat="#{Saml::Kit::Namespaces::URI}">
+ <AttributeValue>#{email}</AttributeValue>
+ </Attribute>
+ <Attribute Name="created_at" NameFormat="#{Saml::Kit::Namespaces::URI}">
+ <AttributeValue>#{created_at.iso8601}</AttributeValue>
+ </Attribute>
+ </AttributeStatement>
+ </Assertion>
+XML
+ end
+
+ it 'parses the encrypted assertion' do
+ certificate_pem, private_key_pem = ::Xml::Kit::SelfSignedCertificate.new.create(passphrase: password)
+ 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(:private_keys).with(use: :encryption).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.strip_heredoc
+ <samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" xmlns:saml="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" Version="2.0" IssueInstant="#{now.iso8601}" Destination="#{assertion_consumer_service_url}" InResponseTo="#{Xml::Kit::Id.generate}">
+ <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.attributes).to match_array([
+ ['created_at', created_at.iso8601],
+ ['email', email]
+ ])
+ end
+ end
+
+ describe 'parsing' do
+ let(:user) { User.new(attributes: attributes) }
+ let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: Xml::Kit::Id.generate, signed?: true, trusted?: true, provider: nil, assertion_consumer_service_url: FFaker::Internet.uri('https'), name_id_format: '', issuer: FFaker::Internet.uri('https')) }
+ let(:attributes) { { name: 'mo' } }
+
+ it 'returns the name id' do
+ subject = described_class.build(user, request)
+ expect(subject.name_id).to eql(user.name_id)
+ end
+
+ it 'returns the single attributes' do
+ subject = described_class.build(user, request)
+ expect(subject.attributes).to eql('name' => 'mo')
+ end
+
+ it 'returns the multiple attributes' do
+ attributes[:age] = 33
+ subject = described_class.build(user, request)
+ expect(subject.attributes).to eql('name' => 'mo', 'age' => '33')
+ end
+ end
+end
spec/saml/service_provider_metadata_spec.rb → spec/saml/kit/service_provider_metadata_spec.rb
@@ -1,9 +1,9 @@
RSpec.describe Saml::Kit::ServiceProviderMetadata do
- 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") }
+ 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 do
subject do
@@ -48,7 +48,7 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
end
end
- describe "#validate" do
+ describe '#validate' do
let(:service_provider_metadata) do
described_class.build(configuration: configuration) do |builder|
builder.entity_id = entity_id
@@ -69,9 +69,9 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
end
it 'is invalid, when given identity provider metadata' do
- subject = described_class.new(IO.read("spec/fixtures/metadata/okta.xml"))
+ subject = described_class.new(IO.read('spec/fixtures/metadata/okta.xml'))
expect(subject).to be_invalid
- expect(subject.errors[:base]).to include(I18n.translate("saml/kit.errors.SPSSODescriptor.invalid"))
+ expect(subject.errors[:base]).to include(I18n.translate('saml/kit.errors.SPSSODescriptor.invalid'))
end
it 'is invalid, when the metadata is nil' do
@@ -89,7 +89,7 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
end
end
subject = described_class.new(xml.target!)
- expect(subject).to_not be_valid
+ expect(subject).not_to be_valid
expect(subject.errors[:base][0]).to include("1:0: ERROR: Element '{urn:oasis:names:tc:SAML:2.0:metadata}EntityDescriptor'")
end
@@ -98,46 +98,47 @@ RSpec.describe Saml::Kit::ServiceProviderMetadata do
metadata_xml = service_provider_metadata.gsub(acs_post_url, new_url)
subject = described_class.new(metadata_xml)
expect(subject).to be_invalid
- expect(subject.errors[:base]).to include("invalid signature.")
+ expect(subject.errors[:digest_value]).to include('is invalid.')
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="#{::Xml::Kit::Id.generate}" 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 = <<-XML.strip_heredoc
+ <?xml version="1.0" encoding="UTF-8"?>
+ <EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="#{::Xml::Kit::Id.generate}" 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
+ describe '#matches?' do
+ subject { described_class.build(configuration: configuration) }
+
let(:configuration) do
config = Saml::Kit::Configuration.new
config.generate_key_pair_for(use: :signing)
config
end
- subject { Saml::Kit::ServiceProviderMetadata.build(configuration: configuration) }
it 'returns true when the fingerprint matches one of the signing certificates' do
certificate = Hash.from_xml(subject.to_xml)['EntityDescriptor']['Signature']['KeyInfo']['X509Data']['X509Certificate']
fingerprint = ::Xml::Kit::Fingerprint.new(certificate)
- expect(subject.matches?(fingerprint)).to be_truthy
+ expect(subject).to be_matches(fingerprint)
end
it 'returns false when the fingerprint does not match one of the signing certificates' do
- certificate, _ = ::Xml::Kit::SelfSignedCertificate.new.create(passphrase: 'password')
+ certificate, = ::Xml::Kit::SelfSignedCertificate.new.create(passphrase: 'password')
fingerprint = ::Xml::Kit::Fingerprint.new(certificate)
- expect(subject.matches?(fingerprint)).to be_falsey
+ expect(subject).not_to be_matches(fingerprint)
end
end
- describe ".build" do
- let(:assertion_consumer_service_url) { FFaker::Internet.uri("https") }
+ describe '.build' do
+ let(:assertion_consumer_service_url) { FFaker::Internet.uri('https') }
it 'provides a nice API for building metadata' do
result = described_class.build do |builder|
spec/saml/kit/signature_spec.rb
@@ -0,0 +1,98 @@
+RSpec.describe Saml::Kit::Signature do
+ subject { described_class.new(signed_document.at_xpath('//ds:Signature')) }
+
+ let(:key_pair) { ::Xml::Kit::KeyPair.generate(use: :signing) }
+ let(:signed_document) do
+ Saml::Kit::AuthenticationRequest.build do |x|
+ x.sign_with(key_pair)
+ end
+ end
+ let(:xml_hash) { Hash.from_xml(subject.to_xml) }
+
+ specify { expect(subject.digest_value).to eql(xml_hash['Signature']['SignedInfo']['Reference']['DigestValue']) }
+ specify { expect(subject.digest_method).to eql(xml_hash['Signature']['SignedInfo']['Reference']['DigestMethod']['Algorithm']) }
+ specify { expect(subject.signature_value).to eql(xml_hash['Signature']['SignatureValue']) }
+ specify { expect(subject.signature_method).to eql(xml_hash['Signature']['SignedInfo']['SignatureMethod']['Algorithm']) }
+ specify { expect(subject.canonicalization_method).to eql(xml_hash['Signature']['SignedInfo']['CanonicalizationMethod']['Algorithm']) }
+ specify { expect(subject.transforms).to eql(xml_hash['Signature']['SignedInfo']['Reference']['Transforms']['Transform'].map { |x| x['Algorithm'] }) }
+ specify do
+ expected = ::Xml::Kit::Certificate.new(xml_hash['Signature']['KeyInfo']['X509Data']['X509Certificate'], use: :signing)
+ expect(subject.certificate).to eql(expected)
+ end
+
+ describe '#valid?' do
+ it 'returns true when the signature is valid' do
+ expect(subject).to be_valid
+ end
+
+ it 'is invalid when the xml has been tampered' do
+ signed_document.at_xpath('//saml:Issuer').content = 'INVALID'
+ expect(subject).not_to be_valid
+ expect(subject.errors[:digest_value]).to be_present
+ end
+
+ it 'is invalid when the signature is missing' do
+ subject = described_class.new(nil)
+ expect(subject).not_to be_valid
+ expect(subject.errors[:base]).to match_array(['is missing.'])
+ end
+
+ describe 'certificate validation' do
+ let(:key_pair) { ::Xml::Kit::KeyPair.new(expired_certificate, private_key, nil, :signing) }
+ let(:private_key) { OpenSSL::PKey::RSA.new(2048) }
+ let(:expired_certificate) do
+ certificate = OpenSSL::X509::Certificate.new
+ certificate.not_before = not_before
+ certificate.not_after = not_after
+ certificate.public_key = private_key.public_key
+ certificate.sign(private_key, OpenSSL::Digest::SHA256.new)
+ certificate
+ end
+
+ context 'when the certificate is expired' do
+ let(:not_before) { 10.minutes.ago }
+ let(:not_after) { 1.minute.ago }
+
+ it 'is invalid' do
+ expect(subject).to be_invalid
+ expect(subject.errors[:certificate]).to match_array([
+ "Not valid before #{expired_certificate.not_before}. Not valid after #{expired_certificate.not_after}."
+ ])
+ end
+ end
+
+ context 'when the certificate is not active yet' do
+ let(:not_before) { 10.minutes.from_now }
+ let(:not_after) { 20.minute.from_now }
+
+ it 'invalid' do
+ expect(subject).to be_invalid
+ expect(subject.errors[:certificate]).to match_array([
+ "Not valid before #{expired_certificate.not_before}. Not valid after #{expired_certificate.not_after}."
+ ])
+ end
+ end
+ end
+ end
+
+ describe '#to_h' do
+ it 'returns a hash representation of the signature' do
+ expected = Hash.from_xml(signed_document.to_s)['AuthnRequest']['Signature']
+ expect(subject.to_h).to eql(expected)
+ end
+ end
+
+ describe '#present?' do
+ context 'when a signature is not present' do
+ it 'return false' do
+ expect(described_class.new(nil)).not_to be_present
+ end
+ end
+
+ context 'when a signature is present' do
+ it 'returns true' do
+ expect(subject).to be_present
+ end
+ end
+ end
+end
spec/saml/assertion_spec.rb
@@ -1,63 +0,0 @@
-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
-
- it 'interprets integers correctly' do
- configuration.clock_drift = 30
- 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)
- expect(subject).to be_active
- expect(subject).to_not be_expired
- end
- end
-
- describe "#present?" do
- it 'returns false when the assertion is empty' do
- xml_hash = { 'Response' => { } }
- subject = described_class.new(xml_hash)
- expect(subject).to_not be_present
- end
-
- it 'returns true when the assertion is present' do
- xml_hash = { 'Response' => { 'Assertion' => { 'Conditions' => { } } } }
- subject = described_class.new(xml_hash)
- expect(subject).to be_present
- end
- end
-end
spec/saml/default_registry_spec.rb
@@ -1,93 +0,0 @@
-RSpec.describe Saml::Kit::DefaultRegistry do
- subject { described_class.new }
- let(:entity_id) { FFaker::Internet.http_url }
- let(:service_provider_metadata) do
- Saml::Kit::ServiceProviderMetadata.build do |builder|
- builder.entity_id = entity_id
- end
- end
- let(:identity_provider_metadata) do
- Saml::Kit::IdentityProviderMetadata.build do |builder|
- builder.entity_id = entity_id
- end
- end
-
- describe "#metadata_for" do
- it 'returns the metadata for the entity_id' do
- subject.register(service_provider_metadata)
- expect(subject.metadata_for(entity_id)).to eql(service_provider_metadata)
- end
- end
-
- describe "#register_url" do
- let(:url) { FFaker::Internet.http_url }
-
- it 'fetches the SP metadata from a remote url and registers it' do
- stub_request(:get, url).
- to_return(status: 200, body: service_provider_metadata.to_xml)
- subject.register_url(url)
-
- result = subject.metadata_for(entity_id)
- expect(result).to be_present
- expect(result).to be_instance_of(Saml::Kit::ServiceProviderMetadata)
- end
-
- it 'fetches the IDP metadata from a remote url' do
- stub_request(:get, url).
- to_return(status: 200, body: identity_provider_metadata.to_xml)
- subject.register_url(url)
-
- result = subject.metadata_for(entity_id)
- expect(result).to be_present
- expect(result).to be_instance_of(Saml::Kit::IdentityProviderMetadata)
- end
-
- it 'registers metadata that serves as both an IDP and SP' do
- xml = <<-XML
-<EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="#{::Xml::Kit::Id.generate}" 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>
- <AssertionConsumerService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}" index="0" isDefault="true"/>
- </SPSSODescriptor>
- <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
- <SingleLogoutService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}"/>
- <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
- <SingleSignOnService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}"/>
- <SingleSignOnService Binding="#{Saml::Kit::Bindings::HTTP_REDIRECT}" Location="#{FFaker::Internet.uri("https")}"/>
- </IDPSSODescriptor>
- <Organization>
- <OrganizationName xml:lang="en">Acme, Inc</OrganizationName>
- <OrganizationDisplayName xml:lang="en">Acme, Inc</OrganizationDisplayName>
- <OrganizationURL xml:lang="en">http://localhost:5000/</OrganizationURL>
- </Organization>
- <ContactPerson contactType="technical">
- <Company>mailto:hi@example.com</Company>
- </ContactPerson>
-</EntityDescriptor>
- XML
- stub_request(:get, url).to_return(status: 200, body: xml)
- subject.register_url(url)
-
- result = subject.metadata_for(entity_id)
- expect(result).to be_present
- expect(result).to be_instance_of(Saml::Kit::CompositeMetadata)
- end
- end
-
- describe "#each" do
- it 'yields each registered metadata' do
- idp = Saml::Kit::IdentityProviderMetadata.build do |config|
- config.entity_id = "idp"
- end
- sp = Saml::Kit::ServiceProviderMetadata.build do |config|
- config.entity_id = "sp"
- end
-
- subject.register(idp)
- subject.register(sp)
-
- expect(subject.map(&:to_xml)).to match_array([idp.to_xml, sp.to_xml])
- end
- end
-end
spec/saml/logout_response_spec.rb
@@ -1,2 +0,0 @@
-RSpec.describe Saml::Kit::LogoutResponse do
-end
spec/saml/metadata_spec.rb
@@ -1,65 +0,0 @@
-RSpec.describe Saml::Kit::Metadata do
- describe ".from" do
- subject { described_class }
-
- it 'returns an identity provider metadata' do
- xml = Saml::Kit::IdentityProviderMetadata.build.to_xml
- expect(subject.from(xml)).to be_instance_of(Saml::Kit::IdentityProviderMetadata)
- end
-
- it 'returns a service provider metadata' do
- xml = Saml::Kit::ServiceProviderMetadata.build.to_xml
- expect(subject.from(xml)).to be_instance_of(Saml::Kit::ServiceProviderMetadata)
- end
-
- it 'returns a composite' do
- xml = <<-XML
-<EntityDescriptor xmlns="#{Saml::Kit::Namespaces::METADATA}" ID="#{Xml::Kit::Id.generate}" entityID="#{FFaker::Internet.uri("https")}">
- <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>
- <AssertionConsumerService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}" index="0" isDefault="true"/>
- </SPSSODescriptor>
- <IDPSSODescriptor WantAuthnRequestsSigned="true" protocolSupportEnumeration="#{Saml::Kit::Namespaces::PROTOCOL}">
- <SingleLogoutService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}"/>
- <NameIDFormat>#{Saml::Kit::Namespaces::PERSISTENT}</NameIDFormat>
- <SingleSignOnService Binding="#{Saml::Kit::Bindings::HTTP_POST}" Location="#{FFaker::Internet.uri("https")}"/>
- <SingleSignOnService Binding="#{Saml::Kit::Bindings::HTTP_REDIRECT}" Location="#{FFaker::Internet.uri("https")}"/>
- </IDPSSODescriptor>
- <Organization>
- <OrganizationName xml:lang="en">Acme, Inc</OrganizationName>
- <OrganizationDisplayName xml:lang="en">Acme, Inc</OrganizationDisplayName>
- <OrganizationURL xml:lang="en">http://localhost:5000/</OrganizationURL>
- </Organization>
- <ContactPerson contactType="technical">
- <Company>mailto:hi@example.com</Company>
- </ContactPerson>
-</EntityDescriptor>
- XML
- result = subject.from(xml)
- expect(result).to be_present
-
- expect(result.single_sign_on_services.count).to eql(2)
- expect(result.assertion_consumer_services.count).to eql(1)
- expect(result.single_logout_services.count).to eql(2)
- expect(result.organization_name).to eql("Acme, Inc")
- expect(result.organization_url).to eql("http://localhost:5000/")
- expect(result.contact_person_company).to eql("mailto:hi@example.com")
- end
- end
-
- describe "#certificates" do
- it 'returns each certificate when missing a "use"' do
- configuration = Saml::Kit::Configuration.new do |config|
- config.generate_key_pair_for(use: :signing)
- end
- xml = Saml::Kit::Metadata.build_xml(configuration: configuration) do |x|
- x.embed_signature = false
- x.build_identity_provider
- end
- modified_xml = xml.gsub(/use/, 'misuse')
- subject = described_class.from(modified_xml)
- expect(subject.certificates.count).to eql(1)
- end
- end
-end
spec/saml/response_spec.rb
@@ -1,523 +0,0 @@
-RSpec.describe Saml::Kit::Response do
- describe "#valid?" do
- let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: ::Xml::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) }
- let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: { id: SecureRandom.uuid }) }
- let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
- let(:metadata) { instance_double(Saml::Kit::IdentityProviderMetadata) }
- subject { described_class.build(user, request, configuration: configuration) }
- let(:configuration) do
- Saml::Kit::Configuration.new do |config|
- config.entity_id = request.issuer
- config.registry = registry
- config.generate_key_pair_for(use: :signing)
- end
- end
-
- it 'is valid' do
- allow(registry).to receive(:metadata_for).and_return(metadata)
- allow(metadata).to receive(:matches?).and_return(true)
- expect(subject).to be_valid
- 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
- end
-
- it 'is invalid if the document has been tampered with' do
- allow(registry).to receive(:metadata_for).and_return(metadata)
- allow(metadata).to receive(:matches?).and_return(true)
- status_code = FFaker::Movie.title
- xml = described_class.build(user, request) do |builder|
- builder.status_code = status_code
- end.to_xml.gsub(status_code, "TAMPERED")
- subject = described_class.new(xml)
- expect(subject).to be_invalid
- end
-
- it 'is invalid when not a Response' do
- allow(registry).to receive(:metadata_for).and_return(nil)
- subject = described_class.new(Saml::Kit::IdentityProviderMetadata.build.to_xml)
- expect(subject).to be_invalid
- expect(subject.errors[:base]).to include(subject.error_message(:invalid))
- end
-
- it 'is invalid when the fingerprint of the certificate does not match the registered fingerprint' do
- allow(registry).to receive(:metadata_for).and_return(metadata)
- allow(metadata).to receive(:matches?).and_return(false)
- expect(subject).to be_invalid
- expect(subject.errors[:fingerprint]).to be_present
- end
-
- 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)
- id = Xml::Kit::Id.generate
- key_pair = ::Xml::Kit::KeyPair.generate(use: :signing)
- signed_xml = ::Xml::Kit::Signatures.sign(key_pair: key_pair) 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(signed_xml)
- expect(subject).to be_invalid
- expect(subject.errors[:base]).to be_present
- end
-
- it 'validates the version' do
- allow(registry).to receive(:metadata_for).and_return(metadata)
- allow(metadata).to receive(:matches?).and_return(true)
- subject = described_class.build(user, request) do |builder|
- builder.version = "1.1"
- end
- expect(subject).to be_invalid
- expect(subject.errors[:version]).to be_present
- end
-
- it 'validates the id' do
- allow(registry).to receive(:metadata_for).and_return(metadata)
- allow(metadata).to receive(:matches?).and_return(true)
- subject = described_class.build(user, request) do |builder|
- builder.id = nil
- end
- expect(subject).to be_invalid
- expect(subject.errors[:id]).to be_present
- end
-
- it 'validates the status code' do
- allow(registry).to receive(:metadata_for).and_return(metadata)
- allow(metadata).to receive(:matches?).and_return(true)
- subject = described_class.build(user, request) do |builder|
- builder.status_code = Saml::Kit::Namespaces::REQUESTER_ERROR
- end
- expect(subject).to be_invalid
- expect(subject.errors[:status_code]).to be_present
- end
-
- it 'validates the InResponseTo' do
- allow(registry).to receive(:metadata_for).and_return(metadata)
- allow(metadata).to receive(:matches?).and_return(true)
- xml = described_class.build(user, request).to_xml
- subject = described_class.new(xml, request_id: SecureRandom.uuid)
-
- expect(subject).to be_invalid
- expect(subject.errors[:in_response_to]).to be_present
- end
-
- it 'is invalid after a valid session window' do
- allow(registry).to receive(:metadata_for).and_return(metadata)
- allow(metadata).to receive(:matches?).and_return(true)
-
- subject = described_class.build(user, request)
- travel_to Saml::Kit.configuration.session_timeout.from_now + 5.seconds
- expect(subject).to_not be_valid
- expect(subject.errors[:base]).to be_present
- end
-
- it 'is invalid before the valid session window' do
- allow(registry).to receive(:metadata_for).and_return(metadata)
- allow(metadata).to receive(:matches?).and_return(true)
-
- subject = described_class.build(user, request)
- 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
-
- it 'is invalid when the audience does not match the expected issuer' do
- allow(registry).to receive(:metadata_for).and_return(metadata)
- allow(metadata).to receive(:matches?).and_return(true)
-
- allow(configuration).to receive(:issuer).and_return(FFaker::Internet.uri("https"))
- allow(request).to receive(:issuer).and_return(FFaker::Internet.uri("https"))
-
- expect(subject).to be_invalid
- expect(subject.errors[:audience]).to be_present
- end
-
- it 'is invalid' do
- now = Time.now.utc
- destination = FFaker::Internet.uri("https")
- raw_xml = <<-XML
-<?xml version="1.0"?>
-<samlp:Response xmlns:samlp="#{Saml::Kit::Namespaces::PROTOCOL}" ID="#{Xml::Kit::Id.generate}" 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
-
- it 'is invalid when there are 2 assertions' do
- id = Xml::Kit::Id.generate
- issuer = FFaker::Internet.uri("https")
- key_pair = ::Xml::Kit::KeyPair.generate(use: :signing)
- response_options = {
- ID: id,
- Version: "2.0",
- IssueInstant: Time.now.iso8601,
- Consent: Saml::Kit::Namespaces::UNSPECIFIED,
- InResponseTo: request.id,
- xmlns: Saml::Kit::Namespaces::PROTOCOL,
- }
- assertion_options = {
- ID: Xml::Kit::Id.generate,
- IssueInstant: Time.now.iso8601,
- Version: "2.0",
- xmlns: Saml::Kit::Namespaces::ASSERTION,
- }
- xml = ::Xml::Kit::Signatures.sign(key_pair: key_pair) do |xml, signature|
- xml.instruct!
- xml.Response response_options do
- xml.Issuer(issuer, xmlns: Saml::Kit::Namespaces::ASSERTION)
- xml.Status do
- xml.StatusCode Value: Saml::Kit::Namespaces::SUCCESS
- end
- xml.Assertion(assertion_options) do
- xml.Issuer issuer
- signature.template(assertion_options[:ID])
- xml.Subject do
- xml.NameID FFaker::Internet.email, Format: Saml::Kit::Namespaces::EMAIL_ADDRESS
- xml.SubjectConfirmation Method: Saml::Kit::Namespaces::BEARER do
- xml.SubjectConfirmationData "", InResponseTo: request.id, NotOnOrAfter: 3.hours.from_now.utc.iso8601, Recipient: FFaker::Internet.uri("https")
- end
- end
- xml.Conditions NotBefore: Time.now.utc.iso8601, NotOnOrAfter: 3.hours.from_now.utc.iso8601 do
- xml.AudienceRestriction do
- xml.Audience request.issuer
- end
- end
- xml.AuthnStatement AuthnInstant: Time.now.iso8601, SessionIndex: assertion_options[:ID], SessionNotOnOrAfter: 3.hours.from_now.utc.iso8601 do
- xml.AuthnContext do
- xml.AuthnContextClassRef Saml::Kit::Namespaces::PASSWORD
- end
- end
- end
- new_options = assertion_options.merge(ID: Xml::Kit::Id.generate)
- xml.Assertion(new_options) do
- xml.Issuer issuer
- xml.Subject do
- xml.NameID FFaker::Internet.email, Format: Saml::Kit::Namespaces::EMAIL_ADDRESS
- xml.SubjectConfirmation Method: Saml::Kit::Namespaces::BEARER do
- xml.SubjectConfirmationData "", InResponseTo: request.id, NotOnOrAfter: 3.hours.from_now.utc.iso8601, Recipient: FFaker::Internet.uri("https")
- end
- end
- xml.Conditions NotBefore: Time.now.utc.iso8601, NotOnOrAfter: 3.hours.from_now.utc.iso8601 do
- xml.AudienceRestriction do
- xml.Audience request.issuer
- end
- end
- xml.AuthnStatement AuthnInstant: Time.now.iso8601, SessionIndex: new_options[:ID], SessionNotOnOrAfter: 3.hours.from_now.utc.iso8601 do
- xml.AuthnContext do
- xml.AuthnContextClassRef Saml::Kit::Namespaces::PASSWORD
- end
- end
- end
- end
- end
- subject = described_class.new(xml)
- expect(subject).to_not be_valid
- expect(subject.errors[:assertion]).to be_present
- end
-
- it 'is invalid when the assertion has a signature and has been tampered with' do
- user = double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: { token: SecureRandom.uuid })
- request = Saml::Kit::AuthenticationRequest.build
- document = described_class.build(user, request, configuration: configuration) do |x|
- x.embed_signature = false
- x.assertion.embed_signature = true
- end
-
- altered_xml = document.to_xml.gsub(/token/, 'heck')
- subject = described_class.new(altered_xml)
- expect(subject).to_not be_valid
- expect(subject.errors[:digest_value]).to be_present
- end
- end
-
- describe "#signed?" do
- let(:now) { Time.now.utc }
- let(:id) { Xml::Kit::Id.generate }
- 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_not be_signed
- expect(subject.assertion).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) { Xml::Kit::Id.generate }
- let(:url) { FFaker::Internet.uri("https") }
- let(:certificate) do
- ::Xml::Kit::Certificate.new(
- ::Xml::Kit::SelfSignedCertificate.new.create(passphrase: "password")[0],
- use: :signing
- )
- end
-
- 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.stripped}</ds:X509Certificate>
- </ds:X509Data>
- </KeyInfo>
- </ds:Signature>
- </Assertion>
-</samlp:Response>
- XML
- subject = described_class.new(xml)
- expect(subject.signature).to be_nil
- expect(subject.assertion.signature).to be_present
- expect(subject.assertion.signature.certificate.stripped).to eql(certificate.stripped)
- 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.signature.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.signature).to be_nil
- end
- end
-
- describe "encrypted assertion" do
- let(:id) { Xml::Kit::Id.generate }
- let(:now) { Time.now.utc }
- let(:assertion_consumer_service_url) { FFaker::Internet.uri("https") }
- let(:password) { FFaker::Movie.title }
- let(:email) { FFaker::Internet.email }
- let(:created_at) { DateTime.now }
- let(:assertion) do
- <<-XML
-<Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{id}" IssueInstant="2017-11-23T04:33:58Z" Version="2.0">
- <Issuer>#{FFaker::Internet.uri("https")}</Issuer>
- <Subject>
- <NameID Format="#{Saml::Kit::Namespaces::PERSISTENT}">#{SecureRandom.uuid}</NameID>
- <SubjectConfirmation Method="#{Saml::Kit::Namespaces::BEARER}">
- <SubjectConfirmationData InResponseTo="#{SecureRandom.uuid}" 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>#{Saml::Kit::Namespaces::PASSWORD}</AuthnContextClassRef>
- </AuthnContext>
- </AuthnStatement>
- <AttributeStatement>
- <Attribute Name="email" NameFormat="#{Saml::Kit::Namespaces::URI}">
- <AttributeValue>#{email}</AttributeValue>
- </Attribute>
- <Attribute Name="created_at" NameFormat="#{Saml::Kit::Namespaces::URI}">
- <AttributeValue>#{created_at.iso8601}</AttributeValue>
- </Attribute>
- </AttributeStatement>
-</Assertion>
-XML
- end
-
- it 'parses the encrypted assertion' do
- certificate_pem, private_key_pem = ::Xml::Kit::SelfSignedCertificate.new.create(passphrase: password)
- 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(:private_keys).with(use: :encryption).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="#{assertion_consumer_service_url}" InResponseTo="#{Xml::Kit::Id.generate}">
- <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.attributes).to match_array([
- ["created_at", created_at.iso8601],
- ["email", email]
- ])
- end
- end
-
- describe "parsing" do
- let(:user) { double(:user, name_id_for: SecureRandom.uuid, assertion_attributes_for: attributes) }
- let(:request) { double(:request, id: Xml::Kit::Id.generate, signed?: true, trusted?: true, provider: nil, assertion_consumer_service_url: FFaker::Internet.uri("https"), name_id_format: '', issuer: FFaker::Internet.uri("https")) }
- let(:attributes) { { name: 'mo' } }
-
- it 'returns the name id' do
- subject = described_class.build(user, request)
- expect(subject.name_id).to eql(user.name_id_for)
- end
-
- it 'returns the single attributes' do
- subject = described_class.build(user, request)
- expect(subject.attributes).to eql('name' => 'mo')
- end
-
- it 'returns the multiple attributes' do
- attributes[:age] = 33
- subject = described_class.build(user, request)
- expect(subject.attributes).to eql('name' => 'mo', 'age' => '33')
- end
- end
-end
spec/saml/signature_spec.rb
@@ -1,22 +0,0 @@
-RSpec.describe Saml::Kit::Signature do
- describe "#valid?" do
- let(:key_pair) { ::Xml::Kit::KeyPair.generate(use: :signing) }
-
- it 'returns true when the signature is valid' do
- signed_document = Saml::Kit::AuthenticationRequest.build do |x|
- x.sign_with(key_pair)
- end
- subject = described_class.new(Hash.from_xml(signed_document.to_xml))
- expect(subject).to be_valid
- end
-
- xit 'is invalid when the xml has been tampered' do
- signed_document = Saml::Kit::AuthenticationRequest.build do |x|
- x.sign_with(key_pair)
- end
- tampered_xml = signed_document.to_xml.gsub("Issuer", "Hacked")
- subject = described_class.new(Hash.from_xml(tampered_xml))
- expect(subject).to_not be_valid
- end
- end
-end
spec/support/rspec_benchmark.rb
@@ -0,0 +1,5 @@
+require 'rspec-benchmark'
+
+RSpec.configure do |config|
+ config.include RSpec::Benchmark::Matchers
+end
spec/support/test_helpers.rb
@@ -1,6 +1,6 @@
module TestHelpers
def query_params_from(url)
- Hash[query_for(url).split("&").map { |x| x.split('=', 2) }]
+ Hash[query_for(url).split('&').map { |x| x.split('=', 2) }]
end
def uri_for(url)
spec/support/user.rb
@@ -0,0 +1,16 @@
+class User
+ attr_reader :name_id, :attributes
+
+ def initialize(name_id: SecureRandom.uuid, attributes: {})
+ @name_id = name_id
+ @attributes = attributes
+ end
+
+ def name_id_for(_format)
+ name_id
+ end
+
+ def assertion_attributes_for(_request)
+ attributes
+ end
+end
spec/spec_helper.rb
@@ -2,12 +2,12 @@ require 'simplecov'
SimpleCov.start do
add_filter '/spec/'
end
-require "bundler/setup"
-require "saml/kit"
-require "saml/kit/rspec"
-require "active_support/testing/time_helpers"
-require "ffaker"
-require "webmock/rspec"
+require 'bundler/setup'
+require 'saml/kit'
+require 'saml/kit/rspec'
+require 'active_support/testing/time_helpers'
+require 'ffaker'
+require 'webmock/rspec'
Saml::Kit.configuration.logger.level = Xml::Kit.logger.level = Logger::FATAL
@@ -15,7 +15,7 @@ Dir[File.join(Dir.pwd, 'spec/support/**/*.rb')].each { |f| require f }
RSpec.configure do |config|
config.include ActiveSupport::Testing::TimeHelpers
# Enable flags like --only-failures and --next-failure
- config.example_status_persistence_file_path = ".rspec_status"
+ config.example_status_persistence_file_path = '.rspec_status'
# Disable RSpec exposing methods globally on `Module` and `main`
config.disable_monkey_patching!
@@ -23,7 +23,7 @@ RSpec.configure do |config|
config.expect_with :rspec do |c|
c.syntax = :expect
end
- config.after :each do
+ config.after do
travel_back
end
end
.gitlab-ci.yml
@@ -5,11 +5,11 @@ before_script:
- echo "en_US.UTF-8 UTF-8" > /etc/locale.gen
- locale-gen
- export LC_ALL=en_US.UTF-8
- - ruby -v
- - which ruby
- - gem install bundler --no-ri --no-rdoc
- - bundle install --jobs $(nproc) "${FLAGS[@]}"
rspec:
script:
- - bundle exec rspec
+ - bin/cibuild
+
+lint:
+ script:
+ - bin/lint
.rubocop.yml
@@ -0,0 +1,92 @@
+inherit_from: .rubocop_todo.yml
+
+require:
+ - rubocop/cop/internal_affairs
+ - rubocop-rspec
+
+AllCops:
+ Exclude:
+ - 'coverage/**/*'
+ - 'pkg/**/*'
+ - 'spec/fixtures/**/*'
+ - 'spec/examples/**/*'
+ - 'tmp/**/*'
+ - 'vendor/**/*'
+ TargetRubyVersion: 2.2
+
+Layout/ClassStructure:
+ Enabled: true
+ Categories:
+ module_inclusion:
+ - include
+ - prepend
+ - extend
+ ExpectedOrder:
+ - module_inclusion
+ - constants
+ - public_class_methods
+ - initializer
+ - instance_methods
+ - protected_methods
+ - private_methods
+
+Layout/EndOfLine:
+ EnforcedStyle: lf
+
+Layout/IndentArray:
+ EnforcedStyle: consistent
+
+Layout/IndentHeredoc:
+ EnforcedStyle: active_support
+
+Lint/AmbiguousBlockAssociation:
+ Exclude:
+ - 'spec/**/*.rb'
+
+Lint/InterpolationCheck:
+ Exclude:
+ - 'spec/**/*.rb'
+
+Metrics/BlockLength:
+ Exclude:
+ - '**/**/*.builder'
+ - '**/*.rake'
+ - '*.gemspec'
+ - 'Rakefile'
+ - 'spec/**/*.rb'
+
+Metrics/ModuleLength:
+ Exclude:
+ - 'spec/**/*.rb'
+
+Metrics/LineLength:
+ Exclude:
+ - 'spec/**/*.rb'
+
+Naming/FileName:
+ Exclude:
+ - 'lib/saml-kit.rb'
+
+Style/Documentation:
+ Enabled: false
+
+Style/StringLiterals:
+ EnforcedStyle: 'single_quotes'
+
+Style/TrailingCommaInLiteral:
+ Enabled: false
+
+RSpec/ExampleLength:
+ Max: 80
+
+RSpec/MultipleExpectations:
+ Enabled: false
+
+RSpec/NamedSubject:
+ Enabled: false
+
+RSpec/NestedGroups:
+ Max: 7
+
+RSpec/SubjectStub:
+ Enabled: false
.rubocop_todo.yml
@@ -0,0 +1,45 @@
+# This configuration was generated by
+# `rubocop --auto-gen-config`
+# on 2018-02-16 22:08:54 -0700 using RuboCop version 0.52.1.
+# The point is for the user to remove these configuration records
+# one by one as the offenses are removed from the code base.
+# Note that changes in the inspected code, or installation of new
+# versions of RuboCop, may require this file to be generated again.
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods.
+Lint/UnusedMethodArgument:
+ Exclude:
+ - 'lib/saml/kit/invalid_document.rb'
+
+# Offense count: 2
+Metrics/AbcSize:
+ Max: 16
+
+# Offense count: 3
+# Configuration parameters: CountComments.
+Metrics/ClassLength:
+ Max: 136
+
+# Offense count: 6
+# Configuration parameters: CountComments.
+Metrics/MethodLength:
+ Max: 13
+
+# Offense count: 1
+Style/DateTime:
+ Exclude:
+ - 'lib/saml/kit/assertion.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/IfUnlessModifier:
+ Exclude:
+ - 'lib/saml/kit/builders/authentication_request.rb'
+
+# Offense count: 128
+# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
+# URISchemes: http, https
+Metrics/LineLength:
+ Max: 313
.travis.yml
@@ -1,6 +1,10 @@
sudo: false
language: ruby
rvm:
- - 2.4.2
-before_install: gem install bundler -v 1.15.4
-script: "bundle exec rspec"
+ - 2.2.9
+ - 2.3.6
+ - 2.4.3
+ - 2.5.0
+script:
+ - bin/cibuild
+ - bin/lint
Gemfile
@@ -1,6 +1,6 @@
-source "https://rubygems.org"
+source 'https://rubygems.org'
-git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
+git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
# Specify your gem's dependencies in saml-kit.gemspec
gemspec
Rakefile
@@ -1,6 +1,8 @@
-require "bundler/gem_tasks"
-require "rspec/core/rake_task"
+require 'bundler/gem_tasks'
+require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
+task default: :spec
-task :default => :spec
+require 'rubocop/rake_task'
+RuboCop::RakeTask.new(:rubocop)
saml-kit.gemspec
@@ -1,34 +1,37 @@
-# coding: utf-8
-lib = File.expand_path("../lib", __FILE__)
+
+lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
-require "saml/kit/version"
+require 'saml/kit/version'
Gem::Specification.new do |spec|
- spec.name = "saml-kit"
+ spec.name = 'saml-kit'
spec.version = Saml::Kit::VERSION
- spec.authors = ["mo khan"]
- spec.email = ["mo@mokhan.ca"]
+ spec.authors = ['mo khan']
+ spec.email = ['mo@mokhan.ca']
- spec.summary = %q{A simple toolkit for working with SAML.}
- spec.description = %q{A simple toolkit for working with SAML.}
- spec.homepage = "https://github.com/saml-kit/saml-kit"
- spec.license = "MIT"
+ spec.summary = 'A simple toolkit for working with SAML.'
+ spec.description = 'A simple toolkit for working with SAML.'
+ spec.homepage = 'https://github.com/saml-kit/saml-kit'
+ spec.license = 'MIT'
spec.required_ruby_version = '>= 2.2.0'
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
f.match(%r{^(test|spec|features)/})
end
- spec.metadata["yard.run"] = "yri"
- spec.bindir = "exe"
+ spec.metadata['yard.run'] = 'yri'
+ spec.bindir = 'exe'
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
- spec.require_paths = ["lib"]
+ spec.require_paths = ['lib']
- spec.add_dependency "activemodel", ">= 4.2.0"
- spec.add_dependency "xml-kit", ">= 0.1.5", "<= 1.0.0"
- spec.add_development_dependency "bundler", "~> 1.15"
- spec.add_development_dependency "ffaker", "~> 2.7"
- spec.add_development_dependency "rake", "~> 10.0"
- spec.add_development_dependency "rspec", "~> 3.0"
- spec.add_development_dependency "simplecov", "~> 0.15.1"
- spec.add_development_dependency "webmock", "~> 3.1"
+ spec.add_dependency 'activemodel', '>= 4.2.0'
+ spec.add_dependency 'xml-kit', '>= 0.1.10', '<= 1.0.0'
+ spec.add_development_dependency 'bundler', '~> 1.15'
+ spec.add_development_dependency 'ffaker', '~> 2.7'
+ spec.add_development_dependency 'rake', '~> 10.0'
+ spec.add_development_dependency 'rspec', '~> 3.0'
+ spec.add_development_dependency 'rubocop', '~> 0.52'
+ spec.add_development_dependency 'rubocop-rspec', '~> 1.22'
+ spec.add_development_dependency 'simplecov', '~> 0.15'
+ spec.add_development_dependency 'webmock', '~> 3.1'
+ spec.add_development_dependency 'rspec-benchmark', '~> 0.3'
end