Comparing changes
v1.0.9
→
v1.0.10
38 commits
97 files changed
Commits
Changed files (97)
lib
saml
kit
builders
templates
spec
saml
kit
builders
support
bin/cibuild
@@ -7,8 +7,7 @@ set -e
cd "$(dirname "$0")/.."
-echo "Started at…"
-date "+%H:%M:%S"
+echo [$(date "+%H:%M:%S")] "==> Started at…"
# GC customizations
export RUBY_GC_MALLOC_LIMIT=79000000
@@ -19,4 +18,4 @@ export RUBY_HEAP_SLOTS_GROWTH_FACTOR=1
ruby -v
gem install bundler --no-ri --no-rdoc --conservative
-time bin/test
+bin/test
bin/console
@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
require 'bundler/setup'
require 'saml/kit'
bin/lint
@@ -4,10 +4,8 @@ set -e
[ -z "$DEBUG" ] || set -x
-echo "==> Running setup…"
-date "+%H:%M:%S"
+echo [$(date "+%H:%M:%S")] "==> Running setup…"
bin/setup
-echo "==> Running linters…"
-date "+%H:%M:%S"
+echo [$(date "+%H:%M:%S")] "==> Running linters…"
bundle exec rake rubocop
bin/setup
@@ -4,5 +4,3 @@ IFS=$'\n\t'
set -vx
bundle check || bundle install --jobs $(nproc)
-
-# Do any other automated setup that you need to do here
bin/test
@@ -10,10 +10,8 @@ cd "$(dirname "$0")/.."
[ -z "$DEBUG" ] || set -x
-echo "==> Running setup…"
-date "+%H:%M:%S"
+echo [$(date "+%H:%M:%S")] "==> Running setup…"
bin/setup
-echo "==> Running tests…"
-date "+%H:%M:%S"
+echo [$(date "+%H:%M:%S")] "==> Running tests…"
bundle exec rake spec
exe/saml-kit-create-self-signed-certificate
@@ -1,4 +1,6 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
+
require 'saml/kit'
Saml::Kit.deprecate("Use the 'saml-kit-cli' gem instead. saml-kit-create-self-signed-certificate")
exe/saml-kit-decode-http-post
@@ -1,4 +1,6 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
+
require 'saml/kit'
Saml::Kit.deprecate("Use the 'saml-kit-cli' gem instead. saml-kit-decode-http-post")
exe/saml-kit-decode-http-redirect
@@ -1,4 +1,6 @@
#!/usr/bin/env ruby
+# frozen_string_literal: true
+
require 'saml/kit'
Saml::Kit.deprecate("Use the 'saml-kit-cli' gem instead. saml-kit-decode-http-redirect*")
lib/saml/kit/bindings/binding.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module Bindings
lib/saml/kit/bindings/http_post.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module Bindings
lib/saml/kit/bindings/http_redirect.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module Bindings
lib/saml/kit/bindings/url_builder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module Bindings
lib/saml/kit/builders/templates/assertion.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
xml.Assertion(assertion_options) do
xml.Issuer issuer
signature_for(reference_id: reference_id, xml: xml)
lib/saml/kit/builders/templates/authentication_request.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
xml.instruct!
xml.tag!('samlp:AuthnRequest', request_options) do
xml.tag!('saml:Issuer', issuer)
lib/saml/kit/builders/templates/encrypted_assertion.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
xml.EncryptedAssertion xmlns: Saml::Kit::Namespaces::ASSERTION do
encryption_for(xml: xml) do |xml|
render assertion, xml: xml
lib/saml/kit/builders/templates/identity_provider_metadata.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
xml.IDPSSODescriptor descriptor_options do
configuration.certificates(use: :signing).each do |certificate|
render certificate, xml: xml
lib/saml/kit/builders/templates/logout_request.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
xml.instruct!
xml.LogoutRequest logout_request_options do
xml.Issuer({ xmlns: Saml::Kit::Namespaces::ASSERTION }, issuer)
lib/saml/kit/builders/templates/logout_response.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
xml.instruct!
xml.LogoutResponse logout_response_options do
xml.Issuer(issuer, xmlns: Saml::Kit::Namespaces::ASSERTION)
lib/saml/kit/builders/templates/metadata.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
xml.instruct!
xml.EntityDescriptor entity_descriptor_options do
signature_for(reference_id: id, xml: xml)
lib/saml/kit/builders/templates/response.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
xml.instruct!
xml.Response response_options do
xml.Issuer(issuer, xmlns: Saml::Kit::Namespaces::ASSERTION)
lib/saml/kit/builders/templates/service_provider_metadata.builder
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
xml.SPSSODescriptor descriptor_options do
configuration.certificates(use: :signing).each do |certificate|
render certificate, xml: xml
lib/saml/kit/builders/assertion.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module Builders
lib/saml/kit/builders/authentication_request.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module Builders
lib/saml/kit/builders/encrypted_assertion.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module Builders
lib/saml/kit/builders/identity_provider_metadata.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module Builders
lib/saml/kit/builders/logout_request.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module Builders
lib/saml/kit/builders/logout_response.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module Builders
lib/saml/kit/builders/metadata.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module Builders
lib/saml/kit/builders/response.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module Builders
lib/saml/kit/builders/service_provider_metadata.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module Builders
lib/saml/kit/rspec/have_query_param.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'uri'
RSpec::Matchers.define :have_query_param do |key|
lib/saml/kit/rspec/have_xpath.rb
@@ -1,13 +1,8 @@
+# frozen_string_literal: true
+
RSpec::Matchers.define :have_xpath do |xpath|
match do |actual|
- 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,
- }
- xml_document(actual).xpath(xpath, namespaces).any?
+ xml_document(actual).xpath(xpath, Saml::Kit::Document::NAMESPACES).any?
end
failure_message do |actual|
lib/saml/kit/assertion.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
class Assertion
@@ -18,22 +20,18 @@ module Saml
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
- ))
+ private_keys = (configuration.private_keys(use: :encryption) + private_keys).uniq
+ decrypt!(::Xml::Kit::Decryption.new(private_keys: private_keys))
end
def issuer
- assertion.fetch('Issuer')
+ at_xpath('./saml:Issuer').try(:text)
end
def name_id
- assertion.fetch('Subject', {}).fetch('NameID', nil)
+ at_xpath('./saml:Subject/saml:NameID').try(:text)
end
def signed?
@@ -54,35 +52,26 @@ module Saml
end
def attributes
- @attributes ||=
- begin
- attrs = assertion.fetch('AttributeStatement', {}).fetch('Attribute', [])
- items = if attrs.is_a? Hash
- [[attrs['Name'], attrs['AttributeValue']]]
- else
- attrs.map { |item| [item['Name'], item['AttributeValue']] }
- end
- Hash[items].with_indifferent_access
- end
+ @attributes ||= search('./saml:AttributeStatement/saml:Attribute').inject({}) do |memo, item|
+ memo[item.attribute('Name').value] = item.at_xpath('./saml:AttributeValue', Saml::Kit::Document::NAMESPACES).try(:text)
+ memo
+ end.with_indifferent_access
end
def started_at
- parse_date(assertion.fetch('Conditions', {}).fetch('NotBefore', nil))
+ parse_date(at_xpath('./saml:Conditions/@NotBefore').try(:value))
end
def expired_at
- parse_date(assertion.fetch('Conditions', {}).fetch('NotOnOrAfter', nil))
+ parse_date(at_xpath('./saml:Conditions/@NotOnOrAfter').try(:value))
end
def audiences
- Array(assertion['Conditions']['AudienceRestriction']['Audience'])
- rescue StandardError => error
- Saml::Kit.logger.error(error)
- []
+ search('./saml:Conditions/saml:AudienceRestriction/saml:Audience').map(&:text)
end
def encrypted?
- @xml_hash.fetch('EncryptedAssertion', nil).present?
+ @encrypted
end
def decryptable?
@@ -91,7 +80,7 @@ module Saml
end
def present?
- assertion.present?
+ @node.present?
end
def to_xml(pretty: false)
@@ -102,19 +91,10 @@ module Saml
attr_reader :configuration
- def assertion
- @assertion ||=
- begin
- result = (hash_from(@node)['Response'] || {})['Assertion']
- return result if result.is_a?(Hash)
- {}
- end
- end
-
def decrypt!(decryptor)
- return unless encrypted?
-
- encrypted_assertion = @node.at_xpath('./xmlenc:EncryptedData', Saml::Kit::Document::NAMESPACES)
+ encrypted_assertion = at_xpath('./xmlenc:EncryptedData')
+ @encrypted = encrypted_assertion.present?
+ return unless @encrypted
@node = decryptor.decrypt_node(encrypted_assertion)
rescue Xml::Kit::DecryptionError => error
@cannot_decrypt = true
@@ -151,12 +131,12 @@ module Saml
end
def at_xpath(xpath)
+ return unless @node
@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) || {}
+ def search(xpath)
+ @node.search(xpath, Saml::Kit::Document::NAMESPACES)
end
end
end
lib/saml/kit/authentication_request.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
# This class can be used to parse a SAML AuthnRequest or generate one.
@@ -31,15 +33,19 @@ module Saml
# Extract the AssertionConsumerServiceURL from the AuthnRequest
# <samlp:AuthnRequest AssertionConsumerServiceURL="https://carroll.com/acs"></samlp:AuthnRequest>
def assertion_consumer_service_url
- to_h[name]['AssertionConsumerServiceURL']
+ at_xpath('./*/@AssertionConsumerServiceURL').try(:value)
+ end
+
+ def name_id_format
+ name_id_policy
end
# Extract the NameIDPolicy from the AuthnRequest
# <samlp:AuthnRequest>
# <samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"/>
# </samlp:AuthnRequest>
- def name_id_format
- to_h[name]['NameIDPolicy']['Format']
+ def name_id_policy
+ at_xpath('./*/samlp:NameIDPolicy/@Format').try(:value)
end
# Generate a Response for a specific user.
lib/saml/kit/bindings.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'saml/kit/bindings/binding'
require 'saml/kit/bindings/http_post'
require 'saml/kit/bindings/http_redirect'
lib/saml/kit/buildable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module Buildable
lib/saml/kit/builders.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'saml/kit/xml_templatable'
require 'saml/kit/builders/assertion'
require 'saml/kit/builders/authentication_request'
lib/saml/kit/composite_metadata.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
class CompositeMetadata < Metadata # :nodoc:
@@ -14,7 +16,7 @@ module Saml
def services(type)
xpath = map { |x| "//md:EntityDescriptor/md:#{x.name}/md:#{type}" }.join('|')
- document.find_all(xpath).map do |item|
+ search(xpath).map do |item|
binding = item.attribute('Binding').value
location = item.attribute('Location').value
Saml::Kit::Bindings.create_for(binding, location)
lib/saml/kit/configuration.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
# This class represents the main configuration that is use for generating SAML documents.
lib/saml/kit/default_registry.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
# The default metadata registry is used to fetch the metadata associated with an issuer or entity id.
lib/saml/kit/document.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
class Document
@@ -29,27 +31,27 @@ module Saml
# Returns the ID for the SAML document.
def id
- root.fetch('ID', nil)
+ at_xpath('./*/@ID').try(:value)
end
# Returns the Issuer for the SAML document.
def issuer
- root.fetch('Issuer', nil)
+ at_xpath('./*/saml:Issuer').try(:text)
end
# Returns the Version of the SAML document.
def version
- root.fetch('Version', {})
+ at_xpath('./*/@Version').try(:value)
end
# Returns the Destination of the SAML document.
def destination
- root.fetch('Destination', nil)
+ at_xpath('./*/@Destination').try(:value)
end
# Returns the Destination of the SAML document.
def issue_instant
- Time.parse(root['IssueInstant'])
+ Time.parse(at_xpath('./*/@IssueInstant').try(:value))
end
# Returns the SAML document returned as a Hash.
@@ -102,15 +104,12 @@ module Saml
# @param xml [String] the raw xml string.
# @param configuration [Saml::Kit::Configuration] the configuration to use for unpacking the document.
def to_saml_document(xml, configuration: Saml::Kit.configuration)
- xml_document = ::Xml::Kit::Document.new(xml, namespaces: {
- "samlp": ::Saml::Kit::Namespaces::PROTOCOL
- })
constructor = {
'AuthnRequest' => Saml::Kit::AuthenticationRequest,
'LogoutRequest' => Saml::Kit::LogoutRequest,
'LogoutResponse' => Saml::Kit::LogoutResponse,
'Response' => Saml::Kit::Response,
- }[xml_document.find_by(XPATH).name] || InvalidDocument
+ }[Nokogiri::XML(xml).at_xpath(XPATH, "samlp": ::Saml::Kit::Namespaces::PROTOCOL).name] || InvalidDocument
constructor.new(xml, configuration: configuration)
rescue StandardError => error
Saml::Kit.logger.error(error)
@@ -138,10 +137,6 @@ module Saml
attr_reader :content, :name, :configuration
- def root
- to_h.fetch(name, {})
- end
-
def must_match_xsd
matches_xsd?(PROTOCOL_XSD)
end
@@ -151,7 +146,7 @@ module Saml
end
def expected_type?
- to_h[name].present?
+ at_xpath("./samlp:#{name}").present?
end
def must_be_valid_version
lib/saml/kit/identity_provider_metadata.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
# This class is used to parse the IDPSSODescriptor from a SAML metadata document.
@@ -38,7 +40,7 @@ module Saml
# Returns the IDPSSODescriptor/@WantAuthnRequestsSigned attribute.
def want_authn_requests_signed
xpath = "/md:EntityDescriptor/md:#{name}"
- attribute = document.find_by(xpath).attribute('WantAuthnRequestsSigned')
+ attribute = at_xpath(xpath).attribute('WantAuthnRequestsSigned')
return true if attribute.nil?
attribute.text.casecmp('true').zero?
end
@@ -57,7 +59,7 @@ module Saml
# Returns each of the Attributes in the metadata.
def attributes
- document.find_all("/md:EntityDescriptor/md:#{name}/saml:Attribute").map do |item|
+ search("/md:EntityDescriptor/md:#{name}/saml:Attribute").map do |item|
{
format: item.attribute('NameFormat').try(:value),
name: item.attribute('Name').value,
lib/saml/kit/invalid_document.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
# {include:file:spec/saml/invalid_document_spec.rb}
lib/saml/kit/logout_request.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
# This class can be used to parse a LogoutRequest SAML document.
@@ -36,7 +38,11 @@ module Saml
# Returns the NameID value.
def name_id
- to_h[name]['NameID']
+ at_xpath('./*/saml:NameID').try(:text)
+ end
+
+ def name_id_format
+ at_xpath('./*/saml:NameID/@Format').try(:value)
end
# Generates a Serialized LogoutResponse using the encoding rules for the specified binding.
lib/saml/kit/logout_response.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
# This class is used to parse a LogoutResponse SAML document.
lib/saml/kit/metadata.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
# The Metadata object can be used to parse an XML string of metadata.
@@ -29,11 +31,11 @@ module Saml
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,
- "md": Namespaces::METADATA,
- "saml": Namespaces::ASSERTION,
- "samlp": Namespaces::PROTOCOL,
+ NameFormat: Namespaces::ATTR_SPLAT,
+ ds: ::Xml::Kit::Namespaces::XMLDSIG,
+ md: Namespaces::METADATA,
+ saml: Namespaces::ASSERTION,
+ samlp: Namespaces::PROTOCOL,
}.freeze
validates_presence_of :metadata
@@ -50,36 +52,34 @@ module Saml
# Returns the /EntityDescriptor/@entityID
def entity_id
- document.find_by('/md:EntityDescriptor/@entityID').value
+ at_xpath('/md:EntityDescriptor/@entityID').try(:value)
end
# Returns the supported NameIDFormats.
def name_id_formats
- document.find_all("/md:EntityDescriptor/md:#{name}/md:NameIDFormat").map(&:text)
+ search("/md:EntityDescriptor/md:#{name}/md:NameIDFormat").map(&:text)
end
# Returns the Organization Name
def organization_name
- document.find_by('/md:EntityDescriptor/md:Organization/md:OrganizationName').try(:text)
+ at_xpath('/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)
+ at_xpath('/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)
+ at_xpath('/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
- attribute = item.attribute('use')
- use = attribute.nil? ? nil : item.attribute('use').value
- ::Xml::Kit::Certificate.new(cert, use: use)
+ @certificates ||= search("/md:EntityDescriptor/md:#{name}/md:KeyDescriptor").map do |item|
+ cert = item.at_xpath('./ds:KeyInfo/ds:X509Data/ds:X509Certificate', 'ds' => ::Xml::Kit::Namespaces::XMLDSIG).try(:text)
+ ::Xml::Kit::Certificate.new(cert, use: item.attribute('use').try(:value))
end
end
@@ -97,7 +97,7 @@ 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|
+ search("/md:EntityDescriptor/md:#{name}/md:#{type}").map do |item|
binding = item.attribute('Binding').value
location = item.attribute('Location').value
Saml::Kit::Bindings.create_for(binding, location)
@@ -132,9 +132,7 @@ module Saml
# @param relay_state [String] the relay state to have echo'd back.
# @return [Array] Returns an array with a url and Hash of parameters to send to the other party.
def logout_request_for(user, binding: :http_post, relay_state: nil)
- builder = Saml::Kit::LogoutRequest.builder(user) do |x|
- yield x if block_given?
- end
+ builder = Saml::Kit::LogoutRequest.builder(user) { |x| yield x if block_given? }
request_binding = single_logout_service_for(binding: binding)
request_binding.serialize(builder, relay_state: relay_state)
end
@@ -145,9 +143,7 @@ module Saml
# @param use [Symbol] the type of certificates to look at. Can be `:signing` or `:encryption`.
# @return [Xml::Kit::Certificate] returns the matching `{Xml::Kit::Certificate}`
def matches?(fingerprint, use: :signing)
- certificates.find do |certificate|
- certificate.for?(use) && certificate.fingerprint == fingerprint
- end
+ certificates.find { |x| x.for?(use) && x.fingerprint == fingerprint }
end
# Returns the XML document converted to a Hash.
@@ -159,7 +155,7 @@ module Saml
#
# @param pretty [Symbol] true to return a human friendly version of the XML.
def to_xml(pretty: false)
- document.to_xml(pretty: pretty)
+ pretty ? to_nokogiri.to_xml(indent: 2) : @xml
end
# Returns the XML document as a [String].
@@ -189,13 +185,15 @@ module Saml
# @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')
+ document = Nokogiri::XML(content)
+ return unless document.at_xpath('/md:EntityDescriptor', NAMESPACES)
+ sp = document.at_xpath('/md:EntityDescriptor/md:SPSSODescriptor', NAMESPACES)
+ idp = document.at_xpath('/md:EntityDescriptor/md:IDPSSODescriptor', NAMESPACES)
+ if sp && idp
Saml::Kit::CompositeMetadata.new(content)
- elsif entity_descriptor.keys.include?('SPSSODescriptor')
+ elsif sp
Saml::Kit::ServiceProviderMetadata.new(content)
- elsif entity_descriptor.keys.include?('IDPSSODescriptor')
+ elsif idp
Saml::Kit::IdentityProviderMetadata.new(content)
end
end
@@ -210,16 +208,21 @@ module Saml
attr_reader :xml
- def document
- @document ||= ::Xml::Kit::Document.new(xml, namespaces: NAMESPACES)
+ # @!visibility private
+ def to_nokogiri
+ @nokogiri ||= Nokogiri::XML(xml)
end
def at_xpath(xpath)
- document.find_by(xpath)
+ to_nokogiri.at_xpath(xpath, NAMESPACES)
+ end
+
+ def search(xpath)
+ to_nokogiri.search(xpath, NAMESPACES)
end
def metadata
- document.find_by("/md:EntityDescriptor/md:#{name}").present?
+ at_xpath("/md:EntityDescriptor/md:#{name}").present?
end
def must_contain_descriptor
lib/saml/kit/namespaces.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module Namespaces
lib/saml/kit/null_assertion.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
class NullAssertion
lib/saml/kit/requestable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module Requestable
lib/saml/kit/respondable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module Respondable
@@ -16,12 +18,12 @@ module Saml
# Returns the /Status/StatusCode@Value
def status_code
- to_h.fetch(name, {}).fetch('Status', {}).fetch('StatusCode', {}).fetch('Value', nil)
+ at_xpath('./*/samlp:Status/samlp:StatusCode/@Value').try(:value)
end
# Returns the /InResponseTo attribute.
def in_response_to
- to_h.fetch(name, {}).fetch('InResponseTo', nil)
+ at_xpath('./*/@InResponseTo').try(:value)
end
# Returns true if the Status code is #{Saml::Kit::Namespaces::SUCCESS}
lib/saml/kit/response.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
# {include:file:spec/examples/response_spec.rb}
lib/saml/kit/rspec.rb
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
require 'saml/kit/rspec/have_query_param'
require 'saml/kit/rspec/have_xpath'
lib/saml/kit/serializable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module Serializable
lib/saml/kit/service_provider_metadata.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
# {include:file:spec/examples/service_provider_metadata_spec.rb}
@@ -20,7 +22,7 @@ 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 = at_xpath("/md:EntityDescriptor/md:#{name}").attribute('WantAssertionsSigned')
return true if attribute.nil?
attribute.text.casecmp('true').zero?
end
lib/saml/kit/signature.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
class Signature
lib/saml/kit/translatable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module Translatable
lib/saml/kit/trustable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module Trustable
lib/saml/kit/version.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
module Saml
module Kit
- VERSION = '1.0.9'.freeze
+ VERSION = '1.0.10'.freeze
end
end
lib/saml/kit/xml_templatable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module XmlTemplatable
lib/saml/kit/xsd_validatable.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Saml
module Kit
module XsdValidatable
@@ -5,8 +7,7 @@ module Saml
def matches_xsd?(xsd)
Dir.chdir(File.dirname(xsd)) do
xsd = Nokogiri::XML::Schema(IO.read(xsd))
- document = Nokogiri::XML(to_xml)
- xsd.validate(document).each do |error|
+ xsd.validate(to_nokogiri).each do |error|
errors[:base] << error.message
end
end
lib/saml/kit.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'saml/kit/version'
require 'active_model'
lib/saml-kit.rb
@@ -1,1 +1,3 @@
+# frozen_string_literal: true
+
require 'saml/kit'
spec/saml/kit/bindings/binding_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::Bindings::Binding do
subject { described_class.new(binding: Saml::Kit::Bindings::HTTP_ARTIFACT, location: location) }
spec/saml/kit/bindings/http_post_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::Bindings::HttpPost do
subject { described_class.new(location: location) }
spec/saml/kit/bindings/http_redirect_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::Bindings::HttpRedirect do
subject { described_class.new(location: location) }
spec/saml/kit/bindings/url_builder_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::Bindings::UrlBuilder do
describe '#build' do
let(:xml) { '<xml></xml>' }
spec/saml/kit/builders/authentication_request_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::Builders::AuthenticationRequest do
subject { described_class.new(configuration: configuration) }
spec/saml/kit/builders/identity_provider_metadata_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::Builders::IdentityProviderMetadata do
subject { described_class.new(configuration: configuration) }
spec/saml/kit/builders/logout_request_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::Builders::LogoutRequest do
subject { described_class.new(user, configuration: configuration) }
spec/saml/kit/builders/logout_response_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::Builders::LogoutResponse do
subject { described_class.new(request) }
spec/saml/kit/builders/metadata_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::Builders::Metadata do
describe '.build' do
subject { Saml::Kit::Metadata }
spec/saml/kit/builders/response_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::Builders::Response do
subject { described_class.new(user, request, configuration: configuration) }
spec/saml/kit/builders/service_provider_metadata_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::Builders::ServiceProviderMetadata do
subject { described_class.new(configuration: configuration) }
spec/saml/kit/assertion_spec.rb
@@ -1,4 +1,23 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::Assertion do
+ subject do
+ Saml::Kit::Response.build(user, request) do |x|
+ x.issuer = entity_id
+ end.assertion
+ end
+
+ let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: ::Xml::Kit::Id.generate, issuer: FFaker::Internet.uri('https'), assertion_consumer_service_url: FFaker::Internet.uri('https'), name_id_format: Saml::Kit::Namespaces::PERSISTENT, provider: nil, signed?: true, trusted?: true) }
+ let(:user) { User.new(name_id: SecureRandom.uuid, attributes: { id: SecureRandom.uuid }) }
+ let(:entity_id) { FFaker::Internet.uri('https') }
+
+ specify { expect(subject.issuer).to eql(entity_id) }
+ specify { expect(subject.name_id).to eql(user.name_id) }
+ specify { expect(subject.started_at.to_i).to eql(Time.now.utc.to_i) }
+ specify { expect(subject.expired_at.to_i).to eql(Saml::Kit.configuration.session_timeout.since(Time.now).utc.to_i) }
+ specify { expect(subject.attributes).to eql('id' => user.attributes[:id]) }
+ specify { expect(subject.audiences).to match_array([request.issuer]) }
+
describe '#active?' do
let(:configuration) do
Saml::Kit::Configuration.new do |config|
@@ -12,7 +31,7 @@ RSpec.describe Saml::Kit::Assertion do
travel_to now
not_on_or_after = configuration.session_timeout.since(now).iso8601
xml = <<-XML.strip_heredoc
- <Response>
+ <Response xmlns="#{Saml::Kit::Namespaces::PROTOCOL}">
<Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{Xml::Kit::Id.generate}" IssueInstant="#{now.iso8601}" Version="2.0">
<Issuer>#{FFaker::Internet.uri('https')}</Issuer>
<Subject>
@@ -32,9 +51,11 @@ RSpec.describe Saml::Kit::Assertion do
</AuthnContext>
</AuthnStatement>
</Assertion>
- </Response>
+ </Response>
XML
- subject = described_class.new(Nokogiri::XML(xml), configuration: configuration)
+ document = Nokogiri::XML(xml)
+ node = document.at_xpath('//saml:Assertion', 'saml' => Saml::Kit::Namespaces::ASSERTION)
+ subject = described_class.new(node, configuration: configuration)
travel_to((configuration.clock_drift - 1.second).before(now))
expect(subject).to be_active
expect(subject).not_to be_expired
@@ -47,7 +68,7 @@ XML
not_before = now.utc.iso8601
not_after = configuration.session_timeout.since(now).iso8601
xml = <<-XML.strip_heredoc
- <Response>
+ <Response xmlns="#{Saml::Kit::Namespaces::PROTOCOL}">
<Assertion xmlns="#{Saml::Kit::Namespaces::ASSERTION}" ID="#{Xml::Kit::Id.generate}" IssueInstant="#{now.iso8601}" Version="2.0">
<Issuer>#{FFaker::Internet.uri('https')}</Issuer>
<Subject>
@@ -67,9 +88,11 @@ XML
</AuthnContext>
</AuthnStatement>
</Assertion>
- </Response>
+ </Response>
XML
- subject = described_class.new(Nokogiri::XML(xml), configuration: configuration)
+ document = Nokogiri::XML(xml)
+ node = document.at_xpath('//saml:Assertion', 'saml' => Saml::Kit::Namespaces::ASSERTION)
+ subject = described_class.new(node, configuration: configuration)
expect(subject).to be_active
expect(subject).not_to be_expired
end
@@ -128,6 +151,21 @@ XML
end
end
+ describe '#encrypted?' do
+ it 'returns true when encrypted' do
+ key_pair = Xml::Kit::KeyPair.generate(use: :encryption)
+ response = Saml::Kit::Response.build(user, request) do |x|
+ x.encrypt_with(key_pair)
+ end
+ subject = response.assertion([key_pair.private_key])
+ expect(subject).to be_encrypted
+ end
+
+ it 'returns false when not encrypted' do
+ expect(subject).not_to be_encrypted
+ 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 }) }
spec/saml/kit/authentication_request_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::AuthenticationRequest do
subject { described_class.new(raw_xml, configuration: configuration) }
@@ -22,11 +24,11 @@ RSpec.describe Saml::Kit::AuthenticationRequest do
end
end
- it { expect(subject.issuer).to eql(issuer) }
- it { expect(subject.id).to eql(id) }
- it { expect(subject.assertion_consumer_service_url).to eql(assertion_consumer_service_url) }
- it { expect(subject.name_id_format).to eql(name_id_format) }
- it { expect(subject.destination).to eql(destination) }
+ specify { expect(subject.issuer).to eql(issuer) }
+ specify { expect(subject.id).to eql(id) }
+ specify { expect(subject.assertion_consumer_service_url).to eql(assertion_consumer_service_url) }
+ specify { expect(subject.name_id_format).to eql(name_id_format) }
+ specify { expect(subject.destination).to eql(destination) }
describe '#valid?' do
let(:registry) { instance_double(Saml::Kit::DefaultRegistry) }
spec/saml/kit/bindings_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::Bindings do
describe '.to_symbol' do
subject { described_class }
spec/saml/kit/composite_metadata_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::CompositeMetadata do
subject { described_class.new(xml) }
spec/saml/kit/configuration_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::Configuration do
describe '#generate_key_pair_for' do
subject { described_class.new }
spec/saml/kit/default_registry_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::DefaultRegistry do
subject { described_class.new }
spec/saml/kit/document_spec.rb
@@ -1,4 +1,25 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::Document do
+ subject do
+ Saml::Kit::AuthenticationRequest.build do |x|
+ x.id = id
+ x.issuer = issuer
+ x.destination = destination
+ end
+ end
+
+ let(:id) { Xml::Kit::Id.generate }
+ let(:issuer) { FFaker::Internet.uri('https') }
+ let(:destination) { FFaker::Internet.uri('https') }
+
+ specify { expect(subject.id).to eql(id) }
+ specify { expect(subject.issuer).to eql(issuer) }
+ specify { expect(subject.version).to eql('2.0') }
+ specify { expect(subject.destination).to eql(destination) }
+ specify { expect(subject.issue_instant.to_i).to eql(Time.now.to_i) }
+ specify { expect(Saml::Kit::AuthenticationRequest.new('blah').id).to be_nil }
+
describe '.to_saml_document' do
subject { described_class }
spec/saml/kit/identity_provider_metadata_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::IdentityProviderMetadata do
subject { described_class.new(raw_metadata) }
spec/saml/kit/invalid_document_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::InvalidDocument do
it 'is invalid' do
subject = described_class.new('<xml></xml>')
spec/saml/kit/kit_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit do
it 'has a version number' do
expect(Saml::Kit::VERSION).not_to be nil
spec/saml/kit/logout_request_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::LogoutRequest do
subject { described_class.build(user, configuration: configuration) }
@@ -25,9 +27,7 @@ RSpec.describe Saml::Kit::LogoutRequest do
expect(subject.issue_instant).to eql(Time.now.utc)
end
- it 'parses the version' do
- expect(subject.version).to eql('2.0')
- end
+ specify { expect(subject.version).to eql('2.0') }
it 'parses the destination' do
destination = FFaker::Internet.uri('https')
@@ -37,9 +37,8 @@ RSpec.describe Saml::Kit::LogoutRequest do
expect(subject.destination).to eql(destination)
end
- it 'parses the name_id' do
- expect(subject.name_id).to eql(name_id)
- end
+ specify { expect(subject.name_id).to eql(name_id) }
+ specify { expect(subject.name_id_format).to eql(Saml::Kit::Namespaces::PERSISTENT) }
describe '#valid?' do
let(:metadata) do
spec/saml/kit/logout_response_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::LogoutResponse do
it 'exists' do
expect(described_class).to be(described_class)
spec/saml/kit/metadata_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::Metadata do
describe '.from' do
subject { described_class }
spec/saml/kit/response_spec.rb
@@ -1,9 +1,17 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::Response do
+ subject { described_class.build(user, request) }
+
+ let(:request) { instance_double(Saml::Kit::AuthenticationRequest, id: ::Xml::Kit::Id.generate, issuer: FFaker::Internet.uri('https'), assertion_consumer_service_url: FFaker::Internet.uri('https'), name_id_format: Saml::Kit::Namespaces::PERSISTENT, provider: nil, signed?: true, trusted?: true) }
+ let(:user) { User.new(attributes: { id: SecureRandom.uuid }) }
+
+ specify { expect(subject.status_code).to eql(Saml::Kit::Namespaces::SUCCESS) }
+ specify { expect(subject.in_response_to).to eql(request.id) }
+
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) }
spec/saml/kit/service_provider_metadata_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::ServiceProviderMetadata do
let(:entity_id) { FFaker::Internet.uri('https') }
let(:acs_post_url) { FFaker::Internet.uri('https') }
spec/saml/kit/signature_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
RSpec.describe Saml::Kit::Signature do
subject { described_class.new(signed_document.at_xpath('//ds:Signature')) }
spec/support/rspec_benchmark.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rspec-benchmark'
RSpec.configure do |config|
spec/support/test_helpers.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module TestHelpers
def query_params_from(url)
Hash[query_for(url).split('&').map { |x| x.split('=', 2) }]
spec/support/user.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class User
attr_reader :name_id, :attributes
spec/spec_helper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'simplecov'
SimpleCov.start do
add_filter '/spec/'
.rubocop.yml
@@ -70,6 +70,9 @@ Naming/FileName:
Style/Documentation:
Enabled: false
+Style/EachWithObject:
+ Enabled: false
+
Style/StringLiterals:
EnforcedStyle: 'single_quotes'
Gemfile
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
source 'https://rubygems.org'
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
Rakefile
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'
saml-kit.gemspec
@@ -1,4 +1,6 @@
+# frozen_string_literal: true
+
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'saml/kit/version'