Comparing changes
v1.0.14
→
v1.0.15
22 commits
47 files changed
Commits
Changed files (47)
lib
saml
kit
builders
rspec
spec
saml
kit
builders
support
lib/saml/kit/bindings/binding.rb
@@ -3,6 +3,7 @@
module Saml
module Kit
module Bindings
+ # This class is a base class for SAML bindings.
# {include:file:spec/saml/kit/bindings/binding_spec.rb}
class Binding
attr_reader :binding, :location
lib/saml/kit/bindings/http_post.rb
@@ -3,6 +3,11 @@
module Saml
module Kit
module Bindings
+ # This class is responsible for
+ # serializing/deserializing SAML
+ # documents using the HTTP Post
+ # binding specification.
+ # https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf
# {include:file:spec/saml/kit/bindings/http_post_spec.rb}
class HttpPost < Binding
include Serializable
lib/saml/kit/bindings/http_redirect.rb
@@ -3,6 +3,11 @@
module Saml
module Kit
module Bindings
+ # This class is responsible for
+ # serializing/deserializing SAML
+ # documents using the HTTP Redirect
+ # binding specification.
+ # https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf
# {include:file:spec/saml/kit/bindings/http_redirect_spec.rb}
class HttpRedirect < Binding
include Serializable
@@ -20,31 +25,31 @@ module Saml
def deserialize(params, configuration: Saml::Kit.configuration)
parameters = normalize(params_to_hash(params))
- document = deserialize_document_from!(parameters, configuration)
- ensure_valid_signature!(parameters, document)
+ document = deserialize_document_from(parameters, configuration)
+ ensure_valid_signature(parameters, document)
document
end
private
- def deserialize_document_from!(params, configuration)
+ def deserialize_document_from(params, configuration)
xml = inflate(decode(unescape(saml_param_from(params))))
Saml::Kit::Document.to_saml_document(xml, configuration: configuration)
end
- def ensure_valid_signature!(params, document)
- return if params[:Signature].blank? || params[:SigAlg].blank?
- return if document.provider.nil?
+ def ensure_valid_signature(params, document)
+ signature = params[:Signature]
+ algorithm = params[:SigAlg]
+ provider = document.provider
+ return if signature.blank? || algorithm.blank?
+ return if provider.nil?
- if document.provider.verify(
- algorithm_for(params[:SigAlg]),
- decode(params[:Signature]),
+ return document.signature_verified! if provider.verify(
+ algorithm_for(algorithm),
+ decode(signature),
canonicalize(params)
)
- document.signature_verified!
- else
- raise ArgumentError, 'Invalid Signature'
- end
+ raise ArgumentError, 'Invalid Signature'
end
def canonicalize(params)
@@ -79,7 +84,7 @@ module Saml
def params_to_hash(value)
return value unless value.is_a?(String)
- Hash[URI.parse(value).query.split('&').map { |x| x.split('=', 2) }]
+ Hash[URI.parse(value).query.split('&').map { |xx| xx.split('=', 2) }]
end
end
end
lib/saml/kit/bindings/url_builder.rb
@@ -3,6 +3,11 @@
module Saml
module Kit
module Bindings
+ # This class is responsible for
+ # generating a url as per the
+ # rules for the HTTP redirect binding
+ # specification.
+ # https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf
# {include:file:spec/saml/kit/bindings/url_builder_spec.rb}
class UrlBuilder
include Serializable
@@ -13,15 +18,16 @@ module Saml
end
def build(saml_document, relay_state: nil)
+ destination = saml_document.destination
if configuration.sign?
payload = canonicalize(saml_document, relay_state)
- "#{saml_document.destination}?#{payload}&Signature=#{signature_for(payload)}"
+ "#{destination}?#{payload}&Signature=#{signature_for(payload)}"
else
payload = to_query_string(
saml_document.query_string_parameter => serialize(saml_document.to_xml),
'RelayState' => relay_state
)
- "#{saml_document.destination}?#{payload}"
+ "#{destination}?#{payload}"
end
end
lib/saml/kit/builders/templates/null.builder
@@ -0,0 +1,1 @@
+# frozen_string_literal: true
lib/saml/kit/builders/templates/response.builder
@@ -6,6 +6,7 @@ xml.Response response_options do
signature_for(reference_id: id, xml: xml)
xml.Status do
xml.StatusCode Value: status_code
+ xml.StatusMessage(status_message) if status_message.present?
end
render assertion, xml: xml
end
lib/saml/kit/builders/assertion.rb
@@ -3,6 +3,7 @@
module Saml
module Kit
module Builders
+ # This class is responsible for building a SAML Assertion
# {include:file:lib/saml/kit/builders/templates/assertion.builder}
class Assertion
include XmlTemplatable
lib/saml/kit/builders/encrypted_assertion.rb
@@ -3,6 +3,7 @@
module Saml
module Kit
module Builders
+ # This class is responsible for encrypting an Assertion.
# {include:file:lib/saml/kit/builders/templates/encrypted_assertion.builder}
class EncryptedAssertion
include XmlTemplatable
lib/saml/kit/builders/metadata.rb
@@ -22,14 +22,14 @@ module Saml
end
def build_service_provider
- @service_provider = Saml::Kit::ServiceProviderMetadata.builder(configuration: configuration) do |x|
- yield x if block_given?
+ @service_provider = Saml::Kit::ServiceProviderMetadata.builder(configuration: configuration) do |xx|
+ yield xx if block_given?
end
end
def build_identity_provider
- @identity_provider = Saml::Kit::IdentityProviderMetadata.builder(configuration: configuration) do |x|
- yield x if block_given?
+ @identity_provider = Saml::Kit::IdentityProviderMetadata.builder(configuration: configuration) do |xx|
+ yield xx if block_given?
end
end
lib/saml/kit/builders/null.rb
@@ -0,0 +1,13 @@
+
+# frozen_string_literal: true
+
+module Saml
+ module Kit
+ module Builders
+ # This class is responsible for rendering an empty xml element
+ class Null
+ include XmlTemplatable
+ end
+ end
+ end
+end
lib/saml/kit/builders/response.rb
@@ -9,7 +9,7 @@ module Saml
include XmlTemplatable
attr_reader :user, :request
attr_accessor :id, :reference_id, :now
- attr_accessor :version, :status_code
+ attr_accessor :version, :status_code, :status_message
attr_accessor :issuer, :destination
attr_reader :configuration
@@ -21,6 +21,7 @@ module Saml
@now = Time.now.utc
@version = '2.0'
@status_code = Namespaces::SUCCESS
+ @status_message = nil
@issuer = configuration.entity_id
@encryption_certificate = request.try(:provider).try(:encryption_certificates).try(:last)
@encrypt = encryption_certificate.present?
@@ -31,6 +32,10 @@ module Saml
Saml::Kit::Response.new(to_xml, request_id: request.try(:id), configuration: configuration)
end
+ def assertion=(value)
+ @assertion = value ? value : Null.new
+ end
+
def assertion
@assertion ||=
begin
@@ -54,7 +59,7 @@ module Saml
xmlns: Namespaces::PROTOCOL,
}
options[:Destination] = destination if destination.present?
- options[:InResponseTo] = request.id if request.present?
+ options[:InResponseTo] = request.try(:id) if request.present?
options
end
end
lib/saml/kit/rspec/have_query_param.rb
@@ -8,7 +8,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 { |xxx| xxx.split('=', 2) }]
end
def uri_for(url)
lib/saml/kit/assertion.rb
@@ -2,6 +2,9 @@
module Saml
module Kit
+ # This class validates the Assertion
+ # element nested in a Response element
+ # of a SAML document.
class Assertion
include ActiveModel::Validations
include Translatable
@@ -22,8 +25,10 @@ module Saml
@node = node
@configuration = configuration
@occurred_at = Time.current
+ @cannot_decrypt = false
+ @encrypted = false
private_keys = (configuration.private_keys(use: :encryption) + private_keys).uniq
- decrypt!(::Xml::Kit::Decryption.new(private_keys: private_keys))
+ decrypt(::Xml::Kit::Decryption.new(private_keys: private_keys))
end
def issuer
@@ -83,15 +88,19 @@ module Saml
@node.present?
end
- def to_xml(pretty: false)
- pretty ? @node.to_xml(indent: 2) : @node.to_s
+ def to_xml(pretty: nil)
+ pretty ? @node.to_xml(indent: 2) : to_s
+ end
+
+ def to_s
+ @node.to_s
end
private
attr_reader :configuration
- def decrypt!(decryptor)
+ def decrypt(decryptor)
encrypted_assertion = at_xpath('./xmlenc:EncryptedData')
@encrypted = encrypted_assertion.present?
return unless @encrypted
lib/saml/kit/authentication_request.rb
@@ -54,11 +54,11 @@ module Saml
# @param configuration [Saml::Kit::Configuration] the configuration to use to build the response.
def response_for(user, binding:, relay_state: nil, configuration: Saml::Kit.configuration)
response_binding = provider.assertion_consumer_service_for(binding: binding)
- builder = Saml::Kit::Response.builder(user, self, configuration: configuration) do |x|
- x.embed_signature = provider.want_assertions_signed
- yield x if block_given?
+ response = Saml::Kit::Response.builder(user, self, configuration: configuration) do |builder|
+ builder.embed_signature = provider.want_assertions_signed
+ yield builder if block_given?
end
- response_binding.serialize(builder, relay_state: relay_state)
+ response_binding.serialize(response, relay_state: relay_state)
end
end
end
lib/saml/kit/bindings.rb
@@ -7,6 +7,9 @@ require 'saml/kit/bindings/url_builder'
module Saml
module Kit
+ # This module is responsible for exposing
+ # the different SAML bindings that are
+ # supported by this gem.
module Bindings
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
lib/saml/kit/buildable.rb
@@ -2,6 +2,9 @@
module Saml
module Kit
+ # This module is responsible for
+ # providing an API to build a
+ # document object, xml, or builder class.
module Buildable
extend ActiveSupport::Concern
lib/saml/kit/builders.rb
@@ -8,11 +8,14 @@ require 'saml/kit/builders/identity_provider_metadata'
require 'saml/kit/builders/logout_request'
require 'saml/kit/builders/logout_response'
require 'saml/kit/builders/metadata'
+require 'saml/kit/builders/null'
require 'saml/kit/builders/response'
require 'saml/kit/builders/service_provider_metadata'
module Saml
module Kit
+ # This module contains all the builders classes
+ # that are used to create SAML documents.
module Builders
end
end
lib/saml/kit/composite_metadata.rb
@@ -2,6 +2,11 @@
module Saml
module Kit
+ # This class implements the Composite
+ # design pattern to allow client
+ # component to work with a metadata
+ # that provides an IDPSSODescriptor
+ # and SPSSODescriptor element.
class CompositeMetadata < Metadata # :nodoc:
include Enumerable
attr_reader :service_provider, :identity_provider
@@ -15,7 +20,7 @@ module Saml
end
def services(type)
- xpath = map { |x| "//md:EntityDescriptor/md:#{x.name}/md:#{type}" }.join('|')
+ xpath = map { |xxx| "//md:EntityDescriptor/md:#{xxx.name}/md:#{type}" }.join('|')
search(xpath).map do |item|
binding = item.attribute('Binding').value
location = item.attribute('Location').value
@@ -32,7 +37,7 @@ module Saml
end
def method_missing(name, *args)
- if (target = find { |x| x.respond_to?(name) })
+ if (target = find { |xxx| xxx.respond_to?(name) })
target.public_send(name, *args)
else
super
@@ -40,7 +45,7 @@ module Saml
end
def respond_to_missing?(method, *)
- find { |x| x.respond_to?(method) }
+ find { |xxx| xxx.respond_to?(method) }
end
end
end
lib/saml/kit/configuration.rb
@@ -56,7 +56,7 @@ module Saml
# @param passphrase [String] the password to decrypt the private key.
# @param use [Symbol] the type of key pair, `:signing` or `:encryption`
def add_key_pair(certificate, private_key, passphrase: nil, use: :signing)
- ensure_proper_use!(use)
+ ensure_proper_use(use)
@key_pairs.push(::Xml::Kit::KeyPair.new(certificate, private_key, passphrase, use.to_sym))
end
@@ -65,7 +65,7 @@ module Saml
# @param use [Symbol] the type of key pair, `:signing` or `:encryption`
# @param passphrase [String] the private key passphrase to use.
def generate_key_pair_for(use:, passphrase: SecureRandom.uuid)
- ensure_proper_use!(use)
+ ensure_proper_use(use)
certificate, private_key = ::Xml::Kit::SelfSignedCertificate.new.create(passphrase: passphrase)
add_key_pair(certificate, private_key, passphrase: passphrase, use: use)
end
@@ -74,7 +74,7 @@ module Saml
#
# @param use [Symbol] the type of key pair to return `nil`, `:signing` or `:encryption`
def key_pairs(use: nil)
- use.present? ? @key_pairs.find_all { |x| x.for?(use) } : @key_pairs
+ use.present? ? @key_pairs.find_all { |xxx| xxx.for?(use) } : @key_pairs
end
# Return each certificate for a specific use.
@@ -93,12 +93,12 @@ module Saml
# Returns true if there is at least one signing certificate registered.
def sign?
- certificates(use: :signing).any?
+ @sign ||= certificates(use: :signing).any?
end
private
- def ensure_proper_use!(use)
+ def ensure_proper_use(use)
return if USES.include?(use)
error_message = 'Use must be either :signing or :encryption'
lib/saml/kit/default_registry.rb
@@ -69,6 +69,9 @@ module Saml
end
end
+ # This class is responsible for
+ # making HTTP requests to fetch metadata
+ # from remote locations.
class HttpApi # :nodoc:
def initialize(url, verify_ssl: true)
@uri = URI.parse(url)
lib/saml/kit/document.rb
@@ -2,6 +2,7 @@
module Saml
module Kit
+ # This class is a base class for SAML documents.
class Document
include ActiveModel::Validations
include XsdValidatable
@@ -65,8 +66,8 @@ module Saml
# Returns the SAML document as an XML string.
#
# @param pretty [Boolean] formats the xml or returns the raw xml.
- def to_xml(pretty: false)
- pretty ? to_nokogiri.to_xml(indent: 2) : content
+ def to_xml(pretty: nil)
+ pretty ? to_nokogiri.to_xml(indent: 2) : to_s
end
# Returns the SAML document as an XHTML string.
@@ -77,7 +78,7 @@ module Saml
# @!visibility private
def to_nokogiri
- @to_nokogiri ||= Nokogiri::XML(content)
+ @to_nokogiri ||= Nokogiri::XML(to_s)
end
# @!visibility private
@@ -91,7 +92,7 @@ module Saml
end
def to_s
- to_xml
+ content
end
class << self
lib/saml/kit/identity_provider_metadata.rb
@@ -74,9 +74,9 @@ module Saml
# @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)
- builder = Saml::Kit::AuthenticationRequest.builder(configuration: configuration) do |x|
- x.embed_signature = want_authn_requests_signed
- yield x if block_given?
+ builder = Saml::Kit::AuthenticationRequest.builder(configuration: configuration) do |xxx|
+ xxx.embed_signature = want_authn_requests_signed
+ yield xxx if block_given?
end
request_binding = single_sign_on_service_for(binding: binding)
request_binding.serialize(builder, relay_state: relay_state)
lib/saml/kit/invalid_document.rb
@@ -2,6 +2,8 @@
module Saml
module Kit
+ # This class represents an invalid SAML
+ # document that could not be parsed.
# {include:file:spec/saml/kit/invalid_document_spec.rb}
class InvalidDocument < Document
validate do |model|
lib/saml/kit/logout_request.rb
@@ -51,8 +51,8 @@ module Saml
# @param relay_state [Object] The RelayState to include in the RelayState param.
# @return [Array] Returns an array with a url and Hash of parameters to return to the requestor.
def response_for(binding:, relay_state: nil)
- builder = Saml::Kit::LogoutResponse.builder(self) do |x|
- yield x if block_given?
+ builder = Saml::Kit::LogoutResponse.builder(self) do |xxx|
+ yield xxx if block_given?
end
response_binding = provider.single_logout_service_for(binding: binding)
response_binding.serialize(builder, relay_state: relay_state)
lib/saml/kit/metadata.rb
@@ -110,7 +110,7 @@ module Saml
# @param type [Symbol] can be on the service element like `AssertionConsumerServiceURL`, `SingleSignOnService` or `SingleLogoutService`.
def service_for(binding:, type:)
binding = Saml::Kit::Bindings.binding_for(binding)
- services(type).find { |x| x.binding?(binding) }
+ services(type).find { |xxx| xxx.binding?(binding) }
end
# Returns each of the SingleLogoutService bindings
@@ -132,7 +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) { |x| yield x if block_given? }
+ builder = Saml::Kit::LogoutRequest.builder(user) { |xxx| yield xxx if block_given? }
request_binding = single_logout_service_for(binding: binding)
request_binding.serialize(builder, relay_state: relay_state)
end
@@ -143,7 +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 { |x| x.for?(use) && x.fingerprint == fingerprint }
+ certificates.find { |xxx| xxx.for?(use) && xxx.fingerprint == fingerprint }
end
# Returns the XML document converted to a Hash.
@@ -153,14 +153,14 @@ module Saml
# Returns the XML document as a String.
#
- # @param pretty [Symbol] true to return a human friendly version of the XML.
- def to_xml(pretty: false)
- pretty ? to_nokogiri.to_xml(indent: 2) : @xml
+ # @param pretty [Boolean] true to return a human friendly version of the XML.
+ def to_xml(pretty: nil)
+ pretty ? to_nokogiri.to_xml(indent: 2) : to_s
end
# Returns the XML document as a [String].
def to_s
- to_xml
+ @xml
end
# Verifies the signature and data using the signing certificates.
lib/saml/kit/null_assertion.rb
@@ -2,6 +2,9 @@
module Saml
module Kit
+ # This class is an implementation of the
+ # Null Object pattern for when a Response
+ # is missing an Assertion.
class NullAssertion
include ActiveModel::Validations
include Translatable
lib/saml/kit/requestable.rb
@@ -2,6 +2,10 @@
module Saml
module Kit
+ # This module is responsible for providing
+ # the functionality available to all
+ # SAML request documents.
+ # e.g. AuthnRequest, LogoutRequest.
module Requestable
extend ActiveSupport::Concern
lib/saml/kit/respondable.rb
@@ -2,6 +2,9 @@
module Saml
module Kit
+ # This module provides the behaviours
+ # associated with SAML Response documents.
+ # .e.g. Response, LogoutResponse
module Respondable
extend ActiveSupport::Concern
attr_reader :request_id
@@ -21,6 +24,11 @@ module Saml
at_xpath('./*/samlp:Status/samlp:StatusCode/@Value').try(:value)
end
+ # Returns the /Status/StatusMessage
+ def status_message
+ at_xpath('./*/samlp:Status/samlp:StatusMessage').try(:text)
+ end
+
# Returns the /InResponseTo attribute.
def in_response_to
at_xpath('./*/@InResponseTo').try(:value)
lib/saml/kit/response.rb
@@ -2,6 +2,8 @@
module Saml
module Kit
+ # This class is responsible for validating and
+ # parsing a SAML Response document.
# {include:file:spec/examples/response_spec.rb}
class Response < Document
include Respondable
lib/saml/kit/serializable.rb
@@ -2,6 +2,7 @@
module Saml
module Kit
+ # This module is responsible for serializing/deserialing values.
module Serializable
# Base 64 decodes the value.
#
lib/saml/kit/service_provider_metadata.rb
@@ -2,6 +2,9 @@
module Saml
module Kit
+ # This class represents a
+ # SPSSODescriptor element in a
+ # SAML metadata document.
# {include:file:spec/examples/service_provider_metadata_spec.rb}
class ServiceProviderMetadata < Metadata
def initialize(xml)
lib/saml/kit/signature.rb
@@ -2,6 +2,9 @@
module Saml
module Kit
+ # This class is responsible for
+ # validating an xml digital signature
+ # in an xml document.
class Signature
include ActiveModel::Validations
include Translatable
@@ -34,8 +37,8 @@ module Saml
end
def expected_digest_value
- digests = dsignature.references.map do |x|
- Base64.encode64(x.calculate_digest_value).chomp
+ digests = dsignature.references.map do |xxx|
+ Base64.encode64(xxx.calculate_digest_value).chomp
end
digests.count > 1 ? digests : digests[0]
end
@@ -69,8 +72,12 @@ module Saml
node.present?
end
- def to_xml(pretty: false)
- pretty ? node.to_xml(indent: 2) : node.to_s
+ def to_xml(pretty: nil)
+ pretty ? node.to_xml(indent: 2) : to_s
+ end
+
+ def to_s
+ node.to_s
end
private
lib/saml/kit/translatable.rb
@@ -2,6 +2,9 @@
module Saml
module Kit
+ # This module is responsible
+ # for translating error messages
+ # to the current locale.
module Translatable
# @!visibility private
def error_message(attribute, options = {})
lib/saml/kit/trustable.rb
@@ -2,6 +2,9 @@
module Saml
module Kit
+ # This module is responsible for
+ # validating the trustworthiness
+ # of a saml document.
module Trustable
extend ActiveSupport::Concern
lib/saml/kit/version.rb
@@ -2,6 +2,6 @@
module Saml
module Kit
- VERSION = '1.0.14'.freeze
+ VERSION = '1.0.15'.freeze
end
end
lib/saml/kit/xml_templatable.rb
@@ -2,6 +2,8 @@
module Saml
module Kit
+ # This module is responsible for
+ # generating converting templates to xml.
module XmlTemplatable
TEMPLATES_DIR = Pathname.new(File.join(__dir__, 'builders/templates/'))
include ::Xml::Kit::Templatable
lib/saml/kit/xsd_validatable.rb
@@ -2,6 +2,8 @@
module Saml
module Kit
+ # This module is responsible for validating
+ # xml documents against the SAML XSD's
module XsdValidatable
# @!visibility private
def matches_xsd?(xsd)
lib/saml/kit.rb
@@ -48,7 +48,11 @@ I18n.load_path +=
Dir[File.expand_path('kit/locales/*.yml', File.dirname(__FILE__))]
module Saml
+ # This module is the container for all classes/modules in this gem.
module Kit
+ # This class provides a global access to the
+ # default SAML configuration. This is useful
+ # for long running processes.
class << self
def configuration
@configuration ||= Saml::Kit::Configuration.new
spec/fixtures/no_nameid.saml_response.erb
@@ -0,0 +1,25 @@
+<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Destination="<%= fetch(:destination, 'https://www.example.com') %>" ID="_d2b481a92e895f1436189403b7d5ccd1" InResponseTo="<%= fetch(:in_response_to, "_4db60150-227a-439b-a686-e7c57a9b5f9a") %>" IssueInstant="<%= issue_instant.iso8601 %>" Version="2.0">
+ <saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"><%= fetch(:issuer, 'https://www.example.com') %></saml2:Issuer>
+ <saml2p:Status>
+ <saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"></saml2p:StatusCode>
+ </saml2p:Status>
+ <saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xsd="http://www.w3.org/2001/XMLSchema" ID="_637d9c5c732418d7cef10586f21c21b0" IssueInstant="<%= issue_instant.iso8601 %>" Version="2.0">
+ <saml2:Issuer><%= fetch(:issuer, 'https://www.example.com') %></saml2:Issuer>
+ <saml2:Subject>
+ <saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
+ <saml2:SubjectConfirmationData Address="10.38.131.134" InResponseTo="<%= fetch(:in_response_to, "_4db60150-227a-439b-a686-e7c57a9b5f9a") %>" NotOnOrAfter="<%= fetch(:not_on_or_after, Time.now).iso8601 %>" Recipient="<%= fetch(:recipient, 'https://www.example.com/') %>"></saml2:SubjectConfirmationData>
+ </saml2:SubjectConfirmation>
+ </saml2:Subject>
+ <saml2:Conditions NotBefore="<%= fetch(:not_before, Time.now).iso8601 %>" NotOnOrAfter="<%= fetch(:not_on_or_after, Time.now).iso8601 %>">
+ <saml2:AudienceRestriction>
+ <saml2:Audience><%= fetch(:audience, 'https://www.example.com') %></saml2:Audience>
+ </saml2:AudienceRestriction>
+ </saml2:Conditions>
+ <saml2:AuthnStatement AuthnInstant="<%= issue_instant.iso8601 %>" SessionIndex="_99d6ac14d6ad70a23c89f82ba75c4ae0">
+ <saml2:SubjectLocality Address="10.38.131.134"></saml2:SubjectLocality>
+ <saml2:AuthnContext>
+ <saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml2:AuthnContextClassRef>
+ </saml2:AuthnContext>
+ </saml2:AuthnStatement>
+ </saml2:Assertion>
+</saml2p:Response>
spec/fixtures/requester_error.saml_response.erb
@@ -0,0 +1,7 @@
+<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" ID="<%= Xml::Kit::Id.generate %>" InResponseTo="<%= Xml::Kit::Id.generate %>" IssueInstant="<%= fetch(:issue_instant, Time.now).iso8601 %>" Destination="<%= fetch(:destination, 'https://www.example.com') %>" Version="2.0">
+ <saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"><%= fetch(:issuer, 'https://www.example.com') %></saml2:Issuer>
+ <saml2p:Status>
+ <saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Requester"></saml2p:StatusCode>
+ <saml2p:StatusMessage><%= fetch(:status_message, "Invalid message signature") %></saml2p:StatusMessage>
+ </saml2p:Status>
+</saml2p:Response>
spec/saml/kit/builders/response_spec.rb
@@ -38,6 +38,19 @@ RSpec.describe Saml::Kit::Builders::Response do
expect(result.issuer).to eql(issuer)
expect(result.assertion.issuer).to eql(issuer)
end
+
+ it 'builds a response with a status code' do
+ subject.status_code = Saml::Kit::Namespaces::REQUESTER_ERROR
+ subject.status_message = 'Invalid message signature'
+ result = subject.build
+ expect(result.status_message).to eql('Invalid message signature')
+ end
+
+ it 'builds a response without an assertion' do
+ subject.assertion = nil
+ result = subject.build
+ expect(result.assertion).not_to be_present
+ end
end
describe '#to_xml' do
spec/saml/kit/response_spec.rb
@@ -554,6 +554,19 @@ XML
subject = described_class.build(user, request)
expect(subject.attributes).to eql('name' => 'mo', 'age' => '33')
end
+
+ it 'can parse an assertion without a name id' do
+ xml = expand_template('no_nameid.saml_response', issue_instant: Time.now)
+ subject = described_class.new(xml)
+ expect(subject.name_id).to be_nil
+ end
+
+ it 'parses a response with a status code of Requester' do
+ message = FFaker::Lorem.sentence
+ subject = described_class.new(expand_template('requester_error.saml_response', status_message: message))
+ expect(subject.status_code).to eql(Saml::Kit::Namespaces::REQUESTER_ERROR)
+ expect(subject.status_message).to eql(message)
+ end
end
describe '#build' do
spec/support/erb_templating.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module ErbTemplating
+ class Template
+ def initialize(template_name, data)
+ @erb = ERB.new(IO.read(File.join('spec/fixtures', "#{template_name}.erb")))
+ @data = data
+ end
+
+ def fetch(key, default)
+ @data.fetch(key) { default }
+ end
+
+ def __expand
+ @erb.result(binding)
+ end
+
+ def method_missing(method, *)
+ @data[method] || super
+ end
+
+ def respond_to_missing?(method, *)
+ @data.key?(method)
+ end
+ end
+
+ def expand_template(template_name, data = {})
+ Template.new(template_name, data).__expand
+ end
+end
+
+RSpec.configure do |config|
+ config.include ErbTemplating
+end
spec/support/test_helpers.rb
@@ -2,7 +2,7 @@
module TestHelpers
def query_params_from(url)
- Hash[query_for(url).split('&').map { |x| x.split('=', 2) }]
+ Hash[query_for(url).split('&').map { |xxx| xxx.split('=', 2) }]
end
def uri_for(url)
.reek
@@ -0,0 +1,125 @@
+---
+Attribute:
+ exclude:
+ - Saml::Kit::Assertion#occurred_at
+ - Saml::Kit::Builders::AuthenticationRequest#assertion_consumer_service_url
+ - Saml::Kit::Builders::AuthenticationRequest#destination
+ - Saml::Kit::Builders::AuthenticationRequest#id
+ - Saml::Kit::Builders::AuthenticationRequest#issuer
+ - Saml::Kit::Builders::AuthenticationRequest#name_id_format
+ - Saml::Kit::Builders::AuthenticationRequest#now
+ - Saml::Kit::Builders::AuthenticationRequest#version
+ - Saml::Kit::Builders::IdentityProviderMetadata#attributes
+ - Saml::Kit::Builders::IdentityProviderMetadata#name_id_formats
+ - Saml::Kit::Builders::IdentityProviderMetadata#want_authn_requests_signed
+ - Saml::Kit::Builders::LogoutRequest#destination
+ - Saml::Kit::Builders::LogoutRequest#id
+ - Saml::Kit::Builders::LogoutRequest#issuer
+ - Saml::Kit::Builders::LogoutRequest#name_id_format
+ - Saml::Kit::Builders::LogoutRequest#now
+ - Saml::Kit::Builders::LogoutRequest#version
+ - Saml::Kit::Builders::LogoutResponse#destination
+ - Saml::Kit::Builders::LogoutResponse#id
+ - Saml::Kit::Builders::LogoutResponse#issuer
+ - Saml::Kit::Builders::LogoutResponse#now
+ - Saml::Kit::Builders::LogoutResponse#status_code
+ - Saml::Kit::Builders::LogoutResponse#version
+ - Saml::Kit::Builders::Metadata#contact_email
+ - Saml::Kit::Builders::Metadata#entity_id
+ - Saml::Kit::Builders::Metadata#id
+ - Saml::Kit::Builders::Metadata#identity_provider
+ - Saml::Kit::Builders::Metadata#organization_name
+ - Saml::Kit::Builders::Metadata#organization_url
+ - Saml::Kit::Builders::Metadata#service_provider
+ - Saml::Kit::Builders::Response#destination
+ - Saml::Kit::Builders::Response#id
+ - Saml::Kit::Builders::Response#issuer
+ - Saml::Kit::Builders::Response#now
+ - Saml::Kit::Builders::Response#reference_id
+ - Saml::Kit::Builders::Response#status_code
+ - Saml::Kit::Builders::Response#status_message
+ - Saml::Kit::Builders::Response#version
+ - Saml::Kit::Builders::ServiceProviderMetadata#acs_urls
+ - Saml::Kit::Builders::ServiceProviderMetadata#logout_urls
+ - Saml::Kit::Builders::ServiceProviderMetadata#name_id_formats
+ - Saml::Kit::Builders::ServiceProviderMetadata#want_assertions_signed
+ - Saml::Kit::Configuration#clock_drift
+ - Saml::Kit::Configuration#digest_method
+ - Saml::Kit::Configuration#entity_id
+ - Saml::Kit::Configuration#logger
+ - Saml::Kit::Configuration#registry
+ - Saml::Kit::Configuration#session_timeout
+ - Saml::Kit::Configuration#signature_method
+ - Saml::Kit::Document#registry
+BooleanParameter:
+ exclude:
+ - Saml::Kit::DefaultRegistry#register_url
+ - Saml::Kit::DefaultRegistry::HttpApi#initialize
+ControlParameter:
+ exclude:
+ - Saml::Kit::Assertion#to_xml
+ - Saml::Kit::Bindings::HttpRedirect#algorithm_for
+ - Saml::Kit::Document#to_xml
+ - Saml::Kit::Metadata#to_xml
+ - Saml::Kit::Signature#to_xml
+TooManyInstanceVariables:
+ max_instance_variables: 15
+TooManyMethods:
+ max_methods: 30
+TooManyStatements:
+ max_statements: 10
+UtilityFunction:
+ exclude:
+ - Saml::Kit::Assertion#parse_date
+ - Saml::Kit::Bindings::HttpRedirect#algorithm_for
+ - Saml::Kit::Bindings::HttpRedirect#canonicalize
+ - Saml::Kit::Bindings::HttpRedirect#normalize
+ - Saml::Kit::Bindings::HttpRedirect#params_to_hash
+ - Saml::Kit::NullAssertion#expired_at
+ - Saml::Kit::NullAssertion#started_at
+ - uri_for
+ - xml_document
+ - Saml::Kit::Serializable#decode
+ - Saml::Kit::Serializable#deflate
+ - Saml::Kit::Serializable#encode
+ - Saml::Kit::Serializable#escape
+ - Saml::Kit::Serializable#inflate
+ - Saml::Kit::Serializable#unescape
+LongParameterList:
+ max_params: 4
+DuplicateMethodCall:
+ exclude:
+ - Saml::Kit::Bindings::Binding#saml_param_from
+ - Saml::Kit::Builders::Response#initialize
+ - Saml::Kit::XmlTemplatable#sign?
+FeatureEnvy:
+ exclude:
+ - Saml::Kit::Bindings::Binding#saml_param_from
+ - Saml::Kit::Bindings::HttpRedirect#serialize
+ - Saml::Kit::DefaultRegistry#register
+ - Saml::Kit::Metadata#matches?
+ - Saml::Kit::Signature#expected_digest_value
+ - Saml::Kit::Signature#trusted?
+NilCheck:
+ exclude:
+ - Saml::Kit::Bindings::HttpRedirect#ensure_valid_signature
+ - Saml::Kit::IdentityProviderMetadata#want_authn_requests_signed
+ - Saml::Kit::LogoutRequest#single_logout_service
+ - Saml::Kit::Respondable#must_match_request_id
+ - Saml::Kit::Response#assertion
+ - Saml::Kit::ServiceProviderMetadata#want_assertions_signed
+ - Saml::Kit::Signature#certificate
+ - Saml::Kit::Signature#trusted?
+ - Saml::Kit::Signature#validate_signature
+ - Saml::Kit::XmlTemplatable#sign?
+ManualDispatch:
+ exclude:
+ - Saml::Kit::Builders::Assertion#assertion_attributes
+ - Saml::Kit::CompositeMetadata#method_missing
+ - Saml::Kit::CompositeMetadata#respond_to_missing?
+UnusedParameters:
+ exclude:
+ - Saml::Kit::InvalidDocument#initialize
+TooManyConstants:
+ exclude:
+ - Saml::Kit::Namespaces
Rakefile
@@ -1,15 +1,17 @@
# frozen_string_literal: true
+require 'bundler/audit/task'
require 'bundler/gem_tasks'
+require 'reek/rake/task'
require 'rspec/core/rake_task'
+require 'rubocop/rake_task'
RSpec::Core::RakeTask.new(:spec)
-task default: :spec
-
-require 'rubocop/rake_task'
RuboCop::RakeTask.new(:rubocop)
-
-require 'bundler/audit/task'
Bundler::Audit::Task.new
+Reek::Rake::Task.new(:reek) do |task|
+ task.config_file = '.reek'
+end
-task lint: [:rubocop, 'bundle:audit']
+task lint: [:rubocop, :reek, 'bundle:audit']
+task default: :spec
saml-kit.gemspec
@@ -34,6 +34,7 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'bundler-audit', '~> 0.6'
spec.add_development_dependency 'ffaker', '~> 2.7'
spec.add_development_dependency 'rake', '~> 10.0'
+ spec.add_development_dependency 'reek', '~> 4.8'
spec.add_development_dependency 'rspec', '~> 3.0'
spec.add_development_dependency 'rspec-benchmark', '~> 0.3'
spec.add_development_dependency 'rubocop', '~> 0.52'